Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2024-02-06 09:38:25 +01:00
commit cb6db21e63
50 changed files with 1311 additions and 709 deletions

View file

@ -7,6 +7,7 @@
(ns user (ns user
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.debug :as debug]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.fressian :as fres] [app.common.fressian :as fres]
@ -55,8 +56,12 @@
[promesa.exec :as px])) [promesa.exec :as px]))
(repl/disable-reload! (find-ns 'integrant.core)) (repl/disable-reload! (find-ns 'integrant.core))
(repl/disable-reload! (find-ns 'app.common.debug))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
(add-tap #'debug/tap-handler)
;; --- Benchmarking Tools ;; --- Benchmarking Tools
(defmacro run-quick-bench (defmacro run-quick-bench
@ -132,12 +137,6 @@
;; :v6 v6 ;; :v6 v6
;; }]))) ;; }])))
(defonce debug-tap
(do
(add-tap #(locking debug-tap
(prn "tap debug:" %)))
1))
(defn calculate-frames (defn calculate-frames
[{:keys [data]}] [{:keys [data]}]

View file

@ -21,7 +21,7 @@
<Logger name="com.zaxxer.hikari" level="error"/> <Logger name="com.zaxxer.hikari" level="error"/>
<Logger name="org.postgresql" level="error" /> <Logger name="org.postgresql" level="error" />
<Logger name="app.rpc.commands.binfile" level="debug" /> <Logger name="app.binfile" level="debug" />
<Logger name="app.storage.tmp" level="info" /> <Logger name="app.storage.tmp" level="info" />
<Logger name="app.worker" level="trace" /> <Logger name="app.worker" level="trace" />
<Logger name="app.msgbus" level="info" /> <Logger name="app.msgbus" level="info" />

View file

@ -31,6 +31,7 @@
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn] [app.common.types.container :as ctn]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
[app.common.types.grid :as ctg]
[app.common.types.page :as ctp] [app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@ -105,10 +106,20 @@
;; FILE PREPARATION BEFORE MIGRATION ;; FILE PREPARATION BEFORE MIGRATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def valid-color? (sm/lazy-validator ::ctc/recent-color)) (def valid-recent-color?
(def valid-fill? (sm/lazy-validator ::cts/fill)) (sm/lazy-validator ::ctc/recent-color))
(def valid-stroke? (sm/lazy-validator ::cts/stroke))
(def valid-flow? (sm/lazy-validator ::ctp/flow)) (def valid-color?
(sm/lazy-validator ::ctc/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? (def valid-text-content?
(sm/lazy-validator ::ctsx/content)) (sm/lazy-validator ::ctsx/content))
@ -122,30 +133,61 @@
(def valid-rgb-color-string? (def valid-rgb-color-string?
(sm/lazy-validator ::ctc/rgb-color)) (sm/lazy-validator ::ctc/rgb-color))
(def valid-shape-points?
(sm/lazy-validator ::cts/points))
(def valid-image-attrs?
(sm/lazy-validator ::cts/image-attrs))
(def valid-column-grid-params?
(sm/lazy-validator ::ctg/column-params))
(def valid-square-grid-params?
(sm/lazy-validator ::ctg/square-params))
(defn- prepare-file-data (defn- prepare-file-data
"Apply some specific migrations or fixes to things that are allowed in v1 but not in v2, "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." or that are the result of old bugs."
[file-data libraries] [file-data libraries]
(let [detached-ids (volatile! #{}) (let [detached-ids (volatile! #{})
detach-shape detach-shape
(fn [container shape] (fn [container shape]
;; Detach a shape. If it's inside a component, add it to detached-ids. This list ;; Detach a shape and make necessary adjustments.
;; 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))] (let [is-component? (let [root-shape (ctst/get-shape container (:id container))]
(and (some? root-shape) (nil? (:parent-id root-shape))))] (and (some? root-shape) (nil? (:parent-id root-shape))))
parent (ctst/get-shape container (:parent-id shape))
in-copy? (ctn/in-any-component? (:objects container) parent)]
(letfn [(detach-recursive [container shape first?]
;; If the shape is inside a component, add it to detached-ids. This list is used
;; later to process other copies that was referencing a detached nested copy.
(when is-component? (when is-component?
(vswap! detached-ids conj (:id shape))) (vswap! detached-ids conj (:id shape)))
(ctk/detach-shape shape)))
;; Detach the shape and all children until we find a subinstance.
(if (or first? in-copy? (not (ctk/instance-head? shape)))
(as-> container $
(ctn/update-shape $ (:id shape) ctk/detach-shape)
(reduce #(detach-recursive %1 %2 false)
$
(map (d/getf (:objects container)) (:shapes shape))))
;; If this is a subinstance head and the initial shape whas not itself a
;; nested copy, stop detaching and promote it to root.
(ctn/update-shape container (:id shape) #(assoc % :component-root true))))]
(detach-recursive container shape true))))
fix-bad-children fix-bad-children
(fn [file-data] (fn [file-data]
;; Remove any child that does not exist. And also remove duplicated children. ;; Remove any child that does not exist. And also remove duplicated children.
(letfn [(fix-container (letfn [(fix-container [container]
[container]
(d/update-when container :objects update-vals (partial fix-shape container))) (d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape (fix-shape [container shape]
[container shape]
(let [objects (:objects container)] (let [objects (:objects container)]
(d/update-when shape :shapes (d/update-when shape :shapes
(fn [shapes] (fn [shapes]
@ -160,12 +202,10 @@
fix-missing-image-metadata fix-missing-image-metadata
(fn [file-data] (fn [file-data]
;; Delete broken image shapes with no metadata. ;; Delete broken image shapes with no metadata.
(letfn [(fix-container (letfn [(fix-container [container]
[container]
(d/update-when container :objects #(reduce-kv fix-shape % %))) (d/update-when container :objects #(reduce-kv fix-shape % %)))
(fix-shape (fix-shape [objects id shape]
[objects id shape]
(if (and (cfh/image-shape? shape) (if (and (cfh/image-shape? shape)
(nil? (:metadata shape))) (nil? (:metadata shape)))
(-> objects (-> objects
@ -189,11 +229,28 @@
(dissoc options :background) (dissoc options :background)
options)) options))
(fix-saved-grids [options]
(d/update-when options :saved-grids
(fn [grids]
(cond-> grids
(and (contains? grids :column)
(not (valid-column-grid-params? (:column grids))))
(dissoc :column)
(and (contains? grids :row)
(not (valid-column-grid-params? (:row grids))))
(dissoc :row)
(and (contains? grids :square)
(not (valid-square-grid-params? (:square grids))))
(dissoc :square)))))
(fix-options [options] (fix-options [options]
(-> options (-> options
;; Some pages has invalid data on flows, we proceed just to ;; Some pages has invalid data on flows, we proceed just to
;; delete them. ;; delete them.
(d/update-when :flows #(filterv valid-flow? %)) (d/update-when :flows #(filterv valid-flow? %))
(fix-saved-grids)
(fix-background)))] (fix-background)))]
(update file-data :pages-index update-vals update-page))) (update file-data :pages-index update-vals update-page)))
@ -203,11 +260,19 @@
;; fix that issues. ;; fix that issues.
fix-file-data fix-file-data
(fn [file-data] (fn [file-data]
(letfn [(fix-colors-library [colors]
(let [colors (dissoc colors nil)]
(reduce-kv (fn [colors id color]
(if (valid-color? color)
colors
(dissoc colors id)))
colors
colors)))]
(-> file-data (-> file-data
(d/update-when :colors dissoc nil) (d/update-when :colors fix-colors-library)
(d/update-when :typographies dissoc nil))) (d/update-when :typographies dissoc nil))))
delete-big-geometry-shapes fix-big-geometry-shapes
(fn [file-data] (fn [file-data]
;; At some point in time, we had a bug that generated shapes ;; At some point in time, we had a bug that generated shapes
;; with huge geometries that did not validate the ;; with huge geometries that did not validate the
@ -253,9 +318,16 @@
(fn [shapes] (filterv #(not= id %) shapes))))) (fn [shapes] (filterv #(not= id %) shapes)))))
(and (cfh/text-shape? shape) (and (cfh/text-shape? shape)
(not (seq (:content shape)))) (not (valid-text-content? (:content shape))))
(dissoc objects id) (dissoc objects id)
(and (cfh/path-shape? shape)
(not (valid-path-content? (:content shape))))
(-> objects
(dissoc id)
(d/update-in-when [(:parent-id shape) :shapes]
(fn [shapes] (filterv #(not= id %) shapes))))
:else :else
objects)) objects))
@ -266,25 +338,125 @@
(update :pages-index update-vals update-container) (update :pages-index update-vals update-container)
(update :components update-vals update-container)))) (update :components update-vals update-container))))
fix-misc-shape-issues fix-shape-geometry
(fn [file-data] (fn [file-data]
(letfn [(fix-container [container] (letfn [(fix-container [container]
(d/update-when container :objects update-vals fix-shape)) (d/update-when container :objects update-vals fix-shape))
(fix-shape [shape] (fix-shape [shape]
(cond-> shape (cond
;; Some shapes has invalid gap value (and (cfh/image-shape? shape)
(contains? shape :layout-gap) (valid-image-attrs? shape)
(d/update-in-when [:layout-gap :column-gap] (grc/valid-rect? (:selrect shape))
(fn [gap] (not (valid-shape-points? (:points shape))))
(let [selrect (:selrect shape)
metadata (:metadata shape)
selrect (grc/make-rect
(:x selrect)
(:y selrect)
(:width metadata)
(:height metadata))
points (grc/rect->points selrect)]
(assoc shape
:selrect selrect
:points points))
(and (cfh/text-shape? shape)
(valid-text-content? (:content shape))
(not (valid-shape-points? (:points shape)))
(seq (:position-data shape)))
(let [selrect (->> (:position-data shape)
(map (juxt :x :y :width :height))
(map #(apply grc/make-rect %))
(grc/join-rects))
points (grc/rect->points selrect)]
(assoc shape
:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:selrect selrect
:points points))
(and (or (cfh/rect-shape? shape)
(cfh/svg-raw-shape? shape)
(cfh/circle-shape? shape))
(not (valid-shape-points? (:points shape)))
(grc/valid-rect? (:selrect shape)))
(let [selrect (if (grc/valid-rect? (:svg-viewbox shape))
(:svg-viewbox shape)
(:selrect shape))
points (grc/rect->points selrect)]
(assoc shape
:x (:x selrect)
:y (:y selrect)
:width (:width selrect)
:height (:height selrect)
:selrect selrect
:points points))
(and (= :icon (:type shape))
(grc/valid-rect? (:selrect shape))
(valid-shape-points? (:points shape)))
(-> shape
(assoc :type :rect)
(dissoc :content)
(dissoc :metadata)
(dissoc :segments)
(dissoc :x1 :y1 :x2 :y2))
(and (cfh/group-shape? shape)
(grc/valid-rect? (:selrect shape))
(not (valid-shape-points? (:points shape))))
(assoc shape :points (grc/rect->points (:selrect shape)))
:else
shape))]
(-> file-data
(update :pages-index update-vals fix-container)
(d/update-when :components update-vals fix-container))))
fix-misc-shape-issues
(fn [file-data]
(letfn [(fix-container [container]
(d/update-when container :objects update-vals fix-shape))
(fix-gap-value [gap]
(if (or (= gap ##Inf) (if (or (= gap ##Inf)
(= gap ##-Inf)) (= gap ##-Inf))
0 0
gap))) gap))
(fix-shape [shape]
(cond-> shape
;; Some shapes has invalid gap value
(contains? shape :layout-gap)
(update :layout-gap (fn [layout-gap]
(if (number? layout-gap)
{:row-gap layout-gap :column-gap layout-gap}
(-> layout-gap
(d/update-when :column-gap fix-gap-value)
(d/update-when :row-gap fix-gap-value)))))
;; Fix name if missing
(nil? (:name shape)) (nil? (:name shape))
(assoc :name (d/name (:type shape))) (assoc :name (d/name (:type shape)))
;; Remove v2 info from components that have been copied and pasted
;; from a v2 file
(some? (:main-instance shape))
(dissoc :main-instance)
(and (contains? shape :transform)
(not (gmt/valid-matrix? (:transform shape))))
(assoc :transform (gmt/matrix))
(and (contains? shape :transform-inverse)
(not (gmt/valid-matrix? (:transform-inverse shape))))
(assoc :transform-inverse (gmt/matrix))
;; Fix broken fills ;; Fix broken fills
(seq (:fills shape)) (seq (:fills shape))
(update :fills (fn [fills] (filterv valid-fill? fills))) (update :fills (fn [fills] (filterv valid-fill? fills)))
@ -296,11 +468,7 @@
;; Fix some broken layout related attrs, probably ;; Fix some broken layout related attrs, probably
;; of copypaste on flex layout betatest period ;; of copypaste on flex layout betatest period
(true? (:layout shape)) (true? (:layout shape))
(assoc :layout :flex) (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 (-> file-data
(update :pages-index update-vals fix-container) (update :pages-index update-vals fix-container)
@ -342,13 +510,15 @@
(and (cfh/path-shape? shape) (and (cfh/path-shape? shape)
(seq (:content shape)) (seq (:content shape))
(not (valid-path-content? (:content shape)))) (not (valid-path-content? (:content shape))))
(let [shape (update shape :content fix-path-content) (let [shape (update shape :content fix-path-content)]
[points selrect] (gshp/content->points+selrect shape (:content shape))] (if (not (valid-path-content? (:content shape)))
shape
(let [[points selrect] (gshp/content->points+selrect shape (:content shape))]
(-> shape (-> shape
(dissoc :bool-content) (dissoc :bool-content)
(dissoc :bool-type) (dissoc :bool-type)
(assoc :points points) (assoc :points points)
(assoc :selrect selrect))) (assoc :selrect selrect)))))
;; When we fount a bool shape with no content, ;; When we fount a bool shape with no content,
;; we convert it to a simple rect ;; we convert it to a simple rect
@ -390,18 +560,16 @@
;; Remove invalid colors in :recent-colors ;; Remove invalid colors in :recent-colors
(d/update-when file-data :recent-colors (d/update-when file-data :recent-colors
(fn [colors] (fn [colors]
(filterv valid-color? colors)))) (filterv valid-recent-color? colors))))
fix-broken-parents fix-broken-parents
(fn [file-data] (fn [file-data]
;; Find children shapes whose parent-id is not set to the parent that contains them. ;; Find children shapes whose parent-id is not set to the parent that contains them.
;; Remove them from the parent :shapes list. ;; Remove them from the parent :shapes list.
(letfn [(fix-container (letfn [(fix-container [container]
[container]
(d/update-when container :objects #(reduce-kv fix-shape % %))) (d/update-when container :objects #(reduce-kv fix-shape % %)))
(fix-shape (fix-shape [objects id shape]
[objects id shape]
(reduce (fn [objects child-id] (reduce (fn [objects child-id]
(let [child (get objects child-id)] (let [child (get objects child-id)]
(cond-> objects (cond-> objects
@ -476,20 +644,33 @@
(fn [file-data] (fn [file-data]
;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't. ;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't.
(letfn [(fix-container [container] (letfn [(fix-container [container]
(d/update-when container :objects update-vals (partial fix-shape container))) (reduce fix-shape container (ctn/shapes-seq container)))
(fix-shape [container shape] (fix-shape [container shape]
(let [parent (ctst/get-shape container (:parent-id shape))] (let [shape (ctst/get-shape container (:id shape)) ; Get the possibly updated shape
parent (ctst/get-shape container (:parent-id shape))]
(if (and (ctk/in-component-copy? shape) (if (and (ctk/in-component-copy? shape)
(not (ctk/instance-head? shape)) (not (ctk/instance-head? shape))
(not (ctk/in-component-copy? parent))) (not (ctk/in-component-copy? parent)))
(detach-shape container shape) (detach-shape container shape)
shape)))] container)))]
(-> file-data (-> file-data
(update :pages-index update-vals fix-container) (update :pages-index update-vals fix-container)
(d/update-when :components update-vals fix-container)))) (d/update-when :components update-vals fix-container))))
fix-components-without-id
(fn [file-data]
;; We have detected some components that have no :id attribute.
;; Regenerate it from the components map.
(letfn [(fix-component [id component]
(if (some? (:id component))
component
(assoc component :id id)))]
(-> file-data
(d/update-when :components #(d/mapm fix-component %)))))
remap-refs remap-refs
(fn [file-data] (fn [file-data]
;; Remap shape-refs so that they point to the near main. ;; Remap shape-refs so that they point to the near main.
@ -523,11 +704,9 @@
(if (some? direct-shape-2) (if (some? direct-shape-2)
;; If it exists, there is nothing else to do. ;; If it exists, there is nothing else to do.
container container
;; If not found, detach shape and all children (stopping if a nested instance is reached) ;; If not found, detach shape and all children.
(let [children (ctn/get-children-in-instance (:objects container) (:id shape))] ;; container
(reduce #(ctn/update-shape %1 (:id %2) (partial detach-shape %1)) (detach-shape container shape)))))))
container
children))))))))
container))] container))]
(-> file-data (-> file-data
@ -539,14 +718,64 @@
;; If the user has created a copy and then converted into a path or bool, ;; If the user has created a copy and then converted into a path or bool,
;; detach it because the synchronization will no longer work. ;; detach it because the synchronization will no longer work.
(letfn [(fix-container [container] (letfn [(fix-container [container]
(d/update-when container :objects update-vals (partial fix-shape container))) (reduce fix-shape container (ctn/shapes-seq container)))
(fix-shape [container shape] (fix-shape [container shape]
(if (and (ctk/instance-head? shape) (if (and (ctk/instance-head? shape)
(or (cfh/path-shape? shape) (or (cfh/path-shape? shape)
(cfh/bool-shape? shape))) (cfh/bool-shape? shape)))
(detach-shape container shape) (detach-shape container shape)
shape))] container))]
(-> file-data
(update :pages-index update-vals fix-container)
(d/update-when :components update-vals fix-container))))
wrap-non-group-component-roots
(fn [file-data]
;; Some components have a root that is not a group nor a frame
;; (e.g. a path or a svg-raw). We need to wrap them in a frame
;; for this one to became the root.
(letfn [(fix-component [component]
(let [root-shape (ctst/get-shape component (:id component))]
(if (or (cfh/group-shape? root-shape)
(cfh/frame-shape? root-shape))
component
(let [new-id (uuid/next)
frame (-> (cts/setup-shape
{:type :frame
:id (:id component)
:x (:x (:selrect root-shape))
:y (:y (:selrect root-shape))
:width (:width (:selrect root-shape))
:height (:height (:selrect root-shape))
:name (:name component)
:shapes [new-id]})
(assoc :frame-id nil
:parent-id nil))
root-shape' (assoc root-shape
:id new-id
:parent-id (:id frame)
:frame-id (:id frame))]
(update component :objects assoc
(:id frame) frame
(:id root-shape') root-shape')))))]
(-> file-data
(d/update-when :components update-vals fix-component))))
detach-non-group-instance-roots
(fn [file-data]
;; If there is a copy instance whose root is not a frame or a group, it cannot
;; be easily repaired, and anyway it's not working in production, so detach it.
(letfn [(fix-container [container]
(reduce fix-shape container (ctn/shapes-seq container)))
(fix-shape [container shape]
(if (and (ctk/instance-head? shape)
(not (#{:group :frame} (:type shape))))
(detach-shape container shape)
container))]
(-> file-data (-> file-data
(update :pages-index update-vals fix-container) (update :pages-index update-vals fix-container)
@ -554,7 +783,7 @@
transform-to-frames transform-to-frames
(fn [file-data] (fn [file-data]
;; Transform component and copy heads to frames, and set the ;; Transform component and copy heads fron group to frames, and set the
;; frame-id of its childrens ;; frame-id of its childrens
(letfn [(fix-container [container] (letfn [(fix-container [container]
(d/update-when container :objects update-vals fix-shape)) (d/update-when container :objects update-vals fix-shape))
@ -631,9 +860,8 @@
(fn [file-data] (fn [file-data]
;; Find component heads that are not main-instance but have not :shape-ref. ;; 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. ;; Also shapes that have :shape-ref but are not in a copy.
(letfn [(fix-container (letfn [(fix-container [container]
[container] (reduce fix-shape container (ctn/shapes-seq container)))
(d/update-when container :objects update-vals (partial fix-shape container)))
(fix-shape (fix-shape
[container shape] [container shape]
@ -643,11 +871,30 @@
(and (ctk/in-component-copy? shape) (and (ctk/in-component-copy? shape)
(nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true})))) (nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true}))))
(detach-shape container shape) (detach-shape container shape)
shape))] container))]
(-> file-data (-> file-data
(update :pages-index update-vals fix-container) (update :pages-index update-vals fix-container)
(d/update-when :components update-vals fix-container)))) (d/update-when :components update-vals fix-container))))
fix-component-root-without-component
(fn [file-data]
;; Ensure that if component-root is set component-file and component-id are set too
(letfn [(fix-container [container]
(d/update-when container :objects update-vals fix-shape))
(fix-shape [shape]
(cond-> shape
(and (ctk/instance-root? shape)
(or (not (ctk/instance-head? shape))
(not (some? (:component-file shape)))))
(dissoc :component-id
:component-file
:component-root)))]
(-> file-data
(update :pages-index update-vals fix-container))))
fix-copies-of-detached fix-copies-of-detached
(fn [file-data] (fn [file-data]
;; Find any copy that is referencing a shape inside a component that have ;; Find any copy that is referencing a shape inside a component that have
@ -661,56 +908,42 @@
(fix-shape [shape] (fix-shape [shape]
(cond-> shape (cond-> shape
(@detached-ids (:shape-ref shape)) (@detached-ids (:shape-ref shape))
(dissoc shape (ctk/detach-shape)))]
:component-id
:component-file
:component-root)))]
(-> file-data (-> file-data
(update :pages-index update-vals fix-container) (update :pages-index update-vals fix-container)
(d/update-when :components 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 (-> file-data
(fix-file-data) (fix-file-data)
(fix-page-invalid-options) (fix-page-invalid-options)
(fix-completly-broken-shapes)
(fix-bad-children)
(fix-misc-shape-issues) (fix-misc-shape-issues)
(fix-recent-colors) (fix-recent-colors)
(fix-missing-image-metadata) (fix-missing-image-metadata)
(fix-text-shapes-converted-to-path) (fix-text-shapes-converted-to-path)
(fix-broken-paths) (fix-broken-paths)
(delete-big-geometry-shapes) (fix-big-geometry-shapes)
(fix-shape-geometry)
(fix-completly-broken-shapes)
(fix-bad-children)
(fix-broken-parents) (fix-broken-parents)
(fix-orphan-shapes) (fix-orphan-shapes)
(fix-orphan-copies) (fix-orphan-copies)
(remove-nested-roots) (remove-nested-roots)
(add-not-nested-roots) (add-not-nested-roots)
(fix-components-without-id)
(remap-refs) (remap-refs)
(fix-converted-copies) (fix-converted-copies)
(wrap-non-group-component-roots)
(detach-non-group-instance-roots)
(transform-to-frames) (transform-to-frames)
(remap-frame-ids) (remap-frame-ids)
(fix-frame-ids) (fix-frame-ids)
(fix-component-nil-objects) (fix-component-nil-objects)
(fix-false-copies) (fix-false-copies)
(fix-shape-nil-parent-id) (fix-component-root-without-component)
(fix-copies-of-detached)))) ; <- Do not add fixes after this one (fix-copies-of-detached); <- Do not add fixes after this and fix-orphan-copies call
; This extra call to fix-orphan-copies after fix-copies-of-detached because we can have detached subtrees with invalid shape-ref attributes
(fix-orphan-copies))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; COMPONENTS MIGRATION ;; COMPONENTS MIGRATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -100,8 +100,8 @@
(let [profile (profile/get-profile pool profile-id) (let [profile (profile/get-profile pool profile-id)
project-id (:default-project-id profile)] project-id (:default-project-id profile)]
(db/run! pool (fn [{:keys [::db/conn]}] (db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file conn {:id file-id (create-file cfg {:id file-id
:name (str "Cloned file: " filename) :name (str "Cloned file: " filename)
:project-id project-id :project-id project-id
:profile-id profile-id}) :profile-id profile-id})
@ -141,8 +141,8 @@
{::rres/status 200 {::rres/status 200
::rres/body "OK UPDATED"}) ::rres/body "OK UPDATED"})
(db/run! pool (fn [{:keys [::db/conn]}] (db/run! pool (fn [{:keys [::db/conn] :as cfg}]
(create-file conn {:id file-id (create-file cfg {:id file-id
:name fname :name fname
:project-id project-id :project-id project-id
:profile-id profile-id}) :profile-id profile-id})

View file

@ -131,8 +131,8 @@
(defn- invoke (defn- invoke
[limiter metrics limit-id limit-key limit-label profile-id f params] [limiter metrics limit-id limit-key limit-label profile-id f params]
(let [tpoint (dt/tpoint) (let [tpoint (dt/tpoint)
mlabels (into-array String [(id->str limit-id)])
limit-id (id->str limit-id limit-key) limit-id (id->str limit-id limit-key)
mlabels (into-array String [limit-id])
stats (pbh/get-stats limiter) stats (pbh/get-stats limiter)
id (.incrementAndGet ^AtomicLong idseq)] id (.incrementAndGet ^AtomicLong idseq)]

View file

@ -8,6 +8,7 @@
(:refer-clojure :exclude [assert]) (:refer-clojure :exclude [assert])
(:require (:require
[app.binfile.v1 :as bf.v1] [app.binfile.v1 :as bf.v1]
[app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.db :as db] [app.db :as db]
[app.http.sse :as sse] [app.http.sse :as sse]
@ -50,11 +51,16 @@
::rres/headers {"content-type" "application/octet-stream"} ::rres/headers {"content-type" "application/octet-stream"}
::rres/body (reify rres/StreamableResponseBody ::rres/body (reify rres/StreamableResponseBody
(-write-body-to-stream [_ _ output-stream] (-write-body-to-stream [_ _ output-stream]
(try
(-> cfg (-> cfg
(assoc ::bf.v1/ids #{file-id}) (assoc ::bf.v1/ids #{file-id})
(assoc ::bf.v1/embed-assets embed-assets) (assoc ::bf.v1/embed-assets embed-assets)
(assoc ::bf.v1/include-libraries include-libraries) (assoc ::bf.v1/include-libraries include-libraries)
(bf.v1/export-files! output-stream))))})) (bf.v1/export-files! output-stream))
(catch Throwable cause
(l/err :hint "exception on exporting file"
:file-id (str file-id)
:cause cause)))))}))
;; --- Command: import-binfile ;; --- Command: import-binfile

View file

@ -188,17 +188,27 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn migrate-file! (defn migrate-file!
[file-id & {:keys [rollback? validate? label] :or {rollback? true validate? false}}] [file-id & {:keys [rollback? validate? label cache skip-on-graphic-error?]
:or {rollback? true
validate? false
skip-on-graphic-error? true}}]
(l/dbg :hint "migrate:start" :rollback rollback?) (l/dbg :hint "migrate:start" :rollback rollback?)
(let [tpoint (dt/tpoint) (let [tpoint (dt/tpoint)
file-id (if (string? file-id) file-id (if (string? file-id)
(parse-uuid file-id) (parse-uuid file-id)
file-id)] file-id)
(binding [feat/*stats* (atom {})] cache (if (int? cache)
(cache/create :executor (::wrk/executor main/system)
:max-items cache)
nil)]
(binding [feat/*stats* (atom {})
feat/*cache* cache]
(try (try
(-> (assoc main/system ::db/rollback rollback?) (-> (assoc main/system ::db/rollback rollback?)
(feat/migrate-file! file-id (feat/migrate-file! file-id
:validate? validate? :validate? validate?
:skip-on-graphic-error? skip-on-graphic-error?
:label label)) :label label))
(-> (deref feat/*stats*) (-> (deref feat/*stats*)
@ -212,10 +222,10 @@
(l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed)))))))
(defn migrate-team! (defn migrate-team!
[team-id & {:keys [rollback? skip-on-graphic-error? validate? label] [team-id & {:keys [rollback? skip-on-graphic-error? validate? label cache]
:or {rollback? true :or {rollback? true
validate? true validate? true
skip-on-graphic-error? false}}] skip-on-graphic-error? true}}]
(l/dbg :hint "migrate:start" :rollback rollback?) (l/dbg :hint "migrate:start" :rollback rollback?)
@ -223,11 +233,17 @@
(parse-uuid team-id) (parse-uuid team-id)
team-id) team-id)
stats (atom {}) stats (atom {})
tpoint (dt/tpoint)] tpoint (dt/tpoint)
cache (if (int? cache)
(cache/create :executor (::wrk/executor main/system)
:max-items cache)
nil)]
(add-watch stats :progress-report (report-progress-files tpoint)) (add-watch stats :progress-report (report-progress-files tpoint))
(binding [feat/*stats* stats] (binding [feat/*stats* stats
feat/*cache* cache]
(try (try
(-> (assoc main/system ::db/rollback rollback?) (-> (assoc main/system ::db/rollback rollback?)
(feat/migrate-team! team-id (feat/migrate-team! team-id
@ -286,7 +302,7 @@
sprocs (ps/create :permits max-procs) sprocs (ps/create :permits max-procs)
cache (if (int? cache) cache (if (int? cache)
(cache/create :executor executor (cache/create :executor (::wrk/executor main/system)
:max-items cache) :max-items cache)
nil) nil)
migrate-team migrate-team
@ -382,3 +398,17 @@
(l/dbg :hint "migrate:end" (l/dbg :hint "migrate:end"
:rollback rollback? :rollback rollback?
:elapsed elapsed))))))) :elapsed elapsed)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE PROCESS HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn delete-broken-files
[{:keys [id data] :as file}]
(if (-> data :options :components-v2 true?)
(do
(l/wrn :hint "found old components-v2 format"
:file-id (str id)
:file-name (:name file))
(assoc file :deleted-at (dt/now)))
file))

View file

@ -258,8 +258,11 @@
max-jobs max-jobs
start-at start-at
on-file on-file
validate?
rollback?] rollback?]
:or {max-jobs 1 :or {max-jobs 1
max-items Long/MAX_VALUE
validate? true
rollback? true}}] rollback? true}}]
(l/dbg :hint "process:start" (l/dbg :hint "process:start"
@ -273,19 +276,19 @@
sjobs (ps/create :permits max-jobs) sjobs (ps/create :permits max-jobs)
process-file process-file
(fn [file-id tpoint] (fn [file-id idx tpoint]
(try (try
(l/trc :hint "process:file:start" :file-id (str file-id)) (l/trc :hint "process:file:start" :file-id (str file-id) :index idx)
(db/tx-run! (assoc main/system ::db/rollback rollback?) (db/tx-run! (assoc main/system ::db/rollback rollback?)
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(let [file' (get-file* system file-id) (let [file' (get-file* system file-id)
file (binding [*system* system] file (binding [*system* system]
(on-file file'))] (on-file file'))]
(when (and (some? file) (when (and (some? file) (not (identical? file file')))
(not (identical? file file')))
(cfv/validate-file-schema! file) (when validate?
(cfv/validate-file-schema! file))
(let [file (if (contains? (:features file) "fdata/objects-map") (let [file (if (contains? (:features file) "fdata/objects-map")
(feat.fdata/enable-objects-map file) (feat.fdata/enable-objects-map file)
@ -300,36 +303,43 @@
(db/update! conn :file (db/update! conn :file
{:data (blob/encode (:data file)) {:data (blob/encode (:data file))
:deleted-at (:deleted-at file)
:created-at (:created-at file)
:modified-at (:modified-at file)
:features (db/create-array conn "text" (:features file)) :features (db/create-array conn "text" (:features file))
:revn (:revn file)} :revn (:revn file)}
{:id file-id})))))) {:id file-id}))))))
(catch Throwable cause (catch Throwable cause
(l/wrn :hint "unexpected error on processing file (skiping)" (l/wrn :hint "unexpected error on processing file (skiping)"
:file-id (str file-id) :file-id (str file-id)
:index idx
:cause cause)) :cause cause))
(finally (finally
(ps/release! sjobs) (ps/release! sjobs)
(let [elapsed (dt/format-duration (tpoint))] (let [elapsed (dt/format-duration (tpoint))]
(l/trc :hint "process:file:end" (l/trc :hint "process:file:end"
:file-id (str file-id) :file-id (str file-id)
:index idx
:elapsed elapsed)))))] :elapsed elapsed)))))]
(try (try
(db/tx-run! main/system (db/tx-run! main/system
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(db/exec! conn ["SET statement_timeout = 0"]) (db/exec! conn ["SET statement_timeout = 0"])
(db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
(run! (fn [file-id] (try
(reduce (fn [idx file-id]
(ps/acquire! sjobs) (ps/acquire! sjobs)
(px/run! executor (partial process-file file-id (dt/tpoint)))) (px/run! executor (partial process-file file-id idx (dt/tpoint)))
(inc idx))
0
(->> (db/cursor conn [sql:get-file-ids (or start-at (dt/now))]) (->> (db/cursor conn [sql:get-file-ids (or start-at (dt/now))])
(take max-items) (take max-items)
(map :id))) (map :id)))
(finally
;; Close and await tasks ;; Close and await tasks
(pu/close! executor))) (pu/close! executor)))))
(catch Throwable cause (catch Throwable cause
(l/dbg :hint "process:error" :cause cause)) (l/dbg :hint "process:error" :cause cause))

View file

@ -57,6 +57,14 @@
#?(:cljs (instance? lkm/LinkedMap o) #?(:cljs (instance? lkm/LinkedMap o)
:clj (instance? LinkedMap o))) :clj (instance? LinkedMap o)))
(defn vec2
"Creates a optimized vector compatible type of length 2 backed
internally with MapEntry impl because it has faster access method
for its fields."
[o1 o2]
#?(:clj (clojure.lang.MapEntry. o1 o2)
:cljs (cljs.core/->MapEntry o1 o2 nil)))
#?(:clj #?(:clj
(defmethod print-method clojure.lang.PersistentQueue [q, w] (defmethod print-method clojure.lang.PersistentQueue [q, w]
;; Overload the printer for queues so they look like fish ;; Overload the printer for queues so they look like fish
@ -308,9 +316,12 @@
(defn mapm (defn mapm
"Map over the values of a map" "Map over the values of a map"
([mfn] ([mfn]
(map (fn [[key val]] [key (mfn key val)]))) (map (fn [[key val]] (vec2 key (mfn key val)))))
([mfn coll] ([mfn coll]
(into {} (mapm mfn) coll))) (reduce-kv (fn [coll k v]
(assoc coll k (mfn k v)))
coll
coll)))
(defn removev (defn removev
"Returns a vector of the items in coll for which (fn item) returns logical false" "Returns a vector of the items in coll for which (fn item) returns logical false"

View file

@ -0,0 +1,36 @@
;; 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.debug
(:require
[app.common.logging :as l]
[app.common.pprint :as pp]))
(defn pprint
[expr]
(l/raw! :debug
(binding [*print-level* pp/default-level
*print-length* pp/default-length]
(with-out-str
(println "tap dbg:")
(pp/pprint expr {:max-width pp/default-width})))))
(def store (atom {}))
(defn get-stored
[]
(deref store))
(defn tap-handler
[v]
(if (and (vector? v)
(keyword (first v)))
(let [[command obj] v]
(case command
(:print :prn :pprint) (pprint obj)
:store (reset! store obj)))
(pprint v)))

View file

@ -6,4 +6,4 @@
(ns app.common.files.defaults) (ns app.common.files.defaults)
(def version 44) (def version 46)

View file

@ -484,7 +484,7 @@
(letfn [(red-fn [cur-idx id] (letfn [(red-fn [cur-idx id]
(let [[prev-idx _] (first cur-idx) (let [[prev-idx _] (first cur-idx)
prev-idx (or prev-idx 0) prev-idx (or prev-idx 0)
cur-idx (conj cur-idx [(inc prev-idx) id])] cur-idx (conj cur-idx (d/vec2 (inc prev-idx) id))]
(rec-index cur-idx id))) (rec-index cur-idx id)))
(rec-index [cur-idx id] (rec-index [cur-idx id]
(let [object (get objects id)] (let [object (get objects id)]
@ -509,10 +509,11 @@
(defn order-by-indexed-shapes (defn order-by-indexed-shapes
[objects ids] [objects ids]
(let [ids (if (set? ids) ids (set ids))]
(->> (indexed-shapes objects) (->> (indexed-shapes objects)
(sort-by first) (filter (fn [o] (contains? ids (val o))))
(filter (comp (into #{} ids) second)) (sort-by key)
(map second))) (map val))))
(defn get-index-replacement (defn get-index-replacement
"Given a collection of shapes, calculate their positions "Given a collection of shapes, calculate their positions
@ -542,6 +543,11 @@
[path-vec] [path-vec]
(str/join " / " path-vec)) (str/join " / " path-vec))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path (defn clean-path
"Remove empty items from the path." "Remove empty items from the path."
[path] [path]
@ -607,6 +613,14 @@
"" ""
(join-path (butlast split))))) (join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path (defn last-path
"Returns the last item of the path." "Returns the last item of the path."
[path] [path]

View file

@ -6,6 +6,7 @@
(ns app.common.files.libraries-helpers (ns app.common.files.libraries-helpers
(:require (:require
[app.common.data :as d]
[app.common.files.changes-builder :as pcb] [app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
@ -37,41 +38,50 @@
use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a
component with it, and link all shapes to their corresponding one in the component." component with it, and link all shapes to their corresponding one in the component."
[it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] [it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board]
(let [changes (pcb/empty-changes it page-id) (let [changes (pcb/empty-changes it page-id)
shapes-count (count shapes)
first-shape (first shapes)
from-singe-frame?
(and (= 1 shapes-count)
(cfh/frame-shape? first-shape))
from-singe-frame? (and (= 1 (count shapes)) (-> shapes first cfh/frame-shape?))
[root changes old-root-ids] [root changes old-root-ids]
(if (and (= (count shapes) 1) (if (and (= shapes-count 1)
(or (and (= (:type (first shapes)) :group) (not components-v2)) (or (and (cfh/group-shape? first-shape)
(= (:type (first shapes)) :frame)) (not components-v2))
(not (ctk/instance-head? (first shapes)))) (cfh/frame-shape? first-shape))
(not (ctk/instance-head? first-shape)))
[(first shapes) [first-shape
(-> (pcb/empty-changes it page-id) (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)) (pcb/with-objects objects))
(:shapes (first shapes))] (:shapes first-shape)]
(let [root-name (if (= 1 (count shapes)) (let [root-name (if (= 1 shapes-count)
(:name (first shapes)) (:name first-shape)
"Component 1") "Component 1")
[root changes] (if-not components-v2 shape-ids (into (d/ordered-set) (map :id) shapes)
[root changes]
(if-not components-v2
(prepare-create-group it ; These functions needs to be passed as argument (prepare-create-group it ; These functions needs to be passed as argument
objects ; to avoid a circular dependence objects ; to avoid a circular dependence
page-id page-id
shapes shapes
root-name root-name
(not (ctk/instance-head? (first shapes)))) (not (ctk/instance-head? first-shape)))
(prepare-create-board changes (prepare-create-board changes
(uuid/next) (uuid/next)
(:parent-id (first shapes)) (:parent-id first-shape)
objects objects
(map :id shapes) shape-ids
nil nil
root-name root-name
true))] true))]
[root changes (map :id shapes)])) [root changes shape-ids]))
changes changes
(cond-> changes (cond-> changes
@ -79,8 +89,7 @@
(pcb/update-shapes (pcb/update-shapes
(:shapes root) (:shapes root)
(fn [shape] (fn [shape]
(-> shape (assoc shape :constraints-h :scale :constraints-v :scale))))
(assoc :constraints-h :scale :constraints-v :scale)))))
objects' (assoc objects (:id root) root) objects' (assoc objects (:id root) root)

View file

@ -109,11 +109,14 @@
(assoc :points (grc/rect->points selrect)))))) (assoc :points (grc/rect->points selrect))))))
(fix-empty-points [shape] (fix-empty-points [shape]
(let [shape (cond-> shape (if (empty? (:points shape))
(empty? (:selrect shape)) (cts/setup-rect))] (-> shape
(cond-> shape (update :selrect (fn [selrect]
(empty? (:points shape)) (if (map? selrect)
(assoc :points (grc/rect->points (:selrect shape)))))) (grc/make-rect selrect)
selrect)))
(cts/setup-shape))
shape))
(update-object [object] (update-object [object]
(cond-> object (cond-> object
@ -620,6 +623,10 @@
(-> object (-> object
(assoc :parent-id uuid/zero) (assoc :parent-id uuid/zero)
(assoc :frame-id uuid/zero) (assoc :frame-id uuid/zero)
;; We explicitly dissoc them and let the shape-setup
;; to regenerate it with valid values.
(dissoc :selrect)
(dissoc :points)
(cts/setup-shape)) (cts/setup-shape))
object)) object))
@ -843,3 +850,29 @@
(-> data (-> data
(update :pages-index update-vals update-container) (update :pages-index update-vals update-container)
(update :components update-vals update-container)))) (update :components update-vals update-container))))
(defmethod migrate 45
[data]
(letfn [(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)))
(update-container [container]
(d/update-when container :objects update-vals fix-shape))]
(-> data
(update :pages-index update-vals update-container))))
(defmethod migrate 46
[data]
(letfn [(update-object [object]
(dissoc object :thumbnail))
(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))))

View file

@ -39,16 +39,17 @@
(defn prepare-move-shapes-into-frame (defn prepare-move-shapes-into-frame
[changes frame-id shapes objects] [changes frame-id shapes objects]
(let [ordered-indexes (cfh/order-by-indexed-shapes objects shapes) (let [parent-id (dm/get-in objects [frame-id :parent-id])
parent-id (get-in objects [frame-id :parent-id]) shapes (remove #(= % parent-id) shapes)
ordered-indexes (->> ordered-indexes (remove #(= % parent-id))) to-move (->> shapes
to-move-shapes (map (d/getf objects) ordered-indexes)] (map (d/getf objects))
(if (d/not-empty? to-move-shapes) (not-empty))]
(if to-move
(-> changes (-> changes
(cond-> (not (ctl/any-layout? objects frame-id)) (cond-> (not (ctl/any-layout? objects frame-id))
(pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) (pcb/update-shapes shapes ctl/remove-layout-item-data))
(pcb/update-shapes ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) (pcb/update-shapes shapes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))
(pcb/change-parent frame-id to-move-shapes 0) (pcb/change-parent frame-id to-move 0)
(cond-> (ctl/grid-layout? objects frame-id) (cond-> (ctl/grid-layout? objects frame-id)
(-> (pcb/update-shapes [frame-id] ctl/assign-cells {:with-objects? true}) (-> (pcb/update-shapes [frame-id] ctl/assign-cells {:with-objects? true})
(pcb/reorder-grid-children [frame-id])))) (pcb/reorder-grid-children [frame-id]))))
@ -60,22 +61,31 @@
changes id parent-id objects selected index frame-name without-fill? nil)) changes id parent-id objects selected index frame-name without-fill? nil))
([changes id parent-id objects selected index frame-name without-fill? target-cell-id] ([changes id parent-id objects selected index frame-name without-fill? target-cell-id]
(let [selected-objs (map #(get objects %) selected) (when-let [selected-objs (->> selected
new-index (or index (map (d/getf objects))
(cfh/get-index-replacement selected objects))] (not-empty))]
(when (d/not-empty? selected)
(let [srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
frame-id (dm/get-in objects [selected-id :frame-id]) (let [;; We calculate here the ordered selection because it is used
parent-id (or parent-id (dm/get-in objects [selected-id :parent-id])) ;; multiple times and this avoid the need of creating the index
;; manytimes for single operation.
selected' (cfh/order-by-indexed-shapes objects selected)
new-index (or index
(->> (first selected')
(cfh/get-position-on-parent objects)
(inc)))
srect (gsh/shapes->rect selected-objs)
selected-id (first selected)
selected-obj (get objects selected-id)
frame-id (get selected-obj :frame-id)
parent-id (or parent-id (get selected-obj :parent-id))
base-parent (get objects parent-id) base-parent (get objects parent-id)
layout-props layout-props
(when (and (= 1 (count selected)) (when (and (= 1 (count selected))
(ctl/any-layout? base-parent)) (ctl/any-layout? base-parent))
(let [shape (get objects selected-id)] (select-keys selected-obj ctl/layout-item-props))
(select-keys shape ctl/layout-item-props)))
target-cell-id target-cell-id
(if (and (nil? target-cell-id) (if (and (nil? target-cell-id)
@ -88,13 +98,15 @@
:id)) :id))
target-cell-id) target-cell-id)
attrs {:type :frame attrs
{:type :frame
:x (:x srect) :x (:x srect)
:y (:y srect) :y (:y srect)
:width (:width srect) :width (:width srect)
:height (:height srect)} :height (:height srect)}
shape (cts/setup-shape shape
(cts/setup-shape
(cond-> attrs (cond-> attrs
(some? id) (some? id)
(assoc :id id) (assoc :id id)
@ -113,13 +125,14 @@
(or (not= frame-id uuid/zero) without-fill?) (or (not= frame-id uuid/zero) without-fill?)
(assoc :fills [] :hide-in-viewer true))) (assoc :fills [] :hide-in-viewer true)))
shape (with-meta shape {:index new-index}) shape
(with-meta shape {:index new-index})
[shape changes] [shape changes]
(prepare-add-shape changes shape objects) (prepare-add-shape changes shape objects)
changes changes
(prepare-move-shapes-into-frame changes (:id shape) selected objects) (prepare-move-shapes-into-frame changes (:id shape) selected' objects)
changes changes
(cond-> changes (cond-> changes
@ -143,7 +156,7 @@
(pcb/reorder-grid-children [(:parent-id shape)])))] (pcb/reorder-grid-children [(:parent-id shape)])))]
[shape changes]))))) [shape changes]))))
(defn prepare-create-empty-artboard (defn prepare-create-empty-artboard

View file

@ -98,7 +98,8 @@
(defn- check-geometry (defn- check-geometry
"Validate that the shape has valid coordinates, selrect and points." "Validate that the shape has valid coordinates, selrect and points."
[shape file page] [shape file page]
(when (and (not (#{:path :bool} (:type shape))) (when (and (not (or (cfh/path-shape? shape)
(cfh/bool-shape? shape)))
(or (nil? (:x shape)) ; This may occur in root shape (uuid/zero) in old files (or (nil? (:x shape)) ; This may occur in root shape (uuid/zero) in old files
(nil? (:y shape)) (nil? (:y shape))
(nil? (:width shape)) (nil? (:width shape))
@ -112,61 +113,64 @@
(defn- check-parent-children (defn- check-parent-children
"Validate parent and children exists, and the link is bidirectional." "Validate parent and children exists, and the link is bidirectional."
[shape file page] [shape file page]
(let [parent (ctst/get-shape page (:parent-id shape))] (let [parent (ctst/get-shape page (:parent-id shape))
shape-id (:id shape)
shapes (:shapes shape)]
(if (nil? parent) (if (nil? parent)
(report-error :parent-not-found (report-error :parent-not-found
(str/ffmt "Parent % not found" (:parent-id shape)) (str/ffmt "Parent % not found" (:parent-id shape))
shape file page) shape file page)
(do (do
(when-not (cfh/root? shape) (when-not (cfh/root? shape)
(when-not (some #{(:id shape)} (:shapes parent)) (when-not (some #(= shape-id %) (:shapes parent))
(report-error :child-not-in-parent (report-error :child-not-in-parent
(str/ffmt "Shape % not in parent's children list" (:id shape)) (str/ffmt "Shape % not in parent's children list" shape-id)
shape file page))) shape file page)))
(when-not (= (count (:shapes shape)) (count (distinct (:shapes shape)))) (when-not (= (count shapes) (count (distinct shapes)))
(report-error :duplicated-children (report-error :duplicated-children
(str/ffmt "Shape % has duplicated children" (:id shape)) (str/ffmt "Shape % has duplicated children" shape-id)
shape file page)) shape file page))
(doseq [child-id (:shapes shape)] (doseq [child-id shapes]
(let [child (ctst/get-shape page child-id)] (let [child (ctst/get-shape page child-id)]
(if (nil? child) (if (nil? child)
(report-error :child-not-found (report-error :child-not-found
(str/ffmt "Child % not found in parent %" child-id (:id shape)) (str/ffmt "Child % not found in parent %" child-id shape-id)
shape file page shape file page
:parent-id (:id shape) :parent-id shape-id
:child-id child-id) :child-id child-id)
(when (not= (:parent-id child) (:id shape)) (when (not= (:parent-id child) shape-id)
(report-error :invalid-parent (report-error :invalid-parent
(str/ffmt "Child % has invalid parent %" child-id (:id shape)) (str/ffmt "Child % has invalid parent %" child-id shape-id)
child file page child file page
:parent-id (:id shape)))))))))) :parent-id shape-id)))))))))
(defn- check-frame (defn- check-frame
"Validate that the frame-id shape exists and is indeed a frame. Also "Validate that the frame-id shape exists and is indeed a frame. Also
it must point to the parent shape (if this is a frame) or to the it must point to the parent shape (if this is a frame) or to the
frame-id of the parent (if not)." frame-id of the parent (if not)."
[shape file page] [{:keys [frame-id] :as shape} file page]
(let [frame (ctst/get-shape page (:frame-id shape))] (let [frame (ctst/get-shape page frame-id)]
(if (nil? frame) (if (nil? frame)
(report-error :frame-not-found (report-error :frame-not-found
(str/ffmt "Frame % not found" (:frame-id shape)) (str/ffmt "Frame % not found" frame-id)
shape file page) shape file page)
(if (not= (:type frame) :frame) (if (not= (:type frame) :frame)
(report-error :invalid-frame (report-error :invalid-frame
(str/ffmt "Frame % is not actually a frame" (:frame-id shape)) (str/ffmt "Frame % is not actually a frame" frame-id)
shape file page) shape file page)
(let [parent (ctst/get-shape page (:parent-id shape))] (let [parent (ctst/get-shape page (:parent-id shape))]
(when (some? parent) (when (some? parent)
(if (= (:type parent) :frame) (if (= (:type parent) :frame)
(when-not (= (:frame-id shape) (:id parent)) (when-not (= frame-id (:id parent))
(report-error :invalid-frame (report-error :invalid-frame
(str/ffmt "Frame-id should point to parent %" (:id parent)) (str/ffmt "Frame-id should point to parent %" (:id parent))
shape file page)) shape file page))
(when-not (= (:frame-id shape) (:frame-id parent)) (when-not (= frame-id (:frame-id parent))
(report-error :invalid-frame (report-error :invalid-frame
(str/ffmt "Frame-id should point to parent frame %" (:frame-id parent)) (str/ffmt "Frame-id should point to parent frame %" frame-id)
shape file page))))))))) shape file page)))))))))
(defn- check-component-main-head (defn- check-component-main-head
@ -289,8 +293,7 @@
(check-component-main-head shape file page libraries) (check-component-main-head shape file page libraries)
(check-component-root shape file page) (check-component-root shape file page)
(check-component-not-ref shape file page) (check-component-not-ref shape file page)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :main-top) (:shapes shape)))
(check-shape child-id file page libraries :context :main-top)))
(defn- check-shape-main-root-nested (defn- check-shape-main-root-nested
"Root shape of a nested main instance "Root shape of a nested main instance
@ -301,8 +304,7 @@
(check-component-main-head shape file page libraries) (check-component-main-head shape file page libraries)
(check-component-not-root shape file page) (check-component-not-root shape file page)
(check-component-not-ref shape file page) (check-component-not-ref shape file page)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :main-nested) (:shapes shape)))
(check-shape child-id file page libraries :context :main-nested)))
(defn- check-shape-copy-root-top (defn- check-shape-copy-root-top
"Root shape of a top copy instance "Root shape of a top copy instance
@ -314,8 +316,7 @@
(check-component-not-main-head shape file page libraries) (check-component-not-main-head shape file page libraries)
(check-component-root shape file page) (check-component-root shape file page)
(check-component-ref shape file page libraries) (check-component-ref shape file page libraries)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :copy-top) (:shapes shape)))
(check-shape child-id file page libraries :context :copy-top)))
(defn- check-shape-copy-root-nested (defn- check-shape-copy-root-nested
"Root shape of a nested copy instance "Root shape of a nested copy instance
@ -326,8 +327,7 @@
(check-component-not-main-head shape file page libraries) (check-component-not-main-head shape file page libraries)
(check-component-not-root shape file page) (check-component-not-root shape file page)
(check-component-ref shape file page libraries) (check-component-ref shape file page libraries)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape)))
(check-shape child-id file page libraries :context :copy-nested)))
(defn- check-shape-main-not-root (defn- check-shape-main-not-root
"Not-root shape of a main instance (not any attribute)" "Not-root shape of a main instance (not any attribute)"
@ -335,8 +335,7 @@
(check-component-not-main-not-head shape file page) (check-component-not-main-not-head shape file page)
(check-component-not-root shape file page) (check-component-not-root shape file page)
(check-component-not-ref shape file page) (check-component-not-ref shape file page)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :main-any) (:shapes shape)))
(check-shape child-id file page libraries :context :main-any)))
(defn- check-shape-copy-not-root (defn- check-shape-copy-not-root
"Not-root shape of a copy instance :shape-ref" "Not-root shape of a copy instance :shape-ref"
@ -344,8 +343,7 @@
(check-component-not-main-not-head shape file page) (check-component-not-main-not-head shape file page)
(check-component-not-root shape file page) (check-component-not-root shape file page)
(check-component-ref shape file page libraries) (check-component-ref shape file page libraries)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
(check-shape child-id file page libraries :context :copy-any)))
(defn- check-shape-not-component (defn- check-shape-not-component
"Shape is not in a component or is a fostered children (not any "Shape is not in a component or is a fostered children (not any
@ -354,8 +352,7 @@
(check-component-not-main-not-head shape file page) (check-component-not-main-not-head shape file page)
(check-component-not-root shape file page) (check-component-not-root shape file page)
(check-component-not-ref shape file page) (check-component-not-ref shape file page)
(doseq [child-id (:shapes shape)] (run! #(check-shape % file page libraries :context :not-component) (:shapes shape)))
(check-shape child-id file page libraries :context :not-component)))
(defn- check-shape (defn- check-shape
"Validate referential integrity and semantic coherence of "Validate referential integrity and semantic coherence of
@ -439,6 +436,11 @@
"Objects list cannot be nil" "Objects list cannot be nil"
component file nil))) component file nil)))
(defn- get-orphan-shapes
[{:keys [objects] :as page}]
(let [xf (comp (map #(contains? objects (:parent-id %)))
(map :id))]
(into [] xf (vals objects))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API: VALIDATION FUNCTIONS ;; PUBLIC API: VALIDATION FUNCTIONS
@ -451,18 +453,14 @@
[{:keys [data features] :as file} libraries] [{:keys [data features] :as file} libraries]
(when (contains? features "components/v2") (when (contains? features "components/v2")
(binding [*errors* (volatile! [])] (binding [*errors* (volatile! [])]
(doseq [page (filter :id (ctpl/pages-seq data))]
(let [orphans (->> page
:objects
vals
(filter #(not (contains? (:objects page) (:parent-id %))))
(map :id))]
(check-shape uuid/zero file page libraries)
(doseq [shape-id orphans]
(check-shape shape-id file page libraries))))
(doseq [component (vals (:components data))] (doseq [page (filter :id (ctpl/pages-seq data))]
(check-component component file)) (check-shape uuid/zero file page libraries)
(->> (get-orphan-shapes page)
(run! #(check-shape % file page libraries))))
(->> (vals (:components data))
(run! #(check-component % file)))
(-> *errors* deref not-empty)))) (-> *errors* deref not-empty))))

View file

@ -67,7 +67,8 @@
([a b c d e f] ([a b c d e f]
(pos->Matrix a b c d e f))) (pos->Matrix a b c d e f)))
(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?") (def number-regex
#"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?")
(defn str->matrix (defn str->matrix
[matrix-str] [matrix-str]
@ -76,8 +77,8 @@
(map (comp d/parse-double first)))] (map (comp d/parse-double first)))]
(apply matrix params))) (apply matrix params)))
(sm/def! ::matrix-map (def ^:private schema:matrix-attrs
[:map {:title "MatrixMap"} [:map {:title "MatrixAttrs"}
[:a ::sm/safe-double] [:a ::sm/safe-double]
[:b ::sm/safe-double] [:b ::sm/safe-double]
[:c ::sm/safe-double] [:c ::sm/safe-double]
@ -85,6 +86,10 @@
[:e ::sm/safe-double] [:e ::sm/safe-double]
[:f ::sm/safe-double]]) [:f ::sm/safe-double]])
(def valid-matrix?
(sm/lazy-validator
[:and [:fn matrix?] schema:matrix-attrs]))
(sm/def! ::matrix (sm/def! ::matrix
(letfn [(decode [o] (letfn [(decode [o]
(if (map? o) (if (map? o)
@ -101,7 +106,7 @@
(dm/get-prop o :f) ","))] (dm/get-prop o :f) ","))]
{:type ::matrix {:type ::matrix
:pred matrix? :pred valid-matrix?
:type-properties :type-properties
{:title "matrix" {:title "matrix"
:description "Matrix instance" :description "Matrix instance"

View file

@ -41,12 +41,6 @@
[v] [v]
(instance? Point v)) (instance? Point v))
(sm/def! ::point-map
[:map {:title "PointMap"}
[:x ::sm/safe-number]
[:y ::sm/safe-number]])
;; FIXME: deprecated ;; FIXME: deprecated
(s/def ::x ::us/safe-number) (s/def ::x ::us/safe-number)
(s/def ::y ::us/safe-number) (s/def ::y ::us/safe-number)
@ -57,6 +51,16 @@
(s/def ::point (s/def ::point
(s/and ::point-attrs point?)) (s/and ::point-attrs point?))
(def ^:private schema:point-attrs
[:map {:title "PointAttrs"}
[:x ::sm/safe-number]
[:y ::sm/safe-number]])
(def valid-point?
(sm/lazy-validator
[:and [:fn point?] schema:point-attrs]))
(sm/def! ::point (sm/def! ::point
(letfn [(decode [p] (letfn [(decode [p]
(if (map? p) (if (map? p)
@ -71,7 +75,7 @@
(dm/get-prop p :y)))] (dm/get-prop p :y)))]
{:type ::point {:type ::point
:pred point? :pred valid-point?
:type-properties :type-properties
{:title "point" {:title "point"
:description "Point" :description "Point"

View file

@ -12,6 +12,8 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.record :as rc] [app.common.record :as rc]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.transit :as t])) [app.common.transit :as t]))
(rc/defrecord Rect [x y width height x1 y1 x2 y2]) (rc/defrecord Rect [x y width height x1 y1 x2 y2])
@ -66,6 +68,31 @@
h (mth/max height 0.01)] h (mth/max height 0.01)]
(pos->Rect x y w h x y (+ x w) (+ y h)))))) (pos->Rect x y w h x y (+ x w) (+ y h))))))
(def ^:private schema:rect-attrs
[:map {:title "RectAttrs"}
[:x ::sm/safe-number]
[:y ::sm/safe-number]
[:width ::sm/safe-number]
[:height ::sm/safe-number]
[:x1 ::sm/safe-number]
[:y1 ::sm/safe-number]
[:x2 ::sm/safe-number]
[:y2 ::sm/safe-number]])
(sm/define! ::rect
[:and
{:gen/gen (->> (sg/tuple (sg/small-double)
(sg/small-double)
(sg/small-double)
(sg/small-double))
(sg/fmap #(apply make-rect %)))}
[:fn rect?]
schema:rect-attrs])
(def valid-rect?
(sm/lazy-validator
[:and [:fn rect?] schema:rect-attrs]))
(def empty-rect (def empty-rect
(make-rect 0 0 0.01 0.01)) (make-rect 0 0 0.01 0.01))

View file

@ -92,5 +92,11 @@
(defn resolve-subtree (defn resolve-subtree
"Resolves the subtree but only partialy from-to the parameters" "Resolves the subtree but only partialy from-to the parameters"
[from-id to-id objects] [from-id to-id objects]
(concat
(->> (get-children-seq from-id objects) (->> (get-children-seq from-id objects)
(d/take-until #(= (:id %) to-id)))) (d/take-until #(= (:id %) to-id)))
;; Add the children of `to-id` to the subtree. Rest is to remove the
;; to-id element that is already on the previous sequence
(->> (get-children-seq to-id objects)
rest)))

View file

@ -9,9 +9,26 @@
(:require (:require
[me.flowthing.pp :as pp])) [me.flowthing.pp :as pp]))
(def default-level 8)
(def default-length 25)
(def default-width 120)
#?(:clj
(defn set-defaults
[& {:keys [level width length]}]
(when length
(alter-var-root #'default-length (constantly length)))
(when width
(alter-var-root #'default-width (constantly width)))
(when level
(alter-var-root #'default-level (constantly level)))
nil))
(defn pprint (defn pprint
[expr & {:keys [width level length] [expr & {:keys [width level length]
:or {width 120 level 8 length 25}}] :or {width default-width
level default-level
length default-length}}]
(binding [*print-level* level (binding [*print-level* level
*print-length* length] *print-length* length]
(pp/pprint expr {:max-width width}))) (pp/pprint expr {:max-width width})))

View file

@ -895,7 +895,8 @@
(defn map-nodes [mapfn node] (defn map-nodes [mapfn node]
(let [update-content (let [update-content
(fn [content] (cond->> content (fn [content]
(cond->> content
(vector? content) (vector? content)
(mapv (partial map-nodes mapfn))))] (mapv (partial map-nodes mapfn))))]
@ -922,7 +923,8 @@
value))) value)))
(defn fix-default-values (defn fix-default-values
"Gives values to some SVG elements which defaults won't work when imported into the platform" "Gives values to some SVG elements which defaults won't work when
imported into the platform"
[svg-data] [svg-data]
(let [add-defaults (let [add-defaults
(fn [{:keys [tag attrs] :as node}] (fn [{:keys [tag attrs] :as node}]
@ -984,29 +986,38 @@
(fix-percent-attrs-viewbox [attrs] (fix-percent-attrs-viewbox [attrs]
(d/mapm fix-percent-attr-viewbox attrs)) (d/mapm fix-percent-attr-viewbox attrs))
(fix-percent-attr-numeric [_ attr-val] (fix-percent-attr-numeric-val [val]
(let [is-percent? (str/ends-with? attr-val "%")] (let [val (d/parse-double (str/rtrim val "%"))]
(if is-percent? (str (/ val 100))))
(str (let [attr-num (d/parse-double (str/rtrim attr-val "%"))]
(/ attr-num 100)))
attr-val)))
(fix-percent-attrs-numeric [attrs] (fix-percent-attr-numeric [attrs key val]
(d/mapm fix-percent-attr-numeric attrs)) (cond
(= key :style)
attrs
(str/starts-with? (d/name key) "data-")
attrs
(str/ends-with? val "%")
(assoc attrs key (fix-percent-attr-numeric-val val))
:else
attrs))
(fix-percent-values [node] (fix-percent-values [node]
(let [units (or (get-in node [:attrs :filterUnits]) (let [units (or (get-in node [:attrs :filterUnits])
(get-in node [:attrs :gradientUnits]) (get-in node [:attrs :gradientUnits])
(get-in node [:attrs :patternUnits]) (get-in node [:attrs :patternUnits])
(get-in node [:attrs :clipUnits]))] (get-in node [:attrs :clipUnits]))]
(cond-> node (cond-> node
(or (= "objectBoundingBox" units) (nil? units)) (or (= "objectBoundingBox" units) (nil? units))
(update :attrs fix-percent-attrs-numeric) (update :attrs #(reduce-kv fix-percent-attr-numeric % %))
(not= "objectBoundingBox" units) (not= "objectBoundingBox" units)
(update :attrs fix-percent-attrs-viewbox))))] (update :attrs fix-percent-attrs-viewbox))))]
(->> svg-data (map-nodes fix-percent-values))))) (map-nodes fix-percent-values svg-data))))
(defn collect-images [svg-data] (defn collect-images [svg-data]
(let [redfn (fn [acc {:keys [tag attrs]}] (let [redfn (fn [acc {:keys [tag attrs]}]

View file

@ -193,7 +193,8 @@
(defn create-group (defn create-group
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}] [name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
(let [transform (csvg/parse-transform (:transform attrs)) (let [transform (csvg/parse-transform (:transform attrs))
attrs (-> (d/without-keys attrs csvg/inheritable-props) attrs (-> attrs
(d/without-keys csvg/inheritable-props)
(csvg/attrs->props)) (csvg/attrs->props))
vbox (grc/make-rect offset-x offset-y width height)] vbox (grc/make-rect offset-x offset-y width height)]
(cts/setup-shape (cts/setup-shape
@ -304,6 +305,8 @@
rx (d/nilv r rx) rx (d/nilv r rx)
ry (d/nilv r ry) ry (d/nilv r ry)
rx (d/nilv rx 0)
ry (d/nilv ry 0)
;; There are some svg circles in the internet that does not ;; There are some svg circles in the internet that does not
;; have cx and cy attrs, so we default them to 0 ;; have cx and cy attrs, so we default them to 0

View file

@ -34,7 +34,7 @@
(defn pages-seq (defn pages-seq
[fdata] [fdata]
(vals (:pages-index fdata))) (-> fdata :pages-index vals seq))
(defn update-page (defn update-page
[file-data page-id f] [file-data page-id f]

View file

@ -79,25 +79,6 @@
(def text-align-types (def text-align-types
#{"left" "right" "center" "justify"}) #{"left" "right" "center" "justify"})
(sm/define! ::selrect
[:and
{:title "Selrect"
:gen/gen (->> (sg/tuple (sg/small-double)
(sg/small-double)
(sg/small-double)
(sg/small-double))
(sg/fmap #(apply grc/make-rect %)))}
[:fn grc/rect?]
[:map
[:x ::sm/safe-number]
[:y ::sm/safe-number]
[:x1 ::sm/safe-number]
[:x2 ::sm/safe-number]
[:y1 ::sm/safe-number]
[:y2 ::sm/safe-number]
[:width ::sm/safe-number]
[:height ::sm/safe-number]]])
(sm/define! ::points (sm/define! ::points
[:vector {:gen/max 4 :gen/min 4} ::gpt/point]) [:vector {:gen/max 4 :gen/min 4} ::gpt/point])
@ -133,7 +114,7 @@
[:id ::sm/uuid] [:id ::sm/uuid]
[:name :string] [:name :string]
[:type [::sm/one-of shape-types]] [:type [::sm/one-of shape-types]]
[:selrect ::selrect] [:selrect ::grc/rect]
[:points ::points] [:points ::points]
[:transform ::gmt/matrix] [:transform ::gmt/matrix]
[:transform-inverse ::gmt/matrix] [:transform-inverse ::gmt/matrix]
@ -156,7 +137,7 @@
[:main-instance {:optional true} :boolean] [:main-instance {:optional true} :boolean]
[:remote-synced {:optional true} :boolean] [:remote-synced {:optional true} :boolean]
[:shape-ref {:optional true} ::sm/uuid] [:shape-ref {:optional true} ::sm/uuid]
[:selrect {:optional true} ::selrect] [:selrect {:optional true} ::grc/rect]
[:points {:optional true} ::points] [:points {:optional true} ::points]
[:blocked {:optional true} :boolean] [:blocked {:optional true} :boolean]
[:collapsed {:optional true} :boolean] [:collapsed {:optional true} :boolean]
@ -430,7 +411,7 @@
:name "Path" :name "Path"
:fills [] :fills []
:strokes [{:stroke-style :solid :strokes [{:stroke-style :solid
:stroke-alignment :center :stroke-alignment :inner
:stroke-width 2 :stroke-width 2
:stroke-color clr/black :stroke-color clr/black
:stroke-opacity 1}]}) :stroke-opacity 1}]})

View file

@ -563,7 +563,7 @@
padding: $s-12; padding: $s-12;
border-radius: $br-8; border-radius: $br-8;
z-index: $z-index-10; z-index: $z-index-10;
color: var(--color-foreground-primary); color: var(--modal-title-foreground-color);
background-color: var(--modal-background-color); background-color: var(--modal-background-color);
} }
@ -575,7 +575,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
z-index: $z-index-modal; z-index: $z-index-modal;
background-color: var(--color-background-subtle); background-color: var(--overlay-color);
} }
.modal-container-base { .modal-container-base {

View file

@ -222,6 +222,7 @@
--assets-item-background-color: var(--color-background-tertiary); --assets-item-background-color: var(--color-background-tertiary);
--assets-item-background-color-hover: var(--color-background-quaternary); --assets-item-background-color-hover: var(--color-background-quaternary);
--assets-item-name-background-color: var(--db-secondary-80); // TODO: penpot file has a non-existing token --assets-item-name-background-color: var(--db-secondary-80); // TODO: penpot file has a non-existing token
--assets-item-name-foreground-color-rest: var(--color-foreground-secondary);
--assets-item-name-foreground-color: var(--color-foreground-primary); --assets-item-name-foreground-color: var(--color-foreground-primary);
--assets-item-name-foreground-color-hover: var(--color-foreground-primary); --assets-item-name-foreground-color-hover: var(--color-foreground-primary);
--assets-item-name-foreground-color-disabled: var(--color-foreground-disabled); --assets-item-name-foreground-color-disabled: var(--color-foreground-disabled);

View file

@ -248,7 +248,7 @@
(assoc :stroke-style :solid) (assoc :stroke-style :solid)
(not (contains? new-attrs :stroke-alignment)) (not (contains? new-attrs :stroke-alignment))
(assoc :stroke-alignment :center) (assoc :stroke-alignment :inner)
:always :always
(d/without-nils))] (d/without-nils))]

View file

@ -39,18 +39,7 @@
(def ^:private svgo-config (def ^:private svgo-config
{:multipass false {:multipass false
:plugins :plugins ["safeAndFastPreset"]})
[{:name "safePreset"
:params {:overrides
{:convertColors
{:names2hex true
:shorthex false
:shortname false}
:convertTransform
{:matrixToTransform false
:convertToShorts false
:transformPrecision 4
:leadingZero false}}}}]})
(defn svg->clj (defn svg->clj
[[name text]] [[name text]]

View file

@ -393,6 +393,9 @@
unames (volatile! (cfh/get-used-names (:objects page))) unames (volatile! (cfh/get-used-names (:objects page)))
update-unames! (fn [new-name] (vswap! unames conj new-name)) update-unames! (fn [new-name] (vswap! unames conj new-name))
all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids)
;; We need ids-map for remapping the grid layout. But when duplicating the guides
;; we calculate a new one because the components will have created new shapes.
ids-map (into {} (map #(vector % (uuid/next))) all-ids) ids-map (into {} (map #(vector % (uuid/next))) all-ids)
changes changes
@ -409,7 +412,15 @@
library-data library-data
it it
file-id) file-id)
init-changes))] init-changes))
;; We need to check the changes to get the ids-map
ids-map
(into {}
(comp
(filter #(= :add-obj (:type %)))
(map #(vector (:old-id %) (-> % :obj :id))))
(:redo-changes changes))]
(-> changes (-> changes
(prepare-duplicate-flows shapes page ids-map) (prepare-duplicate-flows shapes page ids-map)
@ -578,14 +589,16 @@
(defn- prepare-duplicate-guides (defn- prepare-duplicate-guides
[changes shapes page ids-map delta] [changes shapes page ids-map delta]
(let [guides (get-in page [:options :guides]) (let [guides (get-in page [:options :guides])
frames (->> shapes frames (->> shapes (filter cfh/frame-shape?))
(filter #(= (:type %) :frame)))
new-guides (reduce new-guides
(reduce
(fn [g frame] (fn [g frame]
(let [new-id (ids-map (:id frame)) (let [new-id (ids-map (:id frame))
new-frame (-> frame new-frame (-> frame (gsh/move delta))
(gsh/move delta))
new-guides (->> guides new-guides
(->> guides
(vals) (vals)
(filter #(= (:frame-id %) (:id frame))) (filter #(= (:frame-id %) (:id frame)))
(map #(-> % (map #(-> %

View file

@ -72,13 +72,15 @@
(watch [it state _] (watch [it state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id) objects (wsh/lookup-page-objects state page-id)
shapes (->> shapes (remove #(dm/get-in objects [% :blocked]))) shapes (->> shapes
(remove #(dm/get-in objects [% :blocked]))
(cfh/order-by-indexed-shapes objects))
changes (-> (pcb/empty-changes it page-id) changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)) (pcb/with-objects objects))
changes (cfsh/prepare-move-shapes-into-frame changes
frame-id changes (cfsh/prepare-move-shapes-into-frame changes frame-id shapes objects)]
shapes
objects)]
(if (some? changes) (if (some? changes)
(rx/of (dch/commit-changes changes)) (rx/of (dch/commit-changes changes))
(rx/empty)))))) (rx/empty))))))
@ -176,12 +178,16 @@
interactions))) interactions)))
(vals objects)) (vals objects))
;; If any of the deleted shapes is a frame with guides ids-set (set ids)
guides (into {} guides-to-remove
(comp (map second) (->> (dm/get-in page [:options :guides])
(remove #(contains? ids (:frame-id %))) (vals)
(map (juxt :id identity))) (filter #(contains? ids-set (:frame-id %)))
(dm/get-in page [:options :guides])) (map :id))
guides
(->> guides-to-remove
(reduce dissoc (dm/get-in page [:options :guides])))
starting-flows starting-flows
(filter (fn [flow] (filter (fn [flow]

View file

@ -58,6 +58,10 @@
;; We need to store the handle-blur ref so we can call it on unmount ;; We need to store the handle-blur ref so we can call it on unmount
dirty-ref (mf/use-ref false) dirty-ref (mf/use-ref false)
;; Last value input by the user we need to store to save on unmount
last-value* (mf/use-var nil)
parse-value parse-value
(mf/use-fn (mf/use-fn
(mf/deps min-value max-value value nillable? default) (mf/deps min-value max-value value nillable? default)
@ -102,7 +106,20 @@
(mf/use-fn (mf/use-fn
(mf/deps wrap-value? min-value max-value parse-value apply-value) (mf/deps wrap-value? min-value max-value parse-value apply-value)
(fn [event up? down?] (fn [event up? down?]
(let [current-value (parse-value)] (let [current-value (parse-value)
current-value
(cond
(and (not current-value) down? max-value)
max-value
(and (not current-value) up? min-value)
min-value
(not current-value)
(d/nilv default 0)
:else
current-value)]
(when current-value (when current-value
(let [increment (cond (let [increment (cond
(kbd/shift? event) (kbd/shift? event)
@ -152,6 +169,13 @@
(update-input value-str) (update-input value-str)
(dom/blur! node))))) (dom/blur! node)))))
handle-key-up
(mf/use-fn
(mf/deps parse-value)
(fn []
;; Store the last value inputed
(reset! last-value* (parse-value))))
handle-mouse-wheel handle-mouse-wheel
(mf/use-fn (mf/use-fn
(mf/deps set-delta) (mf/deps set-delta)
@ -167,7 +191,7 @@
(mf/use-fn (mf/use-fn
(mf/deps parse-value apply-value update-input on-blur) (mf/deps parse-value apply-value update-input on-blur)
(fn [event] (fn [event]
(let [new-value (or (parse-value) default)] (let [new-value (or @last-value* default)]
(if (or nillable? new-value) (if (or nillable? new-value)
(apply-value event new-value) (apply-value event new-value)
(update-input new-value))) (update-input new-value)))
@ -208,6 +232,7 @@
(obj/set! "defaultValue" (fmt/format-number value)) (obj/set! "defaultValue" (fmt/format-number value))
(obj/set! "title" title) (obj/set! "title" title)
(obj/set! "onKeyDown" handle-key-down) (obj/set! "onKeyDown" handle-key-down)
(obj/set! "onKeyUp" handle-key-up)
(obj/set! "onBlur" handle-blur) (obj/set! "onBlur" handle-blur)
(obj/set! "onFocus" handle-focus))] (obj/set! "onFocus" handle-focus))]

View file

@ -22,6 +22,8 @@
placeholder (unchecked-get props "placeholder") placeholder (unchecked-get props "placeholder")
icon (unchecked-get props "icon") icon (unchecked-get props "icon")
autofocus (unchecked-get props "auto-focus") autofocus (unchecked-get props "auto-focus")
id (unchecked-get props "id")
handle-change handle-change
(mf/use-fn (mf/use-fn
@ -51,7 +53,8 @@
children children
[:div {:class (stl/css :search-input-wrapper)} [:div {:class (stl/css :search-input-wrapper)}
icon icon
[:input {:on-change handle-change [:input {:id id
:on-change handle-change
:value value :value value
:auto-focus autofocus :auto-focus autofocus
:placeholder placeholder :placeholder placeholder

View file

@ -71,8 +71,8 @@
[:rect.margin-rect [:rect.margin-rect
{:x (:x rect-data) {:x (:x rect-data)
:y (:y rect-data) :y (:y rect-data)
:width (:width rect-data) :width (max 0 (:width rect-data))
:height (:height rect-data) :height (max 0 (:height rect-data))
:on-pointer-enter on-pointer-enter :on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave :on-pointer-leave on-pointer-leave
:on-pointer-down on-pointer-down :on-pointer-down on-pointer-down

View file

@ -32,8 +32,7 @@
:selected (= editing-stop offset)) :selected (= editing-stop offset))
:data-value (str offset) :data-value (str offset)
:on-click on-select-stop :on-click on-select-stop
:style {:left (dm/str (* offset 100) "%") :style {:left (dm/str (* offset 100) "%")}
:backgroundColor hex}
:key (dm/str offset)} :key (dm/str offset)}
[:div {:class (stl/css :gradient-stop-color) [:div {:class (stl/css :gradient-stop-color)

View file

@ -32,22 +32,31 @@
.gradient-stop-wrapper { .gradient-stop-wrapper {
position: absolute; position: absolute;
width: calc(100% - 2rem); width: calc(100% - $s-40);
left: $s-20;
} }
.gradient-stop { .gradient-stop {
position: absolute; position: absolute;
display: grid; display: grid;
grid-template-columns: 50% 50%; grid-template-columns: 50% 50%;
padding: 0;
width: $s-16; width: $s-16;
height: $s-24; height: $s-24;
border-radius: $br-4; border-radius: $br-4;
margin-top: calc(-1 * $s-2); margin-top: calc(-1 * $s-2);
margin-left: calc(-1 * $s-8); margin-left: calc(-1 * $s-8);
border: $s-2 solid var(--colorpicker-handlers-color); border: $s-2 solid var(--colorpicker-handlers-color);
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=");
left center; background-position: left center;
background-size: 8px;
&.selected { &.selected {
border: $s-2 solid var(--colorpicker-details-color-selected); border: $s-2 solid var(--colorpicker-details-color-selected);
} }
} }
.gradient-stop-color,
.gradient-stop-alpha {
width: 100%;
height: 100%;
}

View file

@ -176,20 +176,6 @@
align-items: center; align-items: center;
} }
.listing-option-btn {
@include flexCenter;
cursor: pointer;
background-color: var(--button-radio-background-color-rest);
&.first {
margin-left: auto;
}
svg {
@extend .button-icon;
}
}
.add-component { .add-component {
@extend .button-tertiary; @extend .button-tertiary;
height: $s-32; height: $s-32;

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.sidebar.options.menus.component (ns app.main.ui.workspace.sidebar.options.menus.component
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.types.component :as ctk] [app.common.types.component :as ctk]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
@ -28,6 +29,7 @@
[app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.timers :as tm]
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -197,18 +199,21 @@
(mf/defc component-group-item (mf/defc component-group-item
[{:keys [item on-enter-group] :as props}] [{:keys [item on-enter-group] :as props}]
(let [group-name (:name item) (let [group-name (:name item)
path (cfh/butlast-path group-name) path (cfh/butlast-path-with-dots group-name)
on-group-click #(on-enter-group group-name)] on-group-click #(on-enter-group group-name)]
[:div {:class (stl/css :component-group) [:div {:class (stl/css :component-group)
:key (uuid/next) :on-click on-group-click :key (uuid/next) :on-click on-group-click
:title group-name} :title group-name}
[:div
[:div {:class (stl/css :path-wrapper)}
(when-not (str/blank? path) (when-not (str/blank? path)
[:span {:class (stl/css :component-group-path)} [:span {:class (stl/css :component-group-path)}
(str "\u00A0/\u00A0" path)]) (str "\u00A0\u2022\u00A0" path)])
[:span {:class (stl/css :component-group-name)} [:span {:class (stl/css :component-group-name)}
(cfh/last-path group-name)]] (cfh/last-path group-name)]]
[:span i/arrow-refactor]]))
[:span {:class (stl/css :arrow-icon)}
i/arrow-refactor]]))
(mf/defc component-swap (mf/defc component-swap
[{:keys [shapes] :as props}] [{:keys [shapes] :as props}]
@ -228,7 +233,9 @@
file-id (if every-same-file? file-id (if every-same-file?
(:component-file shape) (:component-file shape)
current-file-id) current-file-id)
orig-components (map #(ctf/get-component libraries (:component-file %) (:component-id %)) shapes) orig-components (map #(ctf/get-component libraries (:component-file %) (:component-id %)) shapes)
paths (->> orig-components paths (->> orig-components
(map :path) (map :path)
(map cfh/split-path)) (map cfh/split-path))
@ -245,6 +252,7 @@
(cfh/join-path (if (not every-same-file?) (cfh/join-path (if (not every-same-file?)
"" ""
(find-common-path [] 0)))) (find-common-path [] 0))))
filters* (mf/use-state filters* (mf/use-state
{:term "" {:term ""
:file-id file-id :file-id file-id
@ -252,7 +260,9 @@
:listing-thumbs? false}) :listing-thumbs? false})
filters (deref filters*) filters (deref filters*)
is-search? (not (str/blank? (:term filters))) is-search? (not (str/blank? (:term filters)))
current-library-id (if (contains? libraries (:file-id filters)) current-library-id (if (contains? libraries (:file-id filters))
(:file-id filters) (:file-id filters)
current-file-id) current-file-id)
@ -264,7 +274,7 @@
components (->> (get-in libraries [current-library-id :data :components]) components (->> (get-in libraries [current-library-id :data :components])
vals vals
(remove #(true? (:deleted %))) (remove #(true? (:deleted %)))
(map #(assoc % :full-name (cfh/merge-path-item (:path %) (:name %))))) (map #(assoc % :full-name (cfh/merge-path-item-with-dot (:path %) (:name %)))))
get-subgroups (fn [path] get-subgroups (fn [path]
(let [split-path (cfh/split-path path)] (let [split-path (cfh/split-path path)]
@ -335,25 +345,33 @@
toggle-list-style toggle-list-style
(mf/use-fn (mf/use-fn
(fn [style] (fn [style]
(swap! filters* assoc :listing-thumbs? (= style "grid"))))] (swap! filters* assoc :listing-thumbs? (= style "grid"))))
filters-but-last (cfh/butlast-path (:path filters))
last-filters (cfh/last-path (:path filters))
filter-path-with-dots (->> filters-but-last (cfh/split-path) (cfh/join-path-with-dot))]
[:div {:class (stl/css :component-swap)} [:div {:class (stl/css :component-swap)}
[:div {:class (stl/css :element-set-title)} [:div {:class (stl/css :element-set-title)}
[:span (tr "workspace.options.component.swap")]] [:span (tr "workspace.options.component.swap")]]
[:div {:class (stl/css :component-swap-content)} [:div {:class (stl/css :component-swap-content)}
[:div {:class (stl/css :fields-wrapper)}
[:div {:class (stl/css :search-field)} [:div {:class (stl/css :search-field)}
[:& search-bar {:on-change on-search-term-change [:& search-bar {:on-change on-search-term-change
:clear-action on-search-clear-click :clear-action on-search-clear-click
:class (stl/css :search-wrapper)
:id "swap-component-search-filter"
:value (:term filters) :value (:term filters)
:placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name])) :placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name]))
:icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]] :icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]]
[:div {:class (stl/css :select-field)}
[:& select {:class (stl/css :select-library) [:& select {:class (stl/css :select-library)
:default-value current-library-id :default-value current-library-id
:options libraries-options :options libraries-options
:on-change on-library-change}]] :on-change on-library-change}]]
[:div {:class (stl/css :swap-wrapper)}
[:div {:class (stl/css :library-name-wrapper)}
[:div {:class (stl/css :library-name)} current-library-name] [:div {:class (stl/css :library-name)} current-library-name]
[:div {:class (stl/css :listing-options-wrapper)} [:div {:class (stl/css :listing-options-wrapper)}
@ -362,26 +380,25 @@
:on-change toggle-list-style :on-change toggle-list-style
:name "swap-listing-style"} :name "swap-listing-style"}
[:& radio-button {:icon i/view-as-list-refactor [:& radio-button {:icon i/view-as-list-refactor
:icon-class (stl/css :radio-button)
:value "list" :value "list"
:id "swap-opt-list"}] :id "swap-opt-list"}]
[:& radio-button {:icon i/flex-grid-refactor [:& radio-button {:icon i/flex-grid-refactor
:icon-class (stl/css :radio-button)
:value "grid" :value "grid"
:id "swap-opt-grid"}]]] :id "swap-opt-grid"}]]]]
(when-not (or is-search? (str/empty? (:path filters)))
(if (or is-search? (str/empty? (:path filters)))
[:div {:class (stl/css :component-path-empty)}]
[:button {:class (stl/css :component-path) [:button {:class (stl/css :component-path)
:on-click on-go-back :on-click on-go-back
:title (:path filters)} :title filter-path-with-dots}
[:span i/arrow-refactor] [:span {:class (stl/css :back-arrow)} i/arrow-refactor]
[:span (:path filters)]]) (when-not (= "" filter-path-with-dots)
[:span {:class (stl/css :path-name)}
(dm/str "\u00A0\u2022\u00A0" filter-path-with-dots)])
[:span {:class (stl/css :path-name-last)} last-filters]])
(when (empty? items) (when (empty? items)
[:div {:class (stl/css :component-list-empty)} [:div {:class (stl/css :component-list-empty)}
(tr "workspace.options.component.swap.empty")]) (tr "workspace.options.component.swap.empty")]) ;;TODO review this empty space
(when (:listing-thumbs? filters) (when (:listing-thumbs? filters)
[:div {:class (stl/css :component-list)} [:div {:class (stl/css :component-list)}
@ -407,17 +424,20 @@
:component-id current-comp-id :component-id current-comp-id
:is-search is-search? :is-search is-search?
:listing-thumbs (:listing-thumbs? filters)}]) :listing-thumbs (:listing-thumbs? filters)}])
[:& component-group-item {:item item :on-enter-group on-enter-group}]))]]])) [:& component-group-item {:item item
:key (:id item)
:on-enter-group on-enter-group}]))]]]]))
(mf/defc component-ctx-menu (mf/defc component-ctx-menu
[{:keys [menu-entries on-close show] :as props}] [{:keys [menu-entries on-close show main-instance] :as props}]
(let [do-action (let [do-action
(fn [action event] (fn [action event]
(dom/stop-propagation event) (dom/stop-propagation event)
(action) (action)
(on-close))] (on-close))]
[:& dropdown {:show show :on-close on-close} [:& dropdown {:show show :on-close on-close}
[:ul {:class (stl/css :custom-select-dropdown)} [:ul {:class (stl/css-case :custom-select-dropdown true
:not-main (not main-instance))}
(for [entry menu-entries :when (not (nil? entry))] (for [entry menu-entries :when (not (nil? entry))]
[:li {:key (uuid/next) [:li {:key (uuid/next)
:class (stl/css :dropdown-element) :class (stl/css :dropdown-element)
@ -471,10 +491,14 @@
open-component-panel open-component-panel
(mf/use-fn (mf/use-fn
(mf/deps can-swap? shapes) (mf/deps can-swap? shapes)
#(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap)))) (fn []
(let [search-id "swap-component-search-filter"]
(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap)))
(tm/schedule-on-idle #(dom/focus! (dom/get-element search-id))))))
menu-entries (cmm/generate-components-menu-entries shapes components-v2) menu-entries (cmm/generate-components-menu-entries shapes components-v2)
show-menu? (seq menu-entries)] show-menu? (seq menu-entries)
path (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))]
(when (seq shapes) (when (seq shapes)
[:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-set)}
@ -482,8 +506,9 @@
(if swap-opened? (if swap-opened?
[:button {:class (stl/css :title-back) [:button {:class (stl/css :title-back)
:on-click on-component-back} :on-click on-component-back}
[:span i/arrow-refactor] [:span {:class (stl/css :icon-back)} i/arrow-refactor]
[:span (tr "workspace.options.component")]] [:span (tr "workspace.options.component")]]
[:& title-bar {:collapsable true [:& title-bar {:collapsable true
:collapsed (not open?) :collapsed (not open?)
:on-collapsed toggle-content :on-collapsed toggle-content
@ -496,31 +521,40 @@
(when open? (when open?
[:div {:class (stl/css :element-content)} [:div {:class (stl/css :element-content)}
[:div {:class (stl/css :component-wrapper)} [:div {:class (stl/css-case :component-wrapper true
[:div {:class (stl/css-case :component-name-wrapper true :with-actions show-menu?)}
[:button {:class (stl/css-case :component-name-wrapper true
:with-main (and can-swap? (not multi)) :with-main (and can-swap? (not multi))
:swappeable (and can-swap? (not swap-opened?))) :swappeable (and can-swap? (not swap-opened?)))
:on-click open-component-panel} :on-click open-component-panel}
[:span {:class (stl/css :component-icon)} [:span {:class (stl/css :component-icon)}
(if main-instance? (if main-instance?
i/component-refactor i/component-refactor
i/copy-refactor)] i/copy-refactor)]
[:div {:class (stl/css :component-name)} (if multi [:div {:class (stl/css :name-wrapper)}
[:div {:class (stl/css :component-name)}
(if multi
(tr "settings.multiple") (tr "settings.multiple")
(cfh/last-path shape-name))] (cfh/last-path shape-name))]
(when (and can-swap? (not multi))
[:div {:class (stl/css :component-parent-name)}
(cfh/merge-path-item-with-dot path (:name component))])]]
(when show-menu? (when show-menu?
[:div {:class (stl/css :component-actions)} [:div {:class (stl/css :component-actions)}
[:button {:class (stl/css :menu-btn) [:button {:class (stl/css-case :menu-btn true
:selected menu-open?)
:on-click on-menu-click} :on-click on-menu-click}
i/menu-refactor] i/menu-refactor]
[:& component-ctx-menu {:show menu-open? [:& component-ctx-menu {:show menu-open?
:on-close on-menu-close :on-close on-menu-close
:menu-entries menu-entries}]]) :menu-entries menu-entries
(when (and can-swap? (not multi)) :main-instance main-instance?}]])]
[:div {:class (stl/css :component-parent-name)}
(cfh/merge-path-item (:path component) (:name component))])]]
(when swap-opened? (when swap-opened?
[:& component-swap {:shapes copies}]) [:& component-swap {:shapes copies}])

View file

@ -7,33 +7,132 @@
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
.element-set { .element-set {
margin: 0; margin: 0;
padding-top: $s-8;
} }
.element-content { .element-content {
@include flexColumn; @include flexColumn;
margin-bottom: $s-8; }
.title-back {
@include tabTitleTipography;
display: flex;
align-items: center;
gap: $s-4;
width: 100%;
height: $s-32;
padding: 0;
border: 0;
border-radius: $br-8;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
cursor: pointer;
}
.icon-back {
@include flexCenter;
width: $s-12;
height: 100%;
svg {
height: $s-12;
width: $s-12;
stroke: var(--icon-foreground);
transform: rotate(180deg);
}
} }
.component-wrapper { .component-wrapper {
display: flex; width: 100%;
margin: 0 $s-4 0 $s-8; min-height: $s-32;
border-radius: $br-8;
&.with-actions {
display: grid;
grid-template-columns: 1fr $s-28;
gap: $s-2;
}
} }
.component-name-wrapper { .component-name-wrapper {
@extend .asset-element; @include buttonStyle;
@include flexRow; cursor: default;
flex-grow: 1; display: grid;
height: 100%; grid-template-columns: $s-12 1fr;
width: 100%; gap: $s-4;
flex-wrap: wrap; padding: 0 $s-8;
padding: 0 0 0 $s-12; border-radius: $br-8 0 0 $br-8;
margin-top: $s-8; background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
&.with-main { &:hover {
padding-bottom: $s-12; background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
} }
} }
.component-icon {
@include flexCenter;
height: $s-32;
width: $s-12;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.name-wrapper {
@include flexColumn;
min-height: $s-32;
padding: $s-8 0 $s-8 $s-2;
border-radius: $br-8 0 0 $br-8;
}
.component-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
min-height: $s-16;
}
.component-parent-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
min-height: $s-16;
max-width: $s-184;
color: var(--title-foreground-color);
}
.component-actions {
position: relative;
}
.menu-btn {
@extend .button-tertiary;
height: 100%;
width: $s-28;
border-radius: 0 $br-8 $br-8 0;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
svg {
@extend .button-icon;
min-height: $s-16;
min-width: $s-16;
}
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
&.selected {
@extend .button-icon-selected;
}
}
}
.menu-btn.selected {
@extend .button-icon-selected;
}
.copy-text { .copy-text {
@include titleTipography; @include titleTipography;
height: 100%; height: 100%;
@ -43,52 +142,10 @@
margin-right: $s-8; margin-right: $s-8;
} }
.component-icon {
@include flexCenter;
height: $s-24;
width: $s-24;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.component-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
width: 70%;
flex-grow: 2;
margin-left: $s-8;
}
.component-parent-name {
@include titleTipography;
@include textEllipsis;
text-align: left;
max-width: 95%;
padding-left: $s-36;
color: var(--title-foreground-color);
}
.swappeable { .swappeable {
cursor: pointer; cursor: pointer;
} }
.component-actions {
position: relative;
}
.menu-btn {
@extend .button-tertiary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}
.custom-select-dropdown { .custom-select-dropdown {
@extend .dropdown-wrapper; @extend .dropdown-wrapper;
right: 0; right: 0;
@ -96,44 +153,14 @@
width: $s-252; width: $s-252;
} }
.not-main {
top: $s-56;
}
.dropdown-element { .dropdown-element {
@extend .dropdown-element-base; @extend .dropdown-element-base;
} }
.title-back {
@include tabTitleTipography;
cursor: pointer;
width: 100%;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
text-align: left;
border: 0;
margin-bottom: $s-16;
svg {
height: $s-8;
width: $s-8;
stroke: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg);
}
}
.search-field {
display: flex;
align-items: center;
height: $s-32;
margin: $s-16 $s-4 $s-4 $s-12;
border-radius: $br-8;
font-family: "worksans", sans-serif;
background-color: var(--input-background-color);
}
.search-box {
align-items: center;
display: flex;
width: 100%;
}
.icon-wrapper { .icon-wrapper {
display: flex; display: flex;
svg { svg {
@ -175,52 +202,52 @@
.search-icon { .search-icon {
@include flexCenter; @include flexCenter;
width: $s-28; width: $s-12;
margin-left: $s-8;
svg { svg {
@extend .button-icon-small; @extend .button-icon-small;
stroke: var(--icon-foreground); stroke: var(--icon-foreground);
} }
} }
.select-field {
margin: $s-8 $s-4 0 $s-12;
}
.select-library {
padding-left: $s-20;
}
.listing-options-wrapper {
width: 100%;
}
.listing-options {
margin-left: auto;
margin-right: $s-4;
}
.component-path { .component-path {
@include titleTipography; display: flex;
@include textEllipsis; align-items: center;
text-align: left; gap: $s-4;
cursor: pointer;
width: 100%; width: 100%;
height: $s-32;
padding: 0;
border: 0;
background-color: var(--title-background-color); background-color: var(--title-background-color);
color: var(--title-foreground-color); color: var(--title-foreground-color);
border: 0; cursor: pointer;
margin: $s-16 0 $s-12 0; }
padding: 0 $s-16 0 $s-24;
.back-arrow {
@include flexCenter;
height: $s-32;
svg { svg {
height: $s-8; height: $s-12;
width: $s-8; width: $s-12;
stroke: var(--icon-foreground); stroke: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg); transform: rotate(180deg);
} }
} }
.component-path-empty { .path-name {
height: $s-16; @include titleTipography;
@include textEllipsis;
direction: rtl;
height: $s-32;
padding: $s-8 0 $s-8 $s-2;
}
.path-name-last {
@include titleTipography;
@include textEllipsis;
height: $s-32;
padding: $s-8 0 $s-8 $s-2;
color: white;
} }
.component-list-empty { .component-list-empty {
@ -229,10 +256,6 @@
color: $df-secondary; color: $df-secondary;
} }
.component-list {
margin: 0 $s-4 0 $s-8;
}
.component-item { .component-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -280,40 +303,6 @@
} }
} }
.component-group {
@include titleTipography;
text-align: left;
display: flex;
align-items: center;
margin: 0 $s-16 $s-8 $s-8;
justify-content: space-between;
cursor: pointer;
height: $s-24;
svg {
height: $s-8;
width: $s-8;
}
div {
display: flex;
width: 90%;
}
span {
@include textEllipsis;
}
.component-group-path {
direction: rtl;
}
.component-group-name {
color: var(--assets-item-name-foreground-color);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
.component-group-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
}
.component-grid { .component-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, $s-124); grid-template-columns: repeat(2, $s-124);
@ -393,30 +382,107 @@
} }
} }
.element-set-title {
@include tabTitleTipography;
display: flex;
align-items: center;
height: $s-32;
padding-left: $s-2;
color: var(--title-foreground-color);
}
// Component swap
.component-swap {
padding-top: $s-12;
}
.component-swap-content {
@include flexColumn;
gap: $s-16;
}
.fields-wrapper {
@include flexColumn;
gap: $s-4;
}
.search-field {
display: flex;
align-items: center;
height: $s-32;
border-radius: $br-8;
font-family: "worksans", sans-serif;
background-color: var(--input-background-color);
}
.library-name-wrapper {
display: grid;
grid-template-columns: 1fr auto;
}
.library-name { .library-name {
@include titleTipography; @include titleTipography;
@include textEllipsis; @include textEllipsis;
margin: $s-20 $s-4 0 $s-12;
color: var(--title-foreground-color); color: var(--title-foreground-color);
padding: $s-8 0 $s-8 $s-2;
} }
.element-set-title { .swap-wrapper {
@include flexColumn;
gap: $s-4;
}
.listing-options-wrapper {
width: 100%;
}
.listing-options {
display: flex;
align-items: center;
}
.component-group {
@include titleTipography; @include titleTipography;
text-transform: uppercase; display: grid;
margin: $s-16 $s-4 0 $s-12; grid-template-columns: 1fr $s-12;
color: var(--title-foreground-color); height: $s-32;
cursor: pointer;
.component-group-name {
@include textEllipsis;
color: var(--assets-item-name-foreground-color);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
.component-group-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
} }
.radio-button { .arrow-icon {
@include flexCenter;
height: $s-32;
svg { svg {
stroke: var(--icon-foreground);
fill: var(--icon-foreground);
height: $s-12; height: $s-12;
width: $s-12; width: $s-12;
cursor: pointer; stroke: var(--icon-foreground);
} }
} }
.path-wrapper {
display: flex;
max-width: $s-232;
padding: $s-8 0 $s-8 $s-2;
}
.component-group-path {
@include textEllipsis;
direction: rtl;
color: var(--assets-item-name-foreground-color-rest);
}
// Component annotation // Component annotation
.component-annotation { .component-annotation {

View file

@ -221,7 +221,7 @@
(when (and (not multiple?) (or (= :manual cell-mode) (= :area cell-mode))) (when (and (not multiple?) (or (= :manual cell-mode) (= :area cell-mode)))
[:div {:class (stl/css :row)} [:div {:class (stl/css :row)}
[:div {:class (stl/css :grid-coord-group)} [:div {:class (stl/css :grid-coord-group)}
[:span {:class (stl/css :icon)} i/layout-rows] [:span {:class (stl/css :icon)} i/flex-vertical-refactor]
[:div {:class (stl/css :coord-input)} [:div {:class (stl/css :coord-input)}
[:> numeric-input* [:> numeric-input*
{:placeholder "--" {:placeholder "--"
@ -236,7 +236,7 @@
:value column-end}]]] :value column-end}]]]
[:div {:class (stl/css :grid-coord-group)} [:div {:class (stl/css :grid-coord-group)}
[:span {:class (stl/css :icon)} i/layout-columns] [:span {:class (stl/css :icon)} i/flex-horizontal-refactor]
[:div {:class (stl/css :coord-input :double)} [:div {:class (stl/css :coord-input :double)}
[:> numeric-input* [:> numeric-input*
{:placeholder "--" {:placeholder "--"

View file

@ -112,8 +112,7 @@
(fn [color] (fn [color]
(st/emit! (dch/update-shapes (st/emit! (dch/update-shapes
ids ids
#(assoc-in % [:shadow index :color] #(assoc-in % [:shadow index :color] color)))))
(dissoc color :id :file-id))))))
detach-color detach-color
(mf/use-fn (mf/use-fn

View file

@ -147,7 +147,8 @@
:stroke-cap-end stroke-cap-start} index))))) :stroke-cap-end stroke-cap-start} index)))))
on-add-stroke on-add-stroke
(fn [_] (fn [_]
(st/emit! (dc/add-stroke ids {:stroke-style :solid (st/emit! (dc/add-stroke ids {:stroke-alignment :inner
:stroke-style :solid
:stroke-color clr/black :stroke-color clr/black
:stroke-opacity 1 :stroke-opacity 1
:stroke-width 1})) :stroke-width 1}))

View file

@ -255,6 +255,7 @@
:on-focus on-focus :on-focus on-focus
:on-blur on-blur :on-blur on-blur
:on-change handle-opacity-change :on-change handle-opacity-change
:default 100
:min 0 :min 0
:max 100}]])] :max 100}]])]

View file

@ -7,6 +7,7 @@
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
.main-toolbar { .main-toolbar {
cursor: initial;
position: absolute; position: absolute;
top: $s-28; top: $s-28;
left: calc(50% - $s-160); left: calc(50% - $s-160);

View file

@ -125,6 +125,7 @@
height: $s-48; height: $s-48;
margin-left: -50%; margin-left: -50%;
padding: $s-8; padding: $s-8;
cursor: initial;
pointer-events: initial; pointer-events: initial;
width: $s-512; width: $s-512;
} }

View file

@ -7,6 +7,7 @@
@import "refactor/common-refactor.scss"; @import "refactor/common-refactor.scss";
.sub-actions { .sub-actions {
cursor: initial;
pointer-events: initial; pointer-events: initial;
position: absolute; position: absolute;
top: $s-12; top: $s-12;

View file

@ -22,6 +22,7 @@
height: $s-48; height: $s-48;
margin-left: -50%; margin-left: -50%;
padding: $s-8; padding: $s-8;
cursor: initial;
pointer-events: initial; pointer-events: initial;
width: $s-400; width: $s-400;
} }

View file

@ -130,7 +130,11 @@
(on-frame-leave (:id frame)))) (on-frame-leave (:id frame))))
main-instance? (ctk/main-instance? frame) main-instance? (ctk/main-instance? frame)
text-pos-x (if (or (:use-for-thumbnail frame) grid-edition? main-instance?) 15 0)]
text-width (* (:width frame) zoom)
show-icon? (and (or (:use-for-thumbnail frame) grid-edition? main-instance?)
(not (<= text-width 15)))
text-pos-x (if show-icon? 15 0)]
(when (not (:hidden frame)) (when (not (:hidden frame))
[:g.frame-title {:id (dm/str "frame-title-" (:id frame)) [:g.frame-title {:id (dm/str "frame-title-" (:id frame))
@ -138,7 +142,7 @@
:transform (vwu/title-transform frame zoom grid-edition?) :transform (vwu/title-transform frame zoom grid-edition?)
:pointer-events (when (:blocked frame) "none")} :pointer-events (when (:blocked frame) "none")}
(cond (cond
(or (:use-for-thumbnail frame) grid-edition? main-instance?) show-icon?
[:svg {:x 0 [:svg {:x 0
:y -9 :y -9
:width 12 :width 12
@ -157,9 +161,10 @@
main-instance? main-instance?
[:use {:href "#icon-component-refactor"}])]) [:use {:href "#icon-component-refactor"}])])
[:text {:x text-pos-x
:y 0 [:foreignObject {:x text-pos-x
:width (:width frame) :y -11
:width (max 0 (- text-width text-pos-x))
:height 20 :height 20
:class "workspace-frame-label" :class "workspace-frame-label"
:style {:fill color} :style {:fill color}
@ -169,9 +174,11 @@
:on-context-menu on-context-menu :on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter :on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave} :on-pointer-leave on-pointer-leave}
[:div {:class (stl/css :workspace-frame-label)
:style {:color color}}
(if show-id? (if show-id?
(dm/str (dm/str (:id frame)) " - " (:name frame)) (dm/str (dm/str (:id frame)) " - " (:name frame))
(:name frame))]]))) (:name frame))]]])))
(mf/defc frame-titles (mf/defc frame-titles
{::mf/wrap-props false {::mf/wrap-props false

View file

@ -62,3 +62,11 @@
} }
} }
} }
.workspace-frame-label {
font-size: $fs-12;
color: black;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}

View file

@ -37,14 +37,19 @@
;; TODO: REMOVE `VALUE` WHEN COLOR IS INTEGRATED ;; TODO: REMOVE `VALUE` WHEN COLOR IS INTEGRATED
(defn color->background [{:keys [color opacity gradient value]}] (defn color->background [{:keys [color opacity gradient value]}]
(let [color (or color value) (let [color (d/nilv color value)
opacity (or opacity 1)] opacity (or opacity 1)]
(cond (cond
(and gradient (not= :multiple gradient)) (and gradient (not= :multiple gradient))
(gradient->css gradient) (gradient->css gradient)
(not= color :multiple) (and (some? color) (not= color :multiple))
(let [[r g b] (cc/hex->rgb (or color value))] (let [color
(-> (str/replace color "#" "")
(cc/expand-hex)
(cc/prepend-hash))
[r g b] (cc/hex->rgb color)]
(str/fmt "rgba(%s, %s, %s, %s)" r g b opacity)) (str/fmt "rgba(%s, %s, %s, %s)" r g b opacity))
:else "transparent"))) :else "transparent")))