197 lines
5.1 KiB
TypeScript
197 lines
5.1 KiB
TypeScript
|
|
import React, { useCallback, useRef, useEffect } from 'react';
|
||
|
|
import ReactFlow, {
|
||
|
|
Background,
|
||
|
|
Controls,
|
||
|
|
ReactFlowProvider,
|
||
|
|
addEdge,
|
||
|
|
Connection,
|
||
|
|
useReactFlow,
|
||
|
|
applyNodeChanges,
|
||
|
|
applyEdgeChanges,
|
||
|
|
Panel,
|
||
|
|
} from 'reactflow';
|
||
|
|
import { ArrowLeft, Save } from 'lucide-react';
|
||
|
|
import { useFlowStore } from '../store/flowStore';
|
||
|
|
import { ConfigPanel } from './ConfigPanel';
|
||
|
|
import { ComponentsPanel } from './ComponentsPanel';
|
||
|
|
import CustomNode from './CustomNode';
|
||
|
|
import 'reactflow/dist/style.css';
|
||
|
|
|
||
|
|
const nodeTypes = {
|
||
|
|
auth: CustomNode,
|
||
|
|
url: CustomNode,
|
||
|
|
output: CustomNode,
|
||
|
|
logic: CustomNode,
|
||
|
|
variable: CustomNode,
|
||
|
|
'db-find': CustomNode,
|
||
|
|
'db-insert': CustomNode,
|
||
|
|
'db-update': CustomNode,
|
||
|
|
'db-delete': CustomNode,
|
||
|
|
'db-query': CustomNode,
|
||
|
|
};
|
||
|
|
|
||
|
|
interface FlowEditorContentProps {
|
||
|
|
route: any;
|
||
|
|
onClose: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
function FlowEditorContent({ route, onClose }: FlowEditorContentProps) {
|
||
|
|
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||
|
|
const { project } = useReactFlow();
|
||
|
|
const {
|
||
|
|
nodes,
|
||
|
|
edges,
|
||
|
|
setNodes,
|
||
|
|
setEdges,
|
||
|
|
selectedNode,
|
||
|
|
setSelectedNode,
|
||
|
|
updateNodeData,
|
||
|
|
updateRoute,
|
||
|
|
} = useFlowStore();
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// Load saved flow data if it exists
|
||
|
|
if (route.flowData) {
|
||
|
|
setNodes(route.flowData.nodes);
|
||
|
|
setEdges(route.flowData.edges);
|
||
|
|
} else {
|
||
|
|
setNodes([]);
|
||
|
|
setEdges([]);
|
||
|
|
}
|
||
|
|
}, [route, setNodes, setEdges]);
|
||
|
|
|
||
|
|
const onNodesChange = useCallback(
|
||
|
|
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||
|
|
[setNodes]
|
||
|
|
);
|
||
|
|
|
||
|
|
const onEdgesChange = useCallback(
|
||
|
|
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||
|
|
[setEdges]
|
||
|
|
);
|
||
|
|
|
||
|
|
const onConnect = useCallback(
|
||
|
|
(params: Connection) => setEdges((eds) => addEdge(params, eds)),
|
||
|
|
[setEdges]
|
||
|
|
);
|
||
|
|
|
||
|
|
const onDragOver = useCallback((event: React.DragEvent) => {
|
||
|
|
event.preventDefault();
|
||
|
|
event.dataTransfer.dropEffect = 'move';
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const onDrop = useCallback(
|
||
|
|
(event: React.DragEvent) => {
|
||
|
|
event.preventDefault();
|
||
|
|
|
||
|
|
const type = event.dataTransfer.getData('application/reactflow');
|
||
|
|
if (!type || !reactFlowWrapper.current) return;
|
||
|
|
|
||
|
|
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||
|
|
const position = project({
|
||
|
|
x: event.clientX - reactFlowBounds.left - 75,
|
||
|
|
y: event.clientY - reactFlowBounds.top - 25,
|
||
|
|
});
|
||
|
|
|
||
|
|
const newNode = {
|
||
|
|
id: `node_${Date.now()}`,
|
||
|
|
type,
|
||
|
|
position,
|
||
|
|
data: { label: type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ') },
|
||
|
|
};
|
||
|
|
|
||
|
|
setNodes((nds) => [...nds, newNode]);
|
||
|
|
},
|
||
|
|
[project, setNodes]
|
||
|
|
);
|
||
|
|
|
||
|
|
const onNodeClick = useCallback(
|
||
|
|
(_: React.MouseEvent, node: any) => {
|
||
|
|
setSelectedNode(node);
|
||
|
|
},
|
||
|
|
[setSelectedNode]
|
||
|
|
);
|
||
|
|
|
||
|
|
const onPaneClick = useCallback(() => {
|
||
|
|
setSelectedNode(null);
|
||
|
|
}, [setSelectedNode]);
|
||
|
|
|
||
|
|
const handleSave = () => {
|
||
|
|
updateRoute({
|
||
|
|
...route,
|
||
|
|
flowData: {
|
||
|
|
nodes,
|
||
|
|
edges,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
onClose();
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col h-full">
|
||
|
|
<div className="border-b border-gray-200 p-4 flex items-center justify-between bg-white">
|
||
|
|
<div className="flex items-center">
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
|
||
|
|
>
|
||
|
|
<ArrowLeft className="w-5 h-5" />
|
||
|
|
Back to Routes
|
||
|
|
</button>
|
||
|
|
<h2 className="ml-4 text-lg font-semibold">
|
||
|
|
{route.name} ({route.method} {route.url})
|
||
|
|
</h2>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={handleSave}
|
||
|
|
className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
|
||
|
|
>
|
||
|
|
<Save className="w-4 h-4" />
|
||
|
|
Save Changes
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-1 overflow-hidden">
|
||
|
|
<ComponentsPanel />
|
||
|
|
<div className="flex-1 h-full" ref={reactFlowWrapper}>
|
||
|
|
<ReactFlow
|
||
|
|
nodes={nodes}
|
||
|
|
edges={edges}
|
||
|
|
onNodesChange={onNodesChange}
|
||
|
|
onEdgesChange={onEdgesChange}
|
||
|
|
onConnect={onConnect}
|
||
|
|
onDrop={onDrop}
|
||
|
|
onDragOver={onDragOver}
|
||
|
|
onNodeClick={onNodeClick}
|
||
|
|
onPaneClick={onPaneClick}
|
||
|
|
nodeTypes={nodeTypes}
|
||
|
|
deleteKeyCode="Delete"
|
||
|
|
multiSelectionKeyCode="Control"
|
||
|
|
selectionKeyCode="Shift"
|
||
|
|
snapToGrid={true}
|
||
|
|
snapGrid={[15, 15]}
|
||
|
|
defaultViewport={{ x: 0, y: 0, zoom: 1 }}
|
||
|
|
fitView
|
||
|
|
>
|
||
|
|
<Background />
|
||
|
|
<Controls />
|
||
|
|
</ReactFlow>
|
||
|
|
</div>
|
||
|
|
{selectedNode && (
|
||
|
|
<ConfigPanel
|
||
|
|
node={selectedNode}
|
||
|
|
onClose={() => setSelectedNode(null)}
|
||
|
|
onUpdateNode={updateNodeData}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function RouteFlowEditor({ route, onClose }: FlowEditorContentProps) {
|
||
|
|
return (
|
||
|
|
<ReactFlowProvider>
|
||
|
|
<FlowEditorContent route={route} onClose={onClose} />
|
||
|
|
</ReactFlowProvider>
|
||
|
|
);
|
||
|
|
}
|