114 lines
3.1 KiB
TypeScript
114 lines
3.1 KiB
TypeScript
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>
|
|
)
|
|
}
|