🎉 Add the ability to create variants from a selection (#7045)

* 🎉 Add the ability to create variants from a selection

* 📎 Add PR feedback changes

* 💄 Add minor cosmetic changes

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Pablo Alba
2025-08-01 15:39:46 +02:00
committed by GitHub
parent f519c6ef46
commit 07b15819d4
12 changed files with 370 additions and 155 deletions

View File

@@ -17,7 +17,6 @@
[app.common.geom.proportions :as gpp] [app.common.geom.proportions :as gpp]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.types.component :as ctc] [app.common.types.component :as ctc]
[app.common.types.fills :as types.fills] [app.common.types.fills :as types.fills]
@@ -39,7 +38,6 @@
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
[app.main.data.workspace.bool :as dwb] [app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.clipboard :as dwcp] [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.colors :as dwcl]
[app.main.data.workspace.comments :as dwcm] [app.main.data.workspace.comments :as dwcm]
[app.main.data.workspace.common :as dwc] [app.main.data.workspace.common :as dwc]
@@ -683,52 +681,13 @@
;; --- Change Shape Order (D&D Ordering) ;; --- 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 (defn relocate-selected-shapes
[parent-id to-index] [parent-id to-index]
(ptk/reify ::relocate-selected-shapes (ptk/reify ::relocate-selected-shapes
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [selected (dsh/lookup-selected 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 (defn start-editing-selected
[] []
@@ -1169,7 +1128,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [orphans (set (into [] (keys (find-orphan-shapes 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 ;; Sitemap

View File

@@ -21,6 +21,7 @@
[app.main.data.comments :as dc] [app.main.data.comments :as dc]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.workspace.collapse :as dwco]
[app.main.data.workspace.edition :as dwe] [app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
@@ -249,6 +250,44 @@
;; Artboard ;; 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 (defn create-artboard-from-selection
([] ([]
(create-artboard-from-selection nil)) (create-artboard-from-selection nil))
@@ -263,7 +302,7 @@
([id parent-id index name delta] ([id parent-id index name delta]
(ptk/reify ::create-artboard-from-selection (ptk/reify ::create-artboard-from-selection
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [_ state _]
(let [page-id (:current-page-id state) (let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state page-id) objects (dsh/lookup-page-objects state page-id)
selected (->> (dsh/lookup-selected state) selected (->> (dsh/lookup-selected state)
@@ -271,35 +310,10 @@
(remove #(ctn/has-any-copy-parent? objects (get objects %))) (remove #(ctn/has-any-copy-parent? objects (get objects %)))
(remove #(->> % (remove #(->> %
(get objects) (get objects)
(ctc/is-variant?)))) (ctc/is-variant?))))]
changes (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects))
[frame-shape changes] (rx/of (create-artboard-from-shapes selected id parent-id index name delta)))))))
(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))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shape Flags ;; Shape Flags
@@ -379,3 +393,45 @@
;; And finally: toggle the flag value on all the selected shapes ;; And finally: toggle the flag value on all the selected shapes
(rx/of (update-shapes selected #(update % :use-for-thumbnail not)) (rx/of (update-shapes selected #(update % :use-for-thumbnail not))
(dwu/commit-undo-transaction undo-id))))))) (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))))))

View File

