Refactor WorkflowEditor to improve node and edge property panel handling and animation

This commit is contained in:
mlogclub
2026-06-23 19:37:57 +08:00
parent 20dfe49852
commit 17852d0675
@@ -183,7 +183,6 @@ export function WorkflowEditor({
toFlowEdges(definition) toFlowEdges(definition)
) )
const [flowInstance, setFlowInstance] = useState<ReactFlowInstance<WorkflowFlowNode, WorkflowFlowEdge> | null>(null) const [flowInstance, setFlowInstance] = useState<ReactFlowInstance<WorkflowFlowNode, WorkflowFlowEdge> | null>(null)
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null)
const [nodeLibraryCollapsed, setNodeLibraryCollapsed] = useState(false) const [nodeLibraryCollapsed, setNodeLibraryCollapsed] = useState(false)
const [nodeLibraryRendered, setNodeLibraryRendered] = useState(true) const [nodeLibraryRendered, setNodeLibraryRendered] = useState(true)
const [nodeLibraryVisible, setNodeLibraryVisible] = useState(true) const [nodeLibraryVisible, setNodeLibraryVisible] = useState(true)
@@ -198,14 +197,8 @@ export function WorkflowEditor({
const canvasRef = useRef<HTMLElement | null>(null) const canvasRef = useRef<HTMLElement | null>(null)
const pendingNodeDragRef = useRef<PendingNodeDrag | null>(null) const pendingNodeDragRef = useRef<PendingNodeDrag | null>(null)
const suppressNextClickRef = useRef(false) const suppressNextClickRef = useRef(false)
const selectedNode = useMemo( const nodeLibraryAnimationTimerRef = useRef<number | null>(null)
() => nodes.find((node) => node.id === selectedNodeId) ?? null, const propertyPanelAnimationTimerRef = useRef<number | null>(null)
[nodes, selectedNodeId]
)
const selectedEdge = useMemo(
() => edges.find((edge) => edge.id === selectedEdgeId) ?? null,
[edges, selectedEdgeId]
)
const draft = useMemo(() => toDraft(nodes, edges), [nodes, edges]) const draft = useMemo(() => toDraft(nodes, edges), [nodes, edges])
const validation = useMemo( const validation = useMemo(
() => validateWorkflowDraft(draft, nodeSpecs), () => validateWorkflowDraft(draft, nodeSpecs),
@@ -237,40 +230,75 @@ export function WorkflowEditor({
}, [draft, onDefinitionChange]) }, [draft, onDefinitionChange])
useEffect(() => { useEffect(() => {
if (!nodeLibraryCollapsed) { return () => {
setNodeLibraryRendered(true) if (nodeLibraryAnimationTimerRef.current !== null) {
const timer = window.setTimeout(() => { window.clearTimeout(nodeLibraryAnimationTimerRef.current)
setNodeLibraryVisible(true) }
}, 0) if (propertyPanelAnimationTimerRef.current !== null) {
return () => window.clearTimeout(timer) window.clearTimeout(propertyPanelAnimationTimerRef.current)
}
} }
}, [])
const showNodeLibrary = useCallback(() => {
if (nodeLibraryAnimationTimerRef.current !== null) {
window.clearTimeout(nodeLibraryAnimationTimerRef.current)
}
setNodeLibraryCollapsed(false)
setNodeLibraryRendered(true)
nodeLibraryAnimationTimerRef.current = window.setTimeout(() => {
setNodeLibraryVisible(true)
nodeLibraryAnimationTimerRef.current = null
}, 0)
}, [])
const hideNodeLibrary = useCallback(() => {
if (nodeLibraryAnimationTimerRef.current !== null) {
window.clearTimeout(nodeLibraryAnimationTimerRef.current)
}
setNodeLibraryCollapsed(true)
setNodeLibraryVisible(false) setNodeLibraryVisible(false)
const unmountTimer = window.setTimeout(() => setNodeLibraryRendered(false), 220) nodeLibraryAnimationTimerRef.current = window.setTimeout(() => {
return () => window.clearTimeout(unmountTimer) setNodeLibraryRendered(false)
}, [nodeLibraryCollapsed]) nodeLibraryAnimationTimerRef.current = null
useEffect(() => {
if (selectedNode) {
setPropertyPanelNode(selectedNode)
setPropertyPanelEdge(null)
window.setTimeout(() => setPropertyPanelVisible(true), 0)
return
}
if (selectedEdge) {
setPropertyPanelEdge(selectedEdge)
setPropertyPanelNode(null)
window.setTimeout(() => setPropertyPanelVisible(true), 0)
return
}
setPropertyPanelVisible(false)
const timer = window.setTimeout(() => {
setPropertyPanelNode(null)
setPropertyPanelEdge(null)
}, 220) }, 220)
return () => window.clearTimeout(timer) }, [])
}, [selectedNode, selectedEdge])
const showPropertyPanelNode = useCallback((node: WorkflowFlowNode) => {
if (propertyPanelAnimationTimerRef.current !== null) {
window.clearTimeout(propertyPanelAnimationTimerRef.current)
}
setPropertyPanelNode(node)
setPropertyPanelEdge(null)
propertyPanelAnimationTimerRef.current = window.setTimeout(() => {
setPropertyPanelVisible(true)
propertyPanelAnimationTimerRef.current = null
}, 0)
}, [])
const showPropertyPanelEdge = useCallback((edge: WorkflowFlowEdge) => {
if (propertyPanelAnimationTimerRef.current !== null) {
window.clearTimeout(propertyPanelAnimationTimerRef.current)
}
setPropertyPanelEdge(edge)
setPropertyPanelNode(null)
propertyPanelAnimationTimerRef.current = window.setTimeout(() => {
setPropertyPanelVisible(true)
propertyPanelAnimationTimerRef.current = null
}, 0)
}, [])
const hidePropertyPanel = useCallback(() => {
if (propertyPanelAnimationTimerRef.current !== null) {
window.clearTimeout(propertyPanelAnimationTimerRef.current)
}
setPropertyPanelVisible(false)
propertyPanelAnimationTimerRef.current = window.setTimeout(() => {
setPropertyPanelNode(null)
setPropertyPanelEdge(null)
propertyPanelAnimationTimerRef.current = null
}, 220)
}, [])
const onConnect = useCallback( const onConnect = useCallback(
(connection: Connection) => { (connection: Connection) => {
@@ -437,25 +465,38 @@ export function WorkflowEditor({
} }
const updateNodeData = (nodeId: string, data: WorkflowNodeData) => { const updateNodeData = (nodeId: string, data: WorkflowNodeData) => {
const nextData = {
...data,
label: data.name ?? data.nodeType ?? nodeId,
}
setNodes((current) => setNodes((current) =>
current.map((node) => current.map((node) =>
node.id === nodeId node.id === nodeId
? { ? {
...node, ...node,
data: { data: nextData,
...data,
label: data.name ?? data.nodeType ?? node.id,
},
} }
: node : node
) )
) )
setPropertyPanelNode((current) =>
current?.id === nodeId
? {
...current,
data: nextData,
}
: current
)
} }
const selectEdge = useCallback((edgeId: string) => { const selectEdge = useCallback((edgeId: string) => {
setSelectedNodeId(null) const edge = edges.find((item) => item.id === edgeId)
if (!edge) {
return
}
setSelectedEdgeId(edgeId) setSelectedEdgeId(edgeId)
}, []) showPropertyPanelEdge(edge)
}, [edges, showPropertyPanelEdge])
const renderedEdges = useMemo( const renderedEdges = useMemo(
() => () =>
@@ -475,17 +516,21 @@ export function WorkflowEditor({
) )
const updateEdgeCondition = (edgeId: string, condition?: WorkflowEdgeCondition) => { const updateEdgeCondition = (edgeId: string, condition?: WorkflowEdgeCondition) => {
const updateEdge = (edge: WorkflowFlowEdge) => ({
...edge,
label: condition ? "条件" : undefined,
data: condition ? { ...(edge.data as object), condition } : undefined,
})
setEdges((current) => setEdges((current) =>
current.map((edge) => current.map((edge) =>
edge.id === edgeId edge.id === edgeId
? { ? updateEdge(edge)
...edge,
label: condition ? "条件" : undefined,
data: condition ? { ...(edge.data as object), condition } : undefined,
}
: edge : edge
) )
) )
setPropertyPanelEdge((current) =>
current?.id === edgeId ? updateEdge(current) : current
)
} }
const clampNodeLibraryWidth = useCallback((width: number) => { const clampNodeLibraryWidth = useCallback((width: number) => {
@@ -548,7 +593,7 @@ export function WorkflowEditor({
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7 shrink-0 text-muted-foreground hover:text-foreground" className="size-7 shrink-0 text-muted-foreground hover:text-foreground"
onClick={() => setNodeLibraryCollapsed(true)} onClick={hideNodeLibrary}
aria-label="折叠节点库" aria-label="折叠节点库"
> >
<PanelLeftCloseIcon className="size-3.5" /> <PanelLeftCloseIcon className="size-3.5" />
@@ -615,7 +660,7 @@ export function WorkflowEditor({
variant="outline" variant="outline"
size="icon" size="icon"
className="absolute top-12 left-3 z-20 size-7 rounded-full bg-background/95 text-muted-foreground shadow-sm hover:text-foreground" className="absolute top-12 left-3 z-20 size-7 rounded-full bg-background/95 text-muted-foreground shadow-sm hover:text-foreground"
onClick={() => setNodeLibraryCollapsed(false)} onClick={showNodeLibrary}
aria-label="展开节点库" aria-label="展开节点库"
> >
<PanelLeftOpenIcon className="size-3.5" /> <PanelLeftOpenIcon className="size-3.5" />
@@ -637,16 +682,16 @@ export function WorkflowEditor({
onInit={setFlowInstance} onInit={setFlowInstance}
onNodeClick={(event, node) => { onNodeClick={(event, node) => {
event.stopPropagation() event.stopPropagation()
setSelectedNodeId(node.id)
setSelectedEdgeId(null) setSelectedEdgeId(null)
showPropertyPanelNode(node)
}} }}
onEdgeClick={(event, edge) => { onEdgeClick={(event, edge) => {
event.stopPropagation() event.stopPropagation()
selectEdge(edge.id) selectEdge(edge.id)
}} }}
onPaneClick={() => { onPaneClick={() => {
setSelectedNodeId(null)
setSelectedEdgeId(null) setSelectedEdgeId(null)
hidePropertyPanel()
}} }}
fitView fitView
fitViewOptions={fitViewOptions} fitViewOptions={fitViewOptions}