feat: row icons in tables + batch Set icon button

Adds an icon prefix to the Name cell of class-instance tables and a
batch Set icon button to the row-selection action bar. Avatar/text
initials are derived per row, so batch-applying an avatar to multiple
rows yields each row's own initials while preserving picker colors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
scheinriese
2026-05-07 16:57:53 +02:00
parent d19310bd64
commit 502e91fef1
3 changed files with 118 additions and 4 deletions

View File

@@ -1,13 +1,58 @@
(ns frontend.components.selection
"Block selection"
(:require [frontend.db :as db]
(:require [frontend.components.icon :as icon-component]
[frontend.db :as db]
[frontend.handler.db-based.property :as db-property-handler]
[frontend.handler.editor :as editor-handler]
[frontend.handler.property :as property-handler]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
[logseq.shui.ui :as shui]
[rum.core :as rum]))
(defn- icon-data-for-block
"Compute per-block icon-data. For avatar/text, replace the picker's
preview-derived :value with initials derived from each block's own
title — otherwise every batched row would show the first row's initials."
[icon block]
(when icon
(let [block-title (:block/title block)]
(cond
(= :avatar (:type icon))
(let [colors (select-keys (:data icon) [:backgroundColor :color])]
{:type :avatar
:data (cond-> colors
block-title (assoc :value (icon-component/derive-avatar-initials block-title)))})
(= :text (:type icon))
(let [colors (select-keys (:data icon) [:color])]
{:type :text
:data (cond-> colors
block-title (assoc :value (icon-component/derive-initials block-title)))})
(= :image (:type icon))
{:type :image :data (:data icon)}
:else (select-keys icon [:type :id :color])))))
(defn- batch-write-icon!
[blocks icon]
(if (or (nil? icon) (contains? #{:avatar :text} (:type icon)))
;; Per-block writes (avatar/text need per-row initials; clear/nil applies uniformly)
(doseq [block blocks]
(if (nil? icon)
(property-handler/remove-block-property! (:db/id block) :logseq.property/icon)
(db-property-handler/set-block-property!
(:db/id block)
:logseq.property/icon
(icon-data-for-block icon block))))
;; Uniform value across blocks (icon, emoji, image): single batch transaction
(property-handler/batch-set-block-property!
(map :db/id blocks)
:logseq.property/icon
(icon-data-for-block icon (first blocks)))))
(rum/defc action-bar < rum/reactive
[& {:keys [on-cut on-copy selected-blocks hide-dots? button-border? view-parent]
:or {on-cut #(editor-handler/cut-selection-blocks true)}}]
@@ -35,6 +80,31 @@
:on-dialog-close #(state/pub-event! [:editor/hide-action-bar])}])))
(ui/tooltip (ui/icon "hash" {:size 13}) "Set tag"
{:trigger-props {:class "flex"}}))
;; set icon (batch)
(shui/button
(assoc button-opts
:on-pointer-down (fn [^js e]
(util/stop e)
(let [target (.-target e)
first-title (some-> selected-blocks first :block/title)]
(shui/popup-show!
target
(fn [{:keys [id]}]
(icon-component/icon-search
{:on-chosen (fn [_e icon-value keep-popup?]
(batch-write-icon! selected-blocks icon-value)
(when-not (true? keep-popup?)
(shui/popup-hide! id)
(state/pub-event! [:editor/hide-action-bar])))
:icon-value nil
:page-title first-title
:del-btn? false}))
{:align :start
:id :ls-icon-picker
:content-props {:class "ls-icon-picker"
:onEscapeKeyDown #(.preventDefault %)}}))))
(ui/tooltip (ui/icon "mood-smile" {:size 13}) "Set icon"
{:trigger-props {:class "flex"}}))
(shui/button
(assoc button-opts
:on-pointer-down (fn [e]

View File

@@ -46,6 +46,14 @@
.multi-values {
@apply !flex-nowrap;
}
.table-row-icon {
@apply cursor-pointer rounded transition-colors duration-100;
&:hover {
background: var(--rx-gray-04);
}
}
}
.ls-table-header {

View File

@@ -241,7 +241,7 @@
(rum/defc ^:large-vars/cleanup-todo block-title < rum/static
"Used on table view"
[block* {:keys [create-new-block width row property]}]
[block* {:keys [create-new-block width row property property-ident]}]
(let [*ref (hooks/use-ref nil)
[opacity set-opacity!] (hooks/use-state 0)
[focus-timeout set-focus-timeout!] (hooks/use-state nil)
@@ -303,9 +303,45 @@
(save-block-and-focus *ref set-focus-timeout! false))})
(editor-handler/edit-block! block :max {:container-id :unknown-container})))))))}
(if block
[:div.flex.flex-row
[:div.flex.flex-row.items-center.gap-2.min-w-0
(when (and (= property-ident :block/title) (not many?))
(when-let [icon-el (icon-component/get-node-icon-cp block* {:size 16 :color? true})]
[:div.table-row-icon.flex-shrink-0.flex.items-center.justify-center
{:style {:width 20 :height 20}
:on-click (fn [^js e]
(util/stop-propagation e)
(let [own-icon (:logseq.property/icon block*)]
(shui/popup-show!
(.-currentTarget e)
(fn [{:keys [id]}]
(icon-component/icon-search
{:on-chosen (fn [_e icon-value keep-popup?]
(let [icon-data (when icon-value
(cond
(= :text (:type icon-value)) {:type :text :data (:data icon-value)}
(= :avatar (:type icon-value)) {:type :avatar :data (:data icon-value)}
(= :image (:type icon-value)) {:type :image :data (:data icon-value)}
:else (select-keys icon-value [:type :id :color])))]
(if icon-data
(property-handler/set-block-property!
(:db/id block*)
:logseq.property/icon
icon-data)
(property-handler/remove-block-property!
(:db/id block*)
:logseq.property/icon)))
(when-not (true? keep-popup?)
(shui/popup-hide! id)))
:icon-value own-icon
:page-title (:block/title block*)
:del-btn? (some? own-icon)}))
{:align :start
:id :ls-icon-picker
:content-props {:class "ls-icon-picker"
:onEscapeKeyDown #(.preventDefault %)}})))}
icon-el]))
(let [render (fn [block]
[:div
[:div.min-w-0
(inline-title
{:table? true
:block/uuid (:block/uuid block)}