diff --git a/src/frontend/src/components/cardComponent/index.tsx b/src/frontend/src/components/cardComponent/index.tsx index f4afea46b..d87419fe2 100644 --- a/src/frontend/src/components/cardComponent/index.tsx +++ b/src/frontend/src/components/cardComponent/index.tsx @@ -1,5 +1,4 @@ import { useContext, useEffect, useState } from "react"; -import { FlowsContext } from "../../contexts/flowsContext"; import { getComponent, postLikeComponent } from "../../controllers/API"; import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal"; import useAlertStore from "../../stores/alertStore"; @@ -18,6 +17,7 @@ import { CardHeader, CardTitle, } from "../ui/card"; +import useFlowsManagerStore from "../../stores/flowsManagerStore"; export default function CollectionCardComponent({ data, @@ -32,7 +32,7 @@ export default function CollectionCardComponent({ button?: JSX.Element; onDelete?: () => void; }) { - const { addFlow } = useContext(FlowsContext); + const addFlow = useFlowsManagerStore((state) => state.addFlow); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const setValidApiKey = useStoreStore((state) => state.updateValidApiKey); diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index e7cf80587..e2fd345c5 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -1,5 +1,4 @@ import { useContext, useState } from "react"; -import { FlowsContext } from "../../../../contexts/flowsContext"; import { DropdownMenu, DropdownMenuContent, @@ -17,7 +16,7 @@ import IconComponent from "../../../genericIconComponent"; import { Button } from "../../../ui/button"; export const MenuBar = (): JSX.Element => { - const { addFlow } = useContext(FlowsContext); + const addFlow = useFlowsManagerStore((state) => state.addFlow); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const setErrorData = useAlertStore((state) => state.setErrorData); const { undo, redo } = useContext(undoRedoContext); diff --git a/src/frontend/src/contexts/flowsContext.tsx b/src/frontend/src/contexts/flowsContext.tsx deleted file mode 100644 index 4c1750fce..000000000 --- a/src/frontend/src/contexts/flowsContext.tsx +++ /dev/null @@ -1,500 +0,0 @@ -import { AxiosError } from "axios"; -import _, { cloneDeep } from "lodash"; -import { ReactNode, createContext, useContext, useRef, useState } from "react"; -import { - Edge, - Node, - ReactFlowJsonObject, - Viewport, - XYPosition, -} from "reactflow"; -import ShortUniqueId from "short-unique-id"; -import { - deleteFlowFromDatabase, - downloadFlowsFromDatabase, - readFlowsFromDatabase, - saveFlowToDatabase, - updateFlowInDatabase, - uploadFlowsToDatabase, -} from "../controllers/API"; -import useAlertStore from "../stores/alertStore"; -import useFlowStore from "../stores/flowStore"; -import { APIClassType } from "../types/api"; -import { FlowType, NodeDataType } from "../types/flow"; -import { FlowsContextType, FlowsState } from "../types/tabs"; -import { - addVersionToDuplicates, - createFlowComponent, - processFlowEdges, - removeFileNameFromComponents, - updateEdges, - updateIds, -} from "../utils/reactflowUtils"; -import { - createRandomKey, - getRandomDescription, - getRandomName, -} from "../utils/utils"; -import { useTypesStore } from "../stores/typesStore"; - -const uid = new ShortUniqueId({ length: 5 }); - -const FlowsContextInitialValue: FlowsContextType = { - //Remove tab id and get current id from url - tabId: "", - setTabId: (index: string) => {}, - isLoading: true, - flows: [], - setVersion: () => {}, - removeFlow: (id: string) => {}, - addFlow: async ( - newProject: boolean, - flowData?: FlowType, - override?: boolean - ) => "", - downloadFlow: (flow: FlowType) => {}, - downloadFlows: () => {}, - uploadFlows: () => {}, - uploadFlow: async () => "", - saveFlow: async (flow?: FlowType, silent?: boolean) => {}, - tabsState: {}, - setTabsState: () => {}, - saveComponent: async (component: NodeDataType, override: boolean) => "", - deleteComponent: (key: string) => {}, - version: "", - refreshFlows: () => {}, -}; - -export const FlowsContext = createContext( - FlowsContextInitialValue -); - -export function FlowsProvider({ children }: { children: ReactNode }) { - const setSuccessData = useAlertStore((state) => state.setSuccessData); - const setErrorData = useAlertStore((state) => state.setErrorData); - const [tabId, setTabId] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [flows, setFlows] = useState>([]); - const [tabsState, setTabsState] = useState({}); - const setData = useTypesStore((state) => state.setData); - const nodes = useFlowStore((state) => state.nodes); - const edges = useFlowStore((state) => state.edges); - const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); - const setPending = useFlowStore((state) => state.setPending); - const paste = useFlowStore((state) => state.paste); - - function refreshFlows() { - setIsLoading(true); - getTabsDataFromDB().then((DbData) => { - if (DbData) { - try { - processFlows(DbData, false); - setFlows(DbData); - setIsLoading(false); - } catch (e) {} - } - }); - } - - function getTabsDataFromDB() { - //get tabs from db - return readFlowsFromDatabase(); - } - - function 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); - } - }); - setData((prev) => { - let newData = cloneDeep(prev); - newData["saved_components"] = cloneDeep(savedComponents); - return newData; - }); - } - - /** - * Downloads the current flow as a JSON file - */ - 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 - : flows.find((f) => f.id === tabId)!.name - }.json`; - - // simulate a click on the link element to trigger the download - link.click(); - } - - 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(); - }); - } - /** - * Creates a file input and listens to a change event to upload a JSON flow file. - * If the file type is application/json, the file is read and parsed into a JSON object. - * The resulting JSON object is passed to the addFlow function. - */ - async function uploadFlow({ - newProject, - file, - isComponent = false, - position = { x: 10, y: 10 }, - }: { - newProject: boolean; - file?: File; - isComponent?: boolean; - position?: XYPosition; - }): Promise { - return new Promise(async (resolve, reject) => { - let id; - if (file) { - let text = await file.text(); - let fileData = JSON.parse(text); - if ( - newProject && - ((!fileData.is_component && isComponent === true) || - (fileData.is_component !== undefined && - fileData.is_component !== isComponent)) - ) { - reject("You cannot upload a component as a flow or vice versa"); - } else { - if (fileData.flows) { - fileData.flows.forEach((flow: FlowType) => { - id = addFlow(newProject, flow, undefined, position); - }); - resolve(""); - } else { - id = await addFlow(newProject, fileData, undefined, position); - resolve(id); - } - } - } else { - // create a file input - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".json"; - // add a change event listener to the file input - input.onchange = async (e: Event) => { - if ( - (e.target as HTMLInputElement).files![0].type === "application/json" - ) { - const currentfile = (e.target as HTMLInputElement).files![0]; - let text = await currentfile.text(); - let fileData: FlowType = await JSON.parse(text); - - if ( - (!fileData.is_component && isComponent === true) || - (fileData.is_component !== undefined && - fileData.is_component !== isComponent) - ) { - reject("You cannot upload a component as a flow or vice versa"); - } else { - id = await addFlow(newProject, fileData); - resolve(id); - } - } - }; - // trigger the file input click event to open the file dialog - input.click(); - } - }); - } - - function uploadFlows() { - // create a file input - 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(() => { - refreshFlows(); - }); - } - }; - // trigger the file input click event to open the file dialog - input.click(); - } - /** - * Removes a flow from an array of flows based on its id. - * Updates the state of flows and tabIndex using setFlows and setTabIndex hooks. - * @param {string} id - The id of the flow to remove. - */ - async function removeFlow(id: string) { - const index = flows.findIndex((flow) => flow.id === id); - if (index >= 0) { - await deleteFlowFromDatabase(id); - //removes component from data if there is any - setFlows(flows.filter((flow) => flow.id !== id)); - processFlows(flows.filter((flow) => flow.id !== id)); - } - } - - const addFlow = async ( - newProject: Boolean, - flow?: FlowType, - override?: boolean, - position?: XYPosition - ): Promise => { - if (newProject) { - let flowData = flow - ? processDataFromFlow(flow) - : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; - - // Create a new flow with a default name if no flow is provided. - - if (override) { - deleteComponent(flow!.name); - const newFlow = createNewFlow(flowData, flow!); - const { id } = await saveFlowToDatabase(newFlow); - newFlow.id = id; - //setTimeout to prevent update state with wrong state - setTimeout(() => { - addFlowToLocalState(newFlow); - }, 200); - // addFlowToLocalState(newFlow); - return; - } - - const newFlow = createNewFlow(flowData, flow!); - - const newName = addVersionToDuplicates(newFlow, flows); - - newFlow.name = newName; - try { - const { id } = await saveFlowToDatabase(newFlow); - // Change the id to the new id. - newFlow.id = id; - - // Add the new flow to the list of flows. - addFlowToLocalState(newFlow); - - // Return the id - return id; - } catch (error) { - // Handle the error if needed - throw error; // Re-throw the error so the caller can handle it if needed - } - } else { - paste( - { nodes: flow!.data!.nodes, edges: flow!.data!.edges }, - position ?? { x: 10, y: 10 } - ); - } - }; - - const processDataFromFlow = (flow: FlowType, refreshIds = true) => { - let data = flow?.data ? flow.data : null; - if (data) { - processFlowEdges(flow); - //prevent node update for now - // processFlowNodes(flow); - //add animation to text type edges - updateEdges(data.edges); - // updateNodes(data.nodes, data.edges); - if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere - } - return data; - }; - - const createNewFlow = ( - flowData: ReactFlowJsonObject | null, - flow: FlowType - ) => ({ - description: flow?.description ?? getRandomDescription(), - name: flow?.name ?? getRandomName(), - data: flowData, - id: "", - is_component: flow?.is_component ?? false, - }); - - const addFlowToLocalState = (newFlow: FlowType) => { - let newFlows: FlowType[] = []; - setFlows((prevState) => { - newFlows = newFlows.concat(prevState); - newFlows.push(newFlow); - return [...prevState, newFlow]; - }); - processFlows(newFlows); - }; - - /** - * Updates an existing flow with new data - * @param newFlow - The new flow object containing the updated data - */ - function updateFlow(newFlow: FlowType) { - 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; - } - newFlow = { - ...newFlow, - }; - return newFlows; - }); - } - - const saveTimeoutId = useRef(null); - - const saveCurrentFlow = ( - nodes: Node[], - edges: Edge[], - viewport: Viewport - ) => { - // Clear the previous timeout if it exists. - if (saveTimeoutId.current) { - clearTimeout(saveTimeoutId.current); - } - - // Set up a new timeout. - saveTimeoutId.current = setTimeout(() => { - const currentFlow = flows.find((flow: FlowType) => flow.id === tabId); - if (currentFlow) { - saveFlow({ ...currentFlow, data: { nodes, edges, viewport } }, true); - } - }, 300); // Delay of 300ms. - }; - - 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); - if (updatedFlow) { - // updates flow in state - if (!silent) { - setSuccessData({ title: "Changes saved successfully" }); - } - updateFlow(newFlow); - //update tabs state - setPending(false); - } - } catch (err) { - setErrorData({ - title: "Error while saving changes", - list: [(err as AxiosError).message], - }); - } - } - - function saveComponent(component: NodeDataType, override: boolean) { - component.node!.official = false; - return addFlow(true, createFlowComponent(component, version), override); - } - - function deleteComponent(key: string) { - let componentFlow = flows.find( - (componentFlow) => - componentFlow.is_component && componentFlow.name === key - ); - - if (componentFlow) { - removeFlow(componentFlow.id); - } - } - - // Initialize state variable for the version - const [version, setVersion] = useState(""); - - return ( - - {children} - - ); -} diff --git a/src/frontend/src/contexts/index.tsx b/src/frontend/src/contexts/index.tsx index 0bc1913fb..49b8106e6 100644 --- a/src/frontend/src/contexts/index.tsx +++ b/src/frontend/src/contexts/index.tsx @@ -5,7 +5,6 @@ import { TooltipProvider } from "../components/ui/tooltip"; import { ApiInterceptor } from "../controllers/API/api"; import { SSEProvider } from "./SSEContext"; import { AuthProvider } from "./authContext"; -import { FlowsProvider } from "./flowsContext"; import { LocationProvider } from "./locationContext"; import { UndoRedoProvider } from "./undoRedoContext"; @@ -21,9 +20,7 @@ export default function ContextWrapper({ children }: { children: ReactNode }) { - - {children} - + {children} diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 093e6d43b..f3dd8f6bd 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -21,7 +21,6 @@ import ReactFlow, { import GenericNode from "../../../../CustomNodes/GenericNode"; import Chat from "../../../../components/chatComponent"; import Loading from "../../../../components/ui/loading"; -import { FlowsContext } from "../../../../contexts/flowsContext"; import { locationContext } from "../../../../contexts/locationContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import useAlertStore from "../../../../stores/alertStore"; @@ -53,7 +52,7 @@ export default function Page({ flow: FlowType; view?: boolean; }): JSX.Element { - let { uploadFlow } = useContext(FlowsContext); + const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const autoSaveCurrentFlow = useFlowsManagerStore( (state) => state.autoSaveCurrentFlow ); diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index dafd15682..6f53cbd02 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -4,7 +4,6 @@ import ShadTooltip from "../../../../components/ShadTooltipComponent"; import IconComponent from "../../../../components/genericIconComponent"; import { Input } from "../../../../components/ui/input"; import { Separator } from "../../../../components/ui/separator"; -import { FlowsContext } from "../../../../contexts/flowsContext"; import ApiModal from "../../../../modals/ApiModal"; import ExportModal from "../../../../modals/exportModal"; import ShareModal from "../../../../modals/shareModal"; @@ -32,7 +31,7 @@ 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 } = useContext(FlowsContext); + const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const saveFlow = useFlowsManagerStore((state) => state.saveFlow); const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); const nodes = useFlowStore((state) => state.nodes); diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx index 41d9ebd34..0e817852b 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/sideBarDraggableComponent/index.tsx @@ -7,7 +7,6 @@ import { SelectTrigger, } from "../../../../../components/ui/select-custom"; import { AuthContext } from "../../../../../contexts/authContext"; -import { FlowsContext } from "../../../../../contexts/flowsContext"; import { APIClassType } from "../../../../../types/api"; import { createFlowComponent, @@ -16,6 +15,7 @@ import { } from "../../../../../utils/reactflowUtils"; import { removeCountFromString } from "../../../../../utils/utils"; import { useDarkStore } from "../../../../../stores/darkStore"; +import useFlowsManagerStore from "../../../../../stores/flowsManagerStore"; export default function SidebarDraggableComponent({ sectionName, @@ -37,7 +37,9 @@ export default function SidebarDraggableComponent({ official: boolean; }) { const [open, setOpen] = useState(false); - const { deleteComponent } = useContext(FlowsContext); + const deleteComponent = useFlowsManagerStore( + (state) => state.deleteComponent + ); const version = useDarkStore((state) => state.version); const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 }); const popoverRef = useRef(null); diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index c97a635cf..84b00208a 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -8,7 +8,6 @@ import { SelectItem, SelectTrigger, } from "../../../../components/ui/select-custom"; -import { FlowsContext } from "../../../../contexts/flowsContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import ConfirmationModal from "../../../../modals/ConfirmationModal"; import EditNodeModal from "../../../../modals/EditNodeModal"; @@ -74,7 +73,7 @@ export default function NodeToolbarComponent({ const setNodes = useFlowStore((state) => state.setNodes); const setEdges = useFlowStore((state) => state.setEdges); - const { saveComponent } = useContext(FlowsContext); + const saveComponent = useFlowsManagerStore((state) => state.saveComponent); const flows = useFlowsManagerStore((state) => state.flows); const version = useDarkStore((state) => state.version); const { takeSnapshot } = useContext(undoRedoContext); diff --git a/src/frontend/src/pages/MainPage/components/components/index.tsx b/src/frontend/src/pages/MainPage/components/components/index.tsx index 697c9615c..5ee0821b2 100644 --- a/src/frontend/src/pages/MainPage/components/components/index.tsx +++ b/src/frontend/src/pages/MainPage/components/components/index.tsx @@ -6,7 +6,6 @@ import CardsWrapComponent from "../../../../components/cardsWrapComponent"; import IconComponent from "../../../../components/genericIconComponent"; import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent"; 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"; @@ -16,8 +15,9 @@ export default function ComponentsComponent({ }: { is_component?: boolean; }) { - const { removeFlow, uploadFlow, addFlow } = - useContext(FlowsContext); + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); + const removeFlow = useFlowsManagerStore((state) => state.removeFlow); const isLoading = useFlowsManagerStore((state) => state.isLoading); const flows = useFlowsManagerStore((state) => state.flows); const setSuccessData = useAlertStore((state) => state.setSuccessData); diff --git a/src/frontend/src/pages/MainPage/index.tsx b/src/frontend/src/pages/MainPage/index.tsx index 17ef52c59..74b3e224c 100644 --- a/src/frontend/src/pages/MainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/index.tsx @@ -7,13 +7,12 @@ import PageLayout from "../../components/pageLayout"; import SidebarNav from "../../components/sidebarComponent"; import { Button } from "../../components/ui/button"; 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 { addFlow, uploadFlow } = - useContext(FlowsContext); + const addFlow = useFlowsManagerStore((state) => state.addFlow); + const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const setCurrentFlowId = useFlowsManagerStore( (state) => state.setCurrentFlowId ); diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 491ab25f3..6b1ffa974 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -1,18 +1,27 @@ import { AxiosError } from "axios"; -import { Edge, Node, Viewport } from "reactflow"; +import { Edge, Node, Viewport, XYPosition } from "reactflow"; import { create } from "zustand"; import { + deleteFlowFromDatabase, readFlowsFromDatabase, + saveFlowToDatabase, updateFlowInDatabase, uploadFlowsToDatabase, } from "../controllers/API"; -import { FlowType } from "../types/flow"; +import { FlowType, NodeDataType } from "../types/flow"; import { FlowState } from "../types/tabs"; import { FlowsManagerStoreType } from "../types/zustand/flowsManager"; -import { processFlows } from "../utils/reactflowUtils"; +import { + addVersionToDuplicates, + createFlowComponent, + createNewFlow, + processDataFromFlow, + processFlows, +} from "../utils/reactflowUtils"; import useAlertStore from "./alertStore"; import useFlowStore from "./flowStore"; import { useTypesStore } from "./typesStore"; +import { useDarkStore } from "./darkStore"; let saveTimeoutId: NodeJS.Timeout | null = null; @@ -154,6 +163,173 @@ const useFlowsManagerStore = create((set, get) => ({ input.click(); }); }, + addFlow: async ( + newProject: Boolean, + flow?: FlowType, + override?: boolean, + position?: XYPosition + ): Promise => { + if (newProject) { + let flowData = flow + ? processDataFromFlow(flow) + : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; + + // Create a new flow with a default name if no flow is provided. + + if (override) { + get().deleteComponent(flow!.name); + const newFlow = createNewFlow(flowData!, flow!); + const { id } = await saveFlowToDatabase(newFlow); + newFlow.id = id; + //setTimeout to prevent update state with wrong state + setTimeout(() => { + const { data, flows } = processFlows([newFlow, ...get().flows]); + get().setFlows(flows); + set({ isLoading: false }); + useTypesStore.setState((state) => ({ + data: { ...state.data, ["saved_components"]: data }, + })); + }, 200); + // addFlowToLocalState(newFlow); + return; + } + + const newFlow = createNewFlow(flowData!, flow!); + + const newName = addVersionToDuplicates(newFlow, get().flows); + + newFlow.name = newName; + try { + const { id } = await saveFlowToDatabase(newFlow); + // Change the id to the new id. + newFlow.id = id; + + // Add the new flow to the list of flows. + const { data, flows } = processFlows([newFlow, ...get().flows]); + get().setFlows(flows); + set({ isLoading: false }); + useTypesStore.setState((state) => ({ + data: { ...state.data, ["saved_components"]: data }, + })); + + // Return the id + return id; + } catch (error) { + // Handle the error if needed + throw error; // Re-throw the error so the caller can handle it if needed + } + } else { + useFlowStore + .getState() + .paste( + { nodes: flow!.data!.nodes, edges: flow!.data!.edges }, + position ?? { x: 10, y: 10 } + ); + } + }, + removeFlow: async (id: string) => { + return new Promise((resolve) => { + const index = get().flows.findIndex((flow) => flow.id === id); + if (index >= 0) { + deleteFlowFromDatabase(id).then(() => { + const { data, flows } = processFlows( + get().flows.filter((flow) => flow.id !== id) + ); + get().setFlows(flows); + set({ isLoading: false }); + useTypesStore.setState((state) => ({ + data: { ...state.data, ["saved_components"]: data }, + })); + resolve(); + }); + } + }); + }, + deleteComponent: async (key: string) => { + return new Promise((resolve) => { + let componentFlow = get().flows.find( + (componentFlow) => + componentFlow.is_component && componentFlow.name === key + ); + + if (componentFlow) { + get() + .removeFlow(componentFlow.id) + .then(() => { + resolve(); + }); + } + }); + }, + uploadFlow: async ({ + newProject, + file, + isComponent = false, + position = { x: 10, y: 10 }, + }: { + newProject: boolean; + file?: File; + isComponent?: boolean; + position?: XYPosition; + }): Promise => { + return new Promise(async (resolve, reject) => { + let id; + if (file) { + let text = await file.text(); + let fileData = JSON.parse(text); + if ( + newProject && + ((!fileData.is_component && isComponent === true) || + (fileData.is_component !== undefined && + fileData.is_component !== isComponent)) + ) { + reject("You cannot upload a component as a flow or vice versa"); + } else { + if (fileData.flows) { + fileData.flows.forEach((flow: FlowType) => { + id = get().addFlow(newProject, flow, undefined, position); + }); + resolve(""); + } else { + id = await get().addFlow(newProject, fileData, undefined, position); + resolve(id); + } + } + } else { + // create a file input + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + // add a change event listener to the file input + input.onchange = async (e: Event) => { + if ( + (e.target as HTMLInputElement).files![0].type === "application/json" + ) { + const currentfile = (e.target as HTMLInputElement).files![0]; + let text = await currentfile.text(); + let fileData: FlowType = await JSON.parse(text); + + if ( + (!fileData.is_component && isComponent === true) || + (fileData.is_component !== undefined && + fileData.is_component !== isComponent) + ) { + reject("You cannot upload a component as a flow or vice versa"); + } else { + id = await get().addFlow(newProject, fileData); + resolve(id); + } + } + }; + // trigger the file input click event to open the file dialog + input.click(); + } + }); + }, + saveComponent: (component: NodeDataType, override: boolean) => { + component.node!.official = false; + return get().addFlow(true, createFlowComponent(component, useDarkStore.getState().version), override); + }, })); export default useFlowsManagerStore; diff --git a/src/frontend/src/types/zustand/flowsManager/index.ts b/src/frontend/src/types/zustand/flowsManager/index.ts index 2e7e56a71..5a3c01350 100644 --- a/src/frontend/src/types/zustand/flowsManager/index.ts +++ b/src/frontend/src/types/zustand/flowsManager/index.ts @@ -1,4 +1,4 @@ -import { Node, Edge, Viewport } from "reactflow"; +import { Node, Edge, Viewport, XYPosition } from "reactflow"; import { FlowType } from "../../flow"; import { FlowState, FlowsState } from "../../tabs"; @@ -17,4 +17,9 @@ export type FlowsManagerStoreType = { saveFlow: (flow: FlowType, silent?: boolean) => Promise; autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => void; uploadFlows: () => Promise; + uploadFlow: ({newProject, file, isComponent, position}: {newProject: boolean, file?: File, isComponent?: boolean, position?: XYPosition}) => Promise; + addFlow: (newProject: boolean, flow?: FlowType, override?: boolean, position?: XYPosition) => Promise; + deleteComponent: (key: string) => Promise; + removeFlow: (id: string) => Promise; + saveComponent: (component: any, override: boolean) => Promise; }; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 727750900..e4b6b2c85 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -12,6 +12,7 @@ import { LANGFLOW_SUPPORTED_TYPES, specialCharsRegex, } from "../constants/constants"; +import { downloadFlowsFromDatabase } from "../controllers/API"; import { APIClassType, APIKindType, @@ -32,8 +33,13 @@ import { unselectAllNodesType, updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; -import { createRandomKey, getFieldTitle, toTitleCase } from "./utils"; -import { downloadFlowsFromDatabase } from "../controllers/API"; +import { + createRandomKey, + getFieldTitle, + getRandomDescription, + getRandomName, + toTitleCase, +} from "./utils"; const uid = new ShortUniqueId({ length: 5 }); export function cleanEdges(nodes: Node[], edges: Edge[]) { @@ -189,6 +195,7 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => { // updateNodes(data.nodes, data.edges); if (refreshIds) updateIds(data); // Assuming updateIds is defined elsewhere } + return data; }; export function updateIds(newFlow: ReactFlowJsonObject) { @@ -1226,11 +1233,7 @@ export function downloadFlow( // create a link element and set its properties const link = document.createElement("a"); link.href = jsonString; - link.download = `${ - flowName && flowName != "" - ? flowName - : flow.name - }.json`; + link.download = `${flowName && flowName != "" ? flowName : flow.name}.json`; // simulate a click on the link element to trigger the download link.click(); @@ -1250,4 +1253,17 @@ export function downloadFlows() { // simulate a click on the link element to trigger the download link.click(); }); -} \ No newline at end of file +} + +export const createNewFlow = ( + flowData: ReactFlowJsonObject, + flow: FlowType +) => { + return { + description: flow?.description ?? getRandomDescription(), + name: flow?.name ?? getRandomName(), + data: flowData, + id: "", + is_component: flow?.is_component ?? false, + }; +};