Files
glitch_automata_lab/backend/admin/src/PresetAssetTree.tsx
T

114 lines
3.1 KiB
TypeScript
Raw Normal View History

2026-07-01 12:10:12 +02:00
import React from 'react'
import type { PresetNode } from './types.js'
interface PresetAssetTreeProps {
assignedNodeId?: string
nodes: PresetNode[]
selectedNodeId?: string
onSelect: (node: PresetNode) => void
}
interface TreeItem {
node: PresetNode
children: TreeItem[]
}
function compareNodes(left: PresetNode, right: PresetNode) {
return left.sort_order - right.sort_order || left.name.localeCompare(right.name)
}
function buildTree(nodes: PresetNode[]) {
const byParent = new Map<string | null, PresetNode[]>()
for (const node of nodes) {
const siblings = byParent.get(node.parent_id) ?? []
siblings.push(node)
byParent.set(node.parent_id, siblings)
}
function build(parentId: string | null): TreeItem[] {
return (byParent.get(parentId) ?? [])
.sort(compareNodes)
.map((node) => ({ node, children: build(node.id) }))
}
return build(null)
}
export function PresetAssetTree({
assignedNodeId,
nodes,
selectedNodeId,
onSelect
}: PresetAssetTreeProps) {
const tree = React.useMemo(() => buildTree(nodes), [nodes])
if (tree.length === 0) return <p className="empty">This CA library has no presets.</p>
return (
<div className="asset-tree">
{tree.map((item) => (
<AssetTreeItem
assignedNodeId={assignedNodeId}
item={item}
key={item.node.id}
selectedNodeId={selectedNodeId}
onSelect={onSelect}
/>
))}
</div>
)
}
function AssetTreeItem({
assignedNodeId,
item,
selectedNodeId,
onSelect
}: {
assignedNodeId?: string
item: TreeItem
selectedNodeId?: string
onSelect: (node: PresetNode) => void
}) {
const [open, setOpen] = React.useState(true)
const selected = item.node.id === selectedNodeId
const assigned = item.node.id === assignedNodeId
return (
<div className="asset-tree-item">
<div className={`asset-tree-row${selected ? ' selected' : ''}${assigned ? ' assigned' : ''}`}>
<button
aria-label={item.children.length ? `${open ? 'Collapse' : 'Expand'} ${item.node.name}` : `${item.node.name} has no children`}
className="asset-tree-arrow"
disabled={!item.children.length}
type="button"
onClick={() => setOpen((current) => !current)}
>
{item.children.length ? (open ? '▾' : '▸') : '·'}
</button>
<button className="asset-tree-select" type="button" onClick={() => onSelect(item.node)}>
<strong>{item.node.name}</strong>
<small>
{item.node.kind}
{assigned ? ' · current' : ''}
</small>
{item.node.description ? <span>{item.node.description}</span> : null}
</button>
</div>
{open && item.children.length ? (
<div className="asset-tree-children">
{item.children.map((child) => (
<AssetTreeItem
assignedNodeId={assignedNodeId}
item={child}
key={child.node.id}
selectedNodeId={selectedNodeId}
onSelect={onSelect}
/>
))}
</div>
) : null}
</div>
)
}