Files
zui/src/components/base/NodeHeaderTitle.tsx
2026-03-09 14:04:13 +01:00

83 lines
2.6 KiB
TypeScript

import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
import FlowContext from '../../lib/flowContext'
import { replaceNodeIdInGraph } from '../../lib/flowUtils'
type Props = {
nodeId: string
displayTitle: string
}
export function NodeHeaderTitle({ nodeId, displayTitle }: Props) {
const ctx = useContext(FlowContext)
const nodes = ctx?.nodes ?? []
const setNodes = ctx?.setNodes
const edges = ctx?.edges ?? []
const setEdges = ctx?.setEdges
const renamingNodeId = ctx?.renamingNodeId ?? null
const setRenamingNodeId = ctx?.setRenamingNodeId
const [inputValue, setInputValue] = useState(nodeId)
const inputRef = useRef<HTMLInputElement>(null)
const isRenaming = renamingNodeId === nodeId
useEffect(() => {
if (isRenaming) {
setInputValue(nodeId)
inputRef.current?.focus()
inputRef.current?.select()
}
}, [isRenaming, nodeId])
const applyRename = useCallback(() => {
if (!setNodes || !setEdges || !setRenamingNodeId) return
const newId = inputValue.trim()
if (!newId || newId === nodeId) {
setRenamingNodeId(null)
return
}
const existingIds = nodes.map((n: any) => n.id)
if (existingIds.includes(newId)) {
return
}
const { nodes: nextNodes, edges: nextEdges } = replaceNodeIdInGraph(nodes, edges, nodeId, newId)
setNodes(nextNodes)
setEdges(nextEdges)
setRenamingNodeId(null)
}, [nodeId, inputValue, nodes, edges, setNodes, setEdges, setRenamingNodeId])
const cancelRename = useCallback(() => {
setRenamingNodeId?.(null)
}, [setRenamingNodeId])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
applyRename()
} else if (e.key === 'Escape') {
e.preventDefault()
cancelRename()
}
},
[applyRename, cancelRename]
)
if (!isRenaming) {
return <>{displayTitle}</>
}
return (
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={cancelRename}
className="nodrag nopan flex-1 min-w-0 rounded border border-input bg-background px-1.5 py-0 text-sm font-semibold outline-none focus:ring-1 focus:ring-ring"
data-slot="base-node-title"
/>
)
}