diff --git a/src/frontend/src/contexts/flowsContext.tsx b/src/frontend/src/contexts/flowsContext.tsx index 37182244a..0b9f1fbec 100644 --- a/src/frontend/src/contexts/flowsContext.tsx +++ b/src/frontend/src/contexts/flowsContext.tsx @@ -122,7 +122,6 @@ export function FlowsProvider({ children }: { children: ReactNode }) { const [tabId, setTabId] = useState(""); const [isLoading, setIsLoading] = useState(false); const [flows, setFlows] = useState>([]); - const [id, setId] = useState(uid()); const { reactFlowInstance, setData } = useContext(typesContext); const [lastCopiedSelection, setLastCopiedSelection] = useState<{ nodes: any; diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 4667a9216..c12017947 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -31,6 +31,7 @@ import { FlowType, NodeType, targetHandleType } from "../../../../types/flow"; import { generateFlow, generateNodeFromFlow, + getNodeId, isValidConnection, scapeJSONParse, validateSelection, @@ -39,6 +40,7 @@ import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils"; import ConnectionLineComponent from "../ConnectionLineComponent"; import SelectionMenu from "../SelectionMenuComponent"; import ExtraSidebar from "../extraSidebarComponent"; +import useFlow from "../../../../stores/flowManagerStore"; const nodeTypes = { genericNode: GenericNode, @@ -53,34 +55,23 @@ export default function Page({ }): JSX.Element { let { uploadFlow, - getNodeId, - paste, - lastCopiedSelection, - setLastCopiedSelection, - deleteNode, - deleteEdge, + saveFlow, } = useContext(FlowsContext); const { types, - reactFlowInstance, - setReactFlowInstance, templates, setFilterEdge, } = useContext(typesContext); const reactFlowWrapper = useRef(null); + const [lastCopiedSelection, setLastCopiedSelection] = useState<{ + nodes: any; + edges: any; + } | null>(null); + const { takeSnapshot } = useContext(undoRedoContext); - const { - nodes, - edges, - setNodes, - setEdges, - onNodesChange, - onEdgesChange, - setPending, - saveFlow, - isPending, - } = useContext(FlowsContext); + + const { reactFlowInstance, setReactFlowInstance, nodes, edges, onNodesChange, onEdgesChange, onConnect, setNodes, setEdges, deleteNode, deleteEdge, setPending, isPending, paste } = useFlow(); const position = useRef({ x: 0, y: 0 }); const [lastSelection, setLastSelection] = @@ -183,30 +174,10 @@ export default function Page({ }; }, [flow, reactFlowInstance]); - const onConnect = useCallback( + const onConnectMod = useCallback( (params: Connection) => { takeSnapshot(); - setEdges((eds) => - addEdge( - { - ...params, - data: { - targetHandle: scapeJSONParse(params.targetHandle!), - sourceHandle: scapeJSONParse(params.sourceHandle!), - }, - style: { stroke: "#555" }, - className: - ((scapeJSONParse(params.targetHandle!) as targetHandleType) - .type === "Text" - ? "stroke-foreground " - : "stroke-foreground ") + " stroke-connection", - animated: - (scapeJSONParse(params.targetHandle!) as targetHandleType) - .type === "Text", - }, - eds - ) - ); + onConnect(params); }, [setEdges, takeSnapshot, addEdge] ); @@ -404,7 +375,7 @@ export default function Page({ edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} - onConnect={onConnect} + onConnect={onConnectMod} disableKeyboardA11y={true} onInit={setReactFlowInstance} nodeTypes={nodeTypes} diff --git a/src/frontend/src/stores/flowManagerStore.ts b/src/frontend/src/stores/flowManagerStore.ts index 2d66c9a8c..14f583e93 100644 --- a/src/frontend/src/stores/flowManagerStore.ts +++ b/src/frontend/src/stores/flowManagerStore.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from "lodash"; import { Connection, Edge, @@ -7,72 +8,217 @@ import { OnConnect, OnEdgesChange, OnNodesChange, + ReactFlowInstance, addEdge, applyEdgeChanges, applyNodeChanges, } from "reactflow"; -import ShortUniqueId from "short-unique-id"; import { create } from "zustand"; -const uid = new ShortUniqueId({ length: 5 }); +import { + NodeDataType, + NodeType, + sourceHandleType, + targetHandleType, +} from "../types/flow"; +import { + cleanEdges, + getHandleId, + getNodeId, + scapeJSONParse, + scapedJSONStringfy, +} from "../utils/reactflowUtils"; type RFState = { + reactFlowInstance: ReactFlowInstance | null; + setReactFlowInstance: (newState: ReactFlowInstance) => void; nodes: Node[]; edges: Edge[]; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; + setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void; + setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void; onConnect: OnConnect; - deleteNode: (nodeId: string) => void; - deleteEdge: (edgeId: string) => void; + deleteNode: (nodeId: string | Array) => void; + deleteEdge: (edgeId: string | Array) => void; isBuilt: boolean; paste: ( selection: { nodes: any; edges: any }, position: { x: number; y: number; paneX?: number; paneY?: number } ) => void; - lastCopiedSelection: { nodes: any; edges: any }; - nodeId: string; - incrementNodeId: () => void; isPending: boolean; + setPending: (pending: boolean) => void; }; // this is our useStore hook that we can use in our components to get parts of the store and call actions const useFlow = create((set, get) => ({ + reactFlowInstance: null, + setReactFlowInstance: (newState) => { + set({ reactFlowInstance: newState }); + }, nodes: [], edges: [], isBuilt: false, - copiedSelection: { nodes: [], edges: [] }, onNodesChange: (changes: NodeChange[]) => { set({ nodes: applyNodeChanges(changes, get().nodes), }); + if (!get().isPending) set({ isPending: true }); }, onEdgesChange: (changes: EdgeChange[]) => { set({ edges: applyEdgeChanges(changes, get().edges), }); + if (!get().isPending) set({ isPending: true }); + }, + setNodes: (change) => { + let newChange = typeof change === "function" ? change(get().nodes) : change; + let newEdges = cleanEdges(newChange, get().edges); + + set({ edges: newEdges }); + set({ nodes: newChange }); + }, + setEdges: (change) => { + let newChange = typeof change === "function" ? change(get().edges) : change; + + set({ edges: newChange }); }, onConnect: (connection: Connection) => { set({ - edges: addEdge(connection, get().edges), + edges: addEdge( + { + ...connection, + data: { + targetHandle: scapeJSONParse(connection.targetHandle!), + sourceHandle: scapeJSONParse(connection.sourceHandle!), + }, + style: { stroke: "#555" }, + className: + ((scapeJSONParse(connection.targetHandle!) as targetHandleType) + .type === "Text" + ? "stroke-foreground " + : "stroke-foreground ") + " stroke-connection", + animated: + (scapeJSONParse(connection.targetHandle!) as targetHandleType) + .type === "Text", + }, + get().edges + ), }); }, deleteNode: (nodeId) => { - set({ - nodes: get().nodes.filter((node) => node.id !== nodeId), - edges: get().edges.filter((edge) => edge.source !== nodeId), - }); + get().setNodes( + get().nodes.filter((node) => + typeof nodeId === "string" + ? node.id !== nodeId + : !nodeId.includes(node.id) + ) + ); }, deleteEdge: (edgeId) => { - set({ - edges: get().edges.filter((edge) => edge.id !== edgeId), - }); + get().setEdges( + get().edges.filter((edge) => + typeof edgeId === "string" + ? edge.id !== edgeId + : !edgeId.includes(edge.id) + ) + ); }, - paste: (selection, position) => {}, - lastCopiedSelection: { nodes: [], edges: [] }, - nodeId: uid(), - incrementNodeId: () => { - set((state) => ({ nodeId: uid() })); + paste: (selection, position) => { + let minimumX = Infinity; + let minimumY = Infinity; + let idsMap = {}; + let newNodes: Node[] = get().nodes; + let newEdges = get().edges; + selection.nodes.forEach((node: Node) => { + if (node.position.y < minimumY) { + minimumY = node.position.y; + } + if (node.position.x < minimumX) { + minimumX = node.position.x; + } + }); + + const insidePosition = position.paneX + ? { x: position.paneX + position.x, y: position.paneY! + position.y } + : get().reactFlowInstance!.screenToFlowPosition({ + x: position.x, + y: position.y, + }); + + selection.nodes.forEach((node: NodeType) => { + // Generate a unique node ID + let newId = getNodeId(node.data.type); + idsMap[node.id] = newId; + + // Create a new node object + const newNode: NodeType = { + id: newId, + type: "genericNode", + position: { + x: insidePosition.x + node.position!.x - minimumX, + y: insidePosition.y + node.position!.y - minimumY, + }, + data: { + ...cloneDeep(node.data), + id: newId, + }, + }; + + // Add the new node to the list of nodes in state + newNodes = newNodes + .map((node) => ({ ...node, selected: false })) + .concat({ ...newNode, selected: false }); + }); + set({ nodes: newNodes }); + + selection.edges.forEach((edge: Edge) => { + let source = idsMap[edge.source]; + let target = idsMap[edge.target]; + const sourceHandleObject: sourceHandleType = scapeJSONParse( + edge.sourceHandle! + ); + let sourceHandle = scapedJSONStringfy({ + ...sourceHandleObject, + id: source, + }); + sourceHandleObject.id = source; + + edge.data.sourceHandle = sourceHandleObject; + const targetHandleObject: targetHandleType = scapeJSONParse( + edge.targetHandle! + ); + let targetHandle = scapedJSONStringfy({ + ...targetHandleObject, + id: target, + }); + targetHandleObject.id = target; + edge.data.targetHandle = targetHandleObject; + let id = getHandleId(source, sourceHandle, target, targetHandle); + newEdges = addEdge( + { + source, + target, + sourceHandle, + targetHandle, + id, + data: cloneDeep(edge.data), + style: { stroke: "#555" }, + className: + targetHandleObject.type === "Text" + ? "stroke-gray-800 " + : "stroke-gray-900 ", + animated: targetHandleObject.type === "Text", + selected: false, + }, + newEdges.map((edge) => ({ ...edge, selected: false })) + ); + }); + set({ edges: newEdges }); }, isPending: false, + setPending: (pending: boolean) => { + set({ isPending: pending }); + }, })); export default useFlow; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 44a16cf4e..91fd39c83 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -27,6 +27,7 @@ import { updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; import { getFieldTitle, toTitleCase } from "./utils"; +const uid = new ShortUniqueId({ length: 5 }); export function cleanEdges(nodes: Node[], edges: Edge[]) { let newEdges = _.cloneDeep(edges); @@ -495,6 +496,19 @@ export function getMiddlePoint(nodes: Node[]) { return { x: averageX, y: averageY }; } +export function getNodeId(nodeType: string) { + return nodeType + "-" + uid(); +} + +export function getHandleId(source: string, sourceHandle: string, target: string, targetHandle: string){ + return "reactflow__edge-" + + source + + sourceHandle + + "-" + + target + + targetHandle; +} + export function generateFlow( selection: OnSelectionChangeParams, nodes: Node[],