From 751bed411796ca2838e3ebaf7ba59b6b2050abd5 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Wed, 7 May 2025 09:29:41 +0200 Subject: [PATCH] :sparkles: Manage overrides on variants switch --- common/src/app/common/logic/libraries.cljc | 54 +++++++++++++++++-- common/src/app/common/logic/variants.cljc | 45 ++++++++++++++++ common/src/app/common/types/shape.cljc | 44 +++++++++++++++ .../app/main/data/workspace/libraries.cljs | 22 +++++--- .../sidebar/options/menus/component.cljs | 2 +- 5 files changed, 153 insertions(+), 14 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 7f0fa513a..2566f51f2 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -25,6 +25,7 @@ [app.common.types.file :as ctf] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] @@ -43,6 +44,12 @@ (def log-shape-ids #{}) (def log-container-ids #{}) +(def updatable-attrs (->> (seq (keys ctk/sync-attrs)) + ;; We don't update the flex-child attrs + (remove ctk/swap-keep-attrs) + ;; We don't do automatic update of the `layout-grid-cells` property. + (remove #(= :layout-grid-cells %)))) + (defn enabled-shape? [id container] (or (empty? log-shape-ids) @@ -1704,11 +1711,7 @@ origin-shape (reposition-shape origin-shape origin-root dest-root) touched (get dest-shape :touched #{})] - (loop [attrs (->> (seq (keys ctk/sync-attrs)) - ;; We don't update the flex-child attrs - (remove ctk/swap-keep-attrs) - ;; We don't do automatic update of the `layout-grid-cells` property. - (remove #(= :layout-grid-cells %))) + (loop [attrs updatable-attrs roperations [] uoperations '()] @@ -1730,6 +1733,47 @@ roperations' uoperations'))))))) +(defn update-attrs-on-switch + "Copy attributes that have changed in the origin shape to the dest shape. Used on variants switch" + [changes dest-shape origin-shape dest-root origin-root origin-ref-shape container] + (let [;; We need to sync only the position relative to the origin of the component. + ;; (see update-attrs for a full explanation) + origin-shape (reposition-shape origin-shape origin-root dest-root) + touched (get dest-shape :touched #{}) + touched-origin (get origin-shape :touched #{})] + + (loop [attrs updatable-attrs + roperations [{:type :set-touched :touched (:touched origin-shape)}] + uoperations (list {:type :set-touched :touched (:touched dest-shape)})] + (if-let [attr (first attrs)] + (let [attr-group (get ctk/sync-attrs attr) + [roperations' uoperations'] + (if (or + ;; If the attribute is not valid for the destiny, don't copy it + (not (cts/is-allowed-attr? attr (:type dest-shape))) + ;; If the values are already equal, don't copy it + (= (get origin-shape attr) (get dest-shape attr)) + ;; If the referenced shape on the original component doesn't have the same value, don't copy it + ;; Exceptions: :points :selrect and :content can be different + (and + (not (contains? #{:points :selrect :content} attr)) + (not= (get origin-ref-shape attr) (get dest-shape attr))) + ;; The :content attr cant't be copied to elements of different type + (and (= attr :content) (not= (:type origin-shape) (:type dest-shape))) + ;; If the attr is not touched in the origin shape, don't copy it + (not (touched-origin attr-group))) + [roperations uoperations] + (add-update-attr-operations attr dest-shape origin-shape roperations uoperations touched))] + (recur (next attrs) + roperations' + uoperations')) + (cond-> changes + (> (count roperations) 1) + (add-update-attr-changes dest-shape container roperations uoperations) + + :always + (generate-update-tokens container dest-shape origin-shape touched false)))))) + (defn- propagate-attrs "Helper that puts the origin attributes (attrs) into dest but only if not touched the group or if omit-touched? flag is true" diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 73573d48d..1e76ad095 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -1,12 +1,30 @@ (ns app.common.logic.variants (:require [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] [app.common.files.variant :as cfv] [app.common.logic.libraries :as cll] [app.common.logic.variant-properties :as clvp] + [app.common.types.components-list :as ctcl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] [app.common.types.variant :as ctv])) +(defn- generate-path + [path objects base-id shape] + (let [get-type #(case % + :frame :container + :group :container + :rect :shape + :circle :shape + :bool :shape + :path :shape + %)] + (if (= base-id (:id shape)) + path + (generate-path (str path " " (:name shape) (get-type (:type shape))) objects base-id (get objects (:parent-id shape)))))) + (defn generate-add-new-variant [changes shape variant-id new-component-id new-shape-id prop-num] (let [data (pcb/get-library-data changes) @@ -28,3 +46,30 @@ (-> changes (clvp/generate-update-property-value new-component-id prop-num value) (pcb/change-parent (:parent-id shape) [new-shape] 0)))) + +(defn generate-keep-touched + [changes new-shape original-shape original-shapes page] + (let [data (pcb/get-library-data changes) + objects (pcb/get-objects changes) + + orig-comp (ctcl/get-component data (:component-id original-shape) true) + + new-path-map (into {} + (map (fn [shape] {(generate-path "" objects (:id new-shape) shape) shape})) + (cfh/get-children-with-self objects (:id new-shape))) + + orig-touched (filter (comp seq :touched) original-shapes) + orig-objects (into {} (map (juxt :id identity) original-shapes)) + container (ctn/make-container page :page)] + (reduce + (fn [changes touched-shape] + (let [path (generate-path "" orig-objects (:id original-shape) touched-shape) + related-shape (get new-path-map path) + orig-ref-shape (ctf/get-ref-shape data orig-comp touched-shape)] + (if related-shape + (cll/update-attrs-on-switch + changes related-shape touched-shape new-shape original-shape orig-ref-shape container) + changes))) + changes + orig-touched))) + diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 09938b9ba..c367d22ef 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -397,6 +397,50 @@ (or (some :fill-image fills) (some :stroke-image strokes))) +;; Valid attributes + +(def ^:private allowed-shape-attrs #{:page-id :component-id :component-file :component-root :main-instance + :remote-synced :shape-ref :touched :blocked :collapsed :locked + :hidden :masked-group :fills :proportion :proportion-lock :constraints-h + :constraints-v :fixed-scroll :r1 :r2 :r3 :r4 :opacity :grids :exports + :strokes :blend-mode :interactions :shadow :blur :grow-type :applied-tokens + :plugin-data}) +(def ^:private allowed-shape-geom-attrs #{:x :y :width :height}) +(def ^:private allowed-shape-base-attrs #{:id :name :type :selrect :points :transform :transform-inverse :parent-id :frame-id}) +(def ^:private allowed-bool-attrs #{:shapes :bool-type :content}) +(def ^:private allowed-group-attrs #{:shapes}) +(def ^:private allowed-frame-attrs #{:shapes :hide-fill-on-export :show-content :hide-in-viewer}) +(def ^:private allowed-image-attrs #{:metadata}) +(def ^:private allowed-svg-attrs #{:content}) +(def ^:private allowed-path-attrs #{:content}) +(def ^:private allowed-text-attrs #{:content}) +(def ^:private allowed-generic-attrs (set/union allowed-shape-attrs allowed-shape-geom-attrs allowed-shape-base-attrs)) + +(defn is-allowed-attr? + [attr type] + (case type + :group (or (contains? allowed-group-attrs attr) + (contains? allowed-generic-attrs attr)) + :frame (or (contains? allowed-frame-attrs attr) + (contains? allowed-generic-attrs attr)) + :bool (or (contains? allowed-bool-attrs attr) + (contains? allowed-shape-attrs attr) + (contains? allowed-shape-base-attrs attr)) + :rect (contains? allowed-generic-attrs attr) + :circle (contains? allowed-generic-attrs attr) + :image (or (contains? allowed-image-attrs attr) + (contains? allowed-generic-attrs attr)) + :svg-raw (or (contains? allowed-svg-attrs attr) + (contains? allowed-generic-attrs attr)) + :path (or (contains? allowed-path-attrs attr) + (contains? allowed-shape-attrs attr) + (contains? allowed-shape-base-attrs attr)) + :text (or (contains? allowed-text-attrs attr) + (contains? allowed-generic-attrs attr)))) + + + + ;; --- Initialization (def ^:private minimal-rect-attrs diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 53a70f4bf..777c63b11 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -16,6 +16,7 @@ [app.common.logging :as log] [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] + [app.common.logic.variants :as clv] [app.common.types.color :as ctc] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] @@ -988,7 +989,7 @@ (defn component-swap "Swaps a component with another one" - [shape file-id id-new-component] + [shape file-id id-new-component keep-touched?] (dm/assert! (uuid? id-new-component)) (dm/assert! (uuid? file-id)) (ptk/reify ::component-swap @@ -996,12 +997,13 @@ (watch [it state _] ;; First delete shapes so we have space in the layout otherwise we can have problems ;; in the grid creating new rows/columns to make space - (let [libraries (dsh/lookup-libraries state) - page (dsh/lookup-page state) - objects (:objects page) - parent (get objects (:parent-id shape)) + (let [libraries (dsh/lookup-libraries state) + page (dsh/lookup-page state) + objects (:objects page) + parent (get objects (:parent-id shape)) - ldata (dsh/lookup-file-data state file-id) + ldata (dsh/lookup-file-data state file-id) + orig-shapes (when keep-touched? (cfh/get-children-with-self objects (:id shape))) ;; If the target parent is a grid layout we need to pass the target cell target-cell (when (ctl/grid-layout? parent) @@ -1018,7 +1020,11 @@ [new-shape all-parents changes] (-> (pcb/empty-changes it (:id page)) (pcb/set-undo-group undo-group) - (cll/generate-component-swap objects shape ldata page libraries id-new-component index target-cell keep-props-values))] + (cll/generate-component-swap objects shape ldata page libraries id-new-component index target-cell keep-props-values)) + + changes (if keep-touched? + (clv/generate-keep-touched changes new-shape shape orig-shapes page) + changes)] (rx/of (dwu/start-undo-transaction undo-id) @@ -1047,7 +1053,7 @@ :undo-id undo-id) (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) - (rx/map #(component-swap % file-id id-new-component) (rx/from shapes)) + (rx/map #(component-swap % file-id id-new-component false) (rx/from shapes)) (rx/of (dwu/commit-undo-transaction undo-id)) (rx/of (dwsp/open-specialized-panel :component-swap))))))) 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 f6429ce52..3c04cfd73 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 @@ -327,7 +327,7 @@ (filter #(= (dm/get-in % [:variant-properties pos :value]) val))) nearest-comp (apply min-key #(ctv/distance target-props (:variant-properties %)) valid-comps)] (when nearest-comp - (st/emit! (dwl/component-swap shape (:component-file shape) (:id nearest-comp))))))))] + (st/emit! (dwl/component-swap shape (:component-file shape) (:id nearest-comp) true)))))))] [:* (for [[pos prop] (map vector (range) properties)]