Add "Delete group" to assets panel context menu (#9151)

When working with large asset groups, users asked for a one-click way
to remove every asset under a group path. Multi-select across hundreds
of items is impractical, and ungrouping first and then deleting leaves
the orphaned items in the flat list.

This change adds a "Delete group" option to the assets-panel
context-menu for three asset types that already carry group structure:

- Components (including variants — sibling variants sharing a variant
  container are deduplicated, and the container is deleted once via
  the same dispatch the per-item delete uses in file_library.cljs).
- Colors.
- Typographies.

A confirmation modal is shown before deletion, with the count of
assets to be removed, so the action is never silent. All deletes run
inside a single undo transaction, so one Cmd+Z restores the whole
group.

Changes
-------

- `assets/groups.cljs` — `asset-group-title*` accepts an optional
  `on-delete-group` prop and conditionally adds the menu entry
  between "Ungroup" and "Combine as variants". When the callback is
  not supplied the option is hidden, so asset sections that do not
  implement it stay unaffected.
- `assets/components.cljs` — threads `on-delete-group` through the
  recursive `components-group*` and defines the section-level
  handler, dispatching to `dwsh/delete-shapes` for variant containers
  and `dwl/delete-component` for plain components.
- `assets/colors.cljs` — same threading + a simple `dwl/delete-color`
  dispatch per color in the group.
- `assets/typographies.cljs` — same threading + a
  `dwl/delete-typography` dispatch per typography in the group.
- `translations/en.po` — three new strings: the menu label
  (`workspace.assets.delete-group`) and the modal title/message
  (`modals.delete-asset-group.title`/`.message`, plural-aware).

Github #9141

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
This commit is contained in:
FairyPiggyDev 2026-04-28 09:59:05 -04:00 committed by GitHub
parent b0ce644752
commit 61ce4b9e0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 156 additions and 5 deletions

View File

@ -8,6 +8,7 @@
### :sparkles: New features & Enhancements
- Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141)
- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736)
- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328)
- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987)

View File

@ -280,7 +280,7 @@
(mf/defc colors-group
[{:keys [file-id prefix groups open-groups force-open? local? selected
multi-colors? multi-assets? on-asset-click on-assets-delete
on-clear-selection on-group on-rename-group on-ungroup colors
on-clear-selection on-group on-rename-group on-ungroup on-delete-group colors
selected-full]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
@ -325,7 +325,8 @@
:path prefix
:is-group-open group-open?
:on-rename on-rename-group
:on-ungroup on-ungroup}]
:on-ungroup on-ungroup
:on-delete-group on-delete-group}]
(when group-open?
[:*
(let [colors (get groups "" [])]
@ -378,6 +379,7 @@
:on-group on-group
:on-rename-group on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:colors colors
:selected-full selected-full}]))])]))
@ -499,6 +501,39 @@
file-id))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
;; Issue #9141. Delete every color under a group path in a
;; single undo transaction, after user confirmation.
on-delete-group
(mf/use-fn
(mf/deps colors on-clear-selection)
(fn [path]
(let [group-colors
(->> colors
(filter #(str/starts-with? (:path %) path)))
;; Hoisted so the start/commit pair is bound to the
;; same symbol regardless of how `do-delete` is
;; invoked by the confirm modal. Review suggestion
;; on PR #9151.
undo-id (js/Symbol)
do-delete
(fn []
(on-clear-selection)
(st/emit! (dwu/start-undo-transaction undo-id))
(run! st/emit!
(map #(dwl/delete-color {:id (:id %)}) group-colors))
(st/emit! (dwu/commit-undo-transaction undo-id)))]
(when (seq group-colors)
(st/emit!
(modal/show
{:type :confirm
:title (tr "modals.delete-asset-group.title")
:message (tr "modals.delete-asset-group.message"
(i18n/c (count group-colors)))
:accept-label (tr "labels.delete")
:on-accept do-delete}))))))
on-asset-click
(mf/use-fn (mf/deps groups on-asset-click) (partial on-asset-click groups))]
@ -533,5 +568,6 @@
:on-group on-group
:on-rename-group on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:colors colors
:selected-full selected-full}]]]))

