From d44648d6c1404e57fbd4b15a6c0d41db8fe32704 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Sat, 6 Jan 2024 00:27:36 -0300 Subject: [PATCH] added saveFlow function to Zustand state and implemented it on the project --- src/frontend/src/App.tsx | 1 - .../components/parameterComponent/index.tsx | 1 - .../chatComponent/buildTrigger/index.tsx | 2 +- .../src/components/chatComponent/index.tsx | 1 - src/frontend/src/contexts/undoRedoContext.tsx | 1 - .../src/modals/flowSettingsModal/index.tsx | 2 +- src/frontend/src/modals/shareModal/index.tsx | 3 +- src/frontend/src/pages/AdminPage/index.tsx | 1 - .../components/PageComponent/index.tsx | 6 +- .../extraSidebarComponent/index.tsx | 6 +- .../components/nodeToolbarComponent/index.tsx | 4 +- .../MainPage/components/components/index.tsx | 4 +- src/frontend/src/stores/flowStore.ts | 17 +++ src/frontend/src/stores/flowsManagerStore.ts | 123 +++++++++++------- .../src/types/zustand/flowsManager/index.ts | 3 + src/frontend/src/utils/reactflowUtils.ts | 28 +++- 16 files changed, 141 insertions(+), 62 deletions(-) diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index ab4ea80ca..f4acd7cd7 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -16,7 +16,6 @@ import { FETCH_ERROR_MESSAGE, } from "./constants/constants"; import { AuthContext } from "./contexts/authContext"; -import { FlowsContext } from "./contexts/flowsContext"; import { locationContext } from "./contexts/locationContext"; import { getHealth, getVersion } from "./controllers/API"; import Router from "./routes"; diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index 83425e4ec..959865766 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -26,7 +26,6 @@ import { LANGFLOW_SUPPORTED_TYPES, TOOLTIP_EMPTY, } from "../../../../constants/constants"; -import { FlowsContext } from "../../../../contexts/flowsContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import { postCustomComponentUpdate } from "../../../../controllers/API"; import useAlertStore from "../../../../stores/alertStore"; diff --git a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx index 6cee73615..534036221 100644 --- a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx +++ b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx @@ -26,7 +26,6 @@ export default function BuildTrigger({ isBuilt: boolean; }): JSX.Element { const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE(); - const { saveFlow } = useContext(FlowsContext); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); const setErrorData = useAlertStore((state) => state.setErrorData); @@ -34,6 +33,7 @@ export default function BuildTrigger({ const setCurrentFlowState = useFlowsManagerStore( (state) => state.setCurrentFlowState ); + const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const [isIconTouched, setIsIconTouched] = useState(false); const eventClick = isBuilding ? "pointer-events-none" : ""; diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index 22362bca5..b975e2b9f 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -5,7 +5,6 @@ import BuildTrigger from "./buildTrigger"; import ChatTrigger from "./chatTrigger"; import * as _ from "lodash"; -import { FlowsContext } from "../../contexts/flowsContext"; import { getBuildStatus } from "../../controllers/API"; import FormModal from "../../modals/formModal"; import useFlowStore from "../../stores/flowStore"; diff --git a/src/frontend/src/contexts/undoRedoContext.tsx b/src/frontend/src/contexts/undoRedoContext.tsx index 679e6cf9d..095878b8c 100644 --- a/src/frontend/src/contexts/undoRedoContext.tsx +++ b/src/frontend/src/contexts/undoRedoContext.tsx @@ -13,7 +13,6 @@ import { undoRedoContextType, } from "../types/typesContext"; import { isWrappedWithClass } from "../utils/utils"; -import { FlowsContext } from "./flowsContext"; import useFlowsManagerStore from "../stores/flowsManagerStore"; const initialValue = { diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx index 2598c7c16..c892c173f 100644 --- a/src/frontend/src/modals/flowSettingsModal/index.tsx +++ b/src/frontend/src/modals/flowSettingsModal/index.tsx @@ -13,7 +13,7 @@ export default function FlowSettingsModal({ open, setOpen, }: FlowSettingsPropsType): JSX.Element { - const { saveFlow } = useContext(FlowsContext); + const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const flows = useFlowsManagerStore((state) => state.flows); useEffect(() => { diff --git a/src/frontend/src/modals/shareModal/index.tsx b/src/frontend/src/modals/shareModal/index.tsx index 3992fb89a..52833d5ce 100644 --- a/src/frontend/src/modals/shareModal/index.tsx +++ b/src/frontend/src/modals/shareModal/index.tsx @@ -24,6 +24,7 @@ import { getTagsIds } from "../../utils/storeUtils"; import ConfirmationModal from "../ConfirmationModal"; import BaseModal from "../baseModal"; import { useDarkStore } from "../../stores/darkStore"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; export default function ShareModal({ component, @@ -57,7 +58,7 @@ export default function ShareModal({ const [unavaliableNames, setUnavaliableNames] = useState< { id: string; name: string }[] >([]); - const { saveFlow } = useContext(FlowsContext); + const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const [loadingNames, setLoadingNames] = useState(false); diff --git a/src/frontend/src/pages/AdminPage/index.tsx b/src/frontend/src/pages/AdminPage/index.tsx index 7434c3369..71cca7bd7 100644 --- a/src/frontend/src/pages/AdminPage/index.tsx +++ b/src/frontend/src/pages/AdminPage/index.tsx @@ -21,7 +21,6 @@ import { ADMIN_HEADER_TITLE, } from "../../constants/constants"; import { AuthContext } from "../../contexts/authContext"; -import { FlowsContext } from "../../contexts/flowsContext"; import { addUser, deleteUser, diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index d5d9f0f26..38e8b3422 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -42,6 +42,7 @@ import ConnectionLineComponent from "../ConnectionLineComponent"; import SelectionMenu from "../SelectionMenuComponent"; import ExtraSidebar from "../extraSidebarComponent"; import { useTypesStore } from "../../../../stores/typesStore"; +import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; const nodeTypes = { genericNode: GenericNode, @@ -54,7 +55,8 @@ export default function Page({ flow: FlowType; view?: boolean; }): JSX.Element { - let { uploadFlow, saveFlow } = useContext(FlowsContext); + let { uploadFlow } = useContext(FlowsContext); + const autoSaveCurrentFlow = useFlowsManagerStore((state) => state.autoSaveCurrentFlow); const types = useTypesStore((state) => state.types); const templates = useTypesStore((state) => state.templates); const setFilterEdge = useTypesStore((state) => state.setFilterEdge); @@ -202,7 +204,7 @@ export default function Page({ const onNodeDragStop: NodeDragHandler = useCallback(() => { // 👇 make dragging a node undoable - saveFlow(undefined, true); + autoSaveCurrentFlow(nodes, edges, reactFlowInstance?.getViewport()!); // 👉 you can place your event handlers here }, [takeSnapshot]); diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 019956192..bc75c0e84 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -32,7 +32,9 @@ export default function ExtraSidebar(): JSX.Element { const templates = useTypesStore((state) => state.templates); const getFilterEdge = useTypesStore((state) => state.getFilterEdge); const setFilterEdge = useTypesStore((state) => state.setFilterEdge); - const { uploadFlow, saveFlow } = useContext(FlowsContext); + const { uploadFlow } = useContext(FlowsContext); + const saveFlow = useFlowsManagerStore((state) => state.saveFlow); + const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const hasStore = useStoreStore((state) => state.hasStore); const hasApiKey = useStoreStore((state) => state.hasApiKey); @@ -302,7 +304,7 @@ export default function ExtraSidebar(): JSX.Element { (isPending ? "" : "button-disable") } onClick={(event) => { - saveFlow(); + saveFlow({...currentFlow, data: {...currentFlow.data!, viewport: reactFlowInstance?.getViewport()!} }, true); }} > state.setNodes); const setEdges = useFlowStore((state) => state.setEdges); - const { saveComponent, flows } = useContext(FlowsContext); + const { saveComponent } = useContext(FlowsContext); + const flows = useFlowsManagerStore((state) => state.flows); const version = useDarkStore((state) => state.version); const { takeSnapshot } = useContext(undoRedoContext); const [showModalAdvanced, setShowModalAdvanced] = useState(false); diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 5f65609be..c44301714 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -9,14 +9,16 @@ import { Button } from "../../../../components/ui/button"; import { FlowsContext } from "../../../../contexts/flowsContext"; import useAlertStore from "../../../../stores/alertStore"; import { FlowType } from "../../../../types/flow"; +import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; export default function ComponentsComponent({ is_component = true, }: { is_component?: boolean; }) { - const { flows, removeFlow, uploadFlow, addFlow, isLoading } = + const { removeFlow, uploadFlow, addFlow, isLoading } = useContext(FlowsContext); + const flows = useFlowsManagerStore((state) => state.flows); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const [pageSize, setPageSize] = useState(10); diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 16d7985e4..8de906c90 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -24,6 +24,7 @@ import { scapeJSONParse, scapedJSONStringfy, } from "../utils/reactflowUtils"; +import useFlowsManagerStore from "./flowsManagerStore"; // this is our useStore hook that we can use in our components to get parts of the store and call actions const useFlowStore = create((set, get) => ({ @@ -55,11 +56,27 @@ const useFlowStore = create((set, get) => ({ set({ edges: newEdges }); set({ nodes: newChange }); + + useFlowsManagerStore + .getState() + .autoSaveCurrentFlow( + newChange, + newEdges, + get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 } + ); }, setEdges: (change) => { let newChange = typeof change === "function" ? change(get().edges) : change; set({ edges: newChange }); + + useFlowsManagerStore + .getState() + .autoSaveCurrentFlow( + get().nodes, + newChange, + get().reactFlowInstance?.getViewport() ?? { x: 0, y: 0, zoom: 1 } + ); }, setNode: (id: string, change: Node | ((oldState: Node) => Node)) => { let newChange = diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 4f7cf8424..817c49a40 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -1,40 +1,19 @@ -import { cloneDeep } from "lodash"; -import ShortUniqueId from "short-unique-id"; +import { AxiosError } from "axios"; +import { Edge, Node, Viewport } from "reactflow"; import { create } from "zustand"; -import { readFlowsFromDatabase } from "../controllers/API"; -import { APIClassType } from "../types/api"; -import { FlowType, NodeDataType } from "../types/flow"; +import { + readFlowsFromDatabase, + updateFlowInDatabase, +} from "../controllers/API"; +import { FlowType } from "../types/flow"; import { FlowState } from "../types/tabs"; import { FlowsManagerStoreType } from "../types/zustand/flowsManager"; -import { processDataFromFlow } from "../utils/reactflowUtils"; -import { createRandomKey } from "../utils/utils"; -import { useTypesStore } from "./typesStore"; +import { processFlows } from "../utils/reactflowUtils"; import useAlertStore from "./alertStore"; +import useFlowStore from "./flowStore"; +import { useTypesStore } from "./typesStore"; -const uid = new ShortUniqueId({ length: 5 }); - -const processFlows = (DbData: FlowType[], skipUpdate = true) => { - let savedComponents: { [key: string]: APIClassType } = {}; - DbData.forEach((flow: FlowType) => { - try { - if (!flow.data) { - return; - } - if (flow.data && flow.is_component) { - (flow.data.nodes[0].data as NodeDataType).node!.display_name = - flow.name; - savedComponents[ - createRandomKey((flow.data.nodes[0].data as NodeDataType).type, uid()) - ] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!); - return; - } - if (!skipUpdate) processDataFromFlow(flow, false); - } catch (e) { - console.log(e); - } - }); - return { data: savedComponents, flows: DbData }; -}; +let saveTimeoutId: NodeJS.Timeout | null = null; const useFlowsManagerStore = create((set, get) => ({ currentFlowId: "", @@ -69,21 +48,73 @@ const useFlowsManagerStore = create((set, get) => ({ refreshFlows: () => { return new Promise((resolve, reject) => { set({ isLoading: true }); - readFlowsFromDatabase().then((dbData) => { - if (dbData) { - const { data, flows } = processFlows(dbData, false); - set({ flows, isLoading: false }); - useTypesStore.setState((state) => ({ - data: { ...state.data, ["saved_components"]: data }, - })); - resolve(); - } - }).catch((e) => { - useAlertStore.getState().setErrorData({ - title: "Could not load flows from database", + readFlowsFromDatabase() + .then((dbData) => { + if (dbData) { + const { data, flows } = processFlows(dbData, false); + set({ flows, isLoading: false }); + useTypesStore.setState((state) => ({ + data: { ...state.data, ["saved_components"]: data }, + })); + resolve(); + } + }) + .catch((e) => { + useAlertStore.getState().setErrorData({ + title: "Could not load flows from database", + }); + reject(e); + }); + }); + }, + autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => { + // Clear the previous timeout if it exists. + if (saveTimeoutId) { + clearTimeout(saveTimeoutId); + } + + // Set up a new timeout. + saveTimeoutId = setTimeout(() => { + if (get().currentFlow) { + get().saveFlow( + { ...get().currentFlow!, data: { nodes, edges, viewport } }, + true + ); + } + }, 300); // Delay of 300ms. + }, + saveFlow: (flow: FlowType, silent?: boolean) => { + return new Promise((resolve, reject) => { + updateFlowInDatabase(flow) + .then((updatedFlow) => { + if (updatedFlow) { + // updates flow in state + if (!silent) { + useAlertStore + .getState() + .setSuccessData({ title: "Changes saved successfully" }); + } + set((oldState) => ({ + flows: oldState.flows.map((flow) => { + if (flow.id === updatedFlow.id) { + return updatedFlow; + } + return flow; + }), + })); + //update tabs state + + useFlowStore.setState({ isPending: false }); + resolve(); + } + }) + .catch((err) => { + useAlertStore.getState().setErrorData({ + title: "Error while saving changes", + list: [(err as AxiosError).message], + }); + reject(err); }); - reject(e); - }); }); }, })); diff --git a/src/frontend/src/types/zustand/flowsManager/index.ts b/src/frontend/src/types/zustand/flowsManager/index.ts index 5a4532403..dad1356ab 100644 --- a/src/frontend/src/types/zustand/flowsManager/index.ts +++ b/src/frontend/src/types/zustand/flowsManager/index.ts @@ -1,3 +1,4 @@ +import { Node, Edge, Viewport } from "reactflow"; import { FlowType } from "../../flow"; import { FlowState, FlowsState } from "../../tabs"; @@ -12,4 +13,6 @@ export type FlowsManagerStoreType = { currentFlowState: FlowState | undefined; setCurrentFlowState: (state: FlowState | ((oldState: FlowState | undefined) => FlowState)) => void; refreshFlows: () => Promise; + saveFlow: (flow: FlowType, silent?: boolean) => Promise; + autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => void; }; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 6686614e2..039be107b 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -1,4 +1,4 @@ -import _ from "lodash"; +import _, { cloneDeep } from "lodash"; import { Connection, Edge, @@ -13,6 +13,7 @@ import { specialCharsRegex, } from "../constants/constants"; import { + APIClassType, APIKindType, APIObjectType, APITemplateType, @@ -31,7 +32,7 @@ import { unselectAllNodesType, updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; -import { getFieldTitle, toTitleCase } from "./utils"; +import { createRandomKey, getFieldTitle, toTitleCase } from "./utils"; const uid = new ShortUniqueId({ length: 5 }); export function cleanEdges(nodes: Node[], edges: Edge[]) { @@ -153,6 +154,29 @@ export function updateTemplate( return clonedObject; } +export const processFlows = (DbData: FlowType[], skipUpdate = true) => { + let savedComponents: { [key: string]: APIClassType } = {}; + DbData.forEach((flow: FlowType) => { + try { + if (!flow.data) { + return; + } + if (flow.data && flow.is_component) { + (flow.data.nodes[0].data as NodeDataType).node!.display_name = + flow.name; + savedComponents[ + createRandomKey((flow.data.nodes[0].data as NodeDataType).type, uid()) + ] = cloneDeep((flow.data.nodes[0].data as NodeDataType).node!); + return; + } + if (!skipUpdate) processDataFromFlow(flow, false); + } catch (e) { + console.log(e); + } + }); + return { data: savedComponents, flows: DbData }; +}; + export const processDataFromFlow = (flow: FlowType, refreshIds = true) => { let data = flow?.data ? flow.data : null; if (data) {