@@ -20,9 +20,11 @@
[app.common.types.variant :as ctv] [app.common.types.variant :as ctv]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.main.data.changes :as dch] [app.main.data.changes :as dch]
[app.main.data.common :as dcm]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.workspace.colors :as cl] [app.main.data.workspace.colors :as cl]
[app.main.data.workspace.libraries :as dwl] [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.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
@@ -338,83 +340,101 @@
(defn transform-in-variant (defn transform-in-variant
"Given the id of a main shape of a component, creates a variant structure for "Given the id of a main shape of a component, creates a variant structure for
that component" that component"
[main-instance-id] ([main-instance-id]
(ptk/reify ::transform-in-variant (transform-in-variant main-instance-id nil nil [] true true))
ptk/WatchEvent ([main-instance-id variant-id delta prefix duplicate? flex?]
(watch [_ state _] (ptk/reify ::transform-in-variant
(let [variant-id (uuid/next) ptk/WatchEvent
variant-vec [variant-id] (watch [_ state _]
file-id (:current-file-id state) (let [variant-id (or variant-id (uuid/next))
page-id (:current-page-id state) variant-vec [variant-id]
objects (dsh/lookup-page-objects state file-id page-id) file-id (:current-file-id state)
main (get objects main-instance-id) page-id (:current-page-id state)
parent (get objects (:parent-id main)) objects (dsh/lookup-page-objects state file-id page-id)
component-id (:component-id main) main (get objects main-instance-id)
cpath (cfh/split-path (:name main)) parent (get objects (:parent-id main))
name (first cpath) component-id (:component-id main)
num-props (max 1 (dec (count cpath))) ;; If there is a prefix, set is as first item of path
cont-props {:layout-item-h-sizing :auto cpath (-> (:name main)
:layout-item-v-sizing :auto cfh/split-path
:layout-padding {:p1 30 :p2 30 :p3 30 :p4 30} (cond->
:layout-gap {:row-gap 0 :column-gap 20} (seq prefix)
:name name (->> (drop (count prefix))
:r1 20 (cons (cfh/join-path prefix))
:r2 20 vec)))
:r3 20
:r4 20 name (first cpath)
:is-variant-container true} num-props (max 1 (dec (count cpath)))
main-props {:layout-item-h-sizing :fix base-props {:is-variant-container true
:layout-item-v-sizing :fix :name name
:variant-id variant-id :r1 20
:name name} :r2 20
stroke-props {:stroke-alignment :inner :r3 20
:stroke-style :solid :r4 20}
:stroke-color "#bb97d8" ;; todo use color var? flex-props {:layout-item-h-sizing :auto
:stroke-opacity 1 :layout-item-v-sizing :auto
:stroke-width 2} :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 ;; Move the position of the variant container so the main shape doesn't
;; change its position ;; change its position
delta (if (ctsl/any-layout? parent) delta (or delta
(gpt/point 0 0) (if (ctsl/any-layout? parent)
(gpt/point -30 -30)) (gpt/point 0 0)
undo-id (js/Symbol)] (gpt/point -30 -30)))
undo-id (js/Symbol)]
;;TODO Refactor all called methods in order to be able to ;;TODO Refactor all called methods in order to be able to
;;generate changes instead of call the events ;;generate changes instead of call the events
(rx/concat (rx/concat
(rx/of (rx/of
(dwu/start-undo-transaction undo-id) (dwu/start-undo-transaction undo-id)
(when (not= name (:name main)) (when (not= name (:name main))
(dwl/rename-component component-id name)) (dwl/rename-component component-id name))
;; Create variant container ;; Create variant container
(dwsh/create-artboard-from-selection variant-id nil nil nil delta) (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}) (cl/remove-all-fills variant-vec {:color clr/black :opacity 1})
(dwsl/create-layout-from-id variant-id :flex) (when flex? (dwsl/create-layout-from-id variant-id :flex))
(dwsh/update-shapes variant-vec #(merge % cont-props)) (dwsh/update-shapes variant-vec #(merge % cont-props))
(dwsh/update-shapes [main-instance-id] #(merge % main-props)) (dwsh/update-shapes [main-instance-id] #(merge % main-props))
(cl/add-stroke variant-vec stroke-props) (cl/add-stroke variant-vec stroke-props)
(set-variant-id component-id variant-id)) (set-variant-id component-id variant-id))
;; Add the necessary number of new properties, with default values ;; Add the necessary number of new properties, with default values
(rx/from (rx/from
(repeatedly num-props (repeatedly num-props
#(add-new-property variant-id {:fill-values? true}))) #(add-new-property variant-id {:fill-values? true})))
;; When the component has path, set the path items as properties values ;; When the component has path, set the path items as properties values
(when (> (count cpath) 1) (when (> (count cpath) 1)
(rx/from (rx/from
(map (map
#(update-property-value component-id % (nth cpath (inc %))) #(update-property-value component-id % (nth cpath (inc %)))
(range num-props)))) (range num-props))))
(rx/of (rx/of
(add-new-variant main-instance-id) (when duplicate? (add-new-variant main-instance-id))
(dwu/commit-undo-transaction undo-id) (dwu/commit-undo-transaction undo-id)
(ptk/data-event :layout/update {:ids [variant-id]}))))))) (when flex?
(ptk/data-event :layout/update {:ids [variant-id]})))))))))
(defn add-component-or-variant (defn add-component-or-variant
"Manage the shared shortcut, and do the pertinent action" "Manage the shared shortcut, and do the pertinent action"
@@ -509,3 +529,91 @@
(rx/of (rename-variant (:variant-id component) name)) (rx/of (rename-variant (:variant-id component) name))
(rx/of (dwl/rename-component-and-main-instance component-id 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)))))))

View File

@@ -42,6 +42,8 @@
[:map [:map
[:name :string] [:name :string]
[:id :string] [:id :string]
[:title {:optional true} [:maybe :string]]
[:disabled {:optional true} [:maybe :boolean]]
[:handler {:optional true} fn?] [:handler {:optional true} fn?]
[:options {:optional true} [:options {:optional true}
[:sequential [:ref ::option]]]] [:sequential [:ref ::option]]]]
@@ -258,7 +260,9 @@
(let [name (:name option) (let [name (:name option)
id (:id option) id (:id option)
sub-options (:options option) sub-options (:options option)
handler (:handler option)] handler (:handler option)
title (:title option)
disabled (:disabled option)]
(when name (when name
(if (= name :separator) (if (= name :separator)
[:li {:key (dm/str "context-item-" index) [:li {:key (dm/str "context-item-" index)
@@ -273,10 +277,12 @@
:role "menuitem" :role "menuitem"
:on-key-down dom/prevent-default} :on-key-down dom/prevent-default}
(if-not sub-options (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-click #(do (dom/stop-propagation %)
(on-close %) (when-not disabled
(handler %)) (on-close %)
(handler %)))
:data-testid id} :data-testid id}
(if (and in-dashboard? (= name "Default")) (if (and in-dashboard? (= name "Default"))
(tr "dashboard.default-team-name") (tr "dashboard.default-team-name")

View File

@@ -105,6 +105,11 @@
} }
} }
.context-menu-action-disabled,
&:hover .context-menu-action-disabled {
color: var(--color-foreground-secondary);
}
&:focus { &:focus {
outline: none; outline: none;
} }

View File

@@ -570,9 +570,14 @@
heads (filter ctk/instance-head? shapes) heads (filter ctk/instance-head? shapes)
components-menu-entries (cmm/generate-components-menu-entries heads) components-menu-entries (cmm/generate-components-menu-entries heads)
variant-container? (and single? (ctk/is-variant-container? (first shapes))) variant-container? (and single? (ctk/is-variant-container? (first shapes)))
do-add-component #(st/emit! (dwl/add-component)) all-main? (every? ctk/main-instance? shapes)
do-add-multiple-components #(st/emit! (dwl/add-multiple-components)) any-variant? (some ctk/is-variant? shapes)
do-add-variant #(st/emit! (dwv/add-new-variant (:id (first 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 (when can-make-component ;; We don't want to change the structure of component copies
[:* [:*
@@ -596,10 +601,17 @@
:on-click (:action entry)}])]) :on-click (:action entry)}])])
(when variant-container? (when variant-container?
[:> menu-separator*] [:*
[:> menu-entry* {:title (tr "workspace.shape.menu.add-variant") [:> menu-separator*]
:shortcut (sc/get-tooltip :create-component) [:> menu-entry* {:title (tr "workspace.shape.menu.add-variant")
:on-click do-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/defc context-menu-delete*
{::mf/props :obj {::mf/props :obj

View File

@@ -317,13 +317,21 @@
(seq (:colors selected)) (seq (:colors selected))
(seq (:typographies selected))) (seq (:typographies selected)))
any-variant? (mf/with-memo [selected components current-component-id] selected-and-current (mf/with-memo [selected components current-component-id]
(let [selected-and-current (-> (d/nilv selected []) (-> (d/nilv selected [])
(conj current-component-id) (conj current-component-id)
set)] set))
(->> components
(filter #(contains? selected-and-current (:id %))) selected-and-current-full (mf/with-memo [selected-and-current]
(some ctc/is-variant?)))) (->> 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?] groups (mf/with-memo [components reverse-sort?]
(grp/group-assets components reverse-sort?)) (grp/group-assets components reverse-sort?))
@@ -497,7 +505,17 @@
(st/emit! (dwl/go-to-component-file file-id component)))))) (st/emit! (dwl/go-to-component-file file-id component))))))
on-asset-click 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 [:& cmm/asset-section {:file-id file-id
:title (tr "workspace.assets.components") :title (tr "workspace.assets.components")
@@ -575,4 +593,10 @@
(when (not multi-assets?) (when (not multi-assets?)
{:name (tr "workspace.shape.menu.show-main") {:name (tr "workspace.shape.menu.show-main")
:id "assets-show-main-component" :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})]}]]]))

