import { useMemo, useState } from 'preact/hooks' import { useFocus } from '../hooks/index.js' import { Octicon } from './index.js' const SEPARATOR = '/' export interface EntryAction { icon: keyof typeof Octicon, label: string, onAction: (entry: string) => unknown, } export interface EntryError { path: string, message: string, } interface Props { entries: string[], onSelect: (entry: string) => unknown, selected?: string, actions?: EntryAction[], errors?: EntryError[], indent?: number, } export function TreeView({ entries, onSelect, selected, actions, errors, indent }: Props) { const roots = useMemo(() => { const groups: Record = {} for (const entry of entries) { const i = entry.indexOf(SEPARATOR) if (i >= 0) { const root = entry.slice(0, i) ;(groups[root] ??= []).push(entry.slice(i + 1)) } } return Object.entries(groups).map(([r, entries]) => { const rootActions = actions?.map(a => ({ ...a, onAction: (e: string) => a.onAction(r + SEPARATOR + e) })) const rootErrors = errors?.flatMap(e => e.path.startsWith(r + SEPARATOR) ? [{ ...e, path: e.path.slice(r.length + SEPARATOR.length) }] : []) return [r, entries, rootActions, rootErrors] as [string, string[], EntryAction[], EntryError[]] }).sort() }, [entries, actions, errors]) const leaves = useMemo(() => { return entries.filter(e => !e.includes(SEPARATOR)) }, [entries]) const [hidden, setHidden] = useState(new Set()) const toggle = (root: string) => { if (hidden.has(root)) { hidden.delete(root) } else { hidden.add(root) } setHidden(new Set(hidden)) } return
{roots.map(([r, entries, actions, errors]) =>
toggle(r)} error={(errors?.length ?? 0) > 0} /> {!hidden.has(r) && onSelect(`${r}${SEPARATOR}${e}`)} selected={selected?.startsWith(r + SEPARATOR) ? selected.substring(r.length + 1) : undefined} actions={actions} errors={errors} indent={(indent ?? 0) + 1} />}
)} {leaves.map(e => onSelect(e)} actions={actions?.map(a => ({ ...a, onAction: () => a.onAction(e) }))} error={errors?.find(er => er.path === e)?.message} />)}
} interface TreeViewEntryProps { icon: keyof typeof Octicon, label: string, active?: boolean, onClick?: () => unknown, actions?: EntryAction[], error?: string | boolean, } function TreeViewEntry({ icon, label, active, onClick, actions, error }: TreeViewEntryProps) { const [focused, setFocus] = useFocus() const onContextMenu = (evt: MouseEvent) => { evt.preventDefault() if (actions?.length) { setFocus() } } return
{Octicon[icon]} {label.replaceAll('\u2215', '/')} {typeof error === 'string' &&
{Octicon.issue_opened}
} {focused &&
{actions?.map(a =>
{ a.onAction(''); e.stopPropagation(); setFocus(false) }}>{Octicon[a.icon]}{a.label}
)}
}
}