diff --git a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx index 534036221..1dcc327ee 100644 --- a/src/frontend/src/components/chatComponent/buildTrigger/index.tsx +++ b/src/frontend/src/components/chatComponent/buildTrigger/index.tsx @@ -5,7 +5,6 @@ import { useSSE } from "../../../contexts/SSEContext"; import { postBuildInit } from "../../../controllers/API"; import { FlowType } from "../../../types/flow"; -import { FlowsContext } from "../../../contexts/flowsContext"; import useAlertStore from "../../../stores/alertStore"; import useFlowStore from "../../../stores/flowStore"; import useFlowsManagerStore from "../../../stores/flowsManagerStore"; diff --git a/src/frontend/src/components/headerComponent/index.tsx b/src/frontend/src/components/headerComponent/index.tsx index a69ca1946..6654760b9 100644 --- a/src/frontend/src/components/headerComponent/index.tsx +++ b/src/frontend/src/components/headerComponent/index.tsx @@ -5,7 +5,6 @@ import AlertDropdown from "../../alerts/alertDropDown"; import { USER_PROJECTS_HEADER } from "../../constants/constants"; import { AuthContext } from "../../contexts/authContext"; -import { FlowsContext } from "../../contexts/flowsContext"; import useAlertStore from "../../stores/alertStore"; import { useDarkStore } from "../../stores/darkStore"; import { useStoreStore } from "../../stores/storeStore"; @@ -22,7 +21,6 @@ import { } from "../ui/dropdown-menu"; import { Separator } from "../ui/separator"; import MenuBar from "./components/menuBar"; -import useFlowsManagerStore from "../../stores/flowsManagerStore"; export default function Header(): JSX.Element { const notificationCenter = useAlertStore((state) => state.notificationCenter); diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index f9d8a79ef..a365b188c 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -1,5 +1,4 @@ import { useContext, useEffect, useState } from "react"; -import { FlowsContext } from "../../contexts/flowsContext"; import { uploadFile } from "../../controllers/API"; import useAlertStore from "../../stores/alertStore"; import { FileComponentType } from "../../types/components"; diff --git a/src/frontend/src/modals/exportModal/index.tsx b/src/frontend/src/modals/exportModal/index.tsx index fa7a688dd..358f5225c 100644 --- a/src/frontend/src/modals/exportModal/index.tsx +++ b/src/frontend/src/modals/exportModal/index.tsx @@ -4,16 +4,14 @@ import IconComponent from "../../components/genericIconComponent"; import { Button } from "../../components/ui/button"; import { Checkbox } from "../../components/ui/checkbox"; import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants"; -import { FlowsContext } from "../../contexts/flowsContext"; import useAlertStore from "../../stores/alertStore"; -import { removeApiKeys } from "../../utils/reactflowUtils"; +import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils"; import BaseModal from "../baseModal"; import { useDarkStore } from "../../stores/darkStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; const ExportModal = forwardRef( (props: { children: ReactNode }, ref): JSX.Element => { - const { downloadFlow } = useContext(FlowsContext); const version = useDarkStore((state) => state.version); const setNoticeData = useAlertStore((state) => state.setNoticeData); const [checked, setChecked] = useState(true); diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx index c892c173f..09c1f1ed5 100644 --- a/src/frontend/src/modals/flowSettingsModal/index.tsx +++ b/src/frontend/src/modals/flowSettingsModal/index.tsx @@ -3,7 +3,6 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent"; import IconComponent from "../../components/genericIconComponent"; import { Button } from "../../components/ui/button"; import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants"; -import { FlowsContext } from "../../contexts/flowsContext"; import { FlowSettingsPropsType } from "../../types/components"; import { FlowType } from "../../types/flow"; import BaseModal from "../baseModal"; diff --git a/src/frontend/src/modals/shareModal/index.tsx b/src/frontend/src/modals/shareModal/index.tsx index 52833d5ce..8a8f864af 100644 --- a/src/frontend/src/modals/shareModal/index.tsx +++ b/src/frontend/src/modals/shareModal/index.tsx @@ -5,7 +5,6 @@ import IconComponent from "../../components/genericIconComponent"; import { TagsSelector } from "../../components/tagsSelectorComponent"; import { Button } from "../../components/ui/button"; import { Checkbox } from "../../components/ui/checkbox"; -import { FlowsContext } from "../../contexts/flowsContext"; import { getStoreComponents, getStoreTags, diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 38e8b3422..093e6d43b 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -17,8 +17,6 @@ import ReactFlow, { SelectionDragHandler, addEdge, updateEdge, - useEdgesState, - useNodesState, } from "reactflow"; import GenericNode from "../../../../CustomNodes/GenericNode"; import Chat from "../../../../components/chatComponent"; @@ -28,6 +26,8 @@ import { locationContext } from "../../../../contexts/locationContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; +import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; +import { useTypesStore } from "../../../../stores/typesStore"; import { APIClassType } from "../../../../types/api"; import { FlowType, NodeType } from "../../../../types/flow"; import { @@ -41,8 +41,6 @@ import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils"; 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, @@ -56,7 +54,9 @@ export default function Page({ view?: boolean; }): JSX.Element { let { uploadFlow } = useContext(FlowsContext); - const autoSaveCurrentFlow = useFlowsManagerStore((state) => state.autoSaveCurrentFlow); + const autoSaveCurrentFlow = useFlowsManagerStore( + (state) => state.autoSaveCurrentFlow + ); const types = useTypesStore((state) => state.types); const templates = useTypesStore((state) => state.templates); const setFilterEdge = useTypesStore((state) => state.setFilterEdge); @@ -160,13 +160,17 @@ export default function Page({ const [loading, setLoading] = useState(true); + const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const timeoutRef = useRef(); useEffect(() => { setLoading(true); if (reactFlowInstance) { - reactFlowInstance.setNodes(flow?.data?.nodes ?? []); - reactFlowInstance.setEdges(flow?.data?.edges ?? []); + useFlowStore.setState({ + nodes: flow?.data?.nodes ?? [], + edges: flow?.data?.edges ?? [], + }); reactFlowInstance.setViewport( flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 } ); @@ -186,7 +190,7 @@ export default function Page({ return () => { clearTimeout(timeoutRef.current); }; - }, [flow, reactFlowInstance]); + }, [currentFlowId, reactFlowInstance]); const onConnectMod = useCallback( (params: Connection) => { diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index bc75c0e84..dafd15682 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -35,6 +35,8 @@ export default function ExtraSidebar(): JSX.Element { const { uploadFlow } = useContext(FlowsContext); const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); + const nodes = useFlowStore((state) => state.nodes); + const edges = useFlowStore((state) => state.edges); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const hasStore = useStoreStore((state) => state.hasStore); const hasApiKey = useStoreStore((state) => state.hasApiKey); @@ -304,7 +306,7 @@ export default function ExtraSidebar(): JSX.Element { (isPending ? "" : "button-disable") } onClick={(event) => { - saveFlow({...currentFlow, data: {...currentFlow.data!, viewport: reactFlowInstance?.getViewport()!} }, true); + saveFlow({...currentFlow, data: {nodes, edges, viewport: reactFlowInstance?.getViewport()!} }, true); }} > state.isLoading); const flows = useFlowsManagerStore((state) => state.flows); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); @@ -51,7 +52,7 @@ export default function ComponentsComponent({ const start = (pageIndex - 1) * pageSize; const end = start + pageSize; setData(all.slice(start, end)); - }, [flows, pageIndex, pageSize]); + }, [flows, isLoading, pageIndex, pageSize]); const [data, setData] = useState([]); diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 7531ad4fb..17ef52c59 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -10,12 +10,16 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants"; import { FlowsContext } from "../../contexts/flowsContext"; import useAlertStore from "../../stores/alertStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; +import { downloadFlows } from "../../utils/reactflowUtils"; export default function HomePage(): JSX.Element { - const { downloadFlows, uploadFlows, addFlow, uploadFlow } = + const { addFlow, uploadFlow } = useContext(FlowsContext); const setCurrentFlowId = useFlowsManagerStore( (state) => state.setCurrentFlowId ); + const uploadFlows = useFlowsManagerStore( + (state) => state.uploadFlows + ); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const location = useLocation(); diff --git a/src/frontend/src/pages/ProfileSettingsPage/index.tsx b/src/frontend/src/pages/ProfileSettingsPage/index.tsx index 8c2d5136b..ca9b30d34 100644 --- a/src/frontend/src/pages/ProfileSettingsPage/index.tsx +++ b/src/frontend/src/pages/ProfileSettingsPage/index.tsx @@ -8,7 +8,6 @@ import InputComponent from "../../components/inputComponent"; import { Button } from "../../components/ui/button"; import { CONTROL_PATCH_USER_STATE } from "../../constants/constants"; import { AuthContext } from "../../contexts/authContext"; -import { FlowsContext } from "../../contexts/flowsContext"; import { resetPassword, updateUser } from "../../controllers/API"; import useAlertStore from "../../stores/alertStore"; import { diff --git a/src/frontend/src/pages/StorePage/index.tsx b/src/frontend/src/pages/StorePage/index.tsx index 1cf5c5f13..c54893033 100644 --- a/src/frontend/src/pages/StorePage/index.tsx +++ b/src/frontend/src/pages/StorePage/index.tsx @@ -21,7 +21,6 @@ import { SelectValue, } from "../../components/ui/select"; import { AuthContext } from "../../contexts/authContext"; -import { FlowsContext } from "../../contexts/flowsContext"; import { checkHasApiKey, getStoreComponents, diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 817c49a40..491ab25f3 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -4,6 +4,7 @@ import { create } from "zustand"; import { readFlowsFromDatabase, updateFlowInDatabase, + uploadFlowsToDatabase, } from "../controllers/API"; import { FlowType } from "../types/flow"; import { FlowState } from "../types/tabs"; @@ -25,6 +26,12 @@ const useFlowsManagerStore = create((set, get) => ({ })); }, flows: [], + setFlows: (flows: FlowType[]) => { + set({ + flows, + currentFlow: flows.find((flow) => flow.id === get().currentFlowId), + }); + }, currentFlow: undefined, isLoading: true, setIsLoading: (isLoading: boolean) => set({ isLoading }), @@ -52,7 +59,8 @@ const useFlowsManagerStore = create((set, get) => ({ .then((dbData) => { if (dbData) { const { data, flows } = processFlows(dbData, false); - set({ flows, isLoading: false }); + get().setFlows(flows); + set({ isLoading: false }); useTypesStore.setState((state) => ({ data: { ...state.data, ["saved_components"]: data }, })); @@ -94,14 +102,14 @@ const useFlowsManagerStore = create((set, get) => ({ .getState() .setSuccessData({ title: "Changes saved successfully" }); } - set((oldState) => ({ - flows: oldState.flows.map((flow) => { + get().setFlows( + get().flows.map((flow) => { if (flow.id === updatedFlow.id) { return updatedFlow; } return flow; - }), - })); + }) + ); //update tabs state useFlowStore.setState({ isPending: false }); @@ -117,6 +125,35 @@ const useFlowsManagerStore = create((set, get) => ({ }); }); }, + uploadFlows: () => { + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + // add a change event listener to the file input + input.onchange = (event: Event) => { + // check if the file type is application/json + if ( + (event.target as HTMLInputElement).files![0].type === + "application/json" + ) { + // get the file from the file input + const file = (event.target as HTMLInputElement).files![0]; + // read the file as text + const formData = new FormData(); + formData.append("file", file); + uploadFlowsToDatabase(formData).then(() => { + get() + .refreshFlows() + .then(() => { + resolve(); + }); + }); + } + }; + // trigger the file input click event to open the file dialog + input.click(); + }); + }, })); export default useFlowsManagerStore; diff --git a/src/frontend/src/types/zustand/flowsManager/index.ts b/src/frontend/src/types/zustand/flowsManager/index.ts index dad1356ab..2e7e56a71 100644 --- a/src/frontend/src/types/zustand/flowsManager/index.ts +++ b/src/frontend/src/types/zustand/flowsManager/index.ts @@ -4,6 +4,7 @@ import { FlowState, FlowsState } from "../../tabs"; export type FlowsManagerStoreType = { flows: Array; + setFlows: (flows: FlowType[]) => void; currentFlow: FlowType | undefined; currentFlowId: string; setCurrentFlowId: (currentFlowId: string) => void; @@ -15,4 +16,5 @@ export type FlowsManagerStoreType = { refreshFlows: () => Promise; saveFlow: (flow: FlowType, silent?: boolean) => Promise; autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => void; + uploadFlows: () => Promise; }; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 039be107b..727750900 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -33,6 +33,7 @@ import { updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; import { createRandomKey, getFieldTitle, toTitleCase } from "./utils"; +import { downloadFlowsFromDatabase } from "../controllers/API"; const uid = new ShortUniqueId({ length: 5 }); export function cleanEdges(nodes: Node[], edges: Edge[]) { @@ -1205,3 +1206,48 @@ export function templatesGenerator(data: APIObjectType) { return acc; }, {}); } + +export function downloadFlow( + flow: FlowType, + flowName: string, + flowDescription?: string +) { + let clonedFlow = cloneDeep(flow); + removeFileNameFromComponents(clonedFlow); + // create a data URI with the current flow data + const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( + JSON.stringify({ + ...clonedFlow, + name: flowName, + description: flowDescription, + }) + )}`; + + // create a link element and set its properties + const link = document.createElement("a"); + link.href = jsonString; + link.download = `${ + flowName && flowName != "" + ? flowName + : flow.name + }.json`; + + // simulate a click on the link element to trigger the download + link.click(); +} + +export function downloadFlows() { + downloadFlowsFromDatabase().then((flows) => { + const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( + JSON.stringify(flows) + )}`; + + // create a link element and set its properties + const link = document.createElement("a"); + link.href = jsonString; + link.download = `flows.json`; + + // simulate a click on the link element to trigger the download + link.click(); + }); +} \ No newline at end of file