Files
tolaria/src/hooks/usePropertyPanelState.ts
Test dab5f3bc41 feat: normalize system properties to _archived, _trashed, _trashed_at
Write operations now use underscore-prefixed canonical keys (_archived,
_trashed, _trashed_at). Read operations accept both old keys (Archived,
archived, Trashed, trashed, Trashed at, trashed_at) and new keys via
serde aliases, ensuring backward compatibility without migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:32:48 +02:00

151 lines
6.0 KiB
TypeScript

import { useMemo, useState, useCallback } from 'react'
import type { VaultEntry } from '../types'
import type { FrontmatterValue } from '../components/Inspector'
import type { ParsedFrontmatter } from '../utils/frontmatter'
import {
type PropertyDisplayMode,
loadDisplayModeOverrides,
saveDisplayModeOverride,
removeDisplayModeOverride,
} from '../utils/propertyTypes'
import { containsWikilinks } from '../components/DynamicPropertiesPanel'
// Keys to skip showing in Properties (handled by dedicated UI or internal)
// Compared case-insensitively via isVisibleProperty()
const SKIP_KEYS = new Set(['aliases', 'workspace', 'title', 'type', 'is_a', 'is a', '_trashed', 'trashed', '_trashed_at', 'trashed_at', 'trashed at', '_archived', 'archived', 'archived_at', 'icon'])
function coerceValue(raw: string): FrontmatterValue {
if (raw.toLowerCase() === 'true') return true
if (raw.toLowerCase() === 'false') return false
if (!isNaN(Number(raw)) && raw.trim() !== '') return Number(raw)
return raw
}
function parseNewValue(rawValue: string): FrontmatterValue {
if (!rawValue.includes(',')) return rawValue.trim() || ''
const items = rawValue.split(',').map(s => s.trim()).filter(s => s)
return items.length === 1 ? items[0] : items
}
function reconcileListUpdate(
newItems: string[],
onUpdate: (key: string, value: FrontmatterValue) => void,
onDelete: ((key: string) => void) | undefined,
key: string,
) {
if (newItems.length === 0) onDelete?.(key)
else if (newItems.length === 1) onUpdate(key, newItems[0])
else onUpdate(key, newItems)
}
function deriveTypeInfo(entries: VaultEntry[] | undefined, entryIsA: string | null) {
const typeEntries = (entries ?? []).filter(e => e.isA === 'Type')
const typeColorKeys: Record<string, string | null> = {}
const typeIconKeys: Record<string, string | null> = {}
for (const e of typeEntries) {
typeColorKeys[e.title] = e.color ?? null
typeIconKeys[e.title] = e.icon ?? null
}
return {
availableTypes: typeEntries.map(e => e.title).sort((a, b) => a.localeCompare(b)),
customColorKey: entryIsA ? (typeColorKeys[entryIsA] ?? null) : null,
typeColorKeys,
typeIconKeys,
}
}
function collectVaultStatuses(entries: VaultEntry[] | undefined): string[] {
const seen = new Set<string>()
for (const e of entries ?? []) {
if (e.status) seen.add(e.status)
}
return Array.from(seen).sort((a, b) => a.localeCompare(b))
}
function collectAllVaultTags(entries: VaultEntry[] | undefined): Record<string, string[]> {
if (!entries) return {}
const tagsByKey = new Map<string, Set<string>>()
for (const entry of entries) {
if (!entry.properties) continue
for (const [key, value] of Object.entries(entry.properties)) {
if (!Array.isArray(value)) continue
let set = tagsByKey.get(key)
if (!set) { set = new Set(); tagsByKey.set(key, set) }
for (const tag of value) set.add(String(tag))
}
}
const result: Record<string, string[]> = {}
for (const [key, set] of tagsByKey) result[key] = Array.from(set).sort((a, b) => a.localeCompare(b))
return result
}
function isVisibleProperty([key, value]: [string, FrontmatterValue]): boolean {
return !SKIP_KEYS.has(key.toLowerCase()) && !containsWikilinks(value)
}
function parseAddedValue(rawValue: string, mode: PropertyDisplayMode): FrontmatterValue {
if (mode === 'boolean') return rawValue.toLowerCase() === 'true'
if (mode === 'tags') {
const items = rawValue.split(',').map(s => s.trim()).filter(s => s)
return items
}
return parseNewValue(rawValue)
}
function persistModeOverride(key: string, mode: PropertyDisplayMode | null) {
if (mode === null) removeDisplayModeOverride(key)
else saveDisplayModeOverride(key, mode)
}
export interface PropertyPanelDeps {
entries: VaultEntry[] | undefined
entryIsA: string | null
frontmatter: ParsedFrontmatter
onUpdateProperty?: (key: string, value: FrontmatterValue) => void
onDeleteProperty?: (key: string) => void
onAddProperty?: (key: string, value: FrontmatterValue) => void
}
export function usePropertyPanelState(deps: PropertyPanelDeps) {
const { entries, entryIsA, frontmatter, onUpdateProperty, onDeleteProperty, onAddProperty } = deps
const [editingKey, setEditingKey] = useState<string | null>(null)
const [showAddDialog, setShowAddDialog] = useState(false)
const [displayOverrides, setDisplayOverrides] = useState(() => loadDisplayModeOverrides())
const { availableTypes, customColorKey, typeColorKeys, typeIconKeys } = useMemo(() => deriveTypeInfo(entries, entryIsA), [entries, entryIsA])
const vaultStatuses = useMemo(() => collectVaultStatuses(entries), [entries])
const vaultTagsByKey = useMemo(() => collectAllVaultTags(entries), [entries])
const propertyEntries = useMemo(() => Object.entries(frontmatter).filter(isVisibleProperty), [frontmatter])
const handleSaveValue = useCallback((key: string, newValue: string) => {
setEditingKey(null)
if (onUpdateProperty) onUpdateProperty(key, coerceValue(newValue))
}, [onUpdateProperty])
const handleSaveList = useCallback((key: string, newItems: string[]) => {
if (!onUpdateProperty) return
reconcileListUpdate(newItems, onUpdateProperty, onDeleteProperty, key)
}, [onUpdateProperty, onDeleteProperty])
const handleAdd = useCallback((rawKey: string, rawValue: string, mode: PropertyDisplayMode) => {
if (!rawKey.trim() || !onAddProperty) return
onAddProperty(rawKey.trim(), parseAddedValue(rawValue, mode))
if (mode !== 'text') {
persistModeOverride(rawKey.trim(), mode)
setDisplayOverrides(loadDisplayModeOverrides())
}
setShowAddDialog(false)
}, [onAddProperty])
const handleDisplayModeChange = useCallback((key: string, mode: PropertyDisplayMode | null) => {
persistModeOverride(key, mode)
setDisplayOverrides(loadDisplayModeOverrides())
}, [])
return {
editingKey, setEditingKey, showAddDialog, setShowAddDialog, displayOverrides,
availableTypes, customColorKey, typeColorKeys, typeIconKeys, vaultStatuses, vaultTagsByKey, propertyEntries,
handleSaveValue, handleSaveList, handleAdd, handleDisplayModeChange,
}
}