From 61ce4b9e0d0e31fa3472b85884e0c4cfade77a8f Mon Sep 17 00:00:00 2001 From: FairyPiggyDev Date: Tue, 28 Apr 2026 09:59:05 -0400 Subject: [PATCH] :sparkles: Add "Delete group" to assets panel context menu (#9151) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: FairyPiggyDev --- CHANGES.md | 1 + .../ui/workspace/sidebar/assets/colors.cljs | 40 ++++++++++++- .../workspace/sidebar/assets/components.cljs | 60 ++++++++++++++++++- .../ui/workspace/sidebar/assets/groups.cljs | 8 ++- .../sidebar/assets/typographies.cljs | 38 +++++++++++- frontend/translations/en.po | 14 +++++ 6 files changed, 156 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 11b308b336..3fccd4f985 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index c0395cf4f4..83df80b9db 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -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}]]])) 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 077742d71d..2d9d1b72e7 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs index 4a781fa48a..c7e72c871b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/groups.cljs @@ -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") diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index 082fecb996..cf95f8b477 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -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?}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4b774cd775..f2c15954b7 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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"