|
|
@@ -19,6 +19,10 @@ import ReactFlow, {
|
|
|
ConnectionLineType,
|
|
|
SelectionMode,
|
|
|
PanOnScrollMode,
|
|
|
+ BaseEdge,
|
|
|
+ EdgeProps,
|
|
|
+ getSmoothStepPath,
|
|
|
+ EdgeLabelRenderer,
|
|
|
} from 'reactflow'
|
|
|
import { BackgroundVariant } from '@reactflow/background'
|
|
|
import 'reactflow/dist/style.css'
|
|
|
@@ -70,6 +74,98 @@ const nodeTypes = {
|
|
|
workflowNode: WorkflowNode,
|
|
|
}
|
|
|
|
|
|
+// Custom edge that routes around nodes for cleaner layout
|
|
|
+// For "false" and "done" edges, route further to the right before going down
|
|
|
+// For back-edges (target above source), route to the left
|
|
|
+function SmartEdge({
|
|
|
+ id,
|
|
|
+ sourceX,
|
|
|
+ sourceY,
|
|
|
+ targetX,
|
|
|
+ targetY,
|
|
|
+ sourcePosition,
|
|
|
+ targetPosition,
|
|
|
+ style,
|
|
|
+ markerEnd,
|
|
|
+ data,
|
|
|
+ label,
|
|
|
+}: EdgeProps) {
|
|
|
+ const handleType = data?.handleType || 'main'
|
|
|
+ const isBackEdge = targetY < sourceY - 10 // Target is above source
|
|
|
+ const isSideEdge = handleType === 'false' || handleType === 'done'
|
|
|
+ const isGoingDown = targetY > sourceY + 50 // Target is significantly below
|
|
|
+
|
|
|
+ let edgePath: string
|
|
|
+ let labelX: number
|
|
|
+ let labelY: number
|
|
|
+
|
|
|
+ if (isBackEdge) {
|
|
|
+ // Back-edge: route to the left of both nodes, then up
|
|
|
+ const offsetX = -120 // Go left, further out to avoid nodes
|
|
|
+ const midY = (sourceY + targetY) / 2
|
|
|
+
|
|
|
+ edgePath = `M ${sourceX} ${sourceY}
|
|
|
+ L ${sourceX} ${sourceY + 15}
|
|
|
+ L ${Math.min(sourceX, targetX) + offsetX} ${sourceY + 15}
|
|
|
+ L ${Math.min(sourceX, targetX) + offsetX} ${targetY - 15}
|
|
|
+ L ${targetX} ${targetY - 15}
|
|
|
+ L ${targetX} ${targetY}`
|
|
|
+ labelX = Math.min(sourceX, targetX) + offsetX - 15
|
|
|
+ labelY = midY
|
|
|
+ } else if (isSideEdge && isGoingDown) {
|
|
|
+ // Side edge (false/done) going down: always route to the right to avoid nodes
|
|
|
+ const offsetX = 130 // Go right, enough to clear nodes
|
|
|
+ const midY = (sourceY + targetY) / 2
|
|
|
+
|
|
|
+ edgePath = `M ${sourceX} ${sourceY}
|
|
|
+ L ${sourceX} ${sourceY + 25}
|
|
|
+ L ${Math.max(sourceX, targetX) + offsetX} ${sourceY + 25}
|
|
|
+ L ${Math.max(sourceX, targetX) + offsetX} ${targetY - 25}
|
|
|
+ L ${targetX} ${targetY - 25}
|
|
|
+ L ${targetX} ${targetY}`
|
|
|
+ labelX = Math.max(sourceX, targetX) + offsetX + 15
|
|
|
+ labelY = midY
|
|
|
+ } else {
|
|
|
+ // Regular edge: use standard smoothstep
|
|
|
+ const [path, lx, ly] = getSmoothStepPath({
|
|
|
+ sourceX,
|
|
|
+ sourceY,
|
|
|
+ sourcePosition,
|
|
|
+ targetX,
|
|
|
+ targetY,
|
|
|
+ targetPosition,
|
|
|
+ borderRadius: 8,
|
|
|
+ })
|
|
|
+ edgePath = path
|
|
|
+ labelX = lx
|
|
|
+ labelY = ly
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
|
|
|
+ {label && (
|
|
|
+ <EdgeLabelRenderer>
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
|
+ pointerEvents: 'all',
|
|
|
+ }}
|
|
|
+ className="nodrag nopan px-1.5 py-0.5 rounded text-xs font-medium bg-slate-800 text-slate-300 border border-slate-600"
|
|
|
+ >
|
|
|
+ {label}
|
|
|
+ </div>
|
|
|
+ </EdgeLabelRenderer>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+const edgeTypes = {
|
|
|
+ smart: SmartEdge,
|
|
|
+}
|
|
|
+
|
|
|
function WorkflowEditorInner() {
|
|
|
const { id } = useParams<{ id: string }>()
|
|
|
const navigate = useNavigate()
|
|
|
@@ -791,11 +887,12 @@ function WorkflowEditorInner() {
|
|
|
sourceHandle: c.sourceOutput,
|
|
|
target: c.targetNodeId,
|
|
|
targetHandle: c.targetInput,
|
|
|
- type: 'smoothstep' as const,
|
|
|
+ type: 'smart' as const,
|
|
|
style: { strokeWidth: 2, stroke: edgeColor },
|
|
|
animated: false,
|
|
|
label: c.sourceOutput !== 'main' ? c.sourceOutput : undefined,
|
|
|
labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
|
|
|
+ data: { handleType: c.sourceOutput || 'main' },
|
|
|
labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
|
|
|
zIndex: 1,
|
|
|
}
|
|
|
@@ -862,12 +959,13 @@ function WorkflowEditorInner() {
|
|
|
sourceHandle: conn.sourceOutput,
|
|
|
target: conn.targetNodeId,
|
|
|
targetHandle: conn.targetInput,
|
|
|
- type: 'smoothstep',
|
|
|
+ type: 'smart',
|
|
|
style: { strokeWidth: 2, stroke: edgeColor },
|
|
|
animated: executionState.status === 'running',
|
|
|
label: conn.sourceOutput !== 'main' ? conn.sourceOutput : undefined,
|
|
|
labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
|
|
|
labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
|
|
|
+ data: { handleType: conn.sourceOutput || 'main' },
|
|
|
zIndex: 1,
|
|
|
}
|
|
|
})
|
|
|
@@ -958,11 +1056,12 @@ function WorkflowEditorInner() {
|
|
|
|
|
|
const styledEdge = {
|
|
|
...params,
|
|
|
- type: 'smoothstep',
|
|
|
+ type: 'smart',
|
|
|
style: { strokeWidth: 2, stroke: edgeColor },
|
|
|
label: sourceHandle !== 'main' ? sourceHandle : undefined,
|
|
|
labelStyle: { fill: edgeColor, fontWeight: 500, fontSize: 10 },
|
|
|
labelBgStyle: { fill: 'var(--color-bg)', fillOpacity: 0.9 },
|
|
|
+ data: { handleType: sourceHandle },
|
|
|
zIndex: 1,
|
|
|
}
|
|
|
setEdges((eds) => addEdge(styledEdge, eds))
|
|
|
@@ -1935,8 +2034,9 @@ function WorkflowEditorInner() {
|
|
|
nodes={isViewingExecution && viewedNodes ? viewedNodes : nodes}
|
|
|
edges={isViewingExecution && viewedEdges ? viewedEdges : edges}
|
|
|
nodeTypes={nodeTypes}
|
|
|
+ edgeTypes={edgeTypes}
|
|
|
defaultEdgeOptions={{
|
|
|
- type: 'smoothstep',
|
|
|
+ type: 'smart',
|
|
|
style: { strokeWidth: 2, stroke: '#64748b' },
|
|
|
animated: false,
|
|
|
}}
|