Refactor WorkflowEditor to improve node and edge property panel handling and animation
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user