View File

@@ -778,6 +778,9 @@
copies (filter ctk/in-component-copy? shapes) copies (filter ctk/in-component-copy? shapes)
can-swap? (boolean (seq copies)) 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 ;; For when it's only one shape
shape (first shapes) shape (first shapes)
id (:id shape) id (:id shape)
@@ -790,6 +793,7 @@
data (dm/get-in libraries [(:component-file shape) :data]) data (dm/get-in libraries [(:component-file shape) :data])
variants? (features/use-feature "variants/v1") variants? (features/use-feature "variants/v1")
is-variant? (when variants? (ctk/is-variant? component)) is-variant? (when variants? (ctk/is-variant? component))
main-instance? (ctk/main-instance? shape) main-instance? (ctk/main-instance? shape)
components (mapv #(ctf/resolve-component % components (mapv #(ctf/resolve-component %
@@ -830,6 +834,9 @@
(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap))) (when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap)))
(tm/schedule-on-idle #(dom/focus! (dom/get-element search-id)))))) (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 ;; NOTE: function needed for force rerender from the bottom
;; components. This is because `component-annotation` ;; components. This is because `component-annotation`
;; component changes the component but that has no direct ;; component changes the component but that has no direct
@@ -937,6 +944,11 @@
:shapes shapes :shapes shapes
:data data}]) :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) (when (dbg/enabled? :display-touched)
[:div ":touched " (str (:touched shape))])])]))) [:div ":touched " (str (:touched shape))])])])))

