Enhance WorkflowEditor connection handling and edge rendering for improved user experience
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
type ConnectionLineComponentProps,
|
type ConnectionLineComponentProps,
|
||||||
type Edge,
|
type Edge,
|
||||||
type EdgeProps,
|
type EdgeProps,
|
||||||
|
type FinalConnectionState,
|
||||||
type Node,
|
type Node,
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
type ReactFlowInstance,
|
type ReactFlowInstance,
|
||||||
@@ -85,6 +86,7 @@ type WorkflowEdgeRenderData = WorkflowEditorEdge["data"] & {
|
|||||||
active?: boolean
|
active?: boolean
|
||||||
onSelect?: (edgeId: string) => void
|
onSelect?: (edgeId: string) => void
|
||||||
}
|
}
|
||||||
|
type WorkflowFinalConnectionState = FinalConnectionState
|
||||||
|
|
||||||
type PendingNodeDrag = {
|
type PendingNodeDrag = {
|
||||||
spec: AIWorkflowNodeSpec
|
spec: AIWorkflowNodeSpec
|
||||||
@@ -119,6 +121,8 @@ const defaultEdgeOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowHandleRadius = 8
|
||||||
|
|
||||||
function toFlowNodes(definition: AIWorkflowDefinition): WorkflowFlowNode[] {
|
function toFlowNodes(definition: AIWorkflowDefinition): WorkflowFlowNode[] {
|
||||||
return fromApiDefinition(definition).nodes.map((node) => ({
|
return fromApiDefinition(definition).nodes.map((node) => ({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
@@ -337,6 +341,47 @@ export function WorkflowEditor({
|
|||||||
[edges, nodeSpecs, setEdges, setNodes]
|
[edges, nodeSpecs, setEdges, setNodes]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const connectToNode = useCallback(
|
||||||
|
(connectionState: WorkflowFinalConnectionState, targetNodeId: string) => {
|
||||||
|
if (!connectionState.fromHandle || connectionState.toHandle || connectionState.fromHandle.nodeId === targetNodeId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromHandle = connectionState.fromHandle
|
||||||
|
const source = fromHandle.type === "target" ? targetNodeId : fromHandle.nodeId
|
||||||
|
const target = fromHandle.type === "target" ? fromHandle.nodeId : targetNodeId
|
||||||
|
const connection = {
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
sourceHandle: fromHandle.type === "target" ? null : fromHandle.id ?? null,
|
||||||
|
targetHandle: fromHandle.type === "target" ? fromHandle.id ?? null : null,
|
||||||
|
} satisfies Connection
|
||||||
|
onConnect(connection)
|
||||||
|
},
|
||||||
|
[onConnect]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onConnectEnd = useCallback(
|
||||||
|
(event: MouseEvent | TouchEvent, connectionState: WorkflowFinalConnectionState) => {
|
||||||
|
if (connectionState.toHandle) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const point = getEventClientPoint(event)
|
||||||
|
if (!point) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nodeElement = document
|
||||||
|
.elementFromPoint(point.x, point.y)
|
||||||
|
?.closest<HTMLElement>(".react-flow__node[data-id]")
|
||||||
|
const targetNodeId = nodeElement?.dataset.id
|
||||||
|
if (!targetNodeId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connectToNode(connectionState, targetNodeId)
|
||||||
|
},
|
||||||
|
[connectToNode]
|
||||||
|
)
|
||||||
|
|
||||||
const addNode = (spec: AIWorkflowNodeSpec) => {
|
const addNode = (spec: AIWorkflowNodeSpec) => {
|
||||||
setNodes((current) => {
|
setNodes((current) => {
|
||||||
const node = createWorkflowNodeFromSpec(
|
const node = createWorkflowNodeFromSpec(
|
||||||
@@ -679,6 +724,7 @@ export function WorkflowEditor({
|
|||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
onConnectEnd={onConnectEnd}
|
||||||
onInit={setFlowInstance}
|
onInit={setFlowInstance}
|
||||||
onNodeClick={(event, node) => {
|
onNodeClick={(event, node) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@@ -981,14 +1027,49 @@ function normalizeConditionRight(value: string) {
|
|||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowConnectionLine({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) {
|
function getEventClientPoint(event: MouseEvent | TouchEvent) {
|
||||||
|
if ("changedTouches" in event) {
|
||||||
|
const touch = event.changedTouches[0] ?? event.touches[0]
|
||||||
|
return touch ? { x: touch.clientX, y: touch.clientY } : null
|
||||||
|
}
|
||||||
|
return { x: event.clientX, y: event.clientY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEdgeEndpointOffset(position: Position, amount: number) {
|
||||||
|
switch (position) {
|
||||||
|
case Position.Left:
|
||||||
|
return { x: amount, y: 0 }
|
||||||
|
case Position.Right:
|
||||||
|
return { x: -amount, y: 0 }
|
||||||
|
case Position.Top:
|
||||||
|
return { x: 0, y: amount }
|
||||||
|
case Position.Bottom:
|
||||||
|
return { x: 0, y: -amount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkflowConnectionLine({
|
||||||
|
fromX,
|
||||||
|
fromY,
|
||||||
|
fromPosition,
|
||||||
|
toX,
|
||||||
|
toY,
|
||||||
|
toPosition,
|
||||||
|
toHandle,
|
||||||
|
}: ConnectionLineComponentProps) {
|
||||||
|
const sourceOffset = getEdgeEndpointOffset(fromPosition ?? Position.Right, workflowHandleRadius)
|
||||||
|
const targetOffset = getEdgeEndpointOffset(toPosition ?? Position.Left, toHandle ? workflowHandleRadius : 0)
|
||||||
|
const sourceX = fromX + sourceOffset.x
|
||||||
|
const sourceY = fromY + sourceOffset.y
|
||||||
|
const targetX = toX + targetOffset.x
|
||||||
|
const targetY = toY + targetOffset.y
|
||||||
const [edgePath] = getBezierPath({
|
const [edgePath] = getBezierPath({
|
||||||
sourceX: fromX,
|
sourceX,
|
||||||
sourceY: fromY,
|
sourceY,
|
||||||
sourcePosition: Position.Right,
|
sourcePosition: fromPosition ?? Position.Right,
|
||||||
targetX: toX,
|
targetX,
|
||||||
targetY: toY,
|
targetY,
|
||||||
targetPosition: Position.Left,
|
targetPosition: toPosition ?? Position.Left,
|
||||||
curvature: 0.18,
|
curvature: 0.18,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1020,12 +1101,14 @@ function WorkflowCanvasEdge({
|
|||||||
data,
|
data,
|
||||||
markerEnd,
|
markerEnd,
|
||||||
}: EdgeProps<WorkflowFlowEdge>) {
|
}: EdgeProps<WorkflowFlowEdge>) {
|
||||||
|
const sourceOffset = getEdgeEndpointOffset(sourcePosition, workflowHandleRadius)
|
||||||
|
const targetOffset = getEdgeEndpointOffset(targetPosition, workflowHandleRadius)
|
||||||
const [edgePath, labelX, labelY] = getBezierPath({
|
const [edgePath, labelX, labelY] = getBezierPath({
|
||||||
sourceX,
|
sourceX: sourceX + sourceOffset.x,
|
||||||
sourceY,
|
sourceY: sourceY + sourceOffset.y,
|
||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetX,
|
targetX: targetX + targetOffset.x,
|
||||||
targetY,
|
targetY: targetY + targetOffset.y,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
curvature: 0.18,
|
curvature: 0.18,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user