diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 54458cc01..ba211897e 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -73,10 +73,8 @@ export default function ParameterComponent({ const { tabId, flows, - updateFlow, nodes, edges, - setEdges, setNode, } = useContext(FlowsContext); @@ -154,7 +152,7 @@ export default function ParameterComponent({ newData.node!.template[name].value = newValue; return newData; }); - + renderTooltips(); }; diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 3e7554023..e4ff8a692 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -30,11 +30,9 @@ export default function GenericNode({ xPos: number; yPos: number; }): JSX.Element { - const { updateFlow, flows, tabId, saveCurrentFlow } = - useContext(FlowsContext); const updateNodeInternals = useUpdateNodeInternals(); - const { types, deleteNode } = - useContext(typesContext); + const { types } = useContext(typesContext); + const { deleteNode } = useContext(FlowsContext); const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const [inputName, setInputName] = useState(false); const [nodeName, setNodeName] = useState(data.node!.display_name); @@ -46,7 +44,6 @@ export default function GenericNode({ useState(null); const [handles, setHandles] = useState([]); let numberOfInputs: boolean[] = []; - const { modalContextOpen } = useContext(alertContext); const { takeSnapshot } = useContext(undoRedoContext); @@ -118,7 +115,6 @@ export default function GenericNode({ deleteNode={(id) => { takeSnapshot(); deleteNode(id); - saveCurrentFlow(); }} setShowNode={(show: boolean) => { data.showNode = show; diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index 97497cb5d..129e1f55c 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow"; export default function Chat({ flow }: ChatType): JSX.Element { const [open, setOpen] = useState(false); const [canOpen, setCanOpen] = useState(false); - const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext); + const { tabsState, isBuilt, setIsBuilt, isPending } = useContext(FlowsContext); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -51,9 +51,7 @@ export default function Chat({ flow }: ChatType): JSX.Element { _.cloneDeep(node.data.node?.template) ); if ( - tabsState && - tabsState[flow.id] && - tabsState[flow.id].isPending && + isPending && JSON.stringify(prevNodes) !== JSON.stringify(currentNodes) ) { setIsBuilt(false); diff --git a/src/frontend/src/contexts/flowsContext.tsx b/src/frontend/src/contexts/flowsContext.tsx index 955f32fb3..3902b382b 100644 --- a/src/frontend/src/contexts/flowsContext.tsx +++ b/src/frontend/src/contexts/flowsContext.tsx @@ -15,6 +15,7 @@ import { Node, NodeChange, ReactFlowJsonObject, + Viewport, XYPosition, addEdge, useEdgesState, @@ -74,7 +75,8 @@ const FlowsContextInitialValue: FlowsContextType = { flowData?: FlowType, override?: boolean ) => "", - updateFlow: (newFlow: FlowType) => {}, + deleteNode: () => {}, + deleteEdge: () => {}, incrementNodeId: () => uid(), downloadFlow: (flow: FlowType) => {}, downloadFlows: () => {}, @@ -83,14 +85,13 @@ const FlowsContextInitialValue: FlowsContextType = { isBuilt: false, setIsBuilt: (state: boolean) => {}, hardReset: () => {}, - saveFlow: async (flow: FlowType, silent?: boolean) => {}, + saveFlow: async (flow?: FlowType, silent?: boolean) => {}, lastCopiedSelection: null, setLastCopiedSelection: (selection: any) => {}, isPending: false, setPending: (pending: boolean) => {}, tabsState: {}, setTabsState: (state: FlowsState) => {}, - saveCurrentFlow: () => {}, getNodeId: (nodeType: string) => "", setTweak: (tweak: any) => {}, getTweak: [], @@ -135,7 +136,7 @@ export function FlowsProvider({ children }: { children: ReactNode }) { const [edges, setEdgesInternal, onEdgesChangeInternal] = useEdgesState([]); - const setPending = useCallback( + const setPending = (pending: boolean) => { //@ts-ignore setTabsState((prev: FlowsState) => { @@ -147,17 +148,13 @@ export function FlowsProvider({ children }: { children: ReactNode }) { }, }; }); - }, - [setTabsState] - ); + } const isPending = tabsState[tabId]?.isPending ?? false; const onNodesChange = useCallback( - (nodes: NodeChange[]) => { - onNodesChangeInternal(nodes); - console.log("nodesChangou") - + (change: NodeChange[]) => { + onNodesChangeInternal(change); setPending(true); }, [onNodesChangeInternal, setTabsState, tabId] @@ -166,7 +163,6 @@ export function FlowsProvider({ children }: { children: ReactNode }) { const onEdgesChange = useCallback( (edges: EdgeChange[]) => { onEdgesChangeInternal(edges); - console.log("edgesChangou") setPending(true); }, [onEdgesChangeInternal, setTabsState, tabId] @@ -174,14 +170,18 @@ export function FlowsProvider({ children }: { children: ReactNode }) { const setNodes = (change: Node[] | ((oldState: Node[]) => Node[])) => { let newChange = typeof change === "function" ? change(nodes) : change; + let newEdges = cleanEdges(newChange, edges); - - setEdgesInternal(cleanEdges(newChange, edges)); + saveCurrentFlow(newChange, newEdges, reactFlowInstance?.getViewport() ?? { zoom: 1, x: 0, y: 0 }); + setEdgesInternal(newEdges); setNodesInternal(newChange); }; const setNode = (id: string, change: Node | ((oldState: Node) => Node)) => { - let newChange = typeof change === "function" ? change(nodes.find((node) => node.id === id)!) : change; + let newChange = + typeof change === "function" + ? change(nodes.find((node) => node.id === id)!) + : change; setNodes((oldNodes) => oldNodes.map((node) => { @@ -197,8 +197,11 @@ export function FlowsProvider({ children }: { children: ReactNode }) { return nodes.find((node) => node.id === id); }; - const setEdges = (edges: Edge[] | ((oldState: Edge[]) => Edge[])) => { - setEdgesInternal(edges); + const setEdges = (change: Edge[] | ((oldState: Edge[]) => Edge[])) => { + let newChange = typeof change === "function" ? change(edges) : change; + + saveCurrentFlow(nodes, newChange, reactFlowInstance?.getViewport() ?? { zoom: 1, x: 0, y: 0 }); + setEdgesInternal(newChange); }; useEffect(() => { @@ -724,14 +727,22 @@ export function FlowsProvider({ children }: { children: ReactNode }) { }); } - function saveCurrentFlow() { + function saveCurrentFlow(nodes: Node[], edges: Edge[], viewport: Viewport) { const currentFlow = flows.find((flow) => flow.id === tabId); - if (currentFlow && reactFlowInstance && currentFlow.data) { - updateFlow({ ...currentFlow, data: reactFlowInstance?.toObject()! }); + if (currentFlow) { + saveFlow({ ...currentFlow, data: { nodes, edges, viewport } }, true); } } - async function saveFlow(newFlow: FlowType, silent?: boolean) { + async function saveFlow(flow?: FlowType, silent?: boolean) { + let newFlow; + if (!flow) { + const currentFlow = flows.find((flow) => flow.id === tabId)!; + newFlow = { ...currentFlow, data: { nodes, edges, viewport: reactFlowInstance?.getViewport() ?? { zoom: 1, x: 0, y: 0 } } } + } else { + newFlow = flow; + } + try { // updates flow in db const updatedFlow = await updateFlowInDatabase(newFlow); @@ -740,26 +751,9 @@ export function FlowsProvider({ children }: { children: ReactNode }) { if (!silent) { setSuccessData({ title: "Changes saved successfully" }); } - setFlows((prevState) => { - const newFlows = [...prevState]; - const index = newFlows.findIndex((flow) => flow.id === newFlow.id); - if (index !== -1) { - newFlows[index].description = newFlow.description ?? ""; - newFlows[index].data = newFlow.data; - newFlows[index].name = newFlow.name; - } - return newFlows; - }); + updateFlow(newFlow); //update tabs state - setTabsState((prev) => { - return { - ...prev, - [tabId]: { - ...prev[tabId], - isPending: false, - }, - }; - }); + setPending(false); } } catch (err) { setErrorData({ @@ -794,6 +788,31 @@ export function FlowsProvider({ children }: { children: ReactNode }) { }); }, []); + function deleteNode(idx: string | Array) { + + setEdges((oldEdges) => + oldEdges.filter((edge) => + typeof idx === "string" + ? edge.source !== idx && edge.target !== idx + : !idx.includes(edge.source) && !idx.includes(edge.target) + ) + ); + + setNodes((oldNodes) => + oldNodes.filter((node) => + typeof idx === "string" ? node.id !== idx : !idx.includes(node.id) + ) + ); + + } + function deleteEdge(idx: string | Array) { + setEdges((oldEdges) => + oldEdges.filter((edge) => + typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id) + ) + ); + } + return ( {}, - deleteNode: () => {}, types: {}, setTypes: () => {}, templates: {}, @@ -29,7 +28,6 @@ const initialValue: typesContextType = { fetchError: false, setFilterEdge: (filter) => {}, getFilterEdge: [], - deleteEdge: () => {}, }; export const typesContext = createContext(initialValue); @@ -97,37 +95,14 @@ export function TypesProvider({ children }: { children: ReactNode }) { } } - function deleteNode(idx: string | Array) { - if (reactFlowInstance === null) return; - const edges = reactFlowInstance! - .getEdges() - .filter((edge) => - typeof idx === "string" - ? edge.source == idx || edge.target == idx - : idx.includes(edge.source) || idx.includes(edge.target) - ); - reactFlowInstance!.deleteElements({ - nodes: - typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })), - edges, - }); - } - function deleteEdge(idx: string | Array) { - reactFlowInstance!.deleteElements({ - edges: - typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })), - }); - } return ( { data.node = myData.node; //@ts-ignore - setTabsState((prev: FlowsState) => { - return { - ...prev, - [tabId]: { - ...prev[tabId], - isPending: true, - }, - }; - }); + setPending(true); setOpen(false); }} type="submit" diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index d1a5a2c2d..4b5df623f 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -12,15 +12,11 @@ import ReactFlow, { Connection, Controls, Edge, - EdgeChange, - NodeChange, NodeDragHandler, OnSelectionChangeParams, SelectionDragHandler, addEdge, updateEdge, - useEdgesState, - useNodesState, useReactFlow, } from "reactflow"; import GenericNode from "../../../../CustomNodes/GenericNode"; @@ -58,17 +54,13 @@ export default function Page({ view?: boolean; }): JSX.Element { let { - updateFlow, uploadFlow, getNodeId, paste, lastCopiedSelection, setLastCopiedSelection, - tabsState, - saveFlow, - setTabsState, - tabId, - saveCurrentFlow, + deleteNode, + deleteEdge, } = useContext(FlowsContext); const { types, @@ -76,24 +68,16 @@ export default function Page({ setReactFlowInstance, templates, setFilterEdge, - deleteNode, - deleteEdge, } = useContext(typesContext); const reactFlowWrapper = useRef(null); const { takeSnapshot } = useContext(undoRedoContext); - const { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange } = useContext(FlowsContext); + const { nodes, edges, setNodes, setEdges, onNodesChange, onEdgesChange, setPending, saveFlow } = useContext(FlowsContext); const position = useRef({ x: 0, y: 0 }); const [lastSelection, setLastSelection] = useState(null); - const saveCurrentFlowTimeout = () => { - setTimeout(() => { - saveCurrentFlow(); - }, 500); // need to do this because ReactFlow is not asynchronous. - }; - useEffect(() => { const onKeyDown = (event: KeyboardEvent) => { if ( @@ -137,7 +121,6 @@ export default function Page({ takeSnapshot(); deleteNode(lastSelection.nodes.map((node) => node.id)); deleteEdge(lastSelection.edges.map((edge) => edge.id)); - saveCurrentFlowTimeout(); } } }; @@ -157,7 +140,6 @@ export default function Page({ lastCopiedSelection, lastSelection, takeSnapshot, - saveCurrentFlowTimeout, ]); const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); @@ -165,7 +147,6 @@ export default function Page({ const { setExtraComponent, setExtraNavigation } = useContext(locationContext); const { setErrorData } = useContext(alertContext); - const { setViewport } = useReactFlow(); const edgeUpdateSuccessful = useRef(true); const [loading, setLoading] = useState(true); @@ -174,9 +155,11 @@ export default function Page({ useEffect(() => { setLoading(true); - setNodes(flow?.data?.nodes ?? []); - setEdges(flow?.data?.edges ?? []); - setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 }); + if(reactFlowInstance){ + reactFlowInstance.setNodes(flow?.data?.nodes ?? []); + reactFlowInstance.setEdges(flow?.data?.edges ?? []); + reactFlowInstance.setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 }); + } // Clear the previous timeout if (timeoutRef.current) { @@ -194,16 +177,6 @@ export default function Page({ }; }, [flow, reactFlowInstance]); - useEffect(() => { - const interval = setInterval(() => { - saveFlow(flow, true); - }, 30000); - - return () => { - clearInterval(interval); - }; - }, [flow, flow.data]); - const onConnect = useCallback( (params: Connection) => { takeSnapshot(); @@ -228,17 +201,6 @@ export default function Page({ eds ) ); - //@ts-ignore - setTabsState((prev: FlowsState) => { - return { - ...prev, - [tabId]: { - ...prev[tabId], - isPending: true, - }, - }; - }); - saveCurrentFlowTimeout(); }, [setEdges, takeSnapshot, addEdge] ); @@ -249,6 +211,12 @@ export default function Page({ // 👉 you can place your event handlers here }, [takeSnapshot]); + const onNodeDragStop: NodeDragHandler = useCallback(() => { + // 👇 make dragging a node undoable + saveFlow(); + // 👉 you can place your event handlers here + }, [takeSnapshot]); + const onSelectionDragStart: SelectionDragHandler = useCallback(() => { // 👇 make dragging a selection undoable takeSnapshot(); @@ -342,18 +310,12 @@ export default function Page({ } }, // Specify dependencies for useCallback - [getNodeId, reactFlowInstance, setNodes, takeSnapshot] + [getNodeId, setNodes, takeSnapshot] ); useEffect(() => { setExtraComponent(); setExtraNavigation({ title: "Components" }); - - return () => { - if (tabsState && tabsState[flow.id]?.isPending) { - saveFlow(flow); - } - }; }, []); const onEdgeUpdateStart = useCallback(() => { @@ -367,7 +329,7 @@ export default function Page({ setEdges((els) => updateEdge(oldEdge, newConnection, els)); } }, - [reactFlowInstance, setEdges] + [setEdges] ); const onEdgeUpdateEnd = useCallback((_, edge: Edge): void => { @@ -408,18 +370,9 @@ export default function Page({ }, []); const onMove = useCallback(() => { - saveCurrentFlowTimeout(); //@ts-ignore - setTabsState((prev: FlowsState) => { - return { - ...prev, - [tabId]: { - ...prev[tabId], - isPending: true, - }, - }; - }); - }, [setTabsState, saveCurrentFlowTimeout]); + setPending(true); + }, [setPending]); return (
@@ -454,6 +407,7 @@ export default function Page({ onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateEnd={onEdgeUpdateEnd} onNodeDragStart={onNodeDragStart} + onNodeDragStop={onNodeDragStop} onSelectionDragStart={onSelectionDragStart} onSelectionEnd={onSelectionEnd} onSelectionStart={onSelectionStart} @@ -488,7 +442,8 @@ export default function Page({ ) { const { newFlow } = generateFlow( lastSelection!, - reactFlowInstance!, + nodes, + edges, getRandomName() ); const newGroupNode = generateNodeFromFlow( diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 84e34a07b..909a0dcd7 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -28,13 +28,12 @@ import SidebarDraggableComponent from "./sideBarDraggableComponent"; export default function ExtraSidebar(): JSX.Element { const { data, templates, getFilterEdge, setFilterEdge } = useContext(typesContext); - const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } = + const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, isPending } = useContext(FlowsContext); const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext); const { setErrorData } = useContext(alertContext); const [dataFilter, setFilterData] = useState(data); const [search, setSearch] = useState(""); - const isPending = tabsState[tabId]?.isPending; function onDragStart( event: React.DragEvent, data: { type: string; node?: APIClassType } @@ -297,7 +296,7 @@ export default function ExtraSidebar(): JSX.Element { : "button-disable") } onClick={(event) => { - saveFlow(flow!); + saveFlow(); }} > = (changes: ChangesType[]) => void; export type FlowsContextType = { - saveFlow: (flow: FlowType, silent?: boolean) => Promise; + saveFlow: (flow?: FlowType, silent?: boolean) => Promise; tabId: string; isLoading: boolean; setTabId: (index: string) => void; flows: Array; + deleteNode: (idx: string | Array) => void; + deleteEdge: (idx: string | Array) => void; removeFlow: (id: string) => void; addFlow: ( newProject: boolean, @@ -18,7 +20,6 @@ export type FlowsContextType = { override?: boolean, position?: XYPosition ) => Promise; - updateFlow: (newFlow: FlowType) => void; incrementNodeId: () => string; downloadFlow: ( flow: FlowType, @@ -29,7 +30,6 @@ export type FlowsContextType = { uploadFlows: () => void; isBuilt: boolean; setIsBuilt: (state: boolean) => void; - saveCurrentFlow: () => void; uploadFlow: ({ newProject, file, diff --git a/src/frontend/src/types/typesContext/index.ts b/src/frontend/src/types/typesContext/index.ts index 6b0fbbe2c..cfe206694 100644 --- a/src/frontend/src/types/typesContext/index.ts +++ b/src/frontend/src/types/typesContext/index.ts @@ -9,7 +9,6 @@ const data: { [char: string]: string } = {}; export type typesContextType = { reactFlowInstance: ReactFlowInstance | null; setReactFlowInstance: (newState: ReactFlowInstance) => void; - deleteNode: (idx: string | Array) => void; types: typeof types; setTypes: (newState: {}) => void; templates: typeof template; @@ -20,7 +19,6 @@ export type typesContextType = { setFetchError: (newState: boolean) => void; setFilterEdge: (newState) => void; getFilterEdge: any[]; - deleteEdge: (idx: string | Array) => void; }; export type alertContextType = { diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 4f6fb7bc6..9a05f8d2e 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -4,7 +4,6 @@ import { Edge, Node, OnSelectionChangeParams, - ReactFlowInstance, ReactFlowJsonObject, XYPosition, } from "reactflow"; @@ -502,10 +501,11 @@ export function getMiddlePoint(nodes: Node[]) { export function generateFlow( selection: OnSelectionChangeParams, - reactFlowInstance: ReactFlowInstance, + nodes: Node[], + edges: Edge[], name: string ): generateFlowType { - const newFlowData = reactFlowInstance.toObject(); + const newFlowData = {nodes, edges, viewport: { zoom: 1, x: 0, y: 0 }}; const uid = new ShortUniqueId({ length: 5 }); /* remove edges that are not connected to selected nodes on both ends in future we can save this edges to when ungrouping reconect to the old nodes