diff --git a/src/main/frontend/components/block.cljs b/src/main/frontend/components/block.cljs index 490fa59d07..9775a20e63 100644 --- a/src/main/frontend/components/block.cljs +++ b/src/main/frontend/components/block.cljs @@ -3970,9 +3970,12 @@ (:db/id block) :logseq.property.class/default-icon icon-data))) - ;; del — branch on action keyword from the dropdown - (case action - :remove-entirely + ;; del — branch on action keyword from the dropdown. + ;; Use `cond` not `case` — CLJS `case` with keyword + ;; tests has shown subtle interaction bugs elsewhere + ;; in this file. + (cond + (= action :remove-entirely) ;; Suppress inheritance: write the :none sentinel. ;; Don't touch class default-icon — user wants ;; "no icon" only on THIS entity. @@ -3981,6 +3984,7 @@ :logseq.property/icon {:type :none}) + :else ;; :revert (from dropdown) or :remove / nil (from ;; single-option immediate delete): retract the ;; property so inheritance kicks in. diff --git a/src/main/frontend/components/icon.cljs b/src/main/frontend/components/icon.cljs index 61fb166be2..637374d244 100644 --- a/src/main/frontend/components/icon.cljs +++ b/src/main/frontend/components/icon.cljs @@ -3901,6 +3901,14 @@ :on-click #(reset-and-call :remove)} trash-icon) + (= delete-mode :suppress) + (shui/button {:variant :outline :size :sm :data-action "del" + :data-topbar-stop "trash" + :title (t :icon/hide-inherited-icon) + :aria-label (t :icon/hide-inherited-icon) + :on-click #(reset-and-call :remove-entirely)} + trash-icon) + (= delete-mode :two-option) (shui/dropdown-menu (shui/dropdown-menu-trigger @@ -6345,6 +6353,13 @@ :on-click #(flag-delete-and-call :remove)} trash-icon) + (= delete-mode :suppress) + (shui/button {:variant :outline :size :sm :data-action "del" + :title (t :icon/hide-inherited-icon) + :aria-label (t :icon/hide-inherited-icon) + :on-click #(flag-delete-and-call :remove-entirely)} + trash-icon) + (= delete-mode :two-option) (shui/dropdown-menu (shui/dropdown-menu-trigger @@ -6467,9 +6482,13 @@ - `:remove` — entity has an own icon but no class inheritance source (or scope is class default-icon itself, which has no inheritance above it). Single-click retract. - - `:hidden` — nothing to delete: entity has no own icon, OR entity is - suppressed via `:type :none` (restoration lives in the page-title - affordance, not here). + - `:suppress` — entity has NO own icon but IS inheriting a class + default-icon. Single-click writes `{:type :none}` to hide the + inherited icon on this entity only. Recovery is via the page-title + 'Restore icon' affordance. + - `:hidden` — nothing to act on: no own icon and no class default + to suppress, OR entity is already suppressed via `:type :none` + (restoration lives in the page-title affordance, not here). Reads `:block/tags` + `:logseq.property.class/default-icon` so `db-mixins/query` registers them as render deps — the trash UI updates @@ -6491,13 +6510,20 @@ ;; sync-clear path at :3974-3980 already handles this as one action. synced-class? (and entity (ldb/class? entity) + (= own (:logseq.property/icon entity)) (= own (:logseq.property.class/default-icon entity)))] (cond - none? :hidden - (not has-real-own?) :hidden - synced-class? :remove - (or class-default tag-icon) :two-option - :else :remove))) + none? :hidden + ;; No own override but a class default-icon inherits in. + ;; Single-click "Hide inherited" writes :none sentinel. + ;; Tag-icon inheritance (a tag's OWN icon, not its class default) + ;; is intentionally excluded — that's not a "user opted into a + ;; default" relationship, so suppression has no clear UX story there. + (and (not has-real-own?) class-default) :suppress + (not has-real-own?) :hidden + synced-class? :remove + (or class-default tag-icon) :two-option + :else :remove))) (rum/defcs ^:large-vars/cleanup-todo icon-search < rum/reactive db-mixins/query (rum/local "" ::q) @@ -7168,6 +7194,17 @@ :on-click #(reset-and-call :remove)} trash-icon) + (= delete-mode :suppress) + ;; Same trash glyph + single click as :remove, but the action + ;; is :remove-entirely (writes :none) to hide the inherited + ;; class default-icon on this entity. Tooltip differentiates. + (shui/button {:variant :outline :size :sm :data-action "del" + :data-topbar-stop "trash" + :title (t :icon/hide-inherited-icon) + :aria-label (t :icon/hide-inherited-icon) + :on-click #(reset-and-call :remove-entirely)} + trash-icon) + (= delete-mode :two-option) (shui/dropdown-menu (shui/dropdown-menu-trigger @@ -7404,7 +7441,7 @@ (constantly []) (fn [{:keys [id]}] (icon-search - {:on-chosen (fn [e icon-value keep-popup?] + {:on-chosen (fn [e icon-value & [keep-popup?]] ;; Set the optimistic local mirror BEFORE the ;; async DB write fires. Lives at this ;; outermost wrapper so every commit path @@ -7413,7 +7450,13 @@ ;; picker close commits) — they all funnel ;; through this on-chosen. (set-pending-icon! icon-value) - (on-chosen e icon-value) + ;; Forward the third arg as-is — it carries either + ;; `keep-popup?` (a bool, for in-picker partial + ;; commits) or an `action` keyword (for delete + ;; flows like :revert / :remove-entirely). The + ;; downstream on-chosen handles both shapes; we + ;; just need to NOT drop it. + (on-chosen e icon-value keep-popup?) (when-not (true? keep-popup?) (shui/popup-hide! id))) :icon-value normalized-icon-value :page-title page-title diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn index 39f3a90ad3..1918f1815f 100644 --- a/src/resources/dicts/en.edn +++ b/src/resources/dicts/en.edn @@ -864,6 +864,7 @@ :icon/back "Back" :icon/emojis-count "Emojis ({1})" + :icon/hide-inherited-icon "Hide inherited icon" :icon/icons-count "Icons ({1})" :icon/matched-count "Matched ({1})" :icon/remove-entirely "Remove entirely" diff --git a/src/resources/dicts/zh-cn.edn b/src/resources/dicts/zh-cn.edn index 29b7a69148..653e1a6790 100644 --- a/src/resources/dicts/zh-cn.edn +++ b/src/resources/dicts/zh-cn.edn @@ -860,6 +860,7 @@ :icon/back "返回" :icon/emojis-count "表情符号({1})" + :icon/hide-inherited-icon "隐藏继承的图标" :icon/icons-count "图标({1})" :icon/matched-count "匹配结果({1})" :icon/remove-entirely "完全移除"