enforce workflow data edge order

This commit is contained in:
RockChinQ
2026-07-02 00:51:01 +08:00
parent 235b33c335
commit 59538fd5cd
3 changed files with 200 additions and 11 deletions
@@ -11,7 +11,12 @@ import {
SelectionMode,
MarkerType,
} from '@xyflow/react';
import type { Node, NodeTypes, OnSelectionChangeParams } from '@xyflow/react';
import type {
Connection,
Node,
NodeTypes,
OnSelectionChangeParams,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {
useWorkflowStore,
@@ -78,6 +83,7 @@ function WorkflowEditorInner() {
onNodesChange,
onEdgesChange,
onConnect,
isConnectionValid,
addNode,
selectNode,
selectEdge,
@@ -388,6 +394,17 @@ function WorkflowEditorInner() {
return categoryColors[category] || '#6b7280';
}, []);
const handleIsValidConnection = useCallback(
(connection: Connection | WorkflowEdge) =>
isConnectionValid({
source: connection.source,
target: connection.target,
sourceHandle: connection.sourceHandle ?? null,
targetHandle: connection.targetHandle ?? null,
}),
[isConnectionValid],
);
const displayNodes = nodes.map((node) => {
const executionResult = nodeExecutionResults[node.id];
@@ -441,6 +458,7 @@ function WorkflowEditorInner() {
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
isValidConnection={handleIsValidConnection}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
onPaneClick={handlePaneClick}
@@ -452,8 +470,8 @@ function WorkflowEditorInner() {
snapToGrid
snapGrid={[15, 15]}
selectionMode={SelectionMode.Partial}
selectionOnDrag
panOnDrag={[2]} // Right click to pan (left click is for node selection)
selectionOnDrag={false}
panOnDrag
selectNodesOnDrag={false}
defaultEdgeOptions={{
type: 'default',
@@ -128,6 +128,7 @@ interface WorkflowState {
onNodesChange: (changes: NodeChange<WorkflowNode>[]) => void;
onEdgesChange: (changes: EdgeChange<WorkflowEdge>[]) => void;
onConnect: (connection: Connection) => void;
isConnectionValid: (connection: Connection) => boolean;
addNode: (type: string, position: { x: number; y: number }) => string;
updateNodeConfig: (nodeId: string, config: Record<string, unknown>) => void;
@@ -206,6 +207,91 @@ const generateEdgeId = () => `edge_${generateUuidLikeId()}`;
const getWorkflowEdgeType = (edge: WorkflowEdge): WorkflowEdgeType =>
normalizeWorkflowEdgeType(edge.data?.edgeType);
const isControlOrderEdge = (edge: WorkflowEdge): boolean => {
const edgeType = getWorkflowEdgeType(edge);
return edgeType === 'control' || edgeType === 'legacy';
};
const hasForwardControlPath = (
sourceNodeId: string,
targetNodeId: string,
edges: WorkflowEdge[],
): boolean => {
if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) {
return false;
}
const adjacency = new Map<string, string[]>();
edges.forEach((edge) => {
if (!isControlOrderEdge(edge)) {
return;
}
if (!edge.source || !edge.target) {
return;
}
const targets = adjacency.get(edge.source) || [];
targets.push(edge.target);
adjacency.set(edge.source, targets);
});
const visited = new Set<string>();
const stack = [...(adjacency.get(sourceNodeId) || [])];
while (stack.length > 0) {
const current = stack.pop();
if (!current || visited.has(current)) {
continue;
}
if (current === targetNodeId) {
return true;
}
visited.add(current);
stack.push(...(adjacency.get(current) || []));
}
return false;
};
const isWorkflowConnectionValid = (
connection: Connection,
edges: WorkflowEdge[],
): boolean => {
if (!connection.source || !connection.target) {
return false;
}
if (connection.source === connection.target) {
return false;
}
const edgeType = inferWorkflowEdgeType(
connection.sourceHandle,
connection.targetHandle,
);
if (!edgeType) {
return false;
}
const duplicateEdge = edges.some(
(edge) =>
edge.source === connection.source &&
edge.target === connection.target &&
(edge.sourceHandle || null) === (connection.sourceHandle || null) &&
(edge.targetHandle || null) === (connection.targetHandle || null) &&
getWorkflowEdgeType(edge) === edgeType,
);
if (duplicateEdge) {
return false;
}
if (edgeType === 'data') {
return hasForwardControlPath(connection.source, connection.target, edges);
}
return true;
};
const withWorkflowEdgeStyle = (edge: WorkflowEdge): WorkflowEdge => {
const edgeType = getWorkflowEdgeType(edge);
@@ -327,6 +413,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
},
onConnect: (connection) => {
if (!get().isConnectionValid(connection)) {
return;
}
const edgeType = inferWorkflowEdgeType(
connection.sourceHandle,
connection.targetHandle,
@@ -355,6 +445,10 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
get().pushHistory();
},
isConnectionValid: (connection) => {
return isWorkflowConnectionValid(connection, get().edges);
},
// Add new node
addNode: (type, position) => {
const { nodeTypes } = get();