Add intermediate previews and graph reconnection
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState, type DragEvent } from "react";
|
||||
import { useMemo, useRef, useState, type DragEvent } from "react";
|
||||
import {
|
||||
addEdge,
|
||||
applyEdgeChanges,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Controls,
|
||||
MiniMap,
|
||||
ReactFlow,
|
||||
reconnectEdge,
|
||||
type Connection,
|
||||
type Edge,
|
||||
type ReactFlowInstance
|
||||
@@ -22,6 +23,11 @@ import type {
|
||||
PipelineGraphNode
|
||||
} from "../../api/dataflow";
|
||||
import DataflowNode, { type DataflowFlowNode } from "./DataflowNode";
|
||||
import {
|
||||
closestProximityConnection,
|
||||
graphEdgesFromFlow,
|
||||
proximityPreviewEdge
|
||||
} from "./graphInteractions";
|
||||
import { FALLBACK_NODE_LIBRARY, newNode } from "./model";
|
||||
|
||||
const nodeTypes = { dataflow: DataflowNode };
|
||||
@@ -48,6 +54,10 @@ export default function DataflowCanvas({
|
||||
onSelectNode
|
||||
}: DataflowCanvasProps) {
|
||||
const [instance, setInstance] = useState<ReactFlowInstance<DataflowFlowNode, Edge> | null>(null);
|
||||
const [proximityEdge, setProximityEdge] = useState<Edge | null>(null);
|
||||
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
|
||||
const reconnectSuccessful = useRef(true);
|
||||
const reconnectingEdgeId = useRef<string | null>(null);
|
||||
const errorNodeIds = useMemo(
|
||||
() => new Set(diagnostics.filter((item) => item.severity === "error" && item.node_id).map((item) => item.node_id)),
|
||||
[diagnostics]
|
||||
@@ -92,9 +102,14 @@ export default function DataflowCanvas({
|
||||
sourceHandle: edge.source_port ?? "output",
|
||||
targetHandle: edge.target_port ?? "input",
|
||||
type: "smoothstep",
|
||||
className: "dataflow-edge"
|
||||
className: "dataflow-edge",
|
||||
selected: edge.id === selectedEdgeId
|
||||
})),
|
||||
[graph.edges]
|
||||
[graph.edges, selectedEdgeId]
|
||||
);
|
||||
const displayedEdges = useMemo(
|
||||
() => proximityEdge ? [...edges, proximityEdge] : edges,
|
||||
[edges, proximityEdge]
|
||||
);
|
||||
|
||||
const updateNodes = (nextNodes: DataflowFlowNode[]) => {
|
||||
@@ -116,28 +131,32 @@ export default function DataflowCanvas({
|
||||
const updateEdges = (nextEdges: Edge[]) => {
|
||||
onGraphChange({
|
||||
...graph,
|
||||
edges: nextEdges.map((edge) => ({
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
source_port: edge.sourceHandle ?? "output",
|
||||
target_port: edge.targetHandle ?? "input"
|
||||
}))
|
||||
edges: graphEdgesFromFlow(nextEdges)
|
||||
});
|
||||
};
|
||||
|
||||
const connectionError = (
|
||||
connection: Connection | Edge,
|
||||
ignoredEdgeId: string | null = reconnectingEdgeId.current
|
||||
): string | null => definitionConnectionError(
|
||||
ignoredEdgeId
|
||||
? {
|
||||
...graph,
|
||||
edges: graph.edges.filter((edge) => edge.id !== ignoredEdgeId)
|
||||
}
|
||||
: graph,
|
||||
nodeLibrary,
|
||||
{
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
sourcePort: connection.sourceHandle,
|
||||
targetPort: connection.targetHandle
|
||||
}
|
||||
);
|
||||
|
||||
const isValidConnection = (connection: Connection | Edge): boolean => {
|
||||
if (readOnly || !connection.source || !connection.target) return false;
|
||||
return definitionConnectionError(
|
||||
graph,
|
||||
nodeLibrary,
|
||||
{
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
sourcePort: connection.sourceHandle,
|
||||
targetPort: connection.targetHandle
|
||||
}
|
||||
) === null;
|
||||
return connectionError(connection) === null;
|
||||
};
|
||||
|
||||
const onConnect = (connection: Connection) => {
|
||||
@@ -153,6 +172,26 @@ export default function DataflowCanvas({
|
||||
updateEdges(next);
|
||||
};
|
||||
|
||||
const onReconnect = (oldEdge: Edge, connection: Connection) => {
|
||||
if (readOnly || connectionError(connection, oldEdge.id)) return;
|
||||
reconnectSuccessful.current = true;
|
||||
updateEdges(reconnectEdge(
|
||||
oldEdge,
|
||||
connection,
|
||||
edges,
|
||||
{ shouldReplaceId: false }
|
||||
));
|
||||
};
|
||||
|
||||
const proximityConnection = (draggedNode: DataflowFlowNode) => (
|
||||
closestProximityConnection(
|
||||
draggedNode,
|
||||
nodes,
|
||||
graph,
|
||||
nodeLibrary
|
||||
)
|
||||
);
|
||||
|
||||
const onDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
if (readOnly || !instance) return;
|
||||
@@ -177,7 +216,7 @@ export default function DataflowCanvas({
|
||||
>
|
||||
<ReactFlow<DataflowFlowNode, Edge>
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
edges={displayedEdges}
|
||||
nodeTypes={nodeTypes}
|
||||
onInit={setInstance}
|
||||
onNodesChange={(changes) => {
|
||||
@@ -189,14 +228,90 @@ export default function DataflowCanvas({
|
||||
}}
|
||||
onEdgesChange={(changes) => {
|
||||
if (readOnly) return;
|
||||
updateEdges(applyEdgeChanges(changes, edges));
|
||||
const selectedChange = changes.find(
|
||||
(change) => change.type === "select" && change.selected
|
||||
);
|
||||
if (selectedChange?.type === "select") {
|
||||
setSelectedEdgeId(selectedChange.id);
|
||||
} else if (changes.some(
|
||||
(change) => (
|
||||
change.type === "select"
|
||||
&& change.id === selectedEdgeId
|
||||
&& !change.selected
|
||||
)
|
||||
)) {
|
||||
setSelectedEdgeId(null);
|
||||
}
|
||||
const graphChanges = changes.filter((change) => change.type !== "select");
|
||||
if (graphChanges.length) {
|
||||
if (graphChanges.some(
|
||||
(change) => change.type === "remove" && change.id === selectedEdgeId
|
||||
)) {
|
||||
setSelectedEdgeId(null);
|
||||
}
|
||||
updateEdges(applyEdgeChanges(graphChanges, edges));
|
||||
}
|
||||
}}
|
||||
onConnect={onConnect}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={(_event, edge) => {
|
||||
reconnectSuccessful.current = false;
|
||||
reconnectingEdgeId.current = edge.id;
|
||||
setProximityEdge(null);
|
||||
}}
|
||||
onReconnectEnd={(_event, edge) => {
|
||||
if (!reconnectSuccessful.current && !readOnly) {
|
||||
updateEdges(edges.filter((candidate) => candidate.id !== edge.id));
|
||||
setSelectedEdgeId(null);
|
||||
}
|
||||
reconnectSuccessful.current = true;
|
||||
reconnectingEdgeId.current = null;
|
||||
}}
|
||||
onNodeDrag={(_event, node) => {
|
||||
if (readOnly) return;
|
||||
const connection = proximityConnection(node);
|
||||
setProximityEdge(connection ? proximityPreviewEdge(connection) : null);
|
||||
}}
|
||||
onNodeDragStop={(_event, node) => {
|
||||
setProximityEdge(null);
|
||||
if (readOnly) return;
|
||||
const connection = proximityConnection(node);
|
||||
if (!connection) return;
|
||||
const nextEdges = addEdge(
|
||||
{
|
||||
...connection,
|
||||
id: `edge-${crypto.randomUUID()}`,
|
||||
type: "smoothstep",
|
||||
className: "dataflow-edge"
|
||||
},
|
||||
edges
|
||||
);
|
||||
onGraphChange({
|
||||
...graph,
|
||||
nodes: graph.nodes.map((graphNode) => (
|
||||
graphNode.id === node.id
|
||||
? { ...graphNode, position: node.position }
|
||||
: graphNode
|
||||
)),
|
||||
edges: graphEdgesFromFlow(nextEdges)
|
||||
});
|
||||
}}
|
||||
isValidConnection={isValidConnection}
|
||||
onNodeClick={(_event, node) => onSelectNode(node.id)}
|
||||
onPaneClick={() => onSelectNode(null)}
|
||||
onNodeClick={(_event, node) => {
|
||||
setSelectedEdgeId(null);
|
||||
onSelectNode(node.id);
|
||||
}}
|
||||
onEdgeClick={(_event, edge) => {
|
||||
setSelectedEdgeId(edge.id);
|
||||
onSelectNode(null);
|
||||
}}
|
||||
onPaneClick={() => {
|
||||
setSelectedEdgeId(null);
|
||||
onSelectNode(null);
|
||||
}}
|
||||
nodesDraggable={!readOnly}
|
||||
nodesConnectable={!readOnly}
|
||||
edgesReconnectable={!readOnly}
|
||||
elementsSelectable
|
||||
deleteKeyCode={readOnly ? null : ["Backspace", "Delete"]}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
|
||||
Reference in New Issue
Block a user