mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-15 00:46:07 +00:00
enforce workflow data edge order
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user