From 8eb2aaa0a8b671182c3dc763bae728472bba8ed1 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 4 Mar 2025 13:52:40 +0100 Subject: [PATCH] :tada: Create a new variant from an existing one --- .../src/app/common/files/changes_builder.cljc | 43 +++--- common/src/app/common/logic/libraries.cljc | 16 ++- common/src/app/common/logic/variants.cljc | 11 ++ common/src/app/common/types/component.cljc | 2 + .../src/app/common/types/components_list.cljc | 6 +- .../app/main/data/workspace/shortcuts.cljs | 5 +- .../src/app/main/data/workspace/variants.cljs | 123 ++++++++++++++++++ .../app/main/ui/workspace/context_menu.cljs | 13 +- .../ui/workspace/sidebar/assets/common.cljs | 1 + .../sidebar/options/menus/component.cljs | 39 +++--- frontend/translations/en.po | 2 +- frontend/translations/es.po | 2 +- 12 files changed, 207 insertions(+), 56 deletions(-) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 894511e95..f59f3644f 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -155,13 +155,14 @@ (dm/get-in data [:pages-index uuid/zero :objects]))) (defn- apply-changes-local - [changes] + [changes & {:keys [apply-to-library?]}] (dm/assert! "expected valid changes" (check-changes! changes)) (if-let [file-data (::file-data (meta changes))] - (let [index (::applied-changes-count (meta changes)) + (let [library-data (::library-data (meta changes)) + index (::applied-changes-count (meta changes)) redo-changes (:redo-changes changes) new-changes (if (< index (count redo-changes)) (->> (subvec (:redo-changes changes) index) @@ -169,28 +170,12 @@ (assoc :page-id uuid/zero) (dissoc :component-id)))) []) - new-file-data (cfc/process-changes file-data new-changes)] + new-file-data (cfc/process-changes file-data new-changes) + new-library-data (if apply-to-library? + (cfc/process-changes library-data new-changes) + library-data)] (vary-meta changes assoc ::file-data new-file-data - ::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 + ::library-data new-library-data ::applied-changes-count (count redo-changes))) changes)) @@ -932,8 +917,10 @@ (defn add-component ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page] - (add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil)) + (add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil nil nil)) ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation] + (add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation nil nil)) + ([changes id path name new-shapes updated-shapes main-instance-id main-instance-page annotation variant-id variant-properties & {:keys [apply-changes-local-library?]}] (assert-page-id! changes) (assert-objects! changes) (let [page-id (::page-id (meta changes)) @@ -972,7 +959,9 @@ :name name :main-instance-id main-instance-id :main-instance-page main-instance-page - :annotation annotation} + :annotation annotation + :variant-id variant-id + :variant-properties variant-properties} (some? new-shapes) ;; this will be null in components-v2 (assoc :shapes (vec new-shapes)))) (into (map mk-change) updated-shapes)))) @@ -987,7 +976,7 @@ (map mk-change)) updated-shapes)))) - (apply-changes-local))))) + (apply-changes-local {:apply-to-library? apply-changes-local-library?}))))) (defn update-component [changes id update-fn & {:keys [apply-changes-local-library?]}] @@ -1019,7 +1008,7 @@ :variant-properties (:variant-properties prev-component) :objects (:objects prev-component)}) (cond-> apply-changes-local-library? - (apply-changes-local-library))) + (apply-changes-local {:apply-to-library? true}))) changes))) (defn delete-component diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index f7104d7ee..eb6e412a4 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -100,10 +100,10 @@ ;; ---- Components and instances creation ---- -(defn duplicate-component +(defn- duplicate-component "Clone the root shape of the component and all children. Generate new ids from all of them." - [component new-component-id library-data] + [component new-component-id library-data force-id] (let [components-v2 (dm/get-in library-data [:options :components-v2])] (if components-v2 (let [main-instance-page (ctf/get-component-page library-data component) @@ -139,7 +139,8 @@ (:parent-id main-instance-shape) (:objects main-instance-page) :update-new-shape update-new-shape - :update-original-shape update-original-shape) + :update-original-shape update-original-shape + :force-id force-id) remap-frame (fn [shape] @@ -179,7 +180,7 @@ (defn generate-duplicate-component "Create a new component copied from the one with the given id." - [changes library component-id new-component-id components-v2] + [changes library component-id new-component-id components-v2 & {:keys [new-shape-id apply-changes-local-library?]}] (let [component (ctkl/get-component (:data library) component-id) new-name (:name component) @@ -191,7 +192,7 @@ [new-component-shape new-component-shapes ; <- null in components-v2 new-main-instance-shape new-main-instance-shapes] - (duplicate-component component new-component-id (:data library))] + (duplicate-component component new-component-id (:data library) new-shape-id)] [new-main-instance-shape (-> changes @@ -207,7 +208,10 @@ [] (:id new-main-instance-shape) (:id main-instance-page) - (:annotation component)) + (:annotation component) + (:variant-id component) + (:variant-properties component) + {:apply-changes-local-library? apply-changes-local-library?}) ;; Update grid layout if the new main instance is inside (pcb/update-shapes [(:frame-id new-main-instance-shape)] diff --git a/common/src/app/common/logic/variants.cljc b/common/src/app/common/logic/variants.cljc index ceb775024..cdd24f80d 100644 --- a/common/src/app/common/logic/variants.cljc +++ b/common/src/app/common/logic/variants.cljc @@ -61,6 +61,17 @@ (into assigned new-properties))) +(defn extract-properties-values + [data objects variant-id] + (->> (find-related-components data objects variant-id) + (mapcat :variant-properties) + (group-by :name) + (map (fn [[k v]] + {:name k + :values (distinct + (map #(if (str/empty? (:value %)) "--" (:value %)) v))})))) + + (defn generate-update-property-name [changes variant-id pos new-name] (let [data (pcb/get-library-data changes) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 195e3a3e3..14844153e 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -333,6 +333,8 @@ (let [parent (get objects (:parent-id shape))] ;; We don't want to change the structure of component copies (and (not (in-component-copy-not-head? shape)) + ;; We don't want to duplicate variants + (not (is-variant? shape)) ;; Non instance, non copy. We allow (or (not (instance-head? shape)) (not (in-component-copy? parent)))))) diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 1a2edd786..1139a6a66 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -34,12 +34,14 @@ (assoc component :modified-at (dt/now))) (defn add-component - [fdata {:keys [id name path main-instance-id main-instance-page shapes annotation]}] + [fdata {:keys [id name path main-instance-id main-instance-page shapes annotation variant-id variant-properties]}] (let [components-v2 (dm/get-in fdata [:options :components-v2]) fdata (update fdata :components assoc id (touch {:id id :name name :path path}))] (if components-v2 (cond-> (update-in fdata [:components id] assoc :main-instance-id main-instance-id :main-instance-page main-instance-page) - annotation (update-in [:components id] assoc :annotation annotation)) + annotation (update-in [:components id] assoc :annotation annotation) + variant-id (update-in [:components id] assoc :variant-id variant-id) + variant-properties (update-in [:components id] assoc :variant-properties variant-properties)) (let [wrap-object-fn cfeat/*wrap-with-objects-map-fn*] (assoc-in fdata [:components id :objects] diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index f7a7b591c..a54e14cf0 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -26,6 +26,7 @@ [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.variants :as dwv] [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] @@ -121,7 +122,7 @@ :duplicate {:tooltip (ds/meta "D") :command (ds/c-mod "d") :subsections [:edit] - :fn #(emit-when-no-readonly (dw/duplicate-selected true))} + :fn #(emit-when-no-readonly (dwv/duplicate-or-add-variant))} :start-editing {:tooltip (ds/enter) :command "enter" @@ -175,7 +176,7 @@ :create-component {:tooltip (ds/meta "K") :command (ds/c-mod "k") :subsections [:modify-layers] - :fn #(emit-when-no-readonly (dwl/add-component))} + :fn #(emit-when-no-readonly (dwv/add-component-or-variant))} :detach-component {:tooltip (ds/meta-shift "K") :command (ds/c-mod "shift+k") diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index 268549b5d..3578af70d 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -7,17 +7,24 @@ (ns app.main.data.workspace.variants (:require [app.common.colors :as clr] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] [app.common.logic.variants :as clv] + [app.common.types.component :as ctc] + [app.common.types.components-list :as ctkl] [app.common.uuid :as uuid] [app.main.data.changes :as dch] [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.undo :as dwu] + [app.main.features :as features] + [app.util.dom :as dom] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -131,6 +138,70 @@ (dch/commit-changes changes) (dwu/commit-undo-transaction undo-id)))))) +(defn focus-property + [shape-id prop-num] + (ptk/reify ::focus-property + ptk/EffectEvent + (effect [_ _ _] + (dom/focus! (dom/get-element (str "variant-prop-" shape-id prop-num)))))) + + +(defn add-new-variant + "Create a new variant and add it to the variant-container" + [shape-id] + (ptk/reify ::add-new-variant + 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)) + shape (get objects shape-id) + shape (if (ctc/is-variant-container? shape) + (get objects (last (:shapes shape))) + shape) + component-id (:component-id shape) + component (ctkl/get-component data component-id) + + new-component-id (uuid/next) + new-shape-id (uuid/next) + + value (str clv/value-prefix + (-> (clv/extract-properties-values data objects (:variant-id component)) + last + :values + count + inc)) + + prop-num (dec (count (:variant-properties component))) + + + [new-shape changes] (-> (pcb/empty-changes it page-id) + (pcb/with-library-data data) + (pcb/with-objects objects) + (pcb/with-page-id page-id) + (cll/generate-duplicate-component + {:data data} + component-id + new-component-id + true + {:new-shape-id new-shape-id :apply-changes-local-library? true})) + + changes (-> changes + (clv/generate-update-property-value new-component-id prop-num value) + (pcb/change-parent (:parent-id shape) [new-shape] 0)) + + undo-id (js/Symbol)] + (rx/concat + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dwu/commit-undo-transaction undo-id) + (ptk/data-event :layout/update {:ids [(:parent-id shape)]}) + (dws/select-shape new-shape-id)) + (->> (rx/of (focus-property new-shape-id prop-num)) + (rx/delay 250))))))) + (defn transform-in-variant "Given the id of a main shape of a component, creates a variant structure for that component" @@ -174,3 +245,55 @@ (set-variant-id new-component-id variant-id) (add-new-property variant-id {:fill-values? true}) (dwu/commit-undo-transaction undo-id)))))) + +(defn add-component-or-variant + [] + (ptk/reify ::add-component-or-variant + + ptk/WatchEvent + (watch [_ state _] + (let [variants? (features/active-feature? state "variants/v1") + objects (dsh/lookup-page-objects state) + selected-ids (dsh/lookup-selected state) + selected-shapes (map (d/getf objects) selected-ids) + single? (= 1 (count selected-ids)) + first-shape (first selected-shapes) + + transform-in-variant? (and variants? + single? + (not (ctc/is-variant? first-shape)) + (ctc/main-instance? first-shape)) + add-new-variant? (and variants? + (every? ctc/is-variant? selected-shapes)) + undo-id (js/Symbol)] + (cond + transform-in-variant? + (rx/of (transform-in-variant (:id first-shape))) + + add-new-variant? + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/from (map add-new-variant selected-ids)) + (rx/of (dwu/commit-undo-transaction undo-id))) + + :else + (rx/of (dwl/add-component))))))) + +(defn duplicate-or-add-variant + [] + (ptk/reify ::duplicate-or-add-variant + ptk/WatchEvent + (watch [_ state _] + (let [variants? (features/active-feature? state "variants/v1") + objects (dsh/lookup-page-objects state) + selected-ids (dsh/lookup-selected state) + selected-shapes (map (d/getf objects) selected-ids) + add-new-variant? (and variants? + (every? ctc/is-variant? selected-shapes)) + undo-id (js/Symbol)] + (if add-new-variant? + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/from (map add-new-variant selected-ids)) + (rx/of (dwu/commit-undo-transaction undo-id))) + (rx/of (dws/duplicate-selected true))))))) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index df34e8406..4821f1f04 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -27,6 +27,7 @@ [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shortcuts :as sc] + [app.main.data.workspace.variants :as dwv] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] @@ -555,8 +556,10 @@ can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes)) heads (filter ctk/instance-head? shapes) components-menu-entries (cmm/generate-components-menu-entries heads true) + variant-container? (and single? (ctk/is-variant-container? (first shapes))) do-add-component #(st/emit! (dwl/add-component)) - do-add-multiple-components #(st/emit! (dwl/add-multiple-components))] + do-add-multiple-components #(st/emit! (dwl/add-multiple-components)) + do-add-variant #(st/emit! (dwv/add-new-variant (:id (first shapes))))] [:* (when can-make-component ;; We don't want to change the structure of component copies [:* @@ -577,7 +580,13 @@ :title (:title entry) :shortcut (when (contains? entry :shortcut) (sc/get-tooltip (:shortcut entry))) - :on-click (:action entry)}])])])) + :on-click (:action entry)}])]) + + (when variant-container? + [:> menu-separator*] + [:> menu-entry* {:title (tr "workspace.shape.menu.add-variant") + :shortcut (sc/get-tooltip :create-component) + :on-click do-add-variant}])])) (mf/defc context-menu-delete* {::mf/props :obj diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index acd214d0c..e6c0210ba 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -471,5 +471,6 @@ :action do-update-component}) (when (and variants? (not multi) main-instance?) {:title (tr "workspace.shape.menu.add-variant") + :shortcut :create-component :action do-add-variant})]] (filter (complement nil?) menu-entries))) 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 19de318e3..8233c87c9 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 @@ -288,11 +288,12 @@ [:* (for [[pos prop] (map vector (range) properties)] - [:div {:key (str (:id shape) (:name prop)) :class (stl/css :variant-property-container)} + [:div {:key (str (:id shape) pos) :class (stl/css :variant-property-container)} (if (ctk/main-instance? shape) [:* [:span {:class (stl/css :variant-property-name :variant-property-name-bg)} (:name prop)] - [:> combobox* {:default-selected (if (str/empty? (:value prop)) "--" (:value prop)) + [:> combobox* {:id (str "variant-prop-" (:id shape) pos) + :default-selected (if (str/empty? (:value prop)) "--" (:value prop)) :options (clj->js (get-options (:name prop))) :on-change (partial change-property-value pos)}]] @@ -741,7 +742,7 @@ (mf/defc variant-menu* [{:keys [shapes]}] - (let [multi (> (count shapes) 1) + (let [multi? (> (count shapes) 1) shape (first shapes) shape-name (:name shape) @@ -757,6 +758,11 @@ first-variant (get objects (first (:shapes shape))) variant-id (:variant-id first-variant) + dashes-to-end (mf/use-fn + (fn [data] + (let [dashes (if (some #(= % "--") data) ["--"] [])] + (concat (remove #(= % "--") data) dashes)))) + properties (mf/with-memo [data objects variant-id] (->> (dwv/find-related-components data objects variant-id) (mapcat :variant-properties) @@ -764,17 +770,20 @@ (map-indexed (fn [index [k v]] {:name k :pos index - :values (distinct - (map #(if (str/empty? (:value %)) "--" (:value %)) v))})))) + :values (->> v + (map #(if (str/empty? (:value %)) "--" (:value %))) + distinct + dashes-to-end)})))) + menu-open* (mf/use-state false) menu-open? (deref menu-open*) menu-entries [{:title (tr "workspace.shape.menu.add-variant-property") - :action #(st/emit! (dwv/add-new-property variant-id))}] - - show-menu? (seq menu-entries) + :action #(st/emit! (dwv/add-new-property variant-id))} + {:title (tr "workspace.shape.menu.add-variant") + :action #(st/emit! (dwv/add-new-variant (:id shape)))}] on-menu-click (mf/use-fn @@ -821,8 +830,8 @@ [:div {:class (stl/css :element-content)} [:div {:class (stl/css-case :component-wrapper true - :with-actions show-menu? - :without-actions (not show-menu?))} + :with-actions (not multi?) + :without-actions multi?)} [:button {:class (stl/css-case :component-name-wrapper true :with-main true :swappeable false)} @@ -832,12 +841,12 @@ [:div {:class (stl/css :name-wrapper)} [:div {:class (stl/css :component-name)} [:span {:class (stl/css :component-name-inside)} - (if multi + (if multi? (tr "settings.multiple") (cfh/last-path shape-name))]]]] - (when show-menu? + (when-not multi? [:div {:class (stl/css :component-actions)} [:button {:class (stl/css-case :menu-btn true :selected menu-open?) @@ -848,11 +857,11 @@ :on-close on-menu-close :menu-entries menu-entries :main-instance true}]])] - (when-not multi + (when-not multi? [:* - (for [property properties] + (for [[pos property] (map vector (range) properties)] (let [val (str/join ", " (:values property))] - [:div {:key (str (:id shape) (:name property)) :class (stl/css :variant-property-row)} + [:div {:key (str (:id shape) pos) :class (stl/css :variant-property-row)} [:> input-with-values* {:name (:name property) :values val :data-position (:pos property) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 15f3cc84a..54b627bf9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6218,7 +6218,7 @@ msgid "workspace.shape.menu.create-component" msgstr "Create component" msgid "workspace.shape.menu.add-variant" -msgstr "Add variant" +msgstr "Create variant" msgid "workspace.shape.menu.add-variant-property" msgstr "Add new property" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 93c9c7eeb..413a0e4d4 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6232,7 +6232,7 @@ msgid "workspace.shape.menu.create-component" msgstr "Crear componente" msgid "workspace.shape.menu.add-variant" -msgstr "Añadir variante" +msgstr "Crear variante" msgid "workspace.shape.menu.add-variant-property" msgstr "Añadir nueva propiedad"