diff --git a/backend/dev/user.clj b/backend/dev/user.clj index 5d0cb7e37..efe2c935a 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -7,6 +7,7 @@ (ns user (:require [app.common.data :as d] + [app.common.debug :as debug] [app.common.exceptions :as ex] [app.common.files.helpers :as cfh] [app.common.fressian :as fres] @@ -55,8 +56,12 @@ [promesa.exec :as px])) (repl/disable-reload! (find-ns 'integrant.core)) +(repl/disable-reload! (find-ns 'app.common.debug)) + (set! *warn-on-reflection* true) +(add-tap #'debug/tap-handler) + ;; --- Benchmarking Tools (defmacro run-quick-bench @@ -132,12 +137,6 @@ ;; :v6 v6 ;; }]))) -(defonce debug-tap - (do - (add-tap #(locking debug-tap - (prn "tap debug:" %))) - 1)) - (defn calculate-frames [{:keys [data]}] diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index ca1ab6739..46c0ae150 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -21,7 +21,7 @@ - + diff --git a/backend/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj index 548255a5a..71b0c538a 100644 --- a/backend/src/app/features/components_v2.clj +++ b/backend/src/app/features/components_v2.clj @@ -31,6 +31,7 @@ [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.grid :as ctg] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] @@ -105,10 +106,20 @@ ;; FILE PREPARATION BEFORE MIGRATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def valid-color? (sm/lazy-validator ::ctc/recent-color)) -(def valid-fill? (sm/lazy-validator ::cts/fill)) -(def valid-stroke? (sm/lazy-validator ::cts/stroke)) -(def valid-flow? (sm/lazy-validator ::ctp/flow)) +(def valid-recent-color? + (sm/lazy-validator ::ctc/recent-color)) + +(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? (sm/lazy-validator ::ctsx/content)) @@ -122,30 +133,61 @@ (def valid-rgb-color-string? (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 "Apply some specific migrations or fixes to things that are allowed in v1 but not in v2, or that are the result of old bugs." [file-data libraries] (let [detached-ids (volatile! #{}) + detach-shape (fn [container shape] - ;; Detach a shape. If it's inside a component, add it to detached-ids. This list - ;; is used later to process any other copy that was referencing a detached copy. + ;; Detach a shape and make necessary adjustments. (let [is-component? (let [root-shape (ctst/get-shape container (:id container))] - (and (some? root-shape) (nil? (:parent-id root-shape))))] - (when is-component? - (vswap! detached-ids conj (:id shape))) - (ctk/detach-shape shape))) + (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? + (vswap! detached-ids conj (:id 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 (fn [file-data] ;; Remove any child that does not exist. And also remove duplicated children. - (letfn [(fix-container - [container] + (letfn [(fix-container [container] (d/update-when container :objects update-vals (partial fix-shape container))) - (fix-shape - [container shape] + (fix-shape [container shape] (let [objects (:objects container)] (d/update-when shape :shapes (fn [shapes] @@ -160,12 +202,10 @@ fix-missing-image-metadata (fn [file-data] ;; Delete broken image shapes with no metadata. - (letfn [(fix-container - [container] + (letfn [(fix-container [container] (d/update-when container :objects #(reduce-kv fix-shape % %))) - (fix-shape - [objects id shape] + (fix-shape [objects id shape] (if (and (cfh/image-shape? shape) (nil? (:metadata shape))) (-> objects @@ -189,11 +229,28 @@ (dissoc options :background) 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] (-> options ;; Some pages has invalid data on flows, we proceed just to ;; delete them. (d/update-when :flows #(filterv valid-flow? %)) + (fix-saved-grids) (fix-background)))] (update file-data :pages-index update-vals update-page))) @@ -203,11 +260,19 @@ ;; fix that issues. fix-file-data (fn [file-data] - (-> file-data - (d/update-when :colors dissoc nil) - (d/update-when :typographies dissoc nil))) + (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 + (d/update-when :colors fix-colors-library) + (d/update-when :typographies dissoc nil)))) - delete-big-geometry-shapes + fix-big-geometry-shapes (fn [file-data] ;; At some point in time, we had a bug that generated shapes ;; with huge geometries that did not validate the @@ -253,9 +318,16 @@ (fn [shapes] (filterv #(not= id %) shapes))))) (and (cfh/text-shape? shape) - (not (seq (:content shape)))) + (not (valid-text-content? (:content shape)))) (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 objects)) @@ -266,25 +338,125 @@ (update :pages-index update-vals update-container) (update :components update-vals update-container)))) - fix-misc-shape-issues + fix-shape-geometry (fn [file-data] (letfn [(fix-container [container] (d/update-when container :objects update-vals fix-shape)) + (fix-shape [shape] + (cond + (and (cfh/image-shape? shape) + (valid-image-attrs? shape) + (grc/valid-rect? (:selrect shape)) + (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) + (= gap ##-Inf)) + 0 + gap)) + (fix-shape [shape] (cond-> shape ;; Some shapes has invalid gap value (contains? shape :layout-gap) - (d/update-in-when [:layout-gap :column-gap] - (fn [gap] - (if (or (= gap ##Inf) - (= gap ##-Inf)) - 0 - gap))) + (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)) (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 (seq (:fills shape)) (update :fills (fn [fills] (filterv valid-fill? fills))) @@ -296,11 +468,7 @@ ;; Fix some broken layout related attrs, probably ;; of copypaste on flex layout betatest period (true? (:layout shape)) - (assoc :layout :flex) - - (number? (:layout-gap shape)) - (as-> shape (let [n (:layout-gap shape)] - (assoc shape :layout-gap {:row-gap n :column-gap n})))))] + (assoc :layout :flex)))] (-> file-data (update :pages-index update-vals fix-container) @@ -342,13 +510,15 @@ (and (cfh/path-shape? shape) (seq (:content shape)) (not (valid-path-content? (:content shape)))) - (let [shape (update shape :content fix-path-content) - [points selrect] (gshp/content->points+selrect shape (:content shape))] - (-> shape - (dissoc :bool-content) - (dissoc :bool-type) - (assoc :points points) - (assoc :selrect selrect))) + (let [shape (update shape :content fix-path-content)] + (if (not (valid-path-content? (:content shape))) + shape + (let [[points selrect] (gshp/content->points+selrect shape (:content shape))] + (-> shape + (dissoc :bool-content) + (dissoc :bool-type) + (assoc :points points) + (assoc :selrect selrect))))) ;; When we fount a bool shape with no content, ;; we convert it to a simple rect @@ -390,18 +560,16 @@ ;; Remove invalid colors in :recent-colors (d/update-when file-data :recent-colors (fn [colors] - (filterv valid-color? colors)))) + (filterv valid-recent-color? colors)))) fix-broken-parents (fn [file-data] ;; Find children shapes whose parent-id is not set to the parent that contains them. ;; Remove them from the parent :shapes list. - (letfn [(fix-container - [container] + (letfn [(fix-container [container] (d/update-when container :objects #(reduce-kv fix-shape % %))) - (fix-shape - [objects id shape] + (fix-shape [objects id shape] (reduce (fn [objects child-id] (let [child (get objects child-id)] (cond-> objects @@ -476,20 +644,33 @@ (fn [file-data] ;; Detach shapes that were inside a copy (have :shape-ref) but now they aren't. (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] - (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) (not (ctk/instance-head? shape)) (not (ctk/in-component-copy? parent))) (detach-shape container shape) - shape)))] + container)))] (-> file-data (update :pages-index 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 (fn [file-data] ;; Remap shape-refs so that they point to the near main. @@ -523,11 +704,9 @@ (if (some? direct-shape-2) ;; If it exists, there is nothing else to do. container - ;; If not found, detach shape and all children (stopping if a nested instance is reached) - (let [children (ctn/get-children-in-instance (:objects container) (:id shape))] - (reduce #(ctn/update-shape %1 (:id %2) (partial detach-shape %1)) - container - children)))))))) + ;; If not found, detach shape and all children. + ;; container + (detach-shape container shape))))))) container))] (-> file-data @@ -539,14 +718,64 @@ ;; If the user has created a copy and then converted into a path or bool, ;; detach it because the synchronization will no longer work. (letfn [(fix-container [container] - (d/update-when container :objects update-vals (partial fix-shape container))) + (reduce fix-shape container (ctn/shapes-seq container))) (fix-shape [container shape] (if (and (ctk/instance-head? shape) (or (cfh/path-shape? shape) (cfh/bool-shape? shape))) (detach-shape container shape) - shape))] + 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 (update :pages-index update-vals fix-container) @@ -554,7 +783,7 @@ transform-to-frames (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 (letfn [(fix-container [container] (d/update-when container :objects update-vals fix-shape)) @@ -631,9 +860,8 @@ (fn [file-data] ;; Find component heads that are not main-instance but have not :shape-ref. ;; Also shapes that have :shape-ref but are not in a copy. - (letfn [(fix-container - [container] - (d/update-when container :objects update-vals (partial fix-shape container))) + (letfn [(fix-container [container] + (reduce fix-shape container (ctn/shapes-seq container))) (fix-shape [container shape] @@ -643,74 +871,79 @@ (and (ctk/in-component-copy? shape) (nil? (ctn/get-head-shape (:objects container) shape {:allow-main? true})))) (detach-shape container shape) - shape))] + container))] + (-> file-data (update :pages-index 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 (fn [file-data] - ;; Find any copy that is referencing a shape inside a component that have - ;; been detached in a previous fix. If so, undo the nested copy, converting - ;; it into a direct copy. - ;; - ;; WARNING: THIS SHOULD BE CALLED AT THE END OF THE PROCESS. + ;; Find any copy that is referencing a shape inside a component that have + ;; been detached in a previous fix. If so, undo the nested copy, converting + ;; it into a direct copy. + ;; + ;; WARNING: THIS SHOULD BE CALLED AT THE END OF THE PROCESS. (letfn [(fix-container [container] (d/update-when container :objects update-vals fix-shape)) (fix-shape [shape] (cond-> shape (@detached-ids (:shape-ref shape)) - (dissoc shape - :component-id - :component-file - :component-root)))] + (ctk/detach-shape)))] (-> file-data (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) - - fix-shape-nil-parent-id - (fn [file-data] - ;; Ensure that parent-id and frame-id are not nil - (letfn [(fix-container [container] - (d/update-when container :objects update-vals fix-shape)) - - (fix-shape [shape] - (let [frame-id (or (:frame-id shape) - uuid/zero) - parent-id (or (:parent-id shape) - frame-id)] - (assoc shape :frame-id frame-id - :parent-id parent-id)))] - (-> file-data - (update :pages-index update-vals fix-container))))] + (d/update-when :components update-vals fix-container))))] (-> file-data (fix-file-data) (fix-page-invalid-options) - (fix-completly-broken-shapes) - (fix-bad-children) (fix-misc-shape-issues) (fix-recent-colors) (fix-missing-image-metadata) (fix-text-shapes-converted-to-path) (fix-broken-paths) - (delete-big-geometry-shapes) + (fix-big-geometry-shapes) + (fix-shape-geometry) + (fix-completly-broken-shapes) + (fix-bad-children) (fix-broken-parents) (fix-orphan-shapes) (fix-orphan-copies) (remove-nested-roots) (add-not-nested-roots) + (fix-components-without-id) (remap-refs) (fix-converted-copies) + (wrap-non-group-component-roots) + (detach-non-group-instance-roots) (transform-to-frames) (remap-frame-ids) (fix-frame-ids) (fix-component-nil-objects) (fix-false-copies) - (fix-shape-nil-parent-id) - (fix-copies-of-detached)))) ; <- Do not add fixes after this one - + (fix-component-root-without-component) + (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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 3f5d6a7b7..fe1fddc40 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -100,11 +100,11 @@ (let [profile (profile/get-profile pool profile-id) project-id (:default-project-id profile)] - (db/run! pool (fn [{:keys [::db/conn]}] - (create-file conn {:id file-id - :name (str "Cloned file: " filename) - :project-id project-id - :profile-id profile-id}) + (db/run! pool (fn [{:keys [::db/conn] :as cfg}] + (create-file cfg {:id file-id + :name (str "Cloned file: " filename) + :project-id project-id + :profile-id profile-id}) (db/update! conn :file {:data data} {:id file-id}) @@ -141,11 +141,11 @@ {::rres/status 200 ::rres/body "OK UPDATED"}) - (db/run! pool (fn [{:keys [::db/conn]}] - (create-file conn {:id file-id - :name fname - :project-id project-id - :profile-id profile-id}) + (db/run! pool (fn [{:keys [::db/conn] :as cfg}] + (create-file cfg {:id file-id + :name fname + :project-id project-id + :profile-id profile-id}) (db/update! conn :file {:data data} {:id file-id}) diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index cf2942c22..8d43a2cb5 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -131,8 +131,8 @@ (defn- invoke [limiter metrics limit-id limit-key limit-label profile-id f params] (let [tpoint (dt/tpoint) + mlabels (into-array String [(id->str limit-id)]) limit-id (id->str limit-id limit-key) - mlabels (into-array String [limit-id]) stats (pbh/get-stats limiter) id (.incrementAndGet ^AtomicLong idseq)] diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 2621cce6a..8f2216e63 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -8,6 +8,7 @@ (:refer-clojure :exclude [assert]) (:require [app.binfile.v1 :as bf.v1] + [app.common.logging :as l] [app.common.schema :as sm] [app.db :as db] [app.http.sse :as sse] @@ -50,11 +51,16 @@ ::rres/headers {"content-type" "application/octet-stream"} ::rres/body (reify rres/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] - (-> cfg - (assoc ::bf.v1/ids #{file-id}) - (assoc ::bf.v1/embed-assets embed-assets) - (assoc ::bf.v1/include-libraries include-libraries) - (bf.v1/export-files! output-stream))))})) + (try + (-> cfg + (assoc ::bf.v1/ids #{file-id}) + (assoc ::bf.v1/embed-assets embed-assets) + (assoc ::bf.v1/include-libraries include-libraries) + (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 diff --git a/backend/src/app/srepl/components_v2.clj b/backend/src/app/srepl/components_v2.clj index 3db774600..4adf55293 100644 --- a/backend/src/app/srepl/components_v2.clj +++ b/backend/src/app/srepl/components_v2.clj @@ -188,17 +188,27 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (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?) - (let [tpoint (dt/tpoint) + (let [tpoint (dt/tpoint) file-id (if (string? file-id) (parse-uuid file-id) - file-id)] - (binding [feat/*stats* (atom {})] + file-id) + cache (if (int? cache) + (cache/create :executor (::wrk/executor main/system) + :max-items cache) + nil)] + + (binding [feat/*stats* (atom {}) + feat/*cache* cache] (try (-> (assoc main/system ::db/rollback rollback?) (feat/migrate-file! file-id :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error? :label label)) (-> (deref feat/*stats*) @@ -212,22 +222,28 @@ (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) (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 validate? true - skip-on-graphic-error? false}}] + skip-on-graphic-error? true}}] (l/dbg :hint "migrate:start" :rollback rollback?) - (let [team-id (if (string? team-id) - (parse-uuid team-id) - team-id) - stats (atom {}) - tpoint (dt/tpoint)] + (let [team-id (if (string? team-id) + (parse-uuid team-id) + team-id) + stats (atom {}) + 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)) - (binding [feat/*stats* stats] + (binding [feat/*stats* stats + feat/*cache* cache] (try (-> (assoc main/system ::db/rollback rollback?) (feat/migrate-team! team-id @@ -286,7 +302,7 @@ sprocs (ps/create :permits max-procs) cache (if (int? cache) - (cache/create :executor executor + (cache/create :executor (::wrk/executor main/system) :max-items cache) nil) migrate-team @@ -382,3 +398,17 @@ (l/dbg :hint "migrate:end" :rollback rollback? :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)) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index cd5c3f56a..9f9ccc7cc 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -258,8 +258,11 @@ max-jobs start-at on-file + validate? rollback?] :or {max-jobs 1 + max-items Long/MAX_VALUE + validate? true rollback? true}}] (l/dbg :hint "process:start" @@ -273,19 +276,19 @@ sjobs (ps/create :permits max-jobs) process-file - (fn [file-id tpoint] + (fn [file-id idx tpoint] (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?) (fn [{:keys [::db/conn] :as system}] (let [file' (get-file* system file-id) file (binding [*system* system] (on-file file'))] - (when (and (some? file) - (not (identical? file file'))) + (when (and (some? 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") (feat.fdata/enable-objects-map file) @@ -300,36 +303,43 @@ (db/update! conn :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)) :revn (:revn file)} {:id file-id})))))) (catch Throwable cause (l/wrn :hint "unexpected error on processing file (skiping)" :file-id (str file-id) + :index idx :cause cause)) (finally (ps/release! sjobs) (let [elapsed (dt/format-duration (tpoint))] (l/trc :hint "process:file:end" :file-id (str file-id) + :index idx :elapsed elapsed)))))] - (try (db/tx-run! main/system (fn [{:keys [::db/conn] :as system}] (db/exec! conn ["SET statement_timeout = 0"]) (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) - (run! (fn [file-id] - (ps/acquire! sjobs) - (px/run! executor (partial process-file file-id (dt/tpoint)))) - (->> (db/cursor conn [sql:get-file-ids (or start-at (dt/now))]) - (take max-items) - (map :id))) - - ;; Close and await tasks - (pu/close! executor))) + (try + (reduce (fn [idx file-id] + (ps/acquire! sjobs) + (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))]) + (take max-items) + (map :id))) + (finally + ;; Close and await tasks + (pu/close! executor))))) (catch Throwable cause (l/dbg :hint "process:error" :cause cause)) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 12e3f7762..958a3b9b0 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -57,6 +57,14 @@ #?(:cljs (instance? lkm/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 (defmethod print-method clojure.lang.PersistentQueue [q, w] ;; Overload the printer for queues so they look like fish @@ -308,9 +316,12 @@ (defn mapm "Map over the values of a map" ([mfn] - (map (fn [[key val]] [key (mfn key val)]))) + (map (fn [[key val]] (vec2 key (mfn key val))))) ([mfn coll] - (into {} (mapm mfn) coll))) + (reduce-kv (fn [coll k v] + (assoc coll k (mfn k v))) + coll + coll))) (defn removev "Returns a vector of the items in coll for which (fn item) returns logical false" diff --git a/common/src/app/common/debug.clj b/common/src/app/common/debug.clj new file mode 100644 index 000000000..f23c498ed --- /dev/null +++ b/common/src/app/common/debug.clj @@ -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))) diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc index e35914d73..61cd7f118 100644 --- a/common/src/app/common/files/defaults.cljc +++ b/common/src/app/common/files/defaults.cljc @@ -6,4 +6,4 @@ (ns app.common.files.defaults) -(def version 44) +(def version 46) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 46b9ac66e..ff526e2bd 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -484,7 +484,7 @@ (letfn [(red-fn [cur-idx id] (let [[prev-idx _] (first cur-idx) 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] (let [object (get objects id)] @@ -509,10 +509,11 @@ (defn order-by-indexed-shapes [objects ids] - (->> (indexed-shapes objects) - (sort-by first) - (filter (comp (into #{} ids) second)) - (map second))) + (let [ids (if (set? ids) ids (set ids))] + (->> (indexed-shapes objects) + (filter (fn [o] (contains? ids (val o)))) + (sort-by key) + (map val)))) (defn get-index-replacement "Given a collection of shapes, calculate their positions @@ -542,6 +543,11 @@ [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 "Remove empty items from the path." [path] @@ -607,6 +613,14 @@ "" (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 "Returns the last item of the path." [path] diff --git a/common/src/app/common/files/libraries_helpers.cljc b/common/src/app/common/files/libraries_helpers.cljc index bd750dee5..8b7f34aca 100644 --- a/common/src/app/common/files/libraries_helpers.cljc +++ b/common/src/app/common/files/libraries_helpers.cljc @@ -6,6 +6,7 @@ (ns app.common.files.libraries-helpers (:require + [app.common.data :as d] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [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 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] - (let [changes (pcb/empty-changes it page-id) - from-singe-frame? (and (= 1 (count shapes)) (-> shapes first cfh/frame-shape?)) + (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)) + [root changes old-root-ids] - (if (and (= (count shapes) 1) - (or (and (= (:type (first shapes)) :group) (not components-v2)) - (= (:type (first shapes)) :frame)) - (not (ctk/instance-head? (first shapes)))) - - [(first shapes) + (if (and (= shapes-count 1) + (or (and (cfh/group-shape? first-shape) + (not components-v2)) + (cfh/frame-shape? first-shape)) + (not (ctk/instance-head? first-shape))) + [first-shape (-> (pcb/empty-changes it page-id) (pcb/with-objects objects)) - (:shapes (first shapes))] + (:shapes first-shape)] - (let [root-name (if (= 1 (count shapes)) - (:name (first shapes)) + (let [root-name (if (= 1 shapes-count) + (:name first-shape) "Component 1") - [root changes] (if-not components-v2 - (prepare-create-group it ; These functions needs to be passed as argument - objects ; to avoid a circular dependence - page-id - shapes - root-name - (not (ctk/instance-head? (first shapes)))) - (prepare-create-board changes - (uuid/next) - (:parent-id (first shapes)) - objects - (map :id shapes) - nil - root-name - true))] + shape-ids (into (d/ordered-set) (map :id) shapes) - [root changes (map :id shapes)])) + [root changes] + (if-not components-v2 + (prepare-create-group it ; These functions needs to be passed as argument + objects ; to avoid a circular dependence + page-id + shapes + root-name + (not (ctk/instance-head? first-shape))) + (prepare-create-board changes + (uuid/next) + (:parent-id first-shape) + objects + shape-ids + nil + root-name + true))] + + [root changes shape-ids])) changes (cond-> changes @@ -79,8 +89,7 @@ (pcb/update-shapes (:shapes root) (fn [shape] - (-> shape - (assoc :constraints-h :scale :constraints-v :scale))))) + (assoc shape :constraints-h :scale :constraints-v :scale)))) objects' (assoc objects (:id root) root) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 025f1d841..ccb2560f5 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -109,11 +109,14 @@ (assoc :points (grc/rect->points selrect)))))) (fix-empty-points [shape] - (let [shape (cond-> shape - (empty? (:selrect shape)) (cts/setup-rect))] - (cond-> shape - (empty? (:points shape)) - (assoc :points (grc/rect->points (:selrect shape)))))) + (if (empty? (:points shape)) + (-> shape + (update :selrect (fn [selrect] + (if (map? selrect) + (grc/make-rect selrect) + selrect))) + (cts/setup-shape)) + shape)) (update-object [object] (cond-> object @@ -620,6 +623,10 @@ (-> object (assoc :parent-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)) object)) @@ -843,3 +850,29 @@ (-> data (update :pages-index 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)))) diff --git a/common/src/app/common/files/shapes_helpers.cljc b/common/src/app/common/files/shapes_helpers.cljc index 03e3e89c1..f9f814186 100644 --- a/common/src/app/common/files/shapes_helpers.cljc +++ b/common/src/app/common/files/shapes_helpers.cljc @@ -39,16 +39,17 @@ (defn prepare-move-shapes-into-frame [changes frame-id shapes objects] - (let [ordered-indexes (cfh/order-by-indexed-shapes objects shapes) - parent-id (get-in objects [frame-id :parent-id]) - ordered-indexes (->> ordered-indexes (remove #(= % parent-id))) - to-move-shapes (map (d/getf objects) ordered-indexes)] - (if (d/not-empty? to-move-shapes) + (let [parent-id (dm/get-in objects [frame-id :parent-id]) + shapes (remove #(= % parent-id) shapes) + to-move (->> shapes + (map (d/getf objects)) + (not-empty))] + (if to-move (-> changes (cond-> (not (ctl/any-layout? objects frame-id)) - (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) - (pcb/update-shapes ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) - (pcb/change-parent frame-id to-move-shapes 0) + (pcb/update-shapes shapes ctl/remove-layout-item-data)) + (pcb/update-shapes shapes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) + (pcb/change-parent frame-id to-move 0) (cond-> (ctl/grid-layout? objects frame-id) (-> (pcb/update-shapes [frame-id] ctl/assign-cells {:with-objects? true}) (pcb/reorder-grid-children [frame-id])))) @@ -60,90 +61,102 @@ 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] - (let [selected-objs (map #(get objects %) selected) - new-index (or index - (cfh/get-index-replacement selected objects))] - (when (d/not-empty? selected) - (let [srect (gsh/shapes->rect selected-objs) - selected-id (first selected) + (when-let [selected-objs (->> selected + (map (d/getf objects)) + (not-empty))] - frame-id (dm/get-in objects [selected-id :frame-id]) - parent-id (or parent-id (dm/get-in objects [selected-id :parent-id])) - base-parent (get objects parent-id) + (let [;; We calculate here the ordered selection because it is used + ;; 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))) - layout-props - (when (and (= 1 (count selected)) - (ctl/any-layout? base-parent)) - (let [shape (get objects selected-id)] - (select-keys shape ctl/layout-item-props))) + srect (gsh/shapes->rect selected-objs) + selected-id (first selected) + selected-obj (get objects selected-id) - target-cell-id - (if (and (nil? target-cell-id) - (ctl/grid-layout? objects parent-id)) - ;; Find the top-left grid cell of the selected elements - (let [ncols (count (:layout-grid-columns base-parent))] - (->> selected - (map #(ctl/get-cell-by-shape-id base-parent %)) - (apply min-key (fn [{:keys [row column]}] (+ (* ncols row) column))) - :id)) - target-cell-id) + frame-id (get selected-obj :frame-id) + parent-id (or parent-id (get selected-obj :parent-id)) + base-parent (get objects parent-id) - attrs {:type :frame - :x (:x srect) - :y (:y srect) - :width (:width srect) - :height (:height srect)} + layout-props + (when (and (= 1 (count selected)) + (ctl/any-layout? base-parent)) + (select-keys selected-obj ctl/layout-item-props)) - shape (cts/setup-shape - (cond-> attrs - (some? id) - (assoc :id id) + target-cell-id + (if (and (nil? target-cell-id) + (ctl/grid-layout? objects parent-id)) + ;; Find the top-left grid cell of the selected elements + (let [ncols (count (:layout-grid-columns base-parent))] + (->> selected + (map #(ctl/get-cell-by-shape-id base-parent %)) + (apply min-key (fn [{:keys [row column]}] (+ (* ncols row) column))) + :id)) + target-cell-id) - (some? frame-name) - (assoc :name frame-name) + attrs + {:type :frame + :x (:x srect) + :y (:y srect) + :width (:width srect) + :height (:height srect)} - :always - (assoc :frame-id frame-id - :parent-id parent-id - :shapes (into [] selected)) + shape + (cts/setup-shape + (cond-> attrs + (some? id) + (assoc :id id) - (some? layout-props) - (d/patch-object layout-props) + (some? frame-name) + (assoc :name frame-name) - (or (not= frame-id uuid/zero) without-fill?) - (assoc :fills [] :hide-in-viewer true))) + :always + (assoc :frame-id frame-id + :parent-id parent-id + :shapes (into [] selected)) - shape (with-meta shape {:index new-index}) + (some? layout-props) + (d/patch-object layout-props) - [shape changes] - (prepare-add-shape changes shape objects) + (or (not= frame-id uuid/zero) without-fill?) + (assoc :fills [] :hide-in-viewer true))) - changes - (prepare-move-shapes-into-frame changes (:id shape) selected objects) + shape + (with-meta shape {:index new-index}) - changes - (cond-> changes - (ctl/grid-layout? objects (:parent-id shape)) - (-> (pcb/update-shapes - [(:parent-id shape)] - (fn [parent objects] - ;; This restores the grid layout before adding and moving the shapes - ;; this is done because the add+move could have altered the layout and we - ;; want to do it after both operations are completed. Also here we could - ;; asign the new element to a target-cell - (-> parent - (assoc :layout-grid-cells (:layout-grid-cells base-parent)) - (assoc :layout-grid-rows (:layout-grid-rows base-parent)) - (assoc :layout-grid-columns (:layout-grid-columns base-parent)) + [shape changes] + (prepare-add-shape changes shape objects) - (cond-> (some? target-cell-id) - (assoc-in [:layout-grid-cells target-cell-id :shapes] [(:id shape)])) - (ctl/assign-cells objects))) - {:with-objects? true}) + changes + (prepare-move-shapes-into-frame changes (:id shape) selected' objects) - (pcb/reorder-grid-children [(:parent-id shape)])))] + changes + (cond-> changes + (ctl/grid-layout? objects (:parent-id shape)) + (-> (pcb/update-shapes + [(:parent-id shape)] + (fn [parent objects] + ;; This restores the grid layout before adding and moving the shapes + ;; this is done because the add+move could have altered the layout and we + ;; want to do it after both operations are completed. Also here we could + ;; asign the new element to a target-cell + (-> parent + (assoc :layout-grid-cells (:layout-grid-cells base-parent)) + (assoc :layout-grid-rows (:layout-grid-rows base-parent)) + (assoc :layout-grid-columns (:layout-grid-columns base-parent)) - [shape changes]))))) + (cond-> (some? target-cell-id) + (assoc-in [:layout-grid-cells target-cell-id :shapes] [(:id shape)])) + (ctl/assign-cells objects))) + {:with-objects? true}) + + (pcb/reorder-grid-children [(:parent-id shape)])))] + + [shape changes])))) (defn prepare-create-empty-artboard diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 01373f93c..a5ce2e1da 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -98,7 +98,8 @@ (defn- check-geometry "Validate that the shape has valid coordinates, selrect and points." [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 (nil? (:y shape)) (nil? (:width shape)) @@ -112,61 +113,64 @@ (defn- check-parent-children "Validate parent and children exists, and the link is bidirectional." [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) (report-error :parent-not-found (str/ffmt "Parent % not found" (:parent-id shape)) shape file page) (do (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 - (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))) - (when-not (= (count (:shapes shape)) (count (distinct (:shapes shape)))) + (when-not (= (count shapes) (count (distinct shapes))) (report-error :duplicated-children - (str/ffmt "Shape % has duplicated children" (:id shape)) + (str/ffmt "Shape % has duplicated children" shape-id) shape file page)) - (doseq [child-id (:shapes shape)] + (doseq [child-id shapes] (let [child (ctst/get-shape page child-id)] (if (nil? child) (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 - :parent-id (:id shape) + :parent-id shape-id :child-id child-id) - (when (not= (:parent-id child) (:id shape)) + (when (not= (:parent-id child) shape-id) (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 - :parent-id (:id shape)))))))))) + :parent-id shape-id))))))))) (defn- check-frame "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 frame-id of the parent (if not)." - [shape file page] - (let [frame (ctst/get-shape page (:frame-id shape))] + [{:keys [frame-id] :as shape} file page] + (let [frame (ctst/get-shape page frame-id)] (if (nil? frame) (report-error :frame-not-found - (str/ffmt "Frame % not found" (:frame-id shape)) + (str/ffmt "Frame % not found" frame-id) shape file page) (if (not= (:type frame) :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) (let [parent (ctst/get-shape page (:parent-id shape))] (when (some? parent) (if (= (:type parent) :frame) - (when-not (= (:frame-id shape) (:id parent)) + (when-not (= frame-id (:id parent)) (report-error :invalid-frame (str/ffmt "Frame-id should point to parent %" (:id parent)) shape file page)) - (when-not (= (:frame-id shape) (:frame-id parent)) + (when-not (= frame-id (:frame-id parent)) (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))))))))) (defn- check-component-main-head @@ -289,8 +293,7 @@ (check-component-main-head shape file page libraries) (check-component-root shape file page) (check-component-not-ref shape file page) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :main-top))) + (run! #(check-shape % file page libraries :context :main-top) (:shapes shape))) (defn- check-shape-main-root-nested "Root shape of a nested main instance @@ -301,8 +304,7 @@ (check-component-main-head shape file page libraries) (check-component-not-root shape file page) (check-component-not-ref shape file page) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :main-nested))) + (run! #(check-shape % file page libraries :context :main-nested) (:shapes shape))) (defn- check-shape-copy-root-top "Root shape of a top copy instance @@ -314,8 +316,7 @@ (check-component-not-main-head shape file page libraries) (check-component-root shape file page) (check-component-ref shape file page libraries) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :copy-top))) + (run! #(check-shape % file page libraries :context :copy-top) (:shapes shape))) (defn- check-shape-copy-root-nested "Root shape of a nested copy instance @@ -326,8 +327,7 @@ (check-component-not-main-head shape file page libraries) (check-component-not-root shape file page) (check-component-ref shape file page libraries) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :copy-nested))) + (run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape))) (defn- check-shape-main-not-root "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-root shape file page) (check-component-not-ref shape file page) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :main-any))) + (run! #(check-shape % file page libraries :context :main-any) (:shapes shape))) (defn- check-shape-copy-not-root "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-root shape file page) (check-component-ref shape file page libraries) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :copy-any))) + (run! #(check-shape % file page libraries :context :copy-any) (:shapes shape))) (defn- check-shape-not-component "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-root shape file page) (check-component-not-ref shape file page) - (doseq [child-id (:shapes shape)] - (check-shape child-id file page libraries :context :not-component))) + (run! #(check-shape % file page libraries :context :not-component) (:shapes shape))) (defn- check-shape "Validate referential integrity and semantic coherence of @@ -439,6 +436,11 @@ "Objects list cannot be 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 @@ -451,18 +453,14 @@ [{:keys [data features] :as file} libraries] (when (contains? features "components/v2") (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))] - (check-component component file)) + (doseq [page (filter :id (ctpl/pages-seq data))] + (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)))) diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index 52efef50e..d435d861c 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -67,7 +67,8 @@ ([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 [matrix-str] @@ -76,8 +77,8 @@ (map (comp d/parse-double first)))] (apply matrix params))) -(sm/def! ::matrix-map - [:map {:title "MatrixMap"} +(def ^:private schema:matrix-attrs + [:map {:title "MatrixAttrs"} [:a ::sm/safe-double] [:b ::sm/safe-double] [:c ::sm/safe-double] @@ -85,6 +86,10 @@ [:e ::sm/safe-double] [:f ::sm/safe-double]]) +(def valid-matrix? + (sm/lazy-validator + [:and [:fn matrix?] schema:matrix-attrs])) + (sm/def! ::matrix (letfn [(decode [o] (if (map? o) @@ -101,7 +106,7 @@ (dm/get-prop o :f) ","))] {:type ::matrix - :pred matrix? + :pred valid-matrix? :type-properties {:title "matrix" :description "Matrix instance" diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index fbe7e8e41..0a04fa747 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -41,12 +41,6 @@ [v] (instance? Point v)) -(sm/def! ::point-map - [:map {:title "PointMap"} - [:x ::sm/safe-number] - [:y ::sm/safe-number]]) - - ;; FIXME: deprecated (s/def ::x ::us/safe-number) (s/def ::y ::us/safe-number) @@ -57,6 +51,16 @@ (s/def ::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 (letfn [(decode [p] (if (map? p) @@ -71,7 +75,7 @@ (dm/get-prop p :y)))] {:type ::point - :pred point? + :pred valid-point? :type-properties {:title "point" :description "Point" diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc index 445eb06cc..ce01fb0cb 100644 --- a/common/src/app/common/geom/rect.cljc +++ b/common/src/app/common/geom/rect.cljc @@ -12,6 +12,8 @@ [app.common.geom.point :as gpt] [app.common.math :as mth] [app.common.record :as rc] + [app.common.schema :as sm] + [app.common.schema.generators :as sg] [app.common.transit :as t])) (rc/defrecord Rect [x y width height x1 y1 x2 y2]) @@ -66,6 +68,31 @@ h (mth/max height 0.01)] (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 (make-rect 0 0 0.01 0.01)) diff --git a/common/src/app/common/geom/shapes/tree_seq.cljc b/common/src/app/common/geom/shapes/tree_seq.cljc index b846a2129..8ed6b61b8 100644 --- a/common/src/app/common/geom/shapes/tree_seq.cljc +++ b/common/src/app/common/geom/shapes/tree_seq.cljc @@ -92,5 +92,11 @@ (defn resolve-subtree "Resolves the subtree but only partialy from-to the parameters" [from-id to-id objects] - (->> (get-children-seq from-id objects) - (d/take-until #(= (:id %) to-id)))) + (concat + (->> (get-children-seq from-id objects) + (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))) diff --git a/common/src/app/common/pprint.cljc b/common/src/app/common/pprint.cljc index 66925e7ef..e1c9ea39e 100644 --- a/common/src/app/common/pprint.cljc +++ b/common/src/app/common/pprint.cljc @@ -9,9 +9,26 @@ (:require [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 [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 *print-length* length] (pp/pprint expr {:max-width width}))) diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index 6a486f576..d400f01f1 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -895,9 +895,10 @@ (defn map-nodes [mapfn node] (let [update-content - (fn [content] (cond->> content - (vector? content) - (mapv (partial map-nodes mapfn))))] + (fn [content] + (cond->> content + (vector? content) + (mapv (partial map-nodes mapfn))))] (cond-> node (map? node) @@ -922,7 +923,8 @@ value))) (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] (let [add-defaults (fn [{:keys [tag attrs] :as node}] @@ -984,29 +986,38 @@ (fix-percent-attrs-viewbox [attrs] (d/mapm fix-percent-attr-viewbox attrs)) - (fix-percent-attr-numeric [_ attr-val] - (let [is-percent? (str/ends-with? attr-val "%")] - (if is-percent? - (str (let [attr-num (d/parse-double (str/rtrim attr-val "%"))] - (/ attr-num 100))) - attr-val))) + (fix-percent-attr-numeric-val [val] + (let [val (d/parse-double (str/rtrim val "%"))] + (str (/ val 100)))) - (fix-percent-attrs-numeric [attrs] - (d/mapm fix-percent-attr-numeric attrs)) + (fix-percent-attr-numeric [attrs key val] + (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] (let [units (or (get-in node [:attrs :filterUnits]) (get-in node [:attrs :gradientUnits]) (get-in node [:attrs :patternUnits]) (get-in node [:attrs :clipUnits]))] + (cond-> node (or (= "objectBoundingBox" units) (nil? units)) - (update :attrs fix-percent-attrs-numeric) + (update :attrs #(reduce-kv fix-percent-attr-numeric % %)) (not= "objectBoundingBox" units) (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] (let [redfn (fn [acc {:keys [tag attrs]}] diff --git a/common/src/app/common/svg/shapes_builder.cljc b/common/src/app/common/svg/shapes_builder.cljc index a831d3c0e..1796d08a5 100644 --- a/common/src/app/common/svg/shapes_builder.cljc +++ b/common/src/app/common/svg/shapes_builder.cljc @@ -193,7 +193,8 @@ (defn create-group [name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [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)) vbox (grc/make-rect offset-x offset-y width height)] (cts/setup-shape @@ -304,6 +305,8 @@ rx (d/nilv r rx) 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 ;; have cx and cy attrs, so we default them to 0 diff --git a/common/src/app/common/types/pages_list.cljc b/common/src/app/common/types/pages_list.cljc index 1a1dbb566..a5bbda63e 100644 --- a/common/src/app/common/types/pages_list.cljc +++ b/common/src/app/common/types/pages_list.cljc @@ -34,7 +34,7 @@ (defn pages-seq [fdata] - (vals (:pages-index fdata))) + (-> fdata :pages-index vals seq)) (defn update-page [file-data page-id f] diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index cdc7ccb16..2101f90f7 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -79,25 +79,6 @@ (def text-align-types #{"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 [:vector {:gen/max 4 :gen/min 4} ::gpt/point]) @@ -133,7 +114,7 @@ [:id ::sm/uuid] [:name :string] [:type [::sm/one-of shape-types]] - [:selrect ::selrect] + [:selrect ::grc/rect] [:points ::points] [:transform ::gmt/matrix] [:transform-inverse ::gmt/matrix] @@ -156,7 +137,7 @@ [:main-instance {:optional true} :boolean] [:remote-synced {:optional true} :boolean] [:shape-ref {:optional true} ::sm/uuid] - [:selrect {:optional true} ::selrect] + [:selrect {:optional true} ::grc/rect] [:points {:optional true} ::points] [:blocked {:optional true} :boolean] [:collapsed {:optional true} :boolean] @@ -430,7 +411,7 @@ :name "Path" :fills [] :strokes [{:stroke-style :solid - :stroke-alignment :center + :stroke-alignment :inner :stroke-width 2 :stroke-color clr/black :stroke-opacity 1}]}) diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index f5164e4cd..239304682 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -563,7 +563,7 @@ padding: $s-12; border-radius: $br-8; z-index: $z-index-10; - color: var(--color-foreground-primary); + color: var(--modal-title-foreground-color); background-color: var(--modal-background-color); } @@ -575,7 +575,7 @@ height: 100%; width: 100%; z-index: $z-index-modal; - background-color: var(--color-background-subtle); + background-color: var(--overlay-color); } .modal-container-base { diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 9b8d5bfff..6e227fd21 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -222,6 +222,7 @@ --assets-item-background-color: var(--color-background-tertiary); --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-foreground-color-rest: var(--color-foreground-secondary); --assets-item-name-foreground-color: var(--color-foreground-primary); --assets-item-name-foreground-color-hover: var(--color-foreground-primary); --assets-item-name-foreground-color-disabled: var(--color-foreground-disabled); diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 2dd79469e..f947d5820 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -248,7 +248,7 @@ (assoc :stroke-style :solid) (not (contains? new-attrs :stroke-alignment)) - (assoc :stroke-alignment :center) + (assoc :stroke-alignment :inner) :always (d/without-nils))] diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 58b8c7736..c68dfe00a 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -39,18 +39,7 @@ (def ^:private svgo-config {:multipass false - :plugins - [{:name "safePreset" - :params {:overrides - {:convertColors - {:names2hex true - :shorthex false - :shortname false} - :convertTransform - {:matrixToTransform false - :convertToShorts false - :transformPrecision 4 - :leadingZero false}}}}]}) + :plugins ["safeAndFastPreset"]}) (defn svg->clj [[name text]] diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 7c6aecea5..033c96000 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -393,6 +393,9 @@ unames (volatile! (cfh/get-used-names (:objects page))) 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) + + ;; 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) changes @@ -409,7 +412,15 @@ library-data it 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 (prepare-duplicate-flows shapes page ids-map) @@ -578,27 +589,29 @@ (defn- prepare-duplicate-guides [changes shapes page ids-map delta] (let [guides (get-in page [:options :guides]) - frames (->> shapes - (filter #(= (:type %) :frame))) - new-guides (reduce - (fn [g frame] - (let [new-id (ids-map (:id frame)) - new-frame (-> frame - (gsh/move delta)) - new-guides (->> guides - (vals) - (filter #(= (:frame-id %) (:id frame))) - (map #(-> % - (assoc :id (uuid/next)) - (assoc :frame-id new-id) - (assoc :position (if (= (:axis %) :x) - (+ (:position %) (- (:x new-frame) (:x frame))) - (+ (:position %) (- (:y new-frame) (:y frame))))))))] - (cond-> g - (not-empty new-guides) - (conj (into {} (map (juxt :id identity) new-guides)))))) - guides - frames)] + frames (->> shapes (filter cfh/frame-shape?)) + + new-guides + (reduce + (fn [g frame] + (let [new-id (ids-map (:id frame)) + new-frame (-> frame (gsh/move delta)) + + new-guides + (->> guides + (vals) + (filter #(= (:frame-id %) (:id frame))) + (map #(-> % + (assoc :id (uuid/next)) + (assoc :frame-id new-id) + (assoc :position (if (= (:axis %) :x) + (+ (:position %) (- (:x new-frame) (:x frame))) + (+ (:position %) (- (:y new-frame) (:y frame))))))))] + (cond-> g + (not-empty new-guides) + (conj (into {} (map (juxt :id identity) new-guides)))))) + guides + frames)] (-> (pcb/with-page changes page) (pcb/set-page-option :guides new-guides)))) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 6e47b0a13..99e13369a 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -72,13 +72,15 @@ (watch [it state _] (let [page-id (:current-page-id state) 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) (pcb/with-objects objects)) - changes (cfsh/prepare-move-shapes-into-frame changes - frame-id - shapes - objects)] + + changes (cfsh/prepare-move-shapes-into-frame changes frame-id shapes objects)] + (if (some? changes) (rx/of (dch/commit-changes changes)) (rx/empty)))))) @@ -176,12 +178,16 @@ interactions))) (vals objects)) - ;; If any of the deleted shapes is a frame with guides - guides (into {} - (comp (map second) - (remove #(contains? ids (:frame-id %))) - (map (juxt :id identity))) - (dm/get-in page [:options :guides])) + ids-set (set ids) + guides-to-remove + (->> (dm/get-in page [:options :guides]) + (vals) + (filter #(contains? ids-set (:frame-id %))) + (map :id)) + + guides + (->> guides-to-remove + (reduce dissoc (dm/get-in page [:options :guides]))) starting-flows (filter (fn [flow] diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index a5b23b45d..94a2636f7 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -58,6 +58,10 @@ ;; We need to store the handle-blur ref so we can call it on unmount 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 (mf/use-fn (mf/deps min-value max-value value nillable? default) @@ -102,7 +106,20 @@ (mf/use-fn (mf/deps wrap-value? min-value max-value parse-value apply-value) (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 (let [increment (cond (kbd/shift? event) @@ -152,6 +169,13 @@ (update-input value-str) (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 (mf/use-fn (mf/deps set-delta) @@ -167,7 +191,7 @@ (mf/use-fn (mf/deps parse-value apply-value update-input on-blur) (fn [event] - (let [new-value (or (parse-value) default)] + (let [new-value (or @last-value* default)] (if (or nillable? new-value) (apply-value event new-value) (update-input new-value))) @@ -208,6 +232,7 @@ (obj/set! "defaultValue" (fmt/format-number value)) (obj/set! "title" title) (obj/set! "onKeyDown" handle-key-down) + (obj/set! "onKeyUp" handle-key-up) (obj/set! "onBlur" handle-blur) (obj/set! "onFocus" handle-focus))] diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index f6f70a8b9..0db4f1a87 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -22,6 +22,8 @@ placeholder (unchecked-get props "placeholder") icon (unchecked-get props "icon") autofocus (unchecked-get props "auto-focus") + id (unchecked-get props "id") + handle-change (mf/use-fn @@ -51,7 +53,8 @@ children [:div {:class (stl/css :search-input-wrapper)} icon - [:input {:on-change handle-change + [:input {:id id + :on-change handle-change :value value :auto-focus autofocus :placeholder placeholder diff --git a/frontend/src/app/main/ui/flex_controls/margin.cljs b/frontend/src/app/main/ui/flex_controls/margin.cljs index b764972e0..dee2900b3 100644 --- a/frontend/src/app/main/ui/flex_controls/margin.cljs +++ b/frontend/src/app/main/ui/flex_controls/margin.cljs @@ -71,8 +71,8 @@ [:rect.margin-rect {:x (:x rect-data) :y (:y rect-data) - :width (:width rect-data) - :height (:height rect-data) + :width (max 0 (:width rect-data)) + :height (max 0 (:height rect-data)) :on-pointer-enter on-pointer-enter :on-pointer-leave on-pointer-leave :on-pointer-down on-pointer-down diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index 2c861d01d..28ea90b57 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -32,8 +32,7 @@ :selected (= editing-stop offset)) :data-value (str offset) :on-click on-select-stop - :style {:left (dm/str (* offset 100) "%") - :backgroundColor hex} + :style {:left (dm/str (* offset 100) "%")} :key (dm/str offset)} [:div {:class (stl/css :gradient-stop-color) diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss index a9b4107ee..0fe9529f2 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.scss @@ -32,22 +32,31 @@ .gradient-stop-wrapper { position: absolute; - width: calc(100% - 2rem); + width: calc(100% - $s-40); + left: $s-20; } .gradient-stop { position: absolute; display: grid; grid-template-columns: 50% 50%; + padding: 0; width: $s-16; height: $s-24; border-radius: $br-4; margin-top: calc(-1 * $s-2); margin-left: calc(-1 * $s-8); border: $s-2 solid var(--colorpicker-handlers-color); - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") - left center; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII="); + background-position: left center; + background-size: 8px; &.selected { border: $s-2 solid var(--colorpicker-details-color-selected); } } + +.gradient-stop-color, +.gradient-stop-alpha { + width: 100%; + height: 100%; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss index 1b2789457..78fe72fd4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.scss @@ -176,20 +176,6 @@ 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 { @extend .button-tertiary; height: $s-32; diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 8e4e04d4d..b3739681f 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.sidebar.options.menus.component (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.types.component :as ctk] [app.common.types.file :as ctf] @@ -28,6 +29,7 @@ [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] + [app.util.timers :as tm] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -197,18 +199,21 @@ (mf/defc component-group-item [{:keys [item on-enter-group] :as props}] (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)] [:div {:class (stl/css :component-group) :key (uuid/next) :on-click on-group-click :title group-name} - [:div + + [:div {:class (stl/css :path-wrapper)} (when-not (str/blank? path) [:span {:class (stl/css :component-group-path)} - (str "\u00A0/\u00A0" path)]) + (str "\u00A0\u2022\u00A0" path)]) [:span {:class (stl/css :component-group-name)} (cfh/last-path group-name)]] - [:span i/arrow-refactor]])) + + [:span {:class (stl/css :arrow-icon)} + i/arrow-refactor]])) (mf/defc component-swap [{:keys [shapes] :as props}] @@ -228,7 +233,9 @@ file-id (if every-same-file? (:component-file shape) current-file-id) + orig-components (map #(ctf/get-component libraries (:component-file %) (:component-id %)) shapes) + paths (->> orig-components (map :path) (map cfh/split-path)) @@ -245,6 +252,7 @@ (cfh/join-path (if (not every-same-file?) "" (find-common-path [] 0)))) + filters* (mf/use-state {:term "" :file-id file-id @@ -252,7 +260,9 @@ :listing-thumbs? false}) filters (deref filters*) + is-search? (not (str/blank? (:term filters))) + current-library-id (if (contains? libraries (:file-id filters)) (:file-id filters) current-file-id) @@ -264,7 +274,7 @@ components (->> (get-in libraries [current-library-id :data :components]) vals (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] (let [split-path (cfh/split-path path)] @@ -335,89 +345,99 @@ toggle-list-style (mf/use-fn (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 :element-set-title)} [:span (tr "workspace.options.component.swap")]] [:div {:class (stl/css :component-swap-content)} - [:div {:class (stl/css :search-field)} - [:& search-bar {:on-change on-search-term-change - :clear-action on-search-clear-click - :value (:term filters) - :placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name])) - :icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]] + [:div {:class (stl/css :fields-wrapper)} + [:div {:class (stl/css :search-field)} + [:& search-bar {:on-change on-search-term-change + :clear-action on-search-clear-click + :class (stl/css :search-wrapper) + :id "swap-component-search-filter" + :value (:term filters) + :placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name])) + :icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]] - [:div {:class (stl/css :select-field)} [:& select {:class (stl/css :select-library) :default-value current-library-id :options libraries-options :on-change on-library-change}]] - [:div {:class (stl/css :library-name)} current-library-name] + [: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 :listing-options-wrapper)} - [:& radio-buttons {:class (stl/css :listing-options) - :selected (if (:listing-thumbs? filters) "grid" "list") - :on-change toggle-list-style - :name "swap-listing-style"} - [:& radio-button {:icon i/view-as-list-refactor - :icon-class (stl/css :radio-button) - :value "list" - :id "swap-opt-list"}] - [:& radio-button {:icon i/flex-grid-refactor - :icon-class (stl/css :radio-button) - :value "grid" - :id "swap-opt-grid"}]]] + [:div {:class (stl/css :listing-options-wrapper)} + [:& radio-buttons {:class (stl/css :listing-options) + :selected (if (:listing-thumbs? filters) "grid" "list") + :on-change toggle-list-style + :name "swap-listing-style"} + [:& radio-button {:icon i/view-as-list-refactor + :value "list" + :id "swap-opt-list"}] + [:& radio-button {:icon i/flex-grid-refactor + :value "grid" + :id "swap-opt-grid"}]]]] + (when-not (or is-search? (str/empty? (:path filters))) + [:button {:class (stl/css :component-path) + :on-click on-go-back + :title filter-path-with-dots} + [:span {:class (stl/css :back-arrow)} i/arrow-refactor] + (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]]) - (if (or is-search? (str/empty? (:path filters))) - [:div {:class (stl/css :component-path-empty)}] - [:button {:class (stl/css :component-path) - :on-click on-go-back - :title (:path filters)} - [:span i/arrow-refactor] - [:span (:path filters)]]) + (when (empty? items) + [:div {:class (stl/css :component-list-empty)} + (tr "workspace.options.component.swap.empty")]) ;;TODO review this empty space - (when (empty? items) - [:div {:class (stl/css :component-list-empty)} - (tr "workspace.options.component.swap.empty")]) + (when (:listing-thumbs? filters) + [:div {:class (stl/css :component-list)} + (for [item groups] + [:& component-group-item {:item item :on-enter-group on-enter-group}])]) - (when (:listing-thumbs? filters) - [:div {:class (stl/css :component-list)} - (for [item groups] - [:& component-group-item {:item item :on-enter-group on-enter-group}])]) - - [:div {:class (stl/css-case :component-grid (:listing-thumbs? filters) - :component-list (not (:listing-thumbs? filters)))} - (for [item items] - (if (:id item) - (let [data (get-in libraries [current-library-id :data]) - container (ctf/get-component-page data item) - root-shape (ctf/get-component-root data item) - loop? (or (contains? parent-components (:main-instance-id item)) - (contains? parent-components (:id item)))] - [:& component-swap-item {:key (:id item) - :item item - :loop loop? - :shapes shapes - :file-id current-library-id - :root-shape root-shape - :container container - :component-id current-comp-id - :is-search is-search? - :listing-thumbs (:listing-thumbs? filters)}]) - [:& component-group-item {:item item :on-enter-group on-enter-group}]))]]])) + [:div {:class (stl/css-case :component-grid (:listing-thumbs? filters) + :component-list (not (:listing-thumbs? filters)))} + (for [item items] + (if (:id item) + (let [data (get-in libraries [current-library-id :data]) + container (ctf/get-component-page data item) + root-shape (ctf/get-component-root data item) + loop? (or (contains? parent-components (:main-instance-id item)) + (contains? parent-components (:id item)))] + [:& component-swap-item {:key (:id item) + :item item + :loop loop? + :shapes shapes + :file-id current-library-id + :root-shape root-shape + :container container + :component-id current-comp-id + :is-search is-search? + :listing-thumbs (:listing-thumbs? filters)}]) + [:& component-group-item {:item item + :key (:id item) + :on-enter-group on-enter-group}]))]]]])) (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 (fn [action event] (dom/stop-propagation event) (action) (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))] [:li {:key (uuid/next) :class (stl/css :dropdown-element) @@ -471,10 +491,14 @@ open-component-panel (mf/use-fn (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) - show-menu? (seq menu-entries)] + show-menu? (seq menu-entries) + path (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))] (when (seq shapes) [:div {:class (stl/css :element-set)} @@ -482,8 +506,9 @@ (if swap-opened? [:button {:class (stl/css :title-back) :on-click on-component-back} - [:span i/arrow-refactor] + [:span {:class (stl/css :icon-back)} i/arrow-refactor] [:span (tr "workspace.options.component")]] + [:& title-bar {:collapsable true :collapsed (not open?) :on-collapsed toggle-content @@ -496,31 +521,40 @@ (when open? [:div {:class (stl/css :element-content)} - [:div {:class (stl/css :component-wrapper)} - [:div {:class (stl/css-case :component-name-wrapper true - :with-main (and can-swap? (not multi)) - :swappeable (and can-swap? (not swap-opened?))) - :on-click open-component-panel} + [:div {:class (stl/css-case :component-wrapper true + :with-actions show-menu?)} + [:button {:class (stl/css-case :component-name-wrapper true + :with-main (and can-swap? (not multi)) + :swappeable (and can-swap? (not swap-opened?))) + :on-click open-component-panel} + [:span {:class (stl/css :component-icon)} (if main-instance? i/component-refactor i/copy-refactor)] - [:div {:class (stl/css :component-name)} (if multi - (tr "settings.multiple") - (cfh/last-path shape-name))] - (when show-menu? - [:div {:class (stl/css :component-actions)} - [:button {:class (stl/css :menu-btn) - :on-click on-menu-click} - i/menu-refactor] + [:div {:class (stl/css :name-wrapper)} + [:div {:class (stl/css :component-name)} + (if multi + (tr "settings.multiple") + (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? + [:div {:class (stl/css :component-actions)} + [:button {:class (stl/css-case :menu-btn true + :selected menu-open?) + :on-click on-menu-click} + i/menu-refactor] + + [:& component-ctx-menu {:show menu-open? + :on-close on-menu-close + :menu-entries menu-entries + :main-instance main-instance?}]])] - [:& component-ctx-menu {:show menu-open? - :on-close on-menu-close - :menu-entries menu-entries}]]) - (when (and can-swap? (not multi)) - [:div {:class (stl/css :component-parent-name)} - (cfh/merge-path-item (:path component) (:name component))])]] (when swap-opened? [:& component-swap {:shapes copies}]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss index 8f63af2ed..fb0e5a34b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss @@ -7,33 +7,132 @@ @import "refactor/common-refactor.scss"; .element-set { margin: 0; + padding-top: $s-8; } .element-content { @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 { - display: flex; - margin: 0 $s-4 0 $s-8; + width: 100%; + min-height: $s-32; + border-radius: $br-8; + + &.with-actions { + display: grid; + grid-template-columns: 1fr $s-28; + gap: $s-2; + } } .component-name-wrapper { - @extend .asset-element; - @include flexRow; - flex-grow: 1; - height: 100%; - width: 100%; - flex-wrap: wrap; - padding: 0 0 0 $s-12; - margin-top: $s-8; - - &.with-main { - padding-bottom: $s-12; + @include buttonStyle; + cursor: default; + display: grid; + grid-template-columns: $s-12 1fr; + gap: $s-4; + padding: 0 $s-8; + border-radius: $br-8 0 0 $br-8; + background-color: var(--assets-item-background-color); + color: var(--assets-item-name-foreground-color-hover); + &:hover { + 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 { @include titleTipography; height: 100%; @@ -43,52 +142,10 @@ 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 { cursor: pointer; } -.component-actions { - position: relative; -} - -.menu-btn { - @extend .button-tertiary; - height: $s-32; - width: $s-28; - svg { - @extend .button-icon; - } -} - .custom-select-dropdown { @extend .dropdown-wrapper; right: 0; @@ -96,44 +153,14 @@ width: $s-252; } +.not-main { + top: $s-56; +} + .dropdown-element { @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 { display: flex; svg { @@ -175,52 +202,52 @@ .search-icon { @include flexCenter; - width: $s-28; + width: $s-12; + margin-left: $s-8; svg { @extend .button-icon-small; 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 { - @include titleTipography; - @include textEllipsis; - text-align: left; - cursor: pointer; + display: flex; + align-items: center; + gap: $s-4; width: 100%; + height: $s-32; + padding: 0; + border: 0; background-color: var(--title-background-color); color: var(--title-foreground-color); - border: 0; - margin: $s-16 0 $s-12 0; - padding: 0 $s-16 0 $s-24; + cursor: pointer; +} + +.back-arrow { + @include flexCenter; + height: $s-32; svg { - height: $s-8; - width: $s-8; + height: $s-12; + width: $s-12; stroke: var(--icon-foreground); - margin-right: $s-16; transform: rotate(180deg); } } -.component-path-empty { - height: $s-16; +.path-name { + @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 { @@ -229,10 +256,6 @@ color: $df-secondary; } -.component-list { - margin: 0 $s-4 0 $s-8; -} - .component-item { display: flex; 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 { display: grid; 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 { @include titleTipography; @include textEllipsis; - margin: $s-20 $s-4 0 $s-12; 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; - text-transform: uppercase; - margin: $s-16 $s-4 0 $s-12; - color: var(--title-foreground-color); + display: grid; + grid-template-columns: 1fr $s-12; + 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 { - stroke: var(--icon-foreground); - fill: var(--icon-foreground); height: $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 { diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs index f1c83cbbc..383682991 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/grid_cell.cljs @@ -221,7 +221,7 @@ (when (and (not multiple?) (or (= :manual cell-mode) (= :area cell-mode))) [:div {:class (stl/css :row)} [: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)} [:> numeric-input* {:placeholder "--" @@ -236,7 +236,7 @@ :value column-end}]]] [: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)} [:> numeric-input* {:placeholder "--" diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index fb31665c4..3e82f4363 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -112,8 +112,7 @@ (fn [color] (st/emit! (dch/update-shapes ids - #(assoc-in % [:shadow index :color] - (dissoc color :id :file-id)))))) + #(assoc-in % [:shadow index :color] color))))) detach-color (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 337882fe9..13a2984a4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -147,7 +147,8 @@ :stroke-cap-end stroke-cap-start} index))))) on-add-stroke (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-opacity 1 :stroke-width 1})) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 906925620..e3e2e874c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -255,6 +255,7 @@ :on-focus on-focus :on-blur on-blur :on-change handle-opacity-change + :default 100 :min 0 :max 100}]])] diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.scss b/frontend/src/app/main/ui/workspace/top_toolbar.scss index 9777a3f7b..d6140e99e 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.scss +++ b/frontend/src/app/main/ui/workspace/top_toolbar.scss @@ -7,6 +7,7 @@ @import "refactor/common-refactor.scss"; .main-toolbar { + cursor: initial; position: absolute; top: $s-28; left: calc(50% - $s-160); diff --git a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss index 48c4186d4..f12752f6c 100644 --- a/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss +++ b/frontend/src/app/main/ui/workspace/viewport/grid_layout_editor.scss @@ -125,6 +125,7 @@ height: $s-48; margin-left: -50%; padding: $s-8; + cursor: initial; pointer-events: initial; width: $s-512; } diff --git a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss index 52690c53e..c4d346603 100644 --- a/frontend/src/app/main/ui/workspace/viewport/path_actions.scss +++ b/frontend/src/app/main/ui/workspace/viewport/path_actions.scss @@ -7,6 +7,7 @@ @import "refactor/common-refactor.scss"; .sub-actions { + cursor: initial; pointer-events: initial; position: absolute; top: $s-12; diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss index 7f8c2e39f..b83e795d6 100644 --- a/frontend/src/app/main/ui/workspace/viewport/top_bar.scss +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.scss @@ -22,6 +22,7 @@ height: $s-48; margin-left: -50%; padding: $s-8; + cursor: initial; pointer-events: initial; width: $s-400; } diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index b1288fc49..316cf6d0b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -130,7 +130,11 @@ (on-frame-leave (:id 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)) [:g.frame-title {:id (dm/str "frame-title-" (:id frame)) @@ -138,7 +142,7 @@ :transform (vwu/title-transform frame zoom grid-edition?) :pointer-events (when (:blocked frame) "none")} (cond - (or (:use-for-thumbnail frame) grid-edition? main-instance?) + show-icon? [:svg {:x 0 :y -9 :width 12 @@ -157,21 +161,24 @@ main-instance? [:use {:href "#icon-component-refactor"}])]) - [:text {:x text-pos-x - :y 0 - :width (:width frame) - :height 20 - :class "workspace-frame-label" - :style {:fill color} - :visibility (if show-artboard-names? "visible" "hidden") - :on-pointer-down on-pointer-down - :on-double-click on-double-click - :on-context-menu on-context-menu - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave} - (if show-id? - (dm/str (dm/str (:id frame)) " - " (:name frame)) - (:name frame))]]))) + + [:foreignObject {:x text-pos-x + :y -11 + :width (max 0 (- text-width text-pos-x)) + :height 20 + :class "workspace-frame-label" + :style {:fill color} + :visibility (if show-artboard-names? "visible" "hidden") + :on-pointer-down on-pointer-down + :on-double-click on-double-click + :on-context-menu on-context-menu + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave} + [:div {:class (stl/css :workspace-frame-label) + :style {:color color}} + (if show-id? + (dm/str (dm/str (:id frame)) " - " (:name frame)) + (:name frame))]]]))) (mf/defc frame-titles {::mf/wrap-props false diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.scss b/frontend/src/app/main/ui/workspace/viewport/widgets.scss index 2fc2a81da..326057ce1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.scss +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.scss @@ -62,3 +62,11 @@ } } } + +.workspace-frame-label { + font-size: $fs-12; + color: black; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} diff --git a/frontend/src/app/util/color.cljs b/frontend/src/app/util/color.cljs index 79989b389..aeb95d007 100644 --- a/frontend/src/app/util/color.cljs +++ b/frontend/src/app/util/color.cljs @@ -37,14 +37,19 @@ ;; TODO: REMOVE `VALUE` WHEN COLOR IS INTEGRATED (defn color->background [{:keys [color opacity gradient value]}] - (let [color (or color value) + (let [color (d/nilv color value) opacity (or opacity 1)] + (cond (and gradient (not= :multiple gradient)) (gradient->css gradient) - (not= color :multiple) - (let [[r g b] (cc/hex->rgb (or color value))] + (and (some? color) (not= color :multiple)) + (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)) :else "transparent")))