From 014c297458c9f790830cc4d965c8b9f0a321d848 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 20 Feb 2025 10:05:01 +0100 Subject: [PATCH] :tada: Add drag components in or out a variant container --- .../src/app/common/files/changes_builder.cljc | 90 +++++--- common/src/app/common/logic/shapes.cljc | 114 ++++++++-- common/src/app/common/logic/variants.cljc | 140 ++++++++++--- .../app/common/test_helpers/components.cljc | 8 + .../src/app/common/test_helpers/variants.cljc | 25 +++ common/src/app/common/types/container.cljc | 14 +- .../logic/comp_remove_swap_slots_test.cljc | 84 ++++---- .../common_tests/logic/comp_reset_test.cljc | 12 +- .../common_tests/logic/comp_sync_test.cljc | 12 +- .../common_tests/logic/comp_touched_test.cljc | 12 +- .../common_tests/logic/move_shapes_test.cljc | 12 +- .../common_tests/logic/variants_test.cljc | 194 ++++++++++++++++++ frontend/src/app/main/data/workspace.cljs | 17 +- .../src/app/main/data/workspace/groups.cljs | 3 +- .../app/main/data/workspace/transforms.cljs | 17 +- .../src/app/main/data/workspace/variants.cljs | 43 +--- .../app/main/ui/workspace/context_menu.cljs | 4 +- .../sidebar/options/menus/component.cljs | 18 +- 18 files changed, 613 insertions(+), 206 deletions(-) create mode 100644 common/src/app/common/test_helpers/variants.cljc create mode 100644 common/test/common_tests/logic/variants_test.cljc diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 95a781090..894511e95 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -174,6 +174,26 @@ ::applied-changes-count (count redo-changes))) changes)) +(defn apply-changes-local-library + [changes] + (dm/assert! + "expected valid changes" + (check-changes! changes)) + + (if-let [library-data (::library-data (meta changes))] + (let [index (::applied-changes-count (meta changes)) + redo-changes (:redo-changes changes) + new-changes (if (< index (count redo-changes)) + (->> (subvec (:redo-changes changes) index) + (map #(-> % + (assoc :page-id uuid/zero) + (dissoc :component-id)))) + []) + new-library-data (cfc/process-changes library-data new-changes)] + (vary-meta changes assoc ::library-data new-library-data + ::applied-changes-count (count redo-changes))) + changes)) + ;; Page changes (defn add-empty-page @@ -970,37 +990,37 @@ (apply-changes-local))))) (defn update-component - ([changes id update-fn] - (let [library-data (::library-data (meta changes)) - prev-component (get-in library-data [:components id])] - (update-component changes id prev-component update-fn))) - ([changes id prev-component update-fn] - (assert-library! changes) - (let [new-component (update-fn prev-component)] - (if prev-component - (-> changes - (update :redo-changes conj {:type :mod-component - :id id - :name (:name new-component) - :path (:path new-component) - :main-instance-id (:main-instance-id new-component) - :main-instance-page (:main-instance-page new-component) - :annotation (:annotation new-component) - :variant-id (:variant-id new-component) - :variant-properties (:variant-properties new-component) - :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components) - :modified-at (:modified-at new-component)}) - (update :undo-changes conj {:type :mod-component - :id id - :name (:name prev-component) - :path (:path prev-component) - :main-instance-id (:main-instance-id prev-component) - :main-instance-page (:main-instance-page prev-component) - :annotation (:annotation prev-component) - :variant-id (:variant-id prev-component) - :variant-properties (:variant-properties prev-component) - :objects (:objects prev-component)})) - changes)))) + [changes id update-fn & {:keys [apply-changes-local-library?]}] + (assert-library! changes) + (let [library-data (::library-data (meta changes)) + prev-component (get-in library-data [:components id]) + new-component (update-fn prev-component)] + (if prev-component + (-> changes + (update :redo-changes conj {:type :mod-component + :id id + :name (:name new-component) + :path (:path new-component) + :main-instance-id (:main-instance-id new-component) + :main-instance-page (:main-instance-page new-component) + :annotation (:annotation new-component) + :variant-id (:variant-id new-component) + :variant-properties (:variant-properties new-component) + :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components) + :modified-at (:modified-at new-component)}) + (update :undo-changes conj {:type :mod-component + :id id + :name (:name prev-component) + :path (:path prev-component) + :main-instance-id (:main-instance-id prev-component) + :main-instance-page (:main-instance-page prev-component) + :annotation (:annotation prev-component) + :variant-id (:variant-id prev-component) + :variant-properties (:variant-properties prev-component) + :objects (:objects prev-component)}) + (cond-> apply-changes-local-library? + (apply-changes-local-library))) + changes))) (defn delete-component [changes id page-id] @@ -1061,3 +1081,11 @@ (reduce reorder-grid changes))] changes)) + +(defn get-library-data + [changes] + (::library-data (meta changes))) + +(defn get-objects + [changes] + (dm/get-in (::file-data (meta changes)) [:pages-index uuid/zero :objects])) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index b53d2af2a..fa2ba8e5e 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -10,12 +10,14 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] + [app.common.logic.variants :as clv] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.types.token :as cto] - [app.common.uuid :as uuid])) + [app.common.uuid :as uuid] + [cuerdas.core :as str])) (defn- generate-unapply-tokens "When updating attributes that have a token applied, we must unapply it, because the value @@ -239,21 +241,21 @@ (defn generate-relocate - [changes objects parent-id page-id to-index ids & {:keys [cell ignore-parents?]}] - (let [ids (cfh/order-by-indexed-shapes objects ids) - shapes (map (d/getf objects) ids) - parent (get objects parent-id) + [changes parent-id to-index ids & {:keys [cell ignore-parents?]}] + (let [objects (pcb/get-objects changes) + ids (cfh/order-by-indexed-shapes objects ids) + shapes (map (d/getf objects) ids) + parent (get objects parent-id) all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) - parents (if ignore-parents? #{parent-id} all-parents) + parents (if ignore-parents? #{parent-id} all-parents) - children-ids - (->> ids - (mapcat #(cfh/get-children-ids-with-self objects %))) + children-ids (mapcat #(cfh/get-children-ids-with-self objects %) ids) - child-heads - (->> ids - (mapcat #(ctn/get-child-heads objects %)) - (map :id)) + child-heads (mapcat #(ctn/get-child-heads objects %) ids) + + child-heads-ids (map :id child-heads) + + variant-heads (filter ctk/is-variant? child-heads) component-main-parent (ctn/find-component-main objects parent false) @@ -340,9 +342,6 @@ cell (or cell (and index-cell-data [(:row index-cell-data) (:column index-cell-data)]))] (-> changes - (pcb/with-page-id page-id) - (pcb/with-objects objects) - ;; Remove layout-item properties when moving a shape outside a layout (cond-> (not (ctl/any-layout? parent)) (pcb/update-shapes ids ctl/remove-layout-item-data)) @@ -353,7 +352,7 @@ ;; Remove the swap slots if it is moving to a different component (pcb/update-shapes - child-heads + child-heads-ids (fn [shape] (cond-> shape (not= component-main-parent (ctn/find-component-main objects shape false)) @@ -365,7 +364,86 @@ ;; Add component-root property when moving a component outside a component (cond-> (not (ctn/get-instance-root objects parent)) - (pcb/update-shapes child-heads #(assoc % :component-root true))) + (pcb/update-shapes child-heads-ids #(assoc % :component-root true))) + + ;; Remove variant info and rename when moving outside a variant-container + (cond-> (not (ctk/is-variant-container? parent)) + ((fn [changes] + (reduce + (fn [changes shape] + (let [new-name (str/replace (:variant-name shape) #", " " / ") + [cpath cname] (cfh/parse-path-name new-name)] + (-> changes + (pcb/update-component (:component-id shape) + #(-> (dissoc % :variant-id :variant-properties) + (assoc :name cname + :path cpath)) + {:apply-changes-local-library? true}) + (pcb/update-shapes [(:id shape)] + #(-> (dissoc % :variant-id :variant-name) + (assoc :name new-name)))))) + changes + variant-heads)))) + + ;; Add variant info and rename when moving into a different variant-container + (cond-> (ctk/is-variant-container? parent) + ((fn [changes] + (let [get-base-name #(if (some? (:variant-name %)) + (str/replace (:variant-name %) #", " " / ") + (:name %)) + + calc-num-props #(-> % + get-base-name + cfh/split-path + count) + + max-path-items (apply max (map calc-num-props child-heads)) + + first-comp-id (->> parent + :shapes + first + (get objects) + :component-id) + + data (pcb/get-library-data changes) + variant-properties (get-in data [:components first-comp-id :variant-properties]) + num-props (count variant-properties) + num-new-props (if (< max-path-items num-props) + 0 + (- max-path-items num-props)) + + changes (nth + (iterate #(clv/generate-add-new-property % (:id parent)) changes) + num-new-props)] + (reduce + (fn [changes shape] + (if (= (:id parent) (:variant-id shape)) + changes ;; do nothing if we aren't changing the parent + (let [base-name (get-base-name shape) + + ;; we need to get the updated library data to have access to the current properties + data (pcb/get-library-data changes) + + props (clv/path-to-properties + base-name + (get-in data [:components first-comp-id :variant-properties])) + + variant-name (clv/properties-to-name props) + [cpath cname] (cfh/parse-path-name (:name parent))] + + (-> (pcb/update-component changes + (:component-id shape) + #(assoc % :variant-id (:id parent) + :variant-properties props + :name cname + :path cpath) + {:apply-changes-local-library? true}) + (pcb/update-shapes [(:id shape)] + #(assoc % :variant-id (:id parent) + :variant-name variant-name + :name (:name parent))))))) + changes + child-heads))))) ;; Move the shapes (pcb/change-parent parent-id diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index 5c07a90aa..ceb775024 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -5,58 +5,138 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.logic.variants (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.types.components-list :as ctcl] [cuerdas.core :as str])) + +(def property-prefix "Property") +(def property-regex (re-pattern (str property-prefix "(\\d+)"))) +(def value-prefix "Value") + +(defn find-related-components + "Find a list of the components thet belongs to this variant-id" + [data objects variant-id] + (->> (dm/get-in objects [variant-id :shapes]) + (map #(dm/get-in objects [% :component-id])) + (map #(ctcl/get-component data % true)) + reverse)) + + (defn properties-to-name + "Transform the properties into a name, with the values separated by comma" [properties] (->> properties (map :value) + (remove str/empty?) (str/join ", "))) + +(defn next-property-number + "Returns the next property number, to avoid duplicates on the property names" + [properties] + (let [numbers (keep + #(some->> (:name %) (re-find property-regex) second d/parse-integer) + properties) + max-num (if (seq numbers) + (apply max numbers) + 0)] + (inc (max max-num (count properties))))) + + +(defn path-to-properties + "From a list of properties and a name with path, assign each token of the + path as value of a different property" + [path properties] + (let [next-prop-num (next-property-number properties) + cpath (cfh/split-path path) + assigned (mapv #(assoc % :value (nth cpath %2 "")) properties (range)) + remaining (drop (count properties) cpath) + new-properties (map-indexed (fn [i v] {:name (str property-prefix (+ next-prop-num i)) + :value v}) remaining)] + (into assigned new-properties))) + + (defn generate-update-property-name - [changes related-components pos new-name] - (reduce (fn [changes component] - (pcb/update-component - changes (:id component) - #(assoc-in % [:variant-properties pos :name] new-name))) - changes - related-components)) + [changes variant-id pos new-name] + (let [data (pcb/get-library-data changes) + objects (pcb/get-objects changes) + related-components (find-related-components data objects variant-id)] + (reduce (fn [changes component] + (pcb/update-component + changes (:id component) + #(assoc-in % [:variant-properties pos :name] new-name) + {:apply-changes-local-library? true})) + changes + related-components))) (defn generate-remove-property - [changes related-components pos] - (reduce (fn [changes component] - (let [props (:variant-properties component) - props (vec (concat (subvec props 0 pos) (subvec props (inc pos)))) - main-id (:main-instance-id component) - name (properties-to-name props)] - (-> changes - (pcb/update-component (:id component) #(assoc % :variant-properties props)) - (pcb/update-shapes [main-id] #(assoc % :variant-name name))))) - changes - related-components)) + [changes variant-id pos] + (let [data (pcb/get-library-data changes) + objects (pcb/get-objects changes) + related-components (find-related-components data objects variant-id)] + (reduce (fn [changes component] + (let [props (:variant-properties component) + props (d/remove-at-index props pos) + main-id (:main-instance-id component) + name (properties-to-name props)] + (-> changes + (pcb/update-component (:id component) #(assoc % :variant-properties props) + {:apply-changes-local-library? true}) + (pcb/update-shapes [main-id] #(assoc % :variant-name name))))) + changes + related-components))) (defn generate-update-property-value - [changes component-id main-id pos value name] - (-> changes - (pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value)) - (pcb/update-shapes [main-id] #(assoc % :variant-name name)))) + [changes component-id pos value] + (let [data (pcb/get-library-data changes) + component (ctcl/get-component data component-id true) + main-id (:main-instance-id component) + name (-> (:variant-properties component) + (update pos assoc :value value) + properties-to-name)] + (-> changes + (pcb/update-component component-id #(assoc-in % [:variant-properties pos :value] value) + {:apply-changes-local-library? true}) + (pcb/update-shapes [main-id] #(assoc % :variant-name name))))) + (defn generate-add-new-property - [changes related-components property-name] - (let [[_ changes] + [changes variant-id & {:keys [fill-values?]}] + (let [data (pcb/get-library-data changes) + objects (pcb/get-objects changes) + related-components (find-related-components data objects variant-id) + + props (-> related-components first :variant-properties) + next-prop-num (next-property-number props) + property-name (str property-prefix next-prop-num) + + [_ changes] (reduce (fn [[num changes] component] - (let [props (-> (or (:variant-properties component) []) - (conj {:name property-name :value (str "Value" num)})) - main-id (:main-instance-id component) - variant-name (properties-to-name props)] + (let [main-id (:main-instance-id component) + + update-props #(-> (d/nilv % []) + (conj {:name property-name + :value (if fill-values? (str value-prefix num) "")})) + + update-name #(if fill-values? + (if (str/empty? %) + (str value-prefix num) + (str % ", " value-prefix num)) + %)] [(inc num) (-> changes - (pcb/update-component (:id component) #(assoc % :variant-properties props)) - (pcb/update-shapes [main-id] #(assoc % :variant-name variant-name)))])) + (pcb/update-component (:id component) + #(update % :variant-properties update-props) + {:apply-changes-local-library? true}) + (pcb/update-shapes [main-id] #(update % :variant-name update-name)))])) [1 changes] related-components)] changes)) + diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index 150bbeeb4..666406d52 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -57,6 +57,14 @@ :main-instance-page (:id page) :shapes updated-shapes)))))))) +(defn update-component + [file component-label & {:keys [] :as params}] + (let [component-id (thi/id component-label)] + (ctf/update-file-data + file + (fn [file-data] + (ctkl/update-component file-data component-id #(merge % params)))))) + (defn get-component [file label & {:keys [include-deleted?] :or {include-deleted? false}}] (ctkl/get-component (:data file) (thi/id label) include-deleted?)) diff --git a/common/src/app/common/test_helpers/variants.cljc b/common/src/app/common/test_helpers/variants.cljc new file mode 100644 index 000000000..804dd3c7e --- /dev/null +++ b/common/src/app/common/test_helpers/variants.cljc @@ -0,0 +1,25 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.common.test-helpers.variants + (:require + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths])) + +(defn add-variant + [file variant-label component1-label root1-label component2-label root2-label + & {:keys []}] + (let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true) + variant-id (thi/id variant-label)] + + (-> file + (ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2") + (ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1") + (thc/make-component component1-label root1-label) + (thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]}) + (thc/make-component component2-label root2-label) + (thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property1" :value "Value1"}]})))) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index 38d222735..1a2099455 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -535,11 +535,17 @@ (letfn [(get-frame [parent-id] (if (cfh/frame-shape? objects parent-id) parent-id (get-in objects [parent-id :frame-id])))] (let [parent (get objects parent-id) - ;; We can always move the children to the parent they already have. + ;; We can always move the children to the parent they already have. + ;; But if we are pasting, those are new items, so it is considered a change no-changes? - (->> children (every? #(= parent-id (:parent-id %))))] - ;; In case no-changes is true we must ensure we are copy pasting the children in the same position - (if (or (and no-changes? (not pasting?)) (not (invalid-structure-for-component? objects parent children pasting? libraries))) + (and (->> children (every? #(= parent-id (:parent-id %)))) + (not pasting?)) + all-main? + (->> children (every? #(ctk/main-instance? %)))] + (if (or no-changes? + (and (not (invalid-structure-for-component? objects parent children pasting? libraries)) + ;; If we are moving into a variant-container, all the items should be main + (or all-main? (not (ctk/is-variant-container? parent))))) [parent-id (get-frame parent-id)] (recur (:parent-id parent) objects children pasting? libraries)))))) diff --git a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc index 0decac57c..4d9a4d2a8 100644 --- a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc +++ b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc @@ -61,10 +61,10 @@ blue1 (ths/get-shape file :blue1) ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) uuid/zero ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -91,10 +91,10 @@ ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id b2) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -121,10 +121,10 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id yellow) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -134,10 +134,10 @@ yellow' (ths/get-shape file' :frame-yellow) ;; Move yellow into root - changes' (cls/generate-relocate (pcb/empty-changes nil) - (:objects page') + changes' (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page')) + (pcb/with-objects (:objects page'))) uuid/zero ;; parent-id - (:id page') ;; page-id 0 ;; to-index #{(:id yellow')}) ;; ids @@ -164,10 +164,10 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id yellow) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -178,10 +178,10 @@ b2' (ths/get-shape file' :frame-b2) ;; Move yellow into b2 - changes' (cls/generate-relocate (pcb/empty-changes nil) - (:objects page') + changes' (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page')) + (pcb/with-objects (:objects page'))) (:id b2') ;; parent-id - (:id page') ;; page-id 0 ;; to-index #{(:id yellow')}) ;; ids @@ -254,10 +254,10 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id yellow) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -308,10 +308,10 @@ blue1 (ths/get-shape file :blue1) ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:parent-id blue1) ;; parent-id - (:id page) ;; page-id 2 ;; to-index #{(:id blue1)}) ;; ids @@ -338,10 +338,10 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id yellow) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -351,10 +351,10 @@ page' (thf/current-page file') blue1' (ths/get-shape file' :blue1) b1' (ths/get-shape file' :frame-b1) - changes' (cls/generate-relocate (pcb/empty-changes nil) - (:objects page') + changes' (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page')) + (pcb/with-objects (:objects page'))) (:id b1') ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1')}) ;; ids @@ -382,10 +382,10 @@ ;; ==== Action ;; Relocate blue1 into yellow - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id yellow) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id blue1)}) ;; ids @@ -396,10 +396,10 @@ page' (thf/current-page file') blue1' (ths/get-shape file' :blue1) b1' (ths/get-shape file' :frame-b1) - changes' (cls/generate-relocate (pcb/empty-changes nil) - (:objects page') + changes' (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page')) + (pcb/with-objects (:objects page'))) (:id b1') ;; parent-id - (:id page') ;; page-id 0 ;; to-index #{(:id blue1')}) ;; ids @@ -428,10 +428,10 @@ green-copy (ths/get-shape file :green-copy) ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) uuid/zero ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id green-copy)}) ;; ids @@ -459,10 +459,10 @@ b2 (ths/get-shape file :frame-b2) ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id b2) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id green-copy)}) ;; ids diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc index 1c663917e..04abcbeeb 100644 --- a/common/test/common_tests/logic/comp_reset_test.cljc +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -136,10 +136,10 @@ ;; IMPORTANT: as modifying copies structure is now forbidden, this action ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :copy-root) ; parent-id - (:id page) ; page-id 0 ; to-index #{(thi/id :free-shape)}) ; ids @@ -231,10 +231,10 @@ ;; IMPORTANT: as modifying copies structure is now forbidden, this action ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :copy-root) ; parent-id - (:id page) ; page-id 2 ; to-index #{(:id copy-child1)}) ; ids diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc index f970f5fcb..e5e5d4265 100644 --- a/common/test/common_tests/logic/comp_sync_test.cljc +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -195,10 +195,10 @@ page (thf/current-page file) ;; ==== Action - changes1 (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes1 (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :main-root) ; parent-id - (:id page) ; page-id 0 ; to-index #{(thi/id :free-shape)}) ; ids @@ -292,10 +292,10 @@ main-child1 (ths/get-shape file :main-child1) ;; ==== Action - changes1 (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes1 (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :main-root) ; parent-id - (:id page) ; page-id 2 ; to-index #{(:id main-child1)}) ; ids diff --git a/common/test/common_tests/logic/comp_touched_test.cljc b/common/test/common_tests/logic/comp_touched_test.cljc index bb615f7ae..663c24ed1 100644 --- a/common/test/common_tests/logic/comp_touched_test.cljc +++ b/common/test/common_tests/logic/comp_touched_test.cljc @@ -112,10 +112,10 @@ ;; IMPORTANT: as modifying copies structure is now forbidden, this action ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :copy-root) ; parent-id - (:id page) ; page-id 0 ; to-index #{(thi/id :free-shape)}) ; ids @@ -187,10 +187,10 @@ ;; IMPORTANT: as modifying copies structure is now forbidden, this action ;; will not have any effect, and so the parent shape won't also be touched. - changes (cls/generate-relocate (pcb/empty-changes) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (thi/id :copy-root) ; parent-id - (:id page) ; page-id 2 ; to-index #{(:id copy-child1)}) ; ids diff --git a/common/test/common_tests/logic/move_shapes_test.cljc b/common/test/common_tests/logic/move_shapes_test.cljc index f85a72cbe..74f04d539 100644 --- a/common/test/common_tests/logic/move_shapes_test.cljc +++ b/common/test/common_tests/logic/move_shapes_test.cljc @@ -29,10 +29,10 @@ ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) (:id frame-parent) ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id frame-to-move)}) ;; ids @@ -61,10 +61,10 @@ ;; ==== Action - changes (cls/generate-relocate (pcb/empty-changes nil) - (:objects page) + changes (cls/generate-relocate (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-objects (:objects page))) uuid/zero ;; parent-id - (:id page) ;; page-id 0 ;; to-index #{(:id circle)}) ;; ids diff --git a/common/test/common_tests/logic/variants_test.cljc b/common/test/common_tests/logic/variants_test.cljc new file mode 100644 index 000000000..e37229d26 --- /dev/null +++ b/common/test/common_tests/logic/variants_test.cljc @@ -0,0 +1,194 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns common-tests.logic.variants-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.variants :as clv] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.test-helpers.variants :as thv] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-update-property-name + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + v-id (-> (ths/get-shape file :v01) :id) + page (thf/current-page file) + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-update-property-name v-id 0 "NewName1") + (clv/generate-update-property-name v-id 1 "NewName2")) + + + file' (thf/apply-changes file changes) + + + + ;; ==== Get + comp01' (thc/get-component file' :c01) + comp02' (thc/get-component file' :c02)] + + ;; ==== Check + (t/is (= (-> comp01' :variant-properties first :name) "NewName1")) + (t/is (= (-> comp01' :variant-properties last :name) "NewName2")) + (t/is (= (-> comp02' :variant-properties first :name) "NewName1")) + (t/is (= (-> comp02' :variant-properties last :name) "NewName2")))) + + + +(t/deftest test-add-new-property-without-values + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + v-id (-> (ths/get-shape file :v01) :id) + page (thf/current-page file) + + comp01 (thc/get-component file :c01) + comp02 (thc/get-component file :c02) + + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-add-new-property v-id)) + + + file' (thf/apply-changes file changes) + + + + ;; ==== Get + comp01' (thc/get-component file' :c01) + comp02' (thc/get-component file' :c02)] + + ;; ==== Check + (t/is (= (count (:variant-properties comp01)) 1)) + (t/is (= (count (:variant-properties comp01')) 2)) + (t/is (= (count (:variant-properties comp02)) 1)) + (t/is (= (count (:variant-properties comp02')) 2)) + (t/is (= (-> comp01' :variant-properties last :value) "")))) + + + +(t/deftest test-add-new-property-with-values + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + v-id (-> (ths/get-shape file :v01) :id) + page (thf/current-page file) + + comp01 (thc/get-component file :c01) + comp02 (thc/get-component file :c02) + + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-add-new-property v-id {:fill-values? true})) + + + file' (thf/apply-changes file changes) + + + + ;; ==== Get + comp01' (thc/get-component file' :c01) + comp02' (thc/get-component file' :c02)] + + ;; ==== Check + (t/is (= (count (:variant-properties comp01)) 1)) + (t/is (= (count (:variant-properties comp01')) 2)) + (t/is (= (count (:variant-properties comp02)) 1)) + (t/is (= (count (:variant-properties comp02')) 2)) + (t/is (= (-> comp01' :variant-properties last :value) "Value1")))) + + + +(t/deftest test-remove-property + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + v-id (-> (ths/get-shape file :v01) :id) + page (thf/current-page file) + + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-add-new-property v-id)) + + + file (thf/apply-changes file changes) + page (thf/current-page file) + + comp01 (thc/get-component file :c01) + comp02 (thc/get-component file :c02) + + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-remove-property v-id 0)) + + + file' (thf/apply-changes file changes) + + + + ;; ==== Get + comp01' (thc/get-component file' :c01) + comp02' (thc/get-component file' :c02)] + + ;; ==== Check + (t/is (= (count (:variant-properties comp01)) 2)) + (t/is (= (count (:variant-properties comp01')) 1)) + (t/is (= (count (:variant-properties comp02)) 2)) + (t/is (= (count (:variant-properties comp02')) 1)) + (t/is (= (-> comp01' :variant-properties first :name) "Property2")))) + +(t/deftest test-update-property-value + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (thv/add-variant :v01 :c01 :m01 :c02 :m02)) + + page (thf/current-page file) + + comp01 (thc/get-component file :c01) + comp02 (thc/get-component file :c02) + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (pcb/with-page-id (:id page)) + (pcb/with-library-data (:data file)) + (pcb/with-objects (:objects page)) + (clv/generate-update-property-value (:id comp01) 0 "NewValue1") + (clv/generate-update-property-value (:id comp02) 0 "NewValue2")) + + file' (thf/apply-changes file changes) + + ;; ==== Get + comp01' (thc/get-component file' :c01) + comp02' (thc/get-component file' :c02)] + + ;; ==== Check + (t/is (= (-> comp01' :variant-properties first :value) "NewValue1")) + (t/is (= (-> comp02' :variant-properties first :value) "NewValue2")))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 625c55eed..ca7243050 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -890,6 +890,7 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (dsh/lookup-page-objects state page-id) + data (dsh/lookup-file-data state) ;; Ignore any shape whose parent is also intended to be moved ids (cfh/clean-loops objects ids) @@ -899,13 +900,15 @@ all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) - changes (cls/generate-relocate (pcb/empty-changes it) - objects - parent-id - page-id - to-index - ids - :ignore-parents? ignore-parents?) + changes (-> (pcb/empty-changes it) + (pcb/with-page-id page-id) + (pcb/with-objects objects) + (pcb/with-library-data data) + (cls/generate-relocate + parent-id + to-index + ids + :ignore-parents? ignore-parents?)) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index b0f97b055..6d7d18f1c 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -232,7 +232,8 @@ ids (->> ids (remove #(ctn/has-any-copy-parent? objects (get objects %))) ;; components can't be ungrouped - (remove #(ctk/instance-head? (get objects %)))) + (remove #(ctk/instance-head? (get objects %))) + (remove #(ctk/is-variant-container? (get objects %)))) changes-list (sequence (keep prepare) ids) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 5ee973fe0..ea8c7c110 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -930,14 +930,17 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (dsh/lookup-page-objects state page-id) + data (dsh/lookup-file-data state) ids (cleanup-invalid-moving-shapes ids objects frame-id) - changes (cls/generate-relocate (pcb/empty-changes it) - objects - frame-id - page-id - drop-index - ids - :cell cell)] + changes (-> (pcb/empty-changes it) + (pcb/with-page-id page-id) + (pcb/with-objects objects) + (pcb/with-library-data data) + (cls/generate-relocate + frame-id + drop-index + ids + :cell cell))] (when (and (some? frame-id) (d/not-empty? changes)) (rx/of (dch/commit-changes changes) diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index e7d965340..268549b5d 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -10,7 +10,6 @@ [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.logic.variants :as clv] - [app.common.types.components-list :as ctcl] [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.helpers :as dsh] @@ -22,11 +21,7 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(defn find-related-components - [data objects variant-id] - (->> (dm/get-in objects [variant-id :shapes]) - (map #(dm/get-in objects [% :component-id])) - (map #(ctcl/get-component data % true)))) +(dm/export clv/find-related-components) (defn update-property-name "Update the variant property name on the position pos @@ -39,10 +34,11 @@ data (dsh/lookup-file-data state) objects (-> (dsh/get-page data page-id) (get :objects)) - related-components (find-related-components data objects variant-id) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) (pcb/with-library-data data) - (clv/generate-update-property-name related-components pos new-name)) + (clv/generate-update-property-name variant-id pos new-name)) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) @@ -59,17 +55,11 @@ data (dsh/lookup-file-data state) objects (-> (dsh/get-page data page-id) (get :objects)) - component (ctcl/get-component data component-id true) - main-id (:main-instance-id component) - properties (-> (:variant-properties component) - (update pos assoc :value value)) - - name (clv/properties-to-name properties) changes (-> (pcb/empty-changes it page-id) (pcb/with-library-data data) (pcb/with-objects objects) - (clv/generate-update-property-value component-id main-id pos value name)) + (clv/generate-update-property-value component-id pos value)) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) @@ -88,12 +78,11 @@ data (dsh/lookup-file-data state) objects (-> (dsh/get-page data page-id) (get :objects)) - related-components (find-related-components data objects variant-id) changes (-> (pcb/empty-changes it page-id) (pcb/with-library-data data) (pcb/with-objects objects) - (clv/generate-remove-property related-components pos)) + (clv/generate-remove-property variant-id pos)) undo-id (js/Symbol)] (rx/of @@ -105,7 +94,7 @@ (defn add-new-property "Add a new variant property to all the components with this variant-id" - [variant-id] + [variant-id & [options]] (ptk/reify ::add-new-property ptk/WatchEvent (watch [it state _] @@ -114,20 +103,10 @@ objects (-> (dsh/get-page data page-id) (get :objects)) - related-components (find-related-components data objects variant-id) - - - property-name (str "Property" (-> related-components - first - :variant-properties - count - inc)) - changes (-> (pcb/empty-changes it page-id) (pcb/with-library-data data) (pcb/with-objects objects) - (clv/generate-add-new-property related-components property-name)) - + (clv/generate-add-new-property variant-id options)) undo-id (js/Symbol)] (rx/of @@ -155,7 +134,7 @@ (defn transform-in-variant "Given the id of a main shape of a component, creates a variant structure for that component" - [id] + [main-instance-id] (ptk/reify ::transform-in-variant ptk/WatchEvent (watch [_ state _] @@ -165,7 +144,7 @@ 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 id) + main (get objects main-instance-id) main-id (:id main) undo-id (js/Symbol)] @@ -193,5 +172,5 @@ (dwl/duplicate-component file-id (:component-id main) new-component-id) (set-variant-id (:component-id main) variant-id) (set-variant-id new-component-id variant-id) - (add-new-property variant-id) + (add-new-property variant-id {:fill-values? true}) (dwu/commit-undo-transaction undo-id)))))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 1d0de3364..81d63dd32 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -324,8 +324,8 @@ any-in-copy? (some true? (map #(ctn/has-any-copy-parent? objects %) shapes)) ;; components can't be ungrouped - has-frame? (->> shapes (d/seek #(and (cfh/frame-shape? %) (not (ctk/instance-head? %))))) - has-group? (->> shapes (d/seek #(and (cfh/group-shape? %) (not (ctk/instance-head? %))))) + has-frame? (->> shapes (d/seek #(and (cfh/frame-shape? %) (not (ctk/instance-head? %)) (not (ctk/is-variant-container? %))))) + has-group? (->> shapes (d/seek #(and (cfh/group-shape? %) (not (ctk/instance-head? %)) (not (ctk/is-variant-container? %))))) has-bool? (->> shapes (d/seek cfh/bool-shape?)) has-mask? (->> shapes (d/seek :masked-group)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index c1a5b8121..976072e77 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -254,11 +254,11 @@ (mf/deps related-components) (fn [prop-name] (->> related-components - (mapcat (fn [item] - (map :value (filter (fn [prop] (= (:name prop) prop-name)) - (:variant-properties item))))) - (filter some?) + (mapcat (fn [component] + (map :value (filter #(= (:name %) prop-name) + (:variant-properties component))))) distinct + (map #(if (str/empty? %) "--" %)) (map (fn [val] {:label val :id val}))))) filter-matching @@ -278,8 +278,7 @@ (mf/use-fn (mf/deps id-component) (fn [pos value] - (when-not (str/empty? value) - (st/emit! (dwv/update-property-value id-component pos value))))) + (st/emit! (dwv/update-property-value id-component pos value)))) switch-component (mf/use-fn @@ -293,7 +292,7 @@ (if (ctk/main-instance? shape) [:* [:span {:class (stl/css :variant-property-name :variant-property-name-bg)} (:name prop)] - [:> combobox* {:default-selected (str (or (:value prop) "")) + [:> combobox* {:default-selected (if (str/empty? (:value prop)) "--" (:value prop)) :options (clj->js (get-options (:name prop))) :on-change (partial change-property-value pos)}]] @@ -761,7 +760,10 @@ properties (->> (dwv/find-related-components data objects variant-id) (mapcat :variant-properties) (group-by :name) - (map (fn [[k v]] {:name k :values (map :value v)}))) + (map (fn [[k v]] + {:name k + :values (distinct + (map #(if (str/empty? (:value %)) "--" (:value %)) v))}))) menu-open* (mf/use-state false) menu-open? (deref menu-open*)