View File

@@ -808,3 +808,22 @@
right: $s-2; right: $s-2;
top: $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);
}
}

View File

@@ -933,7 +933,7 @@
:else :else
(let [child-id (obj/get child "$id")] (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 :insertChild
(fn [index child] (fn [index child]
@@ -953,7 +953,7 @@
:else :else
(let [child-id (obj/get child "$id")] (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 ;; Only for frames
:addFlexLayout :addFlexLayout

View File

@@ -6872,6 +6872,13 @@ msgstr "Create annotation"
msgid "workspace.shape.menu.create-artboard-from-selection" msgid "workspace.shape.menu.create-artboard-from-selection"
msgstr "Selection to board" 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 #: src/app/main/ui/workspace/context_menu.cljs:573
msgid "workspace.shape.menu.create-component" msgid "workspace.shape.menu.create-component"
msgstr "Create component" msgstr "Create component"

View File

@@ -6850,6 +6850,13 @@ msgstr "Crear una nota"
msgid "workspace.shape.menu.create-artboard-from-selection" msgid "workspace.shape.menu.create-artboard-from-selection"
msgstr "Tablero de selección" 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 #: src/app/main/ui/workspace/context_menu.cljs:573
msgid "workspace.shape.menu.create-component" msgid "workspace.shape.menu.create-component"
msgstr "Crear componente" msgstr "Crear componente"