From 2a210bddc8d269212ef7089385d5c08f8334cbe3 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 14 Oct 2026 22:42:51 +0200 Subject: [PATCH] :sparkles: Build variant actions and update main changes in common Several actions built their changes in frontend events, step by step, each step its own commit, so the pure runner could not run them and one undo could revert half an action. - Renaming a variant member in the layers panel sent three events, each in its own undo transaction: update the properties from the typed formula, remove the properties no member gives a value, and set or clear the member's error. One undo reverted only the last. clvp/generate-rename-member builds the three steps in one changes (with generate-update-properties and generate-remove-empty-properties), and dwv/rename-member commits it once with the same analytics events. update-properties-names-and-values goes; remove-empty-properties and update-error stay for the design tab. - Transforming a component into a variant sent about fourteen events, and combining components as variants that transform and eight more. clv/generate-transform-in-variant and clv/generate-combine-as-variants build those steps as one changes, from the generators common already had, and the events commit them once. The flex layout initializer moves to common as cls/layout-initializer. Combine still sizes the board with two frontend dimension updates inside its undo transaction: the frontend applies them through modifiers, which common has no step for. - update-component split the changes of an update main (the copy's, marked :local-change?, from the main's, which may belong to a library) with two transducers; cll/split-local-changes does it. Its undo-group argument, never passed, goes, and the main's library is the copy head's :component-file, which it looked up through the file data only to read its id back. - dwv/valid-variant-switch? read the libraries from the state and repeated the lookup of clv/find-switch-target. clv/valid-switch? takes the libraries and asks find-switch-target; the plugin's switchVariant and the frontend test call it. Behavior changes: - One undo after a rename of a member, a transform or a combine reverts all of it. After a transform, it reverted only the sync of the copies' names (F6 in case AT); its pins go. - The transform no longer sends the "create-board" analytics event of the board it creates around the main. In the composable suites, renaming a member in the layers panel, removing the empty properties and setting a member's error are variant property edits both runners run, and case AW joins the JVM set. Its rename node drives the real layers-panel rename, which removes the empty properties too, so the step that removed them goes. The golden masters are recorded again for AW (the JVM one gains the case, the frontend one loses that step) and for AT (its final states no longer list the F6 undo failures). AI-assisted-by: claude-opus-5-5 --- common/src/app/common/logic/libraries.cljc | 13 + common/src/app/common/logic/shapes.cljc | 55 +++ .../app/common/logic/variant_properties.cljc | 42 +++ common/src/app/common/logic/variants.cljc | 173 +++++++++ .../test_helpers/composable/comp/cases.cljc | 54 +-- .../test_helpers/composable/comp/nodes.cljc | 78 ++-- .../logic/composable_sync_test.cljc | 3 + .../test/common_tests/logic/golden_master.edn | 2 + .../common_tests/logic/golden_master_test.clj | 1 + frontend/src/app/main/data/workspace.cljs | 36 +- .../app/main/data/workspace/libraries.cljs | 121 +++--- .../app/main/data/workspace/shape_layout.cljs | 54 +-- .../src/app/main/data/workspace/variants.cljs | 346 ++++-------------- frontend/src/app/plugins/shape.cljs | 3 +- .../composable_tests/golden_master.edn | 6 +- .../composable_tests/interpreter.cljs | 26 +- .../data/workspace_variants_test.cljs | 4 +- 17 files changed, 490 insertions(+), 527 deletions(-) diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index 65909de148..9d2ddde99b 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -395,6 +395,19 @@ (mark-local-changes @synced-ids))) changes))) +(defn split-local-changes + "`changes` of an update main (`generate-sync-shape-inverse`) as `[local + main]`: the changes of the copy, in the current file, and those of the + main, which may live in a library; both without the mark that tells them + apart." + [changes] + (let [part (fn [pred] + (let [xf (comp (filter pred) (map #(dissoc % :local-change?)))] + (-> changes + (update :redo-changes #(into [] xf %)) + (update :undo-changes #(into [] xf %)))))] + [(part :local-change?) (part (complement :local-change?))])) + (defn- update-main-shape "Push the copy shape `shape-inst` into `shape-main`, the main shape it pairs with: the copy's values, overrides included, go into the main, and diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 7324acaae2..31fc414f54 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -11,6 +11,8 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.flex-layout :as flex] + [app.common.geom.shapes.grid-layout :as grid] [app.common.logic.variant-properties :as clvp] [app.common.types.component :as ctk] [app.common.types.container :as ctn] @@ -606,6 +608,59 @@ (cond-> (seq empty-variant-cont) (#(second (generate-delete-shapes % empty-variant-cont {}))))))) +(def initial-flex-layout + {:layout :flex + :layout-flex-dir :row + :layout-gap-type :multiple + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start + :layout-justify-content :start + :layout-align-content :stretch + :layout-wrap-type :nowrap + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) + +(def initial-grid-layout + {:layout :grid + :layout-grid-dir :row + :layout-gap-type :multiple + :layout-gap {:row-gap 0 :column-gap 0} + :layout-align-items :start + :layout-justify-items :start + :layout-align-content :stretch + :layout-justify-content :stretch + :layout-padding-type :simple + :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0} + :layout-grid-cells {} + :layout-grid-rows [] + :layout-grid-columns []}) + +(defn layout-initializer + "A function of a shape and the objects that gives the shape a layout of + `type` (`:flex` or `:grid`), with its parameters calculated from its + children when `calculate-params?`. A shape that was not a board + (`from-frame?` false) also shows its content and hides in the viewer." + [type from-frame? calculate-params?] + (let [[initial-layout-data calculate-params] + (case type + :flex [initial-flex-layout flex/calculate-params] + :grid [initial-grid-layout grid/calculate-params])] + + (fn [shape objects] + (let [shape + (-> shape + (merge initial-layout-data) + + ;; If the original shape is not a frame we set clip content and show-viewer to false + (cond-> (not from-frame?) + (assoc :show-content true :hide-in-viewer true))) + + params (when calculate-params? + (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape))] + (cond-> (merge shape params) + (= type :grid) + (-> (ctl/assign-cells objects) ctl/reorder-grid-children)))))) + (defn change-show-in-viewer [shape hide?] (assoc shape :hide-in-viewer hide?)) diff --git a/common/src/app/common/logic/variant_properties.cljc b/common/src/app/common/logic/variant_properties.cljc index 13c89439fa..978d646245 100644 --- a/common/src/app/common/logic/variant_properties.cljc +++ b/common/src/app/common/logic/variant_properties.cljc @@ -14,6 +14,8 @@ [app.common.types.variant :as ctv] [cuerdas.core :as str])) +(declare generate-add-new-property) + (defn- set-variant-properties "Set the variant properties of `component` to `props`, and the variant name of its main to match them." @@ -89,6 +91,46 @@ #(dissoc % :variant-error) #(assoc % :variant-error value)))))) +(defn generate-update-properties + "Change the properties of the component `component-id` of the variant + `variant-id` from `previous` to `updated` (both `[{:name :value}]`): a + property `updated` leaves out keeps its place with no value, one it adds + goes to every component, empty on the others." + [changes component-id variant-id previous updated] + (let [updated (ctv/update-number-in-repeated-prop-names updated) + set-value (fn [changes pos value] + (generate-update-property-value changes component-id pos value))] + (as-> changes $ + (reduce (fn [changes {:keys [name]}] + (set-value changes (ctv/find-index-for-property-name previous name) "")) + $ + (ctv/find-properties-to-remove previous updated)) + (reduce (fn [changes {:keys [name value]}] + (set-value changes (ctv/find-index-for-property-name previous name) value)) + $ + (ctv/find-properties-to-update previous updated)) + (reduce (fn [changes [idx {:keys [name value]}]] + (-> changes + (generate-add-new-property variant-id {:property-name name}) + (set-value (+ idx (count previous)) value))) + $ + (map-indexed vector (ctv/find-properties-to-add previous updated)))))) + +(defn generate-rename-member + "Rename the component `component-id` of the variant `variant-id` as the + layers panel does with the text `name`: a properties formula + (`ctv/valid-properties-formula?`) gives its properties and clears its + error; any other text leaves its properties with no value and becomes its + error. Then the properties with no value in any component go." + [changes component-id variant-id name] + (let [previous (:variant-properties (ctcl/get-component (pcb/get-library-data changes) component-id)) + formula? (ctv/valid-properties-formula? name)] + (-> changes + (generate-update-properties component-id variant-id previous + (if formula? (ctv/properties-formula->map name) [])) + (generate-remove-empty-properties variant-id) + (generate-set-variant-error component-id (when-not formula? name))))) + (defn generate-reorder-variant-properties [changes variant-id from-pos to-space-between-pos] diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 18a63034ce..60955772ad 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -3,16 +3,21 @@ [app.common.data :as d] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] + [app.common.files.shapes-helpers :as cfsh] [app.common.files.variant :as cfv] + [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.logic.duplicate :as cld] [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] [app.common.logic.swap :as clsw] [app.common.logic.variant-properties :as clvp] + [app.common.path-names :as cpn] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.fills :as types.fills] [app.common.types.shape.layout :as ctsl] [app.common.types.variant :as ctv] [app.common.uuid :as uuid])) @@ -41,6 +46,159 @@ :always (pcb/change-parent (:parent-id shape) [new-shape] 0)))) +;; The board of a variant: a frame with rounded corners and a dashed purple +;; border, which lays its members out in a row when it has a layout. +(def ^:private variant-container-attrs + {:is-variant-container true + :r1 20 + :r2 20 + :r3 20 + :r4 20 + :layout-item-absolute true}) + +(def ^:private variant-container-layout-attrs + {:layout-item-h-sizing :auto + :layout-item-v-sizing :auto + :layout-padding {:p1 30 :p2 30 :p3 30 :p4 30} + :layout-gap {:row-gap 0 :column-gap 20}}) + +(def ^:private variant-container-stroke + {:stroke-alignment :inner + :stroke-style :solid + :stroke-color "#bb97d8" + :stroke-opacity 1 + :stroke-width 2}) + +(defn generate-transform-in-variant + "Turn the component of the main `main-id` into the first member of the new + variant `variant-id`: a board around the main, `delta` from it (by default + 30 px up and left, or none in a layout), named after the first item of the + component's path, with one property per other item of the path, each item + its value (`Property 1`, with value `Value 1`, when the path has one item). + `prefix`, the first items of the path, counts as one item, and `wrapper?` + puts the path under `Component`. With `layout?` the board lays its members + out in a row and hugs them, and with `new-shape-id` a second member, + `new-component-id`, is added." + [changes main-id variant-id {:keys [delta prefix wrapper? layout? new-component-id new-shape-id]}] + (let [objects (pcb/get-objects changes) + main (get objects main-id) + component-id (:component-id main) + path (cond-> (cpn/split-path (cond->> (:name main) wrapper? (str "Component/"))) + (seq prefix) + (->> (drop (count prefix)) + (cons (cpn/join-path prefix)) + vec)) + name (first path) + num-props (max 1 (dec (count path))) + delta (or delta + (if (ctsl/any-layout? (get objects (:parent-id main))) + (gpt/point 0 0) + (gpt/point -30 -30))) + + update-shapes + (fn [changes ids update-fn & {:as options}] + (cls/generate-update-shapes changes ids update-fn (pcb/get-objects changes) options)) + + ;; the member is a duplicate of the main as the changes above left it, + ;; so the page of the library data takes their objects + add-member + (fn [changes] + (let [objects (pcb/get-objects changes) + data (-> (pcb/get-library-data changes) + (assoc-in [:pages-index (pcb/get-page-id changes) :objects] objects)) + component (ctkl/get-component data component-id)] + (-> changes + (pcb/with-library-data data) + (generate-add-new-variant (get objects main-id) variant-id new-component-id new-shape-id + (dec (count (:variant-properties component)))))))] + (as-> changes $ + (cond-> $ + (not= name (:name main)) + ;; applied to the library data, so the updates below keep it + (pcb/update-component component-id + #(let [[path name] (cpn/split-group-name name)] + (assoc % :path path :name name)) + {:apply-changes-local-library? true})) + (second (cfsh/prepare-create-artboard-from-selection $ variant-id nil (pcb/get-objects $) + [main-id] nil nil false nil delta)) + (update-shapes $ [variant-id] #(assoc % :fills (types.fills/create))) + (cond-> $ + layout? + (-> (update-shapes [variant-id] (cls/layout-initializer :flex false true) :with-objects? true) + (update-shapes [main-id] #(dissoc % :constraints-h :constraints-v)))) + (update-shapes $ [variant-id] #(merge % variant-container-attrs {:name name} + (when layout? variant-container-layout-attrs))) + (update-shapes $ [main-id] #(assoc % :name name :variant-id variant-id)) + (update-shapes $ [variant-id] #(update % :strokes (fn [strokes] (into [variant-container-stroke] strokes))) + :attrs [:strokes]) + (pcb/update-component $ component-id #(assoc % :variant-id variant-id) + {:apply-changes-local-library? true}) + (reduce (fn [changes _] (clvp/generate-add-new-property changes variant-id {:fill-values? true})) + $ + (range num-props)) + ;; the other items of the path are the values of the properties + (reduce (fn [changes pos] (clvp/generate-update-property-value changes component-id pos (nth path (inc pos)))) + $ + (if (> (count path) 1) (range num-props) [])) + (cond-> $ (some? new-shape-id) (add-member)) + (update-shapes $ [variant-id] #(dissoc % :layout-item-absolute))))) + +(defn- common-prefix + "The items the sequences `xs` start with in common." + [xs] + (->> (apply map vector xs) + (take-while #(apply = %)) + (map first) + vec)) + +(defn- generate-move-into + "Move the shapes `ids` into `parent-id`, at `index`, as the layers panel + does: leaving out those whose parent moves too, and `parent-id` itself or + any shape it is inside." + [changes ids parent-id index] + (let [objects (pcb/get-objects changes) + ids (->> (cfh/clean-loops objects ids) + (remove #(cfh/is-parent? objects parent-id %)))] + (cls/generate-relocate changes parent-id index ids))) + +(defn generate-combine-as-variants + "Combine the components of the mains `ids` into the new variant + `variant-id`: the first one becomes a variant (`generate-transform-in-variant`) + whose board takes in the others, in the order of `ids`, each where it was, + pinned to the board's top left. The board goes to the end of the shape + the mains have in common, and is named with the path items their names + share, or `Component` when they share none. Its size is left to the + caller: the rect of the mains, 30 px larger on each side." + [changes ids variant-id] + (let [objects (pcb/get-objects changes) + shapes (mapv objects ids) + rect (grc/join-rects (map :selrect shapes)) + prefix (common-prefix (map #(cpn/split-path (:name %)) shapes)) + first-shape (first shapes) + common-parent (->> ids + (map #(reverse (cfh/get-parent-ids objects %))) + (common-prefix) + (last)) + index (-> (get objects common-parent) :shapes count inc)] + (as-> changes $ + (generate-transform-in-variant $ (first ids) variant-id + {:delta (gpt/point (- (:x rect) (:x first-shape) 30) + (- (:y rect) (:y first-shape) 30)) + :prefix prefix + :wrapper? (empty? prefix)}) + (generate-move-into $ (set (rest ids)) variant-id 0) + ;; the members in the order of `ids`: the board's children go the other + ;; way, and a change of parent at index 0 reverses the shapes it takes + (let [children (set (get-in (pcb/get-objects $) [variant-id :shapes])) + members (into [] (comp (filter children) (map (pcb/get-objects $))) ids)] + (cond-> $ + (> (count members) 1) + (pcb/change-parent variant-id members 0))) + (cls/generate-update-shapes $ ids + #(assoc % :constraints-h :left :constraints-v :top :fixed-scroll false) + (pcb/get-objects $) {}) + (generate-move-into $ #{variant-id} common-parent index)))) + (defn- generate-path [path objects base-id shape] (let [get-type #(case % @@ -316,6 +474,21 @@ (when (seq candidates) (apply min-key #(ctv/distance target-props (:variant-properties %)) candidates)))))) +(defn valid-switch? + "Whether the variant copy head `shape` can switch its property at position + `pos` to the value `val`: a member of its variant has it." + [libraries shape pos val] + (let [component (ctf/get-component libraries (:component-file shape) (:component-id shape) + :include-deleted? false)] + (and (ctk/instance-head? shape) + (ctk/in-component-copy? shape) + (ctk/is-variant? component) + (nat-int? pos) + (< pos (count (:variant-properties component))) + (string? val) + (or (= val (get-in component [:variant-properties pos :value])) + (some? (find-switch-target libraries shape pos val)))))) + (defn swap-nesting-loop? "Whether swapping, or switching, the copy head `shape` of `objects` for a copy of the component `component-id` of library data `ldata` would nest a diff --git a/common/src/app/common/test_helpers/composable/comp/cases.cljc b/common/src/app/common/test_helpers/composable/comp/cases.cljc index c96f3c3210..b60af59ac3 100644 --- a/common/src/app/common/test_helpers/composable/comp/cases.cljc +++ b/common/src/app/common/test_helpers/composable/comp/cases.cljc @@ -58,11 +58,11 @@ AR — variant property edits keep the copies of a member as they are; a switch then finds its member by the edited properties. AS — an added variant joins the set and is a switch target for copies. - AT..AX — variant management, frontend only: transform a component into a - variant (AT), combine components (AU), rename a variant (AV, F6), - rename a member in the layers panel and remove empty properties - (AW), the shortcuts that add members (AX). Copies of the members - keep their link and overrides. + AT..AX — variant management: transform a component into a variant (AT), + combine components (AU), rename a variant (AV, F6), the shortcuts + that add members (AX), all frontend only, and rename a member in the + layers panel, remove empty properties and set a member's error + (AW). Copies of the members keep their link and overrides. AY — a border radius token on a main reaches untouched copies, and its removal too; a copy's own token survives. AZ — a nested copy pasted out of its main keeps the overrides the main @@ -1358,31 +1358,8 @@ path-name (n/change-property main :name "Button/Primary") first-val #(if (tm/applied? % path-name) "Primary" "Value 1") transform (n/transform-in-variant main) - shortcut (n/variant-shortcut :create-component [main]) - ;; renaming the main to the path's first part syncs the copy's name - ;; in an undo entry of its own: one undo reverts only that sync, so - ;; the component keeps the name and path the transform gave it - f6 (fn [op] - {:bug "F6" - :fails {:undo #{[:object :sync-main-head :frame-id] - [:object :sync-main-head :parent-id] - [:object :sync-main-head :name] - [:object :sync-main-head :variant-id] - [:object :sync-main-head :variant-name] - [:object :sync-main-head :hide-in-viewer] - [:object :unlabelled :extra] - [:object :unlabelled :shapes] - [:component :sync-main-component :name] - [:component :sync-main-component :path] - [:component :sync-main-component :variant-id] - [:component :sync-main-component :variant-properties] - [:component :unlabelled :extra]} - :undo-index #{[:undo-index 1]}} - :op op - :runners #{:frontend} - :when #(tm/applied? % path-name)})] + shortcut (n/variant-shortcut :create-component [main])] {:setup setup/empty-situation - :undo-check {:known-failures [(f6 transform) (f6 shortcut)]} :operation (tm/in-sequence [(n/create-component m base-color :size 100) (n/instantiate-copy m) @@ -1504,15 +1481,15 @@ (t/is (not (contains? (:touched copy) :name-group)))))))})) (defn variant-layer-rename-and-cleanup - "Case AW (frontend only). A copy nests member a of a variant set (a, b; their - rects differ) and overrides the fill of the variant's rect. Member a is - renamed in the layers panel: its value changes and a property Size is - added, empty on b. The set gets a property with no values and the empty + "Case AW. A copy nests member a of a variant set (a, b; their rects + differ) and overrides the fill of the variant's rect. Member a is renamed + in the layers panel: its value changes and a property Size is added, + empty on b. The set gets a property with no values and the empty properties are removed: only that one goes, Size holding a value. A second layers-panel rename drops Size from a, leaving it empty on every - member, and the empty properties are removed again: Size goes. Then - member b gets an error and loses it. The copy stays as it was through - every step, and a switch then finds member b." + member, and the rename removes it. Then member b gets an error and loses + it. The copy stays as it was through every step, and a switch then finds + member b." [] (let [m "main" vset "vset" @@ -1542,11 +1519,6 @@ (t/is (= resized (props s))) (variant-copy-checks s nested (a-id s) green))) (n/rename-variant-in-layers vset "a" [["Property 1" "a2"]]) - (tm/test-that - (fn [s] - (t/is (= [[["Property 1" "a2"] ["Size" ""]] [["Property 1" "b"] ["Size" ""]]] - (props s))))) - (n/remove-empty-properties vset) (tm/test-that (fn [s] (t/is (= [[["Property 1" "a2"]] [["Property 1" "b"]]] (props s))) diff --git a/common/src/app/common/test_helpers/composable/comp/nodes.cljc b/common/src/app/common/test_helpers/composable/comp/nodes.cljc index 3bf1572b54..e013e12a28 100644 --- a/common/src/app/common/test_helpers/composable/comp/nodes.cljc +++ b/common/src/app/common/test_helpers/composable/comp/nodes.cljc @@ -42,6 +42,7 @@ [app.common.types.text :as txt] [app.common.types.tokens-lib :as ctob] [app.common.types.tokens-status :as ctos] + [app.common.types.variant :as ctv] [app.common.uuid :as uuid])) ;; --------------------------------------------------------------------------- @@ -829,7 +830,8 @@ ;; ;; The "Update main component" action on the copy head bound to `target`, ;; through `generate-sync-shape-inverse`. The frontend commits the copy's and -;; the main's changes apart, by `:local-change?`; here they go as one. +;; the main's changes apart (`split-local-changes`); here they go as one, in +;; the order the sync built them. ;; --------------------------------------------------------------------------- (defn- strip-local-change @@ -1436,12 +1438,13 @@ (tm/assign-id (->SwitchVariant target value pos))) ;; --------------------------------------------------------------------------- -;; Variant properties — add, remove, rename, reorder a property of a set, or -;; set the value of a member +;; Variant properties — add, remove, rename, reorder a property of a set, set +;; the value or the error of a member, rename a member in the layers panel, +;; or remove the properties no member gives a value ;; -;; The edits of the design tab's variant properties, through the -;; `app.common.logic.variant-properties` generators (the `dwv` events on the -;; frontend). `variant-property-call` resolves an edit to the generator's +;; The edits of the design tab's variant properties and of the layers panel, +;; through the `app.common.logic.variant-properties` generators (the `dwv` +;; events on the frontend). `variant-property-call` resolves an edit to the generator's ;; arguments, which the frontend events take too. A member is named by the ;; value it was built with (`variant-member`), whatever its value now. ;; --------------------------------------------------------------------------- @@ -1490,7 +1493,15 @@ :set-value [:set-value (variant-member-component-id situation set-name (:member params)) (:pos params) - (:value params)]))) + (:value params)] + :rename-in-layers [:rename-in-layers + (variant-member-component-id situation set-name (:member params)) + variant-id + (:name params)] + :remove-empty [:remove-empty variant-id] + :error [:error + (variant-member-component-id situation set-name (:member params)) + (:value params)]))) (defn- variant-property-changes [changes [edit & args]] @@ -1500,7 +1511,10 @@ :remove (apply clvp/generate-remove-property changes args) :rename (apply clvp/generate-update-property-name changes args) :reorder (apply clvp/generate-reorder-variant-properties changes args) - :set-value (apply clvp/generate-update-property-value changes args))) + :set-value (apply clvp/generate-update-property-value changes args) + :rename-in-layers (apply clvp/generate-rename-member changes args) + :remove-empty (apply clvp/generate-remove-empty-properties changes args) + :error (apply clvp/generate-set-variant-error changes args))) (defrecord EditVariantProperty [set-name edit params] IComponentOperation @@ -1544,6 +1558,30 @@ [set-name from to] (tm/assign-id (->EditVariantProperty set-name :reorder {:from from :to to}))) +(defn rename-variant-in-layers + "Rename set `set-name`'s member built with value `member` in the layers + panel to the properties formula of `properties` (`[[name value] ...]`; + the ones with no value left out), which also removes the properties no + member gives a value (`generate-rename-member`)." + [set-name member properties] + (tm/assign-id + (->EditVariantProperty set-name :rename-in-layers + {:member member + :name (ctv/properties-map->formula + (map (fn [[name value]] {:name name :value value}) properties))}))) + +(defn remove-empty-properties + "Remove the properties of set `set-name` whose value is empty in every + member (`generate-remove-empty-properties`)." + [set-name] + (tm/assign-id (->EditVariantProperty set-name :remove-empty {}))) + +(defn set-variant-error + "Set the error of set `set-name`'s member built with value `member` to + `value`, nil to clear it (`generate-set-variant-error`)." + [set-name member value] + (tm/assign-id (->EditVariantProperty set-name :error {:member member :value value}))) + (defn set-variant-property-value "Set the value of the property at position `pos` of set `set-name`'s member built with value `member` to `value`." @@ -1613,9 +1651,8 @@ ;; Variant actions with no common generator (frontend only) ;; ;; Transform a component into a variant, combine components as variants, -;; rename a variant, rename a member in the layers panel, remove the empty -;; properties, set a member's error, and the shortcuts that add a variant to -;; selected members. Their logic is in the `dwv` events, so the frontend +;; rename a variant, and the shortcuts that add a variant to selected +;; members. Their logic is in the `dwv` events, so the frontend ;; interpreter sends them and the pure runner rejects them. A set these ;; actions build has no `variant-set` record: read it from the file ;; (`component-of`, `variant-component-ids`). @@ -1654,25 +1691,6 @@ [target name] (variant-action :rename {:target target :name name})) -(defn rename-variant-in-layers - "Rename set `set-name`'s member built with value `member` in the layers - panel, to the properties `properties` (`[[name value] ...]`) - (`update-properties-names-and-values`)." - [set-name member properties] - (variant-action :rename-in-layers {:set-name set-name :member member :properties properties})) - -(defn remove-empty-properties - "Remove the properties of set `set-name` whose value is empty in every - member (`remove-empty-properties`)." - [set-name] - (variant-action :remove-empty {:set-name set-name})) - -(defn set-variant-error - "Set the error of set `set-name`'s member built with value `member` to - `value`, nil to clear it (`update-error`)." - [set-name member value] - (variant-action :error {:set-name set-name :member member :value value})) - (defn variant-shortcut "Select the shapes `targets` name and press the shortcut `kind`: `:create-component` (`add-component-or-variant`) or `:duplicate` diff --git a/common/test/common_tests/logic/composable_sync_test.cljc b/common/test/common_tests/logic/composable_sync_test.cljc index 8dc5706cd1..851a348bf2 100644 --- a/common/test/common_tests/logic/composable_sync_test.cljc +++ b/common/test/common_tests/logic/composable_sync_test.cljc @@ -155,6 +155,9 @@ (t/deftest case-as-added-variant-is-a-switch-target (check (cases/added-variant-is-a-switch-target))) +(t/deftest case-aw-variant-layer-rename-and-cleanup + (check (cases/variant-layer-rename-and-cleanup))) + (t/deftest case-ay-token-on-a-main-reaches-copies (check (cases/token-on-a-main-reaches-copies))) diff --git a/common/test/common_tests/logic/golden_master.edn b/common/test/common_tests/logic/golden_master.edn index 0efaec6db5..a36801b4c3 100644 --- a/common/test/common_tests/logic/golden_master.edn +++ b/common/test/common_tests/logic/golden_master.edn @@ -49,6 +49,8 @@ :as [{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-variant-container {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n make-nested-component-with-variant {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n add-variant {:set-name \"vset\", :member \"a\", :component-id 7}\n test\n switch-variant {:target-component 7}", :states ["1247c1fd53a5de3f" "7e0e395e3d0660a0" "1dfa6bfdd7b29e26" "fa90e52391b5ae7f"]} {:transcript "create-component {:component \"main\", :id sync-main-component}\n make-variant-container {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n make-nested-component-with-variant {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n add-variant {:set-name \"vset\", :member \"b\", :component-id 7}\n test\n switch-variant {:target-component 7}", :states ["1247c1fd53a5de3f" "f4146efd3687f0bf" "02f3dd834cc6a5cb" "fb72fa0de22456bd"]}] + :aw + [{:transcript "create-component {:component \"main\", :id sync-main-component}\n make-variant-container {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n make-nested-component-with-variant {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n change-property {:target #fn, :property :fills, :value \"#00ff00\"}\n edit-variant-property {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2, Size=S\"}}\n test\n edit-variant-property {:set-name \"vset\", :edit :add, :params {:property-name \"Empty\"}}\n edit-variant-property {:set-name \"vset\", :edit :remove-empty, :params {}}\n test\n edit-variant-property {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2\"}}\n test\n edit-variant-property {:set-name \"vset\", :edit :error, :params {:member \"b\", :value \"Duplicated\"}}\n test\n edit-variant-property {:set-name \"vset\", :edit :error, :params {:member \"b\", :value nil}}\n test\n switch-variant {:target-component sync-vset-v1-component}", :states ["1247c1fd53a5de3f" "6fdf7a1f1ec406b9" "8c9e9ff80c81a9c2" "6fdf7a1f1ec406b9" "2282ac416e81ba91" "ac2b1a3ccb8fc65c" "2282ac416e81ba91" "c18d55a87e1dcc61" "bd2d896527a50108"]}] :ay [{:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n apply-token {:target #fn, :token-name \"radius.s\", :value 10}\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n unapply-token {:target #fn, :token-name \"radius.l\"}", :states ["59efd69700884e48" "abf5f79fd6265aff" "39b3757cf3962625" "90094c5af982b268"]} {:transcript "create-component {:component \"main\", :id sync-main-component}\n add-tokens {:tokens [[\"radius.s\" 10] [\"radius.l\" 30]]}\n instantiate-copy {:component \"main\", :copy sync-main-copyhead-0}\n apply-token {:target #fn, :token-name \"radius.s\", :value 10}\n apply-token {:target #fn, :token-name \"radius.l\", :value 30}\n test\n one-of -> skip", :states ["59efd69700884e48" "abf5f79fd6265aff" "35f26a160913fed7"]} diff --git a/common/test/common_tests/logic/golden_master_test.clj b/common/test/common_tests/logic/golden_master_test.clj index ebabe6db8a..bb39803ca3 100644 --- a/common/test/common_tests/logic/golden_master_test.clj +++ b/common/test/common_tests/logic/golden_master_test.clj @@ -80,6 +80,7 @@ :aq cases/main-change-reaches-a-copy-nested-in-a-sub-board :ar cases/variant-property-edits-keep-copies :as cases/added-variant-is-a-switch-target + :aw cases/variant-layer-rename-and-cleanup :ay cases/token-on-a-main-reaches-copies :az cases/copy-moved-out-of-its-main-keeps-inherited-overrides)) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 9e703a4bab..a3b685cf25 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -23,10 +23,8 @@ [app.common.render-wasm.wasm :as wasm-state] [app.common.transit :as t] [app.common.types.component :as ctc] - [app.common.types.components-list :as ctkl] [app.common.types.shape :as cts] [app.common.types.tokens-lib :as ctob] - [app.common.types.variant :as ctv] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.changes :as dch] @@ -766,37 +764,6 @@ (update [_ state] (assoc-in state [:workspace-local :shape-for-rename] id)))) -(defn- rename-variant-main - "Apply a layers-panel formula to one variant main. - - A valid formula updates the property names and values. Any other - text keeps the current malformed path: values are cleared and the - text is stored as the variant error." - [file-id shape name] - (ptk/reify ::rename-variant-main - ptk/WatchEvent - (watch [_ state _] - (let [file-data (dsh/lookup-file-data state file-id) - component-id (:component-id shape) - component (ctkl/get-component file-data component-id) - variant-id (:variant-id shape) - variant-properties (:variant-properties component)] - (if (ctv/valid-properties-formula? name) - (rx/of (dwva/update-properties-names-and-values - component-id - variant-id - variant-properties - (ctv/properties-formula->map name)) - (dwva/remove-empty-properties variant-id) - (dwva/update-error component-id)) - (rx/of (dwva/update-properties-names-and-values - component-id - variant-id - variant-properties - {}) - (dwva/remove-empty-properties variant-id) - (dwva/update-error component-id name))))))) - (defn end-rename-shape "End the ongoing shape rename process" ([] (end-rename-shape nil nil)) @@ -849,8 +816,7 @@ (get id)) name (str/trim name)] (if (ctc/variant-formula-rename? shape name) - ;; update-properties-names-and-values clears :shape-for-rename. - (rx/of (rename-variant-main file-id shape name)) + (rx/of (dwva/rename-member (:component-id shape) (:variant-id shape) name)) (rx/of (end-rename-shape id name)))))))) ;; --- Update Selected Shapes attrs diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 1a5a2e1a44..f040cf8e31 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -846,63 +846,43 @@ NOTE: It's possible that the component to update is defined in an external library file, so this function may cause to modify a file different of that the one we are currently editing." - ([id] (update-component id nil)) - ([id undo-group] - (dm/assert! (uuid? id)) - (ptk/reify ::update-component - ptk/WatchEvent - (watch [it state _] - (log/info :msg "UPDATE-COMPONENT of shape" :id (str id) :undo-group undo-group) - (let [page-id (:current-page-id state) + [id] + (dm/assert! (uuid? id)) + (ptk/reify ::update-component + ptk/WatchEvent + (watch [it state _] + (log/info :msg "UPDATE-COMPONENT of shape" :id (str id)) + (let [page-id (:current-page-id state) - libraries (dsh/lookup-libraries state) - file (dsh/lookup-file state) - fdata (:data file) + libraries (dsh/lookup-libraries state) + file (dsh/lookup-file state) + fdata (:data file) - container (ctn/get-container fdata :page page-id) - shape (ctn/get-shape container id)] + container (ctn/get-container fdata :page page-id) + shape (ctn/get-shape container id)] - (when (ctk/instance-head? shape) - (let [changes - (-> (pcb/empty-changes it) - (pcb/set-undo-group undo-group) - (pcb/with-container container) - (cll/generate-sync-shape-inverse libraries container id)) + (when (ctk/instance-head? shape) + (let [[local-changes nonlocal-changes] + (-> (pcb/empty-changes it) + (pcb/with-container container) + (cll/generate-sync-shape-inverse libraries container id) + (cll/split-local-changes))] - ldata (->> (:component-file shape) - (dsh/lookup-file-data state)) + (log/debug :msg "UPDATE-COMPONENT finished" + :js/local-changes (log-changes + (:redo-changes local-changes) + fdata) + :js/nonlocal-changes (log-changes + (:redo-changes nonlocal-changes) + fdata)) - xf-filter (comp - (filter :local-change?) - (map #(dissoc % :local-change?))) - - local-changes (-> changes - (update :redo-changes #(into [] xf-filter %)) - (update :undo-changes #(into [] xf-filter %))) - - xf-remove (comp - (remove :local-change?) - (map #(dissoc % :local-change?))) - - nonlocal-changes (-> changes - (update :redo-changes #(into [] xf-remove %)) - (update :undo-changes #(into [] xf-remove %)))] - - (log/debug :msg "UPDATE-COMPONENT finished" - :js/local-changes (log-changes - (:redo-changes local-changes) - fdata) - :js/nonlocal-changes (log-changes - (:redo-changes nonlocal-changes) - fdata)) - - (rx/of - (when (seq (:redo-changes local-changes)) - (dch/commit-changes (assoc local-changes - :file-id (:id file)))) - (when (seq (:redo-changes nonlocal-changes)) - (dch/commit-changes (assoc nonlocal-changes - :file-id (:id ldata)))))))))))) + (rx/of + (when (seq (:redo-changes local-changes)) + (dch/commit-changes (assoc local-changes + :file-id (:id file)))) + (when (seq (:redo-changes nonlocal-changes)) + (dch/commit-changes (assoc nonlocal-changes + :file-id (:component-file shape))))))))))) (defn- update-component-thumbnail-sync [state component-id file-id tag] @@ -936,26 +916,25 @@ (defn update-component-sync "Update the main of the copy `shape-id` from the copy (see `update-component`), in one undo entry with the sync of its copies." - ([shape-id file-id] (update-component-sync shape-id file-id nil)) - ([shape-id file-id undo-group] - (ptk/reify ::update-component-sync - ptk/WatchEvent - (watch [_ state _] - (let [page (dsh/lookup-page state) - shape (ctn/get-shape page shape-id) - component-id (:component-id shape) - undo-id (js/Symbol)] - (apply rx/of - (concat - [(dwu/start-undo-transaction undo-id) - (update-component shape-id undo-group) + [shape-id file-id] + (ptk/reify ::update-component-sync + ptk/WatchEvent + (watch [_ state _] + (let [page (dsh/lookup-page state) + shape (ctn/get-shape page shape-id) + component-id (:component-id shape) + undo-id (js/Symbol)] + (apply rx/of + (concat + [(dwu/start-undo-transaction undo-id) + (update-component shape-id) - ;; These calls are necessary for properly sync thumbnails - ;; when a main component does not live in the same page. - (update-component-thumbnail-sync state component-id file-id "frame") - (update-component-thumbnail-sync state component-id file-id "component")] - (component-sync-events state component-id file-id undo-group) - [(dwu/commit-undo-transaction undo-id)]))))))) + ;; These calls are necessary for properly sync thumbnails + ;; when a main component does not live in the same page. + (update-component-thumbnail-sync state component-id file-id "frame") + (update-component-thumbnail-sync state component-id file-id "component")] + (component-sync-events state component-id file-id nil) + [(dwu/commit-undo-transaction undo-id)])))))) (defn update-component-thumbnail "Update the thumbnail of the component with the given id, in the diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 2b61791457..82b3886c5a 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -12,9 +12,8 @@ [app.common.files.helpers :as cfh] [app.common.files.shapes-helpers :as cfsh] [app.common.geom.point :as gpt] - [app.common.geom.shapes.flex-layout :as flex] - [app.common.geom.shapes.grid-layout :as grid] [app.common.logic.duplicate :as cld] + [app.common.logic.shapes :as cls] [app.common.types.color :as clr] [app.common.types.component :as ctc] [app.common.types.modifiers :as ctm] @@ -48,55 +47,6 @@ :layout-padding :layout-gap-type]) -(def initial-flex-layout - {:layout :flex - :layout-flex-dir :row - :layout-gap-type :multiple - :layout-gap {:row-gap 0 :column-gap 0} - :layout-align-items :start - :layout-justify-content :start - :layout-align-content :stretch - :layout-wrap-type :nowrap - :layout-padding-type :simple - :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0}}) - -(def initial-grid-layout - {:layout :grid - :layout-grid-dir :row - :layout-gap-type :multiple - :layout-gap {:row-gap 0 :column-gap 0} - :layout-align-items :start - :layout-justify-items :start - :layout-align-content :stretch - :layout-justify-content :stretch - :layout-padding-type :simple - :layout-padding {:p1 0 :p2 0 :p3 0 :p4 0} - :layout-grid-cells {} - :layout-grid-rows [] - :layout-grid-columns []}) - -(defn get-layout-initializer - [type from-frame? calculate-params?] - (let [[initial-layout-data calculate-params] - (case type - :flex [initial-flex-layout flex/calculate-params] - :grid [initial-grid-layout grid/calculate-params])] - - (fn [shape objects] - (let [shape - (-> shape - (merge initial-layout-data) - - ;; If the original shape is not a frame we set clip content and show-viewer to false - (cond-> (not from-frame?) - (assoc :show-content true :hide-in-viewer true))) - - params (when calculate-params? - (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape))] - (cond-> (merge shape params) - (= type :grid) - (-> (ctl/assign-cells objects) ctl/reorder-grid-children)))))) - ;; Never call this directly but through the data-event `:layout/update` ;; Otherwise a lot of cycle dependencies could be generated (defn- update-layout-positions @@ -190,7 +140,7 @@ (let [objects (dsh/lookup-page-objects state) parent (get objects id) undo-id (js/Symbol) - layout-initializer (get-layout-initializer type from-frame? calculate-params?)] + layout-initializer (cls/layout-initializer type from-frame? calculate-params?)] (rx/of (dwu/start-undo-transaction undo-id) (dwsh/update-shapes [id] layout-initializer {:with-objects? true}) diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index ff6a79fb9e..9d5c3501af 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -11,25 +11,20 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.files.variant :as cfv] - [app.common.geom.point :as gpt] + [app.common.geom.rect :as grc] [app.common.logic.variant-properties :as clvp] [app.common.logic.variants :as clv] [app.common.path-names :as cpn] - [app.common.types.color :as clr] [app.common.types.component :as ctc] [app.common.types.components-list :as ctkl] - [app.common.types.file :as ctf] [app.common.types.shape.layout :as ctsl] [app.common.types.variant :as ctv] [app.common.uuid :as uuid] [app.main.data.changes :as dch] - [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] - [app.main.data.workspace.colors :as cl] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] @@ -38,59 +33,45 @@ [potok.v2.core :as ptk])) (defn- variant-changes - "The empty changes of the current page, with its objects and the data of - the file." - [it state] - (let [page-id (:current-page-id state) - data (dsh/lookup-file-data state)] + "The empty changes of the page `page-id` (the current one by default), with + its objects and the data of the file." + [it state & {:keys [page-id] :or {page-id (:current-page-id state)}}] + (let [data (dsh/lookup-file-data state)] (-> (pcb/empty-changes it page-id) (pcb/with-library-data data) (pcb/with-objects (:objects (dsh/get-page data page-id)))))) -(defn update-properties-names-and-values - "Compares the previous properties with the updated ones and executes the correspondent action - for each one depending on if it needs to be removed, updated or added" - [component-id variant-id previous-properties updated-properties] - (ptk/reify ::update-properties-names-and-values +(defn rename-member + "Rename the component `component-id` of the variant `variant-id` from the + layers panel, with the text `name` (`clvp/generate-rename-member`)." + [component-id variant-id name] + (ptk/reify ::rename-member ptk/UpdateEvent (update [_ state] (update state :workspace-local dissoc :shape-for-rename)) ptk/WatchEvent (watch [it state _] - (let [updated-properties (ctv/update-number-in-repeated-prop-names updated-properties) - - properties-to-remove (ctv/find-properties-to-remove previous-properties updated-properties) - properties-to-add (ctv/find-properties-to-add previous-properties updated-properties) - properties-to-update (ctv/find-properties-to-update previous-properties updated-properties) - - changes (reduce - (fn [changes {:keys [name]}] - (-> changes - (clvp/generate-update-property-value component-id (ctv/find-index-for-property-name previous-properties name) ""))) - (variant-changes it state) - properties-to-remove) - - changes (reduce - (fn [changes {:keys [name value]}] - (-> changes - (clvp/generate-update-property-value component-id (ctv/find-index-for-property-name previous-properties name) value))) - changes - properties-to-update) - - changes (reduce - (fn [changes [idx {:keys [name value]}]] - (-> changes - (clvp/generate-add-new-property variant-id {:property-name name}) - (clvp/generate-update-property-value component-id (+ idx (count previous-properties)) value))) - changes - (map-indexed vector properties-to-add))] - + (let [previous (-> (dsh/lookup-file-data state) + (ctkl/get-component component-id) + :variant-properties) + updated (ctv/update-number-in-repeated-prop-names + (if (ctv/valid-properties-formula? name) (ctv/properties-formula->map name) [])) + to-add (ctv/find-properties-to-add previous updated) + changes (-> (variant-changes it state) + (clvp/generate-rename-member component-id variant-id name)) + removed? (< (count (-> (pcb/get-library-data changes) + (ctkl/get-component component-id) + :variant-properties)) + (+ (count previous) (count to-add)))] (rx/of - (when (or (seq properties-to-remove) (seq properties-to-update)) + (when (or (seq (ctv/find-properties-to-remove previous updated)) + (seq (ctv/find-properties-to-update previous updated))) (ev/event {::ev/name "variant-edit-property-value" ::ev/origin "workspace:rename-in-layers"})) - (when (seq properties-to-add) + (when (seq to-add) (ev/event {::ev/name "variant-add-property" ::ev/origin "workspace:rename-in-layers"})) + (when removed? + (ev/event {::ev/name "variant-remove-property" ::ev/origin "workspace:rename-in-layers"})) (dch/commit-changes changes)))))) (defn update-property-name @@ -189,15 +170,6 @@ (rx/of (dch/commit-changes (-> (variant-changes it state) (clvp/generate-reorder-variant-properties variant-id from-pos to-space-between-pos))))))) -(defn- set-variant-id - "Sets the variant-id on a component" - [component-id variant-id] - (ptk/reify ::set-variant-id - ptk/WatchEvent - (watch [it state _] - (rx/of (dch/commit-changes (-> (variant-changes it state) - (pcb/update-component component-id #(assoc % :variant-id variant-id)))))))) - (defn- focus-property [variant-id] (ptk/reify ::focus-property @@ -276,105 +248,24 @@ (defn transform-in-variant "Given the id of a main shape of a component, creates a variant structure for - that component" - ([main-instance-id] - (transform-in-variant main-instance-id nil nil [] false true true)) - ([main-instance-id variant-id delta prefix add-wrapper? duplicate? flex?] - (ptk/reify ::transform-in-variant - ptk/WatchEvent - (watch [_ state _] - (let [variant-id (or variant-id (uuid/next)) - variant-vec [variant-id] - file-id (:current-file-id state) - page-id (:current-page-id state) - objects (dsh/lookup-page-objects state file-id page-id) - main (get objects main-instance-id) - parent (get objects (:parent-id main)) - component-id (:component-id main) - name (if add-wrapper? - (str "Component/" (:name main)) - (:name main)) - ;; If there is a prefix, set is as first item of path - cpath (-> name - cpn/split-path - (cond-> - (seq prefix) - (->> (drop (count prefix)) - (cons (cpn/join-path prefix)) - vec))) - - name (first cpath) - num-props (max 1 (dec (count cpath))) - base-props {:is-variant-container true - :name name - :r1 20 - :r2 20 - :r3 20 - :r4 20 - :layout-item-absolute true} - flex-props {:layout-item-h-sizing :auto - :layout-item-v-sizing :auto - :layout-padding {:p1 30 :p2 30 :p3 30 :p4 30} - :layout-gap {:row-gap 0 :column-gap 20}} - cont-props (if flex? - (into base-props flex-props) - base-props) - main-props {:name name - :variant-id variant-id} - - stroke-props {:stroke-alignment :inner - :stroke-style :solid - :stroke-color "#bb97d8" ;; todo use color var? - :stroke-opacity 1 - :stroke-width 2} - - ;; Move the position of the variant container so the main shape doesn't - ;; change its position - delta (or delta - (if (ctsl/any-layout? parent) - (gpt/point 0 0) - (gpt/point -30 -30))) - undo-id (js/Symbol)] - - - ;;TODO Refactor all called methods in order to be able to - ;;generate changes instead of call the events - - - (rx/concat - (rx/of - (dwu/start-undo-transaction undo-id) - - (when (not= name (:name main)) - (dwl/rename-component component-id name)) - - ;; Create variant container - (dwsh/create-artboard-from-shapes [main-instance-id] variant-id nil nil nil delta flex?) - (cl/remove-all-fills variant-vec {:color clr/black :opacity 1}) - (when flex? (dwsl/create-layout-from-id variant-id :flex)) - (dwsh/update-shapes variant-vec #(merge % cont-props)) - (dwsh/update-shapes [main-instance-id] #(merge % main-props)) - (cl/add-stroke variant-vec stroke-props) - (set-variant-id component-id variant-id)) - - ;; Add the necessary number of new properties, with default values - (rx/from - (repeatedly num-props - #(add-new-property variant-id {:fill-values? true}))) - - ;; When the component has path, set the path items as properties values - (when (> (count cpath) 1) - (rx/from - (map - #(update-property-value component-id % (nth cpath (inc %))) - (range num-props)))) - - (rx/of - (when duplicate? (add-new-variant main-instance-id)) - (dwsh/update-shapes variant-vec #(dissoc % :layout-item-absolute)) - (dwu/commit-undo-transaction undo-id) - (when flex? - (ptk/data-event :layout/update {:ids [variant-id]}))))))))) + that component, with a second member (`clv/generate-transform-in-variant`)." + [main-instance-id] + (ptk/reify ::transform-in-variant + ptk/WatchEvent + (watch [it state _] + (let [variant-id (uuid/next) + new-shape-id (uuid/next) + changes (-> (variant-changes it state) + (clv/generate-transform-in-variant main-instance-id variant-id + {:layout? true + :new-component-id (uuid/next) + :new-shape-id new-shape-id}))] + (rx/concat + (rx/of (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids [variant-id]}) + (dws/select-shape new-shape-id)) + (->> (rx/of (focus-property variant-id)) + (rx/delay 250))))))) (defn add-component-or-variant "Manage the shared shortcut, and do the pertinent action" @@ -475,117 +366,41 @@ (rx/of (rename-variant (:variant-id component) name)) (rx/of (dwl/rename-component-and-main-instance component-id name))))))) -(defn- bounding-rect - "Receives a list of frames (with X, y, width and height) and - calculates a rect that contains them all" - [frames] - (let [xs (map :x frames) - ys (map :y frames) - x2s (map #(+ (:x %) (:width %)) frames) - y2s (map #(+ (:y %) (:height %)) frames) - min-x (apply min xs) - min-y (apply min ys) - max-x (apply max x2s) - max-y (apply max y2s)] - {:x min-x - :y min-y - :width (- max-x min-x) - :height (- max-y min-y)})) - -(defn- common-prefix - [paths] - (->> (apply map vector paths) - (take-while #(apply = %)) - (map first) - vec)) - -(defn- order-variant-children - "Reorders the children of a variant container so that the variant components - (as returned by cfv/find-variant-components, which reverses the children - vector) appear in the given order of ids. Ids that are not children of the - container are ignored; children not covered by ids keep their position at - the end." - [variant-id ordered-ids] - (ptk/reify ::order-variant-children - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - children (set (dm/get-in objects [variant-id :shapes])) - ;; The desired :shapes vector is the reverse of ordered-ids; - ;; change-parent at index 0 inverts the order of the given shapes, - ;; so they are passed in ordered-ids order - shapes (->> ordered-ids - (filter children) - (mapv (d/getf objects)))] - (when (> (count shapes) 1) - (let [changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/change-parent variant-id shapes 0))] - (rx/of (dch/commit-changes changes) - (ptk/data-event :layout/update {:ids [variant-id]})))))))) - (defn combine-as-variants "Combines the components identified by the main-instance ids `ids` into a - new variant container. If `ids` is a sequential collection, its order - determines the order of the resulting variant components; unordered - collections are normalized to the layer-tree order." + new variant container (`clv/generate-combine-as-variants`). If `ids` is a + sequential collection, its order determines the order of the resulting + variant components; unordered collections are normalized to the + layer-tree order." [ids {:keys [page-id trigger variant-id]}] (ptk/reify ::combine-as-variants ptk/WatchEvent (watch [it state stream] - (let [current-page (:current-page-id state) + (let [current-page (:current-page-id state) combine - (fn [current-page] - (let [objects (dsh/lookup-page-objects state current-page) - ids (->> (if (sequential? ids) - ids - (cfh/order-by-indexed-shapes objects ids)) - (cfh/clean-loops objects) - (remove (fn [id] - (let [shape (get objects id)] - (or (not (ctc/main-instance? shape)) - (ctc/is-variant? shape))))) - (vec))] + (fn [page-id] + (let [objects (dsh/lookup-page-objects state page-id) + ids (->> (if (sequential? ids) + ids + (cfh/order-by-indexed-shapes objects ids)) + (cfh/clean-loops objects) + (remove (fn [id] + (let [shape (get objects id)] + (or (not (ctc/main-instance? shape)) + (ctc/is-variant? shape))))) + (vec))] (when (> (count ids) 1) - (let [shapes (mapv #(get objects %) ids) - rect (bounding-rect shapes) - prefix (->> shapes - (mapv #(cpn/split-path (:name %))) - (common-prefix)) - ;; When the common parent is root, add a wrapper - add-wrapper? (empty? prefix) - first-shape (first shapes) - delta (gpt/point (- (:x rect) (:x first-shape) 30) - (- (:y rect) (:y first-shape) 30)) - common-parent (->> ids - (mapv #(-> (cfh/get-parent-ids objects %) reverse)) - common-prefix - last) - index (-> (get objects common-parent) - :shapes - count - inc) - variant-id (or variant-id (uuid/next)) - undo-id (js/Symbol)] - + (let [rect (grc/join-rects (map #(:selrect (get objects %)) ids)) + variant-id (or variant-id (uuid/next)) + changes (-> (variant-changes it state :page-id page-id) + (clv/generate-combine-as-variants ids variant-id)) + undo-id (js/Symbol)] (rx/concat - (if (and page-id (not= current-page page-id)) - (rx/of (dcm/go-to-workspace :page-id page-id)) - (rx/empty)) - (rx/of (dwu/start-undo-transaction undo-id) - (transform-in-variant (first ids) variant-id delta prefix add-wrapper? false false) - (dwsh/relocate-shapes (into #{} (rest ids)) variant-id 0) - ;; relocate-shapes inserts the shapes in layer-tree - ;; order, so restore the intended order afterwards - (order-variant-children variant-id ids) - (dwsh/update-shapes ids #(-> % - (assoc :constraints-h :left) - (assoc :constraints-v :top) - (assoc :fixed-scroll false))) - (dwsh/relocate-shapes #{variant-id} common-parent index) + (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set variant-id)) + (ptk/data-event :layout/update {:ids (into [variant-id] (map #(:parent-id (get objects %))) ids)}) (dwt/update-dimensions [variant-id] :width (+ (:width rect) 60)) (dwt/update-dimensions [variant-id] :height (+ (:height rect) 60)) (ev/event (-> {::ev/name "combine-as-variants" ::ev/origin trigger :number-of-combined (count ids)} @@ -618,29 +433,6 @@ (not (ctc/is-variant? component))))) ids)))) -(defn valid-variant-switch? - [state shape pos val] - (let [libraries (dsh/lookup-libraries state) - component (ctf/get-component libraries - (:component-file shape) - (:component-id shape) - :include-deleted? false) - component-file-data (dm/get-in libraries [(:component-file shape) :data]) - component-page (dsh/get-page component-file-data (:main-instance-page component)) - component-page-objects (:objects component-page) - variant-components (when component - (cfv/find-variant-components component-file-data - component-page-objects - (:variant-id component)))] - (and (ctc/instance-head? shape) - (ctc/in-component-copy? shape) - (ctc/is-variant? component) - (nat-int? pos) - (< pos (count (:variant-properties component))) - (string? val) - (some #(= val (dm/get-in % [:variant-properties pos :value])) - variant-components)))) - (defn combine-selected-as-variants [options] (ptk/reify ::combine-selected-as-variants diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 3e236672a6..56351b456b 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -38,6 +38,7 @@ [app.common.uuid :as uuid] [app.main.data.exports.assets :as de] [app.main.data.exports.wasm :as wasm.exports] + [app.main.data.helpers :as dsh] [app.main.data.persistence :as dwp] [app.main.data.plugins :as dp] [app.main.data.workspace :as dw] @@ -1878,7 +1879,7 @@ :else (let [shape (u/locate-shape file-id page-id id)] - (if (dwv/valid-variant-switch? @st/state shape pos value) + (if (clv/valid-switch? (dsh/lookup-libraries @st/state) shape pos value) (st/emit! (-> (dwv/variants-switch {:shapes [shape] :pos pos :val value}) (se/add-event plugin-id))) (u/not-valid plugin-id :switchVariant "Shape, property, or value is not valid"))))) diff --git a/frontend/test/frontend_tests/composable_tests/golden_master.edn b/frontend/test/frontend_tests/composable_tests/golden_master.edn index b124037386..276f936556 100644 --- a/frontend/test/frontend_tests/composable_tests/golden_master.edn +++ b/frontend/test/frontend_tests/composable_tests/golden_master.edn @@ -59,8 +59,8 @@ [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"a\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "0b85fa1611096bb7" "542884fb9e0b082e"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :member \"b\", :component-id 7}\n \n {:target #fn, :value \"Value 3\", :pos 0}", :states ["df9a042b9d59d699" "73a3942a18403d5e" "3234ee274caa5b9e" "0fd372565af6eae2"]}] :case-at-transformed-component-keeps-its-copies - [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :transform, :params {:target #fn}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "06c3f510cacd8a58"]} - {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "06c3f510cacd8a58"]} + [{:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :transform, :params {:target #fn}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "f8be77e905b63d2f"]} + {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:target #fn, :property :name, :value \"Button/Primary\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "865741f5dae8819e" "7f7bd28b4563fabd" "bca3e99455b8e931" "f8be77e905b63d2f"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n one-of -> \n {:action :transform, :params {:target #fn}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "cfce0b246967964a" "2c3ef0411b35a158" "0d456e1108bb1756"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n one-of -> \n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}\n \n {:target #fn, :value \"Value 2\", :pos 0}", :states ["19168567c3f4c8b1" "cfce0b246967964a" "2c3ef0411b35a158" "0d456e1108bb1756"]}] :case-au-combined-components-keep-their-copies @@ -70,7 +70,7 @@ [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :name, :value \"Mine\"}\n {:action :rename, :params {:target #fn, :name \"Renamed\"}}", :states ["c6e889ca261596ce" "940f36855f4f991d" "d7a54338ab53a791"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n one-of -> \n {:action :rename, :params {:target #fn, :name \"Renamed\"}}", :states ["5eb544d7e28709d7" "5e6794a9b313112e"]}] :case-aw-variant-layer-rename-and-cleanup - [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :rename-in-layers, :params {:set-name \"vset\", :member \"a\", :properties [[\"Property 1\" \"a2\"] [\"Size\" \"S\"]]}}\n \n {:set-name \"vset\", :edit :add, :params {:property-name \"Empty\"}}\n {:action :remove-empty, :params {:set-name \"vset\"}}\n \n {:action :rename-in-layers, :params {:set-name \"vset\", :member \"a\", :properties [[\"Property 1\" \"a2\"]]}}\n \n {:action :remove-empty, :params {:set-name \"vset\"}}\n \n {:action :error, :params {:set-name \"vset\", :member \"b\", :value \"Duplicated\"}}\n \n {:action :error, :params {:set-name \"vset\", :member \"b\", :value nil}}\n \n {:target #fn, :value \"b\", :pos 0}", :states ["df9a042b9d59d699" "4b53486972986d97" "8553f89a9ebbcf54" "4b53486972986d97" "3af1829d8b4ab932" "d65ad905aa97d661" "6daf5f692c6595f1" "d65ad905aa97d661" "cc7d2dfd618fc8cb" "6390b4d83995b629"]}] + [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2, Size=S\"}}\n \n {:set-name \"vset\", :edit :add, :params {:property-name \"Empty\"}}\n {:set-name \"vset\", :edit :remove-empty, :params {}}\n \n {:set-name \"vset\", :edit :rename-in-layers, :params {:member \"a\", :name \"Property 1=a2\"}}\n \n {:set-name \"vset\", :edit :error, :params {:member \"b\", :value \"Duplicated\"}}\n \n {:set-name \"vset\", :edit :error, :params {:member \"b\", :value nil}}\n \n {:target #fn, :value \"b\", :pos 0}", :states ["df9a042b9d59d699" "4b53486972986d97" "8553f89a9ebbcf54" "4b53486972986d97" "d65ad905aa97d661" "6daf5f692c6595f1" "d65ad905aa97d661" "cc7d2dfd618fc8cb" "6390b4d83995b629"]}] :case-ax-variant-shortcuts-add-members [{:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :create-component, :targets [#fn]}}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "3c821d5f87436799"]} {:transcript " {:component \"main\", :id sync-main-component}\n {:variant-set \"vset\", :variant-id sync-vset-vcontainer, :count 2}\n {:component \"main\", :level 0, :outer sync-main-outercomp-0}\n {:component \"main\", :copy sync-main-copyhead-0}\n {:target #fn, :property :fills, :value \"#00ff00\"}\n {:action :shortcut, :params {:kind :duplicate, :targets [#fn]}}", :states ["df9a042b9d59d699" "00b4a79bae39f703" "3c821d5f87436799"]} diff --git a/frontend/test/frontend_tests/composable_tests/interpreter.cljs b/frontend/test/frontend_tests/composable_tests/interpreter.cljs index f98a90fffb..fb6e3711be 100644 --- a/frontend/test/frontend_tests/composable_tests/interpreter.cljs +++ b/frontend/test/frontend_tests/composable_tests/interpreter.cljs @@ -113,7 +113,6 @@ "The workspace events of the `n/VariantAction` `op`." [{:keys [action params]} situation] (let [id #(tm/target-shape-id situation %) - member #(n/member-component situation (:set-name params) (:member params)) select #(dws/select-shapes (into (d/ordered-set) (map id) %))] (case action :transform @@ -129,20 +128,6 @@ [(dwv/rename-comp-or-variant-and-main (:id (n/component-of situation (:target params))) (:name params))] - :rename-in-layers - (let [{:keys [id variant-id variant-properties]} (member)] - [(dwv/update-properties-names-and-values - id variant-id variant-properties - (mapv (fn [[name value]] {:name name :value value}) (:properties params)))]) - - :remove-empty - [(dwv/remove-empty-properties (n/variant-set-id situation (:set-name params)))] - - :error - [(if-let [value (:value params)] - (dwv/update-error (:id (member)) value) - (dwv/update-error (:id (member))))] - :shortcut [(select (:targets params)) (case (:kind params) @@ -252,7 +237,16 @@ :rename (let [[variant-id pos name] args] (dwv/update-property-name variant-id pos name {:trigger "workspace:design-tab"})) :reorder (apply dwv/reorder-variant-properties args) - :set-value (apply dwv/update-property-value args))]) + :set-value (apply dwv/update-property-value args) + ;; the layers panel's rename of the member's main + :rename-in-layers (let [{:keys [set-name params]} op] + (dw/rename-shape-or-variant (:id (n/member-root situation set-name (:member params))) + (last args))) + :remove-empty (apply dwv/remove-empty-properties args) + :error (let [[component-id value] args] + (if (some? value) + (dwv/update-error component-id value) + (dwv/update-error component-id))))]) (instance? n/Detach op) [(dwl/detach-component (tm/target-shape-id situation (:target op)))] diff --git a/frontend/test/frontend_tests/data/workspace_variants_test.cljs b/frontend/test/frontend_tests/data/workspace_variants_test.cljs index a133422fd4..097ad663c7 100644 --- a/frontend/test/frontend_tests/data/workspace_variants_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_variants_test.cljs @@ -13,6 +13,7 @@ rename of a plain component." (:require [app.common.data :as d] + [app.common.logic.variants :as clv] [app.common.test-helpers.components :as cthc] [app.common.test-helpers.compositions :as ctho] [app.common.test-helpers.files :as cthf] @@ -22,6 +23,7 @@ [app.common.types.components-list :as ctkl] [app.common.uuid :as uuid] [app.main.data.common :as dcm] + [app.main.data.helpers :as dsh] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.variants :as dwv] @@ -70,7 +72,7 @@ (let [file (setup-file) state (state-of file) valid? (fn [label pos val] - (dwv/valid-variant-switch? state (cths/get-shape file label) pos val))] + (clv/valid-switch? (dsh/lookup-libraries state) (cths/get-shape file label) pos val))] (t/testing "a variant copy head and a value some member holds" (t/is (valid? :copy01 0 "Value2")) (t/is (valid? :copy01 0 "Value1")))