From 07b15819d45bdeb9afb9fd5c849546c2a43385ee Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 1 Aug 2025 15:39:46 +0200 Subject: [PATCH] :tada: Add the ability to create variants from a selection (#7045) * :tada: Add the ability to create variants from a selection * :paperclip: Add PR feedback changes * :lipstick: Add minor cosmetic changes --------- Co-authored-by: Andrey Antukh --- frontend/src/app/main/data/workspace.cljs | 45 +--- .../src/app/main/data/workspace/shapes.cljs | 112 ++++++--- .../src/app/main/data/workspace/variants.cljs | 232 +++++++++++++----- .../main/ui/components/context_menu_a11y.cljs | 14 +- .../main/ui/components/context_menu_a11y.scss | 5 + .../app/main/ui/workspace/context_menu.cljs | 26 +- .../workspace/sidebar/assets/components.cljs | 42 +++- .../sidebar/options/menus/component.cljs | 12 + .../sidebar/options/menus/component.scss | 19 ++ frontend/src/app/plugins/shape.cljs | 4 +- frontend/translations/en.po | 7 + frontend/translations/es.po | 7 + 12 files changed, 370 insertions(+), 155 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index a59b27270e..f4b3216b53 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -17,7 +17,6 @@ [app.common.geom.proportions :as gpp] [app.common.geom.shapes :as gsh] [app.common.logging :as log] - [app.common.logic.shapes :as cls] [app.common.transit :as t] [app.common.types.component :as ctc] [app.common.types.fills :as types.fills] @@ -39,7 +38,6 @@ [app.main.data.project :as dpj] [app.main.data.workspace.bool :as dwb] [app.main.data.workspace.clipboard :as dwcp] - [app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.colors :as dwcl] [app.main.data.workspace.comments :as dwcm] [app.main.data.workspace.common :as dwc] @@ -683,52 +681,13 @@ ;; --- Change Shape Order (D&D Ordering) -(defn relocate-shapes - [ids parent-id to-index & [ignore-parents?]] - (dm/assert! (every? uuid? ids)) - (dm/assert! (set? ids)) - (dm/assert! (uuid? parent-id)) - (dm/assert! (number? to-index)) - - (ptk/reify ::relocate-shapes - ptk/WatchEvent - (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) - - ;; If we try to move a parent into a child we remove it - ids (filter #(not (cfh/is-parent? objects parent-id %)) ids) - - all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) - - 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) - (dch/commit-changes changes) - (dwco/expand-collapse parent-id) - (ptk/data-event :layout/update {:ids (concat all-parents ids)}) - (dwu/commit-undo-transaction undo-id)))))) - (defn relocate-selected-shapes [parent-id to-index] (ptk/reify ::relocate-selected-shapes ptk/WatchEvent (watch [_ state _] (let [selected (dsh/lookup-selected state)] - (rx/of (relocate-shapes selected parent-id to-index)))))) + (rx/of (dwsh/relocate-shapes selected parent-id to-index)))))) (defn start-editing-selected [] @@ -1169,7 +1128,7 @@ ptk/WatchEvent (watch [_ state _] (let [orphans (set (into [] (keys (find-orphan-shapes state))))] - (rx/of (relocate-shapes orphans uuid/zero 0 true)))))) + (rx/of (dwsh/relocate-shapes orphans uuid/zero 0 true)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Sitemap diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 3f8eb1c9cd..86131c67c3 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -21,6 +21,7 @@ [app.main.data.comments :as dc] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] + [app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.undo :as dwu] @@ -249,6 +250,44 @@ ;; Artboard ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn create-artboard-from-shapes + ([shapes id parent-id index name delta] + (create-artboard-from-shapes shapes id parent-id index name delta true)) + ([shapes id parent-id index name delta layout-update?] + (ptk/reify ::create-artboard-from-shapes + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) + + [frame-shape changes] + (cfsh/prepare-create-artboard-from-selection changes + id + parent-id + objects + shapes + index + name + false + nil + delta) + + undo-id (js/Symbol)] + + (when changes + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set (:id frame-shape))) + (when layout-update? (ptk/data-event :layout/update {:ids [(:id frame-shape)]})) + (ev/event {::ev/name "create-board" + :converted-from (cfh/get-selected-type objects shapes) + :parent-type (cfh/get-shape-type objects (:parent-id frame-shape))}) + (dwu/commit-undo-transaction undo-id)))))))) + (defn create-artboard-from-selection ([] (create-artboard-from-selection nil)) @@ -263,7 +302,7 @@ ([id parent-id index name delta] (ptk/reify ::create-artboard-from-selection ptk/WatchEvent - (watch [it state _] + (watch [_ state _] (let [page-id (:current-page-id state) objects (dsh/lookup-page-objects state page-id) selected (->> (dsh/lookup-selected state) @@ -271,35 +310,10 @@ (remove #(ctn/has-any-copy-parent? objects (get objects %))) (remove #(->> % (get objects) - (ctc/is-variant?)))) + (ctc/is-variant?))))] - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) - [frame-shape changes] - (cfsh/prepare-create-artboard-from-selection changes - id - parent-id - objects - selected - index - name - false - nil - delta) - - undo-id (js/Symbol)] - - (when changes - (rx/of - (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id frame-shape))) - (ptk/data-event :layout/update {:ids [(:id frame-shape)]}) - (ev/event {::ev/name "create-board" - :converted-from (cfh/get-selected-type objects selected) - :parent-type (cfh/get-shape-type objects (:parent-id frame-shape))}) - (dwu/commit-undo-transaction undo-id)))))))) + (rx/of (create-artboard-from-shapes selected id parent-id index name delta))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shape Flags @@ -379,3 +393,45 @@ ;; And finally: toggle the flag value on all the selected shapes (rx/of (update-shapes selected #(update % :use-for-thumbnail not)) (dwu/commit-undo-transaction undo-id))))))) + + +;; --- Change Shape Order (D&D Ordering) + +(defn relocate-shapes + [ids parent-id to-index & [ignore-parents?]] + (dm/assert! (every? uuid? ids)) + (dm/assert! (set? ids)) + (dm/assert! (uuid? parent-id)) + (dm/assert! (number? to-index)) + + (ptk/reify ::relocate-shapes + ptk/WatchEvent + (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) + + ;; If we try to move a parent into a child we remove it + ids (filter #(not (cfh/is-parent? objects parent-id %)) ids) + + all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) + + 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) + (dch/commit-changes changes) + (dwco/expand-collapse parent-id) + (ptk/data-event :layout/update {:ids (concat all-parents ids)}) + (dwu/commit-undo-transaction undo-id)))))) diff --git a/frontend/src/app/main/data/workspace/variants.cljs b/frontend/src/app/main/data/workspace/variants.cljs index 41e8cefaa8..8733abda7f 100644 --- a/frontend/src/app/main/data/workspace/variants.cljs +++ b/frontend/src/app/main/data/workspace/variants.cljs @@ -20,9 +20,11 @@ [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.helpers :as dsh] [app.main.data.workspace.colors :as cl] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] @@ -338,83 +340,101 @@ (defn transform-in-variant "Given the id of a main shape of a component, creates a variant structure for that component" - [main-instance-id] - (ptk/reify ::transform-in-variant - ptk/WatchEvent - (watch [_ state _] - (let [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) - cpath (cfh/split-path (:name main)) - name (first cpath) - num-props (max 1 (dec (count cpath))) - cont-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} - :name name - :r1 20 - :r2 20 - :r3 20 - :r4 20 - :is-variant-container true} - main-props {:layout-item-h-sizing :fix - :layout-item-v-sizing :fix - :variant-id variant-id - :name name} - stroke-props {:stroke-alignment :inner - :stroke-style :solid - :stroke-color "#bb97d8" ;; todo use color var? - :stroke-opacity 1 - :stroke-width 2} + ([main-instance-id] + (transform-in-variant main-instance-id nil nil [] true true)) + ([main-instance-id variant-id delta prefix 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) + ;; If there is a prefix, set is as first item of path + cpath (-> (:name main) + cfh/split-path + (cond-> + (seq prefix) + (->> (drop (count prefix)) + (cons (cfh/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} + 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) + m-base-props {:name name + :variant-id variant-id} + m-flex-props {:layout-item-h-sizing :fix + :layout-item-v-sizing :fix} + main-props (if flex? + (into m-base-props m-flex-props) + m-base-props) + 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 (if (ctsl/any-layout? parent) - (gpt/point 0 0) - (gpt/point -30 -30)) - undo-id (js/Symbol)] + 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) + (rx/concat + (rx/of + (dwu/start-undo-transaction undo-id) - (when (not= name (:name main)) - (dwl/rename-component component-id name)) + (when (not= name (:name main)) + (dwl/rename-component component-id name)) ;; Create variant container - (dwsh/create-artboard-from-selection variant-id nil nil nil delta) - (cl/remove-all-fills variant-vec {:color clr/black :opacity 1}) - (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)) + (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}))) + (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)))) + (when (> (count cpath) 1) + (rx/from + (map + #(update-property-value component-id % (nth cpath (inc %))) + (range num-props)))) - (rx/of - (add-new-variant main-instance-id) - (dwu/commit-undo-transaction undo-id) - (ptk/data-event :layout/update {:ids [variant-id]}))))))) + (rx/of + (when duplicate? (add-new-variant main-instance-id)) + (dwu/commit-undo-transaction undo-id) + (when flex? + (ptk/data-event :layout/update {:ids [variant-id]}))))))))) (defn add-component-or-variant "Manage the shared shortcut, and do the pertinent action" @@ -509,3 +529,91 @@ (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 combine-as-variants + ([] + (combine-as-variants nil {})) + ([selected {:keys [page-id]}] + (ptk/reify ::combine-as-variants + ptk/WatchEvent + (watch [_ state stream] + (let [current-page (:current-page-id state) + + combine + (fn [current-page] + (let [objects (dsh/lookup-page-objects state current-page) + selected (or selected + (->> (dsh/lookup-selected state) + (cfh/clean-loops objects) + (remove (fn [id] + (let [shape (get objects id)] + (or (not (ctc/main-instance? shape)) + (ctc/is-variant? shape))))))) + shapes (mapv #(get objects %) selected) + rect (bounding-rect shapes) + prefix (->> shapes + (mapv #(cfh/split-path (:name %))) + (common-prefix)) + first-shape (first shapes) + delta (gpt/point (- (:x rect) (:x first-shape) 30) + (- (:y rect) (:y first-shape) 30)) + common-parent (->> selected + (mapv #(-> (cfh/get-parent-ids objects %) reverse)) + common-prefix + last) + index (-> (get objects common-parent) + :shapes + count + inc) + variant-id (uuid/next) + undo-id (js/Symbol)] + (rx/concat + (rx/of + (when (and page-id (not= current-page page-id)) + (dcm/go-to-workspace :page-id page-id)) + (dwu/start-undo-transaction undo-id) + (transform-in-variant (first selected) variant-id delta prefix false false) + (dwsh/relocate-shapes (into #{} (-> selected rest reverse)) variant-id 0) + (dwt/update-dimensions [variant-id] :width (+ (:width rect) 60)) + (dwt/update-dimensions [variant-id] :height (+ (:height rect) 60)) + (dwsh/relocate-shapes #{variant-id} common-parent index) + (dwu/commit-undo-transaction undo-id))))) + + redirect-to-page + (fn [page-id] + (rx/merge + (->> stream + (rx/filter (ptk/type? ::dwpg/initialize-page)) + (rx/take 1) + (rx/observe-on :async) + (rx/mapcat (fn [_] (combine page-id)))) + (rx/of (dcm/go-to-workspace :page-id page-id))))] + + (if (and page-id (not= page-id current-page)) + (redirect-to-page page-id) + (combine current-page))))))) + diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs index a3f199ebb4..9160863ee6 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.cljs +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -42,6 +42,8 @@ [:map [:name :string] [:id :string] + [:title {:optional true} [:maybe :string]] + [:disabled {:optional true} [:maybe :boolean]] [:handler {:optional true} fn?] [:options {:optional true} [:sequential [:ref ::option]]]] @@ -258,7 +260,9 @@ (let [name (:name option) id (:id option) sub-options (:options option) - handler (:handler option)] + handler (:handler option) + title (:title option) + disabled (:disabled option)] (when name (if (= name :separator) [:li {:key (dm/str "context-item-" index) @@ -273,10 +277,12 @@ :role "menuitem" :on-key-down dom/prevent-default} (if-not sub-options - [:a {:class (stl/css :context-menu-action) + [:a {:class (stl/css-case :context-menu-action true :context-menu-action-disabled disabled) + :title title :on-click #(do (dom/stop-propagation %) - (on-close %) - (handler %)) + (when-not disabled + (on-close %) + (handler %))) :data-testid id} (if (and in-dashboard? (= name "Default")) (tr "dashboard.default-team-name") diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index a2ced28ced..d15988775d 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -105,6 +105,11 @@ } } + .context-menu-action-disabled, + &:hover .context-menu-action-disabled { + color: var(--color-foreground-secondary); + } + &:focus { outline: none; } diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 99e81678cc..5dac5d5064 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -570,9 +570,14 @@ heads (filter ctk/instance-head? shapes) components-menu-entries (cmm/generate-components-menu-entries heads) 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-variant #(st/emit! (dwv/add-new-variant (:id (first shapes))))] + all-main? (every? ctk/main-instance? shapes) + any-variant? (some ctk/is-variant? shapes) + do-add-component (mf/use-fn #(st/emit! (dwl/add-component))) + do-add-multiple-components (mf/use-fn #(st/emit! (dwl/add-multiple-components))) + do-combine-as-variants (mf/use-fn #(st/emit! (dwv/combine-as-variants))) + do-add-variant (mf/use-fn + (mf/deps shapes) + #(st/emit! (dwv/add-new-variant (:id (first shapes)))))] [:* (when can-make-component ;; We don't want to change the structure of component copies [:* @@ -596,10 +601,17 @@ :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}])])) + [:* + [:> menu-separator*] + [:> menu-entry* {:title (tr "workspace.shape.menu.add-variant") + :shortcut (sc/get-tooltip :create-component) + :on-click do-add-variant}]]) + + (when (and (not single?) all-main? (not any-variant?)) + [:* + [:> menu-separator*] + [:> menu-entry* {:title (tr "workspace.shape.menu.combine-as-variants") + :on-click do-combine-as-variants}]])])) (mf/defc context-menu-delete* {::mf/props :obj diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index c2bed40acb..fb67b63544 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -317,13 +317,21 @@ (seq (:colors selected)) (seq (:typographies selected))) - any-variant? (mf/with-memo [selected components current-component-id] - (let [selected-and-current (-> (d/nilv selected []) - (conj current-component-id) - set)] - (->> components - (filter #(contains? selected-and-current (:id %))) - (some ctc/is-variant?)))) + selected-and-current (mf/with-memo [selected components current-component-id] + (-> (d/nilv selected []) + (conj current-component-id) + set)) + + selected-and-current-full (mf/with-memo [selected-and-current] + (->> components + (filter #(contains? selected-and-current (:id %))))) + + any-variant? (mf/with-memo [selected-and-current] + (some ctc/is-variant? selected-and-current-full)) + + all-same-page? (mf/with-memo [selected-and-current] + (let [page (:main-instance-page (first selected-and-current-full))] + (every? #(= page (:main-instance-page %)) selected-and-current-full))) groups (mf/with-memo [components reverse-sort?] (grp/group-assets components reverse-sort?)) @@ -497,7 +505,17 @@ (st/emit! (dwl/go-to-component-file file-id component)))))) on-asset-click - (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))] + (mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups)) + + on-combine-as-variants + (mf/use-fn + (mf/deps selected-and-current-full) + (fn [event] + (dom/stop-propagation event) + (let [page-id (->> selected-and-current-full first :main-instance-page) + ids (into #{} (map :main-instance-id selected-full))] + + (st/emit! (dwv/combine-as-variants ids {:page-id page-id})))))] [:& cmm/asset-section {:file-id file-id :title (tr "workspace.assets.components") @@ -575,4 +593,10 @@ (when (not multi-assets?) {:name (tr "workspace.shape.menu.show-main") :id "assets-show-main-component" - :handler on-show-main})]}]]])) + :handler on-show-main}) + (when (and is-local multi-components? (not any-variant?)) + {:name (tr "workspace.shape.menu.combine-as-variants") + :id "assets-combine-as-variants" + :title (when-not all-same-page? (tr "workspace.shape.menu.combine-as-variants-error")) + :disabled (not all-same-page?) + :handler on-combine-as-variants})]}]]])) 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 9b571baf0a..2d48f73cb9 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 @@ -778,6 +778,9 @@ copies (filter ctk/in-component-copy? shapes) can-swap? (boolean (seq copies)) + all-main? (every? ctk/main-instance? shapes) + any-variant? (some ctk/is-variant? shapes) + ;; For when it's only one shape shape (first shapes) id (:id shape) @@ -790,6 +793,7 @@ data (dm/get-in libraries [(:component-file shape) :data]) variants? (features/use-feature "variants/v1") is-variant? (when variants? (ctk/is-variant? component)) + main-instance? (ctk/main-instance? shape) components (mapv #(ctf/resolve-component % @@ -830,6 +834,9 @@ (when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap))) (tm/schedule-on-idle #(dom/focus! (dom/get-element search-id)))))) + on-combine-as-variants + #(st/emit! (dwv/combine-as-variants)) + ;; NOTE: function needed for force rerender from the bottom ;; components. This is because `component-annotation` ;; component changes the component but that has no direct @@ -937,6 +944,11 @@ :shapes shapes :data data}]) + (when (and multi all-main? (not any-variant?)) + [:button {:class (stl/css :combine-variant-button) + :on-click on-combine-as-variants} + [:span (tr "workspace.shape.menu.combine-as-variants")]]) + (when (dbg/enabled? :display-touched) [:div ":touched " (str (:touched shape))])])]))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss index 4f27feb5f6..bf94c33ede 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.scss @@ -808,3 +808,22 @@ right: $s-2; top: $s-2; } + +.combine-variant-button { + @include buttonStyle; + @include uppercaseTitleTipography; + cursor: default; + display: flex; + justify-content: center; + align-items: center; + padding: $s-8; + border-radius: $br-8; + background-color: var(--assets-item-background-color); + color: var(--color-foreground-secondary); + cursor: pointer; + + &:hover { + background-color: var(--assets-item-background-color-hover); + color: var(--color-foreground-primary); + } +} diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 1b82f8d2bb..55fe987d8e 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -933,7 +933,7 @@ :else (let [child-id (obj/get child "$id")] - (st/emit! (dw/relocate-shapes #{child-id} id 0)))))) + (st/emit! (dwsh/relocate-shapes #{child-id} id 0)))))) :insertChild (fn [index child] @@ -953,7 +953,7 @@ :else (let [child-id (obj/get child "$id")] - (st/emit! (dw/relocate-shapes #{child-id} id index)))))) + (st/emit! (dwsh/relocate-shapes #{child-id} id index)))))) ;; Only for frames :addFlexLayout diff --git a/frontend/translations/en.po b/frontend/translations/en.po index f51600177d..59387046e2 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6872,6 +6872,13 @@ msgstr "Create annotation" msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Selection to board" +#: src/app/main/ui/workspace/context_menu.cljs:385 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "Combine as variants" + +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "Components need to be in the same page" + #: src/app/main/ui/workspace/context_menu.cljs:573 msgid "workspace.shape.menu.create-component" msgstr "Create component" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ce22c5f01e..296fc7cbea 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6850,6 +6850,13 @@ msgstr "Crear una nota" msgid "workspace.shape.menu.create-artboard-from-selection" msgstr "Tablero de selección" +#: src/app/main/ui/workspace/context_menu.cljs:385 +msgid "workspace.shape.menu.combine-as-variants" +msgstr "Combinar como variantes" + +msgid "workspace.shape.menu.combine-as-variants-error" +msgstr "Los componentes tienen que estar en la misma página" + #: src/app/main/ui/workspace/context_menu.cljs:573 msgid "workspace.shape.menu.create-component" msgstr "Crear componente"