fix: align note list header actions

This commit is contained in:
lucaronin
2026-04-12 22:29:36 +02:00
parent a4397d48de
commit de91167a28
3 changed files with 80 additions and 29 deletions

View File

@@ -123,6 +123,27 @@ describe('NoteList rendering', () => {
expect(screen.getByTitle('Create new note')).toBeInTheDocument()
})
it('uses matching ghost icon button styling for note-list header actions', () => {
renderNoteList({
entries: makeBookTypeEntries(['Priority'], { properties: { Priority: 'High' } }),
selection: { kind: 'filter', filter: 'inbox' },
inboxNoteListProperties: null,
onUpdateInboxNoteListProperties: () => undefined,
})
const buttons = [
screen.getByTitle('Search notes'),
screen.getByTitle('Customize Inbox columns'),
screen.getByTitle('Create new note'),
]
for (const button of buttons) {
expect(button).toHaveAttribute('data-size', 'icon-xs')
expect(button).toHaveAttribute('data-variant', 'ghost')
expect(button).toHaveClass('h-7', 'w-7', 'text-muted-foreground', 'hover:bg-accent')
}
})
it('shows backlinks from outgoing links in entity view', () => {
const entriesWithBacklink = mockEntries.map((entry) =>
entry.path === mockEntries[2].path ? { ...entry, outgoingLinks: ['Build Laputa App'] } : entry,

View File

@@ -3,6 +3,7 @@ import { SlidersHorizontal, DotsSixVertical } from '@phosphor-icons/react'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { cn } from '@/lib/utils'
import {
OPEN_NOTE_LIST_PROPERTIES_EVENT,
type NoteListPropertiesScope,
@@ -18,19 +19,53 @@ import {
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
type NoteListPropertyKey = string
export interface ListPropertiesPopoverProps {
scope: NoteListPropertiesScope
availableProperties: string[]
currentDisplay: string[]
onSave: (value: string[] | null) => void
availableProperties: NoteListPropertyKey[]
currentDisplay: NoteListPropertyKey[]
onSave: (value: NoteListPropertyKey[] | null) => void
triggerTitle: string
triggerClassName?: string
}
function propertyInputId(id: string): string {
function propertyInputId(id: NoteListPropertyKey): string {
return `list-prop-${id.replace(/[^a-z0-9_-]+/gi, '-')}`
}
function SortablePropertyItem({ id, checked, onToggle }: { id: string; checked: boolean; onToggle: (key: string) => void }) {
function getSelectedProperties(currentDisplay: NoteListPropertyKey[], availableProperties: NoteListPropertyKey[]) {
return currentDisplay.filter((property) => availableProperties.includes(property))
}
function getOrderedItems(currentDisplay: NoteListPropertyKey[], availableProperties: NoteListPropertyKey[]) {
const selected = getSelectedProperties(currentDisplay, availableProperties)
const unselected = availableProperties.filter((property) => !selected.includes(property))
return [...selected, ...unselected]
}
function toggleDisplayProperty(currentDisplay: NoteListPropertyKey[], selectedSet: Set<NoteListPropertyKey>, key: NoteListPropertyKey) {
if (selectedSet.has(key)) {
const filtered = currentDisplay.filter((property) => property !== key)
return filtered.length > 0 ? filtered : null
}
return [...currentDisplay, key]
}
function reorderDisplayProperties(event: DragEndEvent, currentDisplay: NoteListPropertyKey[], availableProperties: NoteListPropertyKey[]) {
const { active, over } = event
if (!over || active.id === over.id) return undefined
const selected = getSelectedProperties(currentDisplay, availableProperties)
const oldIndex = selected.indexOf(String(active.id) as NoteListPropertyKey)
const newIndex = selected.indexOf(String(over.id) as NoteListPropertyKey)
if (oldIndex === -1 || newIndex === -1) return undefined
return arrayMove(selected, oldIndex, newIndex)
}
function SortablePropertyItem({ id, checked, onToggle }: { id: NoteListPropertyKey; checked: boolean; onToggle: (key: NoteListPropertyKey) => void }) {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id })
const style = { transform: CSS.Transform.toString(transform), transition }
const inputId = propertyInputId(id)
@@ -80,14 +115,14 @@ export function ListPropertiesPopover({
currentDisplay,
onSave,
triggerTitle,
triggerClassName,
}: ListPropertiesPopoverProps) {
const [open, setOpen] = useState(false)
const orderedItems = useMemo(() => {
const selected = currentDisplay.filter((property) => availableProperties.includes(property))
const unselected = availableProperties.filter((property) => !selected.includes(property))
return [...selected, ...unselected]
}, [availableProperties, currentDisplay])
const orderedItems = useMemo(
() => getOrderedItems(currentDisplay, availableProperties),
[availableProperties, currentDisplay],
)
const selectedSet = useMemo(() => new Set(currentDisplay), [currentDisplay])
@@ -105,23 +140,15 @@ export function ListPropertiesPopover({
}, [scope])
const handleToggle = useCallback((key: string) => {
const nextSelected = selectedSet.has(key)
? currentDisplay.filter((property) => property !== key)
: [...currentDisplay, key]
onSave(nextSelected.length > 0 ? nextSelected : null)
const nextSelected = toggleDisplayProperty(currentDisplay, selectedSet, key)
onSave(nextSelected)
}, [currentDisplay, onSave, selectedSet])
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
const reordered = reorderDisplayProperties(event, currentDisplay, availableProperties)
if (!reordered) return
const selected = currentDisplay.filter((property) => availableProperties.includes(property))
const oldIndex = selected.indexOf(String(active.id))
const newIndex = selected.indexOf(String(over.id))
if (oldIndex === -1 || newIndex === -1) return
const reordered = arrayMove(selected, oldIndex, newIndex)
onSave(reordered.length > 0 ? reordered : null)
onSave(reordered)
}, [availableProperties, currentDisplay, onSave])
if (availableProperties.length === 0) return null
@@ -133,7 +160,7 @@ export function ListPropertiesPopover({
type="button"
variant="ghost"
size="icon-xs"
className="text-muted-foreground hover:text-foreground"
className={cn('h-7 w-7 text-muted-foreground', triggerClassName)}
title={triggerTitle}
aria-label={triggerTitle}
data-testid="list-properties-btn"

View File

@@ -1,11 +1,14 @@
import { MagnifyingGlass, Plus } from '@phosphor-icons/react'
import type { VaultEntry } from '../../types'
import type { SortOption, SortDirection } from '../../utils/noteListHelpers'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { useDragRegion } from '../../hooks/useDragRegion'
import { SortDropdown } from '../SortDropdown'
import { ListPropertiesPopover, type ListPropertiesPopoverProps } from './ListPropertiesPopover'
const NOTE_LIST_ACTION_BUTTON_CLASSNAME = 'h-7 w-7 text-muted-foreground'
export function NoteListHeader({ title, typeDocument, isEntityView, listSort, listDirection, customProperties, sidebarCollapsed, searchVisible, search, propertyPicker, onSortChange, onCreateNote, onOpenType, onToggleSearch, onSearchChange }: {
title: string
typeDocument: VaultEntry | null
@@ -37,13 +40,13 @@ export function NoteListHeader({ title, typeDocument, isEntityView, listSort, li
</h3>
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>
{!isEntityView && <SortDropdown groupLabel="__list__" current={listSort} direction={listDirection} customProperties={customProperties} onChange={onSortChange} />}
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={onToggleSearch} title="Search notes">
<Button type="button" variant="ghost" size="icon-xs" className={NOTE_LIST_ACTION_BUTTON_CLASSNAME} onClick={onToggleSearch} title="Search notes" aria-label="Search notes">
<MagnifyingGlass size={16} />
</button>
{propertyPicker && <ListPropertiesPopover {...propertyPicker} />}
<button className="flex items-center text-muted-foreground transition-colors hover:text-foreground" onClick={() => onCreateNote()} title="Create new note">
</Button>
{propertyPicker && <ListPropertiesPopover {...propertyPicker} triggerClassName={NOTE_LIST_ACTION_BUTTON_CLASSNAME} />}
<Button type="button" variant="ghost" size="icon-xs" className={NOTE_LIST_ACTION_BUTTON_CLASSNAME} onClick={onCreateNote} title="Create new note" aria-label="Create new note">
<Plus size={16} />
</button>
</Button>
</div>
</div>
{searchVisible && (