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")))