diff --git a/common/src/app/common/logic/variant_properties.cljc b/common/src/app/common/logic/variant_properties.cljc index a0224dbe4..2c489fa17 100644 --- a/common/src/app/common/logic/variant_properties.cljc +++ b/common/src/app/common/logic/variant_properties.cljc @@ -59,14 +59,14 @@ (defn generate-add-new-property - [changes variant-id & {:keys [fill-values?]}] + [changes variant-id & {:keys [fill-values? property-name]}] (let [data (pcb/get-library-data changes) objects (pcb/get-objects changes) related-components (cfv/find-variant-components data objects variant-id) props (-> related-components first :variant-properties) next-prop-num (ctv/next-property-number props) - property-name (str ctv/property-prefix next-prop-num) + property-name (or property-name (str ctv/property-prefix next-prop-num)) [_ changes] (reduce (fn [[num changes] component] diff --git a/common/src/app/common/types/variant.cljc b/common/src/app/common/types/variant.cljc index fddf670cb..a8db1e981 100644 --- a/common/src/app/common/types/variant.cljc +++ b/common/src/app/common/types/variant.cljc @@ -86,3 +86,60 @@ new-properties (map-indexed (fn [i v] {:name (str property-prefix (+ next-prop-num i)) :value v}) remaining)] (into assigned new-properties))) + + +(defn properties-map-to-string + "Transforms a map of properties to a string of properties omitting the empty ones" + [properties] + (->> properties + (keep (fn [{:keys [name value]}] + (when (not (str/blank? value)) + (str name "=" value)))) + (str/join ", "))) + + +(defn properties-string-to-map + "Transforms a string of properties to a map of properties" + [s] + (->> (str/split s ",") + (mapv #(str/split % "=")) + (mapv (fn [[k v]] + {:name (str/trim k) + :value (str/trim v)})))) + + +(defn valid-properties-string? + "Checks if a string of properties has a processable format or not" + [s] + (let [pattern #"^([a-zA-Z0-9\s]+=[a-zA-Z0-9\s]+)(,\s*[a-zA-Z0-9\s]+=[a-zA-Z0-9\s]+)*$"] + (not (nil? (re-matches pattern s))))) + + +(defn find-properties-to-remove + "Compares two property maps to find which properties should be removed" + [prev-props upd-props] + (let [upd-names (set (map :name upd-props))] + (filterv #(not (contains? upd-names (:name %))) prev-props))) + + +(defn find-properties-to-update + "Compares two property maps to find which properties should be updated" + [prev-props upd-props] + (filterv #(some (fn [prop] (and (= (:name %) (:name prop)) + (not= (:value %) (:value prop)))) prev-props) upd-props)) + + +(defn find-properties-to-add + "Compares two property maps to find which properties should be added" + [prev-props upd-props] + (let [prev-names (set (map :name prev-props))] + (filterv #(not (contains? prev-names (:name %))) upd-props))) + + +(defn find-index-for-property-name + "Finds the index of a name in a property map" + [props name] + (some (fn [[idx prop]] + (when (= (:name prop) name) + idx)) + (map-indexed vector props))) diff --git a/common/test/common_tests/variant_test.cljc b/common/test/common_tests/variant_test.cljc new file mode 100644 index 000000000..a7a0f7d58 --- /dev/null +++ b/common/test/common_tests/variant_test.cljc @@ -0,0 +1,81 @@ +;; 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.variant-test + (:require + [app.common.types.variant :as ctv] + [clojure.test :as t])) + +(t/deftest convert-between-variant-properties-maps-and-strings + (let [map-with-two-props [{:name "border" :value "yes"} {:name "color" :value "gray"}] + map-with-two-props-one-blank [{:name "border" :value "no"} {:name "color" :value ""}] + map-with-one-prop [{:name "border" :value "no"}] + map-with-spaces [{:name "border 1" :value "of course"} {:name "color 2" :value "dark gray"}] + + string-valid-with-two-props "border=yes, color=gray" + string-valid-with-one-prop "border=no" + string-valid-with-spaces "border 1=of course, color 2=dark gray" + string-invalid "border=yes, color="] + + (t/testing "convert map to string" + (t/is (= (ctv/properties-map-to-string map-with-two-props) string-valid-with-two-props)) + (t/is (= (ctv/properties-map-to-string map-with-two-props-one-blank) string-valid-with-one-prop)) + (t/is (= (ctv/properties-map-to-string map-with-spaces) string-valid-with-spaces))) + + (t/testing "convert string to map" + (t/is (= (ctv/properties-string-to-map string-valid-with-two-props) map-with-two-props)) + (t/is (= (ctv/properties-string-to-map string-valid-with-one-prop) map-with-one-prop)) + (t/is (= (ctv/properties-string-to-map string-valid-with-spaces) map-with-spaces))) + + (t/testing "check if a string is valid" + (t/is (= (ctv/valid-properties-string? string-valid-with-two-props) true)) + (t/is (= (ctv/valid-properties-string? string-valid-with-one-prop) true)) + (t/is (= (ctv/valid-properties-string? string-valid-with-spaces) true)) + (t/is (= (ctv/valid-properties-string? string-invalid) false))))) + + +(t/deftest compare-property-maps + (let [prev-props [{:name "border" :value "yes"} {:name "color" :value "gray"}] + upd-props-1 [{:name "border" :value "yes"}] + upd-props-2 [{:name "border" :value "yes"} {:name "color" :value "blue"}] + upd-props-3 [{:name "border" :value "yes"} {:name "color" :value "gray"} {:name "shadow" :value "large"}] + upd-props-4 [{:name "color" :value "yellow"} {:name "shadow" :value "large"}]] + + (t/testing "a property to remove" + (t/is (= (ctv/find-properties-to-remove prev-props upd-props-1) + [{:name "color" :value "gray"}])) + (t/is (= (ctv/find-properties-to-update prev-props upd-props-1) + [])) + (t/is (= (ctv/find-properties-to-add prev-props upd-props-1) + []))) + + (t/testing "a property to update" + (t/is (= (ctv/find-properties-to-remove prev-props upd-props-2) + [])) + (t/is (= (ctv/find-properties-to-update prev-props upd-props-2) + [{:name "color" :value "blue"}])) + (t/is (= (ctv/find-properties-to-add prev-props upd-props-2) + []))) + + (t/testing "a property to add" + (t/is (= (ctv/find-properties-to-remove prev-props upd-props-3) + [])) + (t/is (= (ctv/find-properties-to-update prev-props upd-props-3) + [])) + (t/is (= (ctv/find-properties-to-add prev-props upd-props-3) + [{:name "shadow" :value "large"}]))) + + (t/testing "properties to remove, update & add" + (t/is (= (ctv/find-properties-to-remove prev-props upd-props-4) + [{:name "border" :value "yes"}])) + (t/is (= (ctv/find-properties-to-update prev-props upd-props-4) + [{:name "color" :value "yellow"}])) + (t/is (= (ctv/find-properties-to-add prev-props upd-props-4) + [{:name "shadow" :value "large"}]))) + + (t/testing "find property index" + (t/is (= (ctv/find-index-for-property-name prev-props "border") 0)) + (t/is (= (ctv/find-index-for-property-name prev-props "color") 1))))) diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index b88f18ff4..8bc9f90d8 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -17,6 +17,7 @@ [app.common.types.component :as ctc] [app.common.types.components-list :as ctkl] [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.helpers :as dsh] @@ -32,6 +33,59 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) +(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 + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :shape-for-rename)) + + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + data (dsh/lookup-file-data state) + objects (-> (dsh/get-page data page-id) + (get :objects)) + + 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 (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/with-library-data data)) + + changes (reduce + (fn [changes {:keys [name]}] + (-> changes + (clvp/generate-update-property-value component-id (ctv/find-index-for-property-name previous-properties name) ""))) + changes + 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)) + + undo-id (js/Symbol)] + + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dwu/commit-undo-transaction undo-id)))))) + (defn update-property-name "Update the variant property name on the position pos in all the components with this variant-id" diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 821e8cd5d..c4762de28 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -11,6 +11,7 @@ [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] @@ -23,7 +24,7 @@ [app.main.ui.context :as ctx] [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.layer-name :refer [layer-name]] + [app.main.ui.workspace.sidebar.layer-name :refer [layer-name*]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -54,10 +55,16 @@ (= uuid/zero (:parent-id item))) absolute? (ctl/item-absolute? item) main-instance? (:main-instance item) + variants? (features/use-feature "variants/v1") is-variant? (when variants? (ctk/is-variant? item)) + is-variant-container? (when variants? (ctk/is-variant-container? item)) + variant-id (when is-variant? (:variant-id item)) variant-name (when is-variant? (:variant-name item)) - is-variant-container? (when variants? (ctk/is-variant-container? item))] + + data (deref refs/workspace-data) + component (ctkl/get-component data (:component-id item)) + variant-properties (:variant-properties component)] [:* [:div {:id id :ref dref @@ -121,21 +128,24 @@ {:shape item :main-instance? main-instance?}]]]) - [:& layer-name {:ref name-ref - :shape-id id - :shape-name name - :is-shape-touched touched? - :disabled-double-click read-only? - :on-start-edit on-disable-drag - :on-stop-edit on-enable-drag - :depth depth - :is-blocked blocked? - :parent-size parent-size - :is-selected selected? - :type-comp (or component-tree? is-variant-container?) - :type-frame (cfh/frame-shape? item) - :variant-name variant-name - :is-hidden hidden?}] + [:> layer-name* {:ref name-ref + :shape-id id + :shape-name name + :is-shape-touched touched? + :disabled-double-click read-only? + :on-start-edit on-disable-drag + :on-stop-edit on-enable-drag + :depth depth + :is-blocked blocked? + :parent-size parent-size + :is-selected selected? + :type-comp (or component-tree? is-variant-container?) + :type-frame (cfh/frame-shape? item) + :variant-id variant-id + :variant-name variant-name + :variant-properties variant-properties + :component-id (:id component) + :is-hidden hidden?}] (when (not read-only?) [:div {:class (stl/css-case diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs index 41d8240f4..53e07b56d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.cljs @@ -9,7 +9,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.types.variant :as ctv] [app.main.data.workspace :as dw] + [app.main.data.workspace.variants :as dwv] [app.main.store :as st] [app.util.debug :as dbg] [app.util.dom :as dom] @@ -24,12 +26,13 @@ (-> (l/in [:workspace-local :shape-for-rename]) (l/derived st/state))) -(mf/defc layer-name +(mf/defc layer-name* {::mf/wrap-props false ::mf/forward-ref true} [{:keys [shape-id shape-name is-shape-touched disabled-double-click on-start-edit on-stop-edit depth parent-size is-selected - type-comp type-frame variant-name is-hidden is-blocked]} external-ref] + type-comp type-frame variant-id variant-name variant-properties + component-id is-hidden is-blocked]} external-ref] (let [edition* (mf/use-state false) edition? (deref edition*) @@ -39,6 +42,9 @@ shape-for-rename (mf/deref lens:shape-for-rename) shape-name (d/nilv variant-name shape-name) + default-value (if variant-id + (ctv/properties-map-to-string variant-properties) + shape-name) has-path? (str/includes? shape-name "/") @@ -54,13 +60,17 @@ accept-edit (mf/use-fn - (mf/deps shape-id on-stop-edit) + (mf/deps shape-id on-stop-edit component-id variant-id variant-name variant-properties) (fn [] (let [name-input (mf/ref-val ref) name (str/trim (dom/get-value name-input))] (on-stop-edit) (reset! edition* false) - (st/emit! (dw/end-rename-shape shape-id name))))) + (if variant-name + (let [valid? (ctv/valid-properties-string? name) + props (if valid? (ctv/properties-string-to-map name) {})] + (st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties props))) + (st/emit! (dw/end-rename-shape shape-id name)))))) cancel-edit (mf/use-fn @@ -99,7 +109,7 @@ :on-blur accept-edit :on-key-down on-key-down :auto-focus true - :default-value (d/nilv shape-name "")}] + :default-value (d/nilv default-value "")}] [:* [:span {:class (stl/css-case