View File

@ -17,6 +17,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwv]
[app.main.refs :as refs]
@ -191,7 +192,7 @@
(mf/defc components-group*
[{:keys [file-id prefix groups open-groups is-force-open renaming is-listing-thumbs selected on-asset-click
on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-context-menu
on-drag-start do-rename cancel-rename on-rename-group on-group on-ungroup on-delete-group on-context-menu
selected-full is-local count-variants on-group-combine-variants]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
@ -246,6 +247,7 @@
:is-can-combine can-combine?
:on-rename on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-group-combine-variants on-group-combine-variants}]
(when group-open?
@ -303,6 +305,7 @@
:cancel-rename cancel-rename
:on-rename-group on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-context-menu on-context-menu
:on-group-combine-variants on-group-combine-variants
:selected-full selected-full
@ -493,6 +496,60 @@
(map #(dwv/rename-comp-or-variant-and-main (:id %) (cmm/ungroup % path)))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
;; Issue #9141. Delete every component under a group path in a
;; single undo transaction, after user confirmation. Variants
;; are handled via their variant container (matching the
;; per-item delete dispatch in file_library.cljs); sibling
;; variants sharing a container are deduplicated so we delete
;; each container only once.
on-delete-group
(mf/use-fn
(mf/deps components on-clear-selection)
(fn [path]
(let [group-components
(->> components
(filter #(cpn/inside-path? (:path %) path)))
{variants true non-variants false}
(group-by (comp boolean ctc/is-variant?) group-components)
;; One delete-shapes per variant container, not per
;; sibling variant within that container.
variant-containers
(->> variants
(group-by :variant-id)
(map (fn [[_ comps]] (first comps))))
;; Hoisted so the start/commit pair is bound to the
;; same symbol regardless of how `do-delete` is
;; invoked by the confirm modal. Review suggestion
;; on PR #9151.
undo-id (js/Symbol)
do-delete
(fn []
(on-clear-selection)
(st/emit! (dwu/start-undo-transaction undo-id))
(run! st/emit!
(map (fn [component]
(dwsh/delete-shapes (:main-instance-page component)
#{(:variant-id component)}))
variant-containers))
(run! st/emit!
(map (fn [component]
(dwl/delete-component {:id (:id component)}))
non-variants))
(st/emit! (dwu/commit-undo-transaction undo-id)))]
(when (seq group-components)
(st/emit!
(modal/show
{:type :confirm
:title (tr "modals.delete-asset-group.title")
:message (tr "modals.delete-asset-group.message"
(i18n/c (count group-components)))
:accept-label (tr "labels.delete")
:on-accept do-delete}))))))
on-group-combine-variants
(mf/use-fn
(mf/deps components on-clear-selection)
@ -602,6 +659,7 @@
:on-rename-group on-rename-group
:on-group on-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-group-combine-variants on-group-combine-variants
:on-context-menu on-context-menu
:selected-full selected-full

View File

@ -23,7 +23,7 @@
[rumext.v2 :as mf]))
(mf/defc asset-group-title*
[{:keys [file-id section path is-group-open on-rename on-ungroup on-group-combine-variants is-can-combine on-add]}]
[{:keys [file-id section path is-group-open on-rename on-ungroup on-delete-group on-group-combine-variants is-can-combine on-add]}]
(when-not (empty? path)
(let [[other-path last-path truncated] (cpn/compact-path path 35 true)
menu-state (mf/use-state cmm/initial-context-menu-state)
@ -69,6 +69,12 @@
{:name (tr "workspace.assets.ungroup")
:id "assets-ungroup-group"
:handler #(on-ungroup path)}]
on-delete-group
(conj
{:name (tr "workspace.assets.delete-group")
:id "assets-delete-group"
:handler #(on-delete-group path)})
is-can-combine
(conj
{:name (tr "workspace.shape.menu.combine-as-variants")

View File

@ -134,7 +134,7 @@
{::mf/wrap-props false}
[{:keys [file-id prefix groups open-groups force-open? file local? selected local-data
editing-id renaming-id on-asset-click handle-change on-rename-group
on-ungroup on-context-menu selected-full is-read-only]}]
on-ungroup on-delete-group on-context-menu selected-full is-read-only]}]
(let [group-open? (if (false? (get open-groups prefix)) ;; if the user has closed it specifically, respect that
false
(get open-groups prefix true))
@ -185,6 +185,7 @@
:is-group-open group-open?
:on-rename on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-add (when (and local? (not is-read-only))
add-typography-to-group)}]
@ -238,6 +239,7 @@
:handle-change handle-change
:on-rename-group on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-context-menu on-context-menu
:selected-full selected-full
:is-read-only is-read-only}]))])]))
@ -352,6 +354,39 @@
(cmm/ungroup % path)))))
(st/emit! (dwu/commit-undo-transaction undo-id)))))
;; Issue #9141. Delete every typography under a group path in a
;; single undo transaction, after user confirmation.
on-delete-group
(mf/use-fn
(mf/deps typographies file-id on-clear-selection)
(fn [path]
(let [group-typographies
(->> typographies
(filter #(str/starts-with? (:path %) path)))
;; Hoisted so the start/commit pair is bound to the
;; same symbol regardless of how `do-delete` is
;; invoked by the confirm modal. Review suggestion
;; on PR #9151.
undo-id (js/Symbol)
do-delete
(fn []
(on-clear-selection)
(st/emit! (dwu/start-undo-transaction undo-id))
(run! st/emit!
(map #(dwl/delete-typography (:id %)) group-typographies))
(st/emit! (dwu/commit-undo-transaction undo-id)))]
(when (seq group-typographies)
(st/emit!
(modal/show
{:type :confirm
:title (tr "modals.delete-asset-group.title")
:message (tr "modals.delete-asset-group.message"
(i18n/c (count group-typographies)))
:accept-label (tr "labels.delete")
:on-accept do-delete}))))))
on-context-menu
(mf/use-fn
(mf/deps selected on-clear-selection read-only?)
@ -441,6 +476,7 @@
:handle-change handle-change
:on-rename-group on-rename-group
:on-ungroup on-ungroup
:on-delete-group on-delete-group
:on-context-menu on-context-menu
:selected-full selected-full
:is-read-only read-only?}]

View File

@ -3561,6 +3561,16 @@ msgstr "Are you sure you want to delete this page?"
msgid "modals.delete-page.title"
msgstr "Delete page"
#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/ui/workspace/sidebar/assets/colors.cljs, src/app/main/ui/workspace/sidebar/assets/typographies.cljs
msgid "modals.delete-asset-group.title"
msgstr "Delete group"
#: src/app/main/ui/workspace/sidebar/assets/components.cljs, src/app/main/ui/workspace/sidebar/assets/colors.cljs, src/app/main/ui/workspace/sidebar/assets/typographies.cljs
msgid "modals.delete-asset-group.message"
msgid_plural "modals.delete-asset-group.message"
msgstr[0] "Are you sure you want to delete this asset?"
msgstr[1] "Are you sure you want to delete these %s assets?"
#: src/app/main/ui/dashboard/project_menu.cljs:73
msgid "modals.delete-project-confirm.accept"
msgstr "Delete project"
@ -5733,6 +5743,10 @@ msgstr "Text Transform"
msgid "workspace.assets.ungroup"
msgstr "Ungroup"
#: src/app/main/ui/workspace/sidebar/assets/groups.cljs
msgid "workspace.assets.delete-group"
msgstr "Delete group"
#: src/app/main/ui/workspace/colorpicker.cljs:428, src/app/main/ui/workspace/colorpicker.cljs:441
msgid "workspace.colorpicker.color-tokens"
msgstr "Color tokens"