From 4d66bf351ddb69ca42d7f1e02000db758dcbce32 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:52:47 -0300 Subject: [PATCH] refactor: add flow and upload flow functionality (#3200) * Removed unused function and fixed tags initial value * Added post save flow * Added update flow patch * removed unused import * created useAddFlow hook to substitute AddFlow function on flowsManager store * fixed post save flow to handle endpoint name as undefined * Fixed add flow hook to use post save flow mutation * removed unused line * changed addFlow to use hook in all components that use addFlow * Removed unused code * removed addFlow of useDuplicateFlows call * made newProject default as true * removed unused variables from addFlow * fixed url of requests of flows * passed functions directly * fix app to display loading on top of the router * fixed promise of addFlow * Added upload flow hook with a lot of modularity * Fixed addFlow naming * Added helper functions for file uploading * Changed upload flow to use helper functions * removed refresh on post * changed paste function to handle when chatinput node exists on paste * Used helper function to create input on FileInput * Used helper function to create input on InputFileComponent * Used helper function to create input on folder upload, and used uploadFlow hook * used uploadFlow hook on dropdown options * used addFlow instead of addComponent on node toolbar * changed upload flow on headerComponent to use hook * Changed pageComponent to use uploadFlow hook * removed useFileDrop dependency * Fixed onFileDrop to use uploadFlow * removed useDropdown dependency * removed unused add and upload functions from flowsManagerStore * Clean flows and refetch when flow change, added loader when is fetching * Changed loading to the useQuery isPending * changed post to add flow * fixed error when uploading other thing that is not a JSON not appearing * changed useAddFlow to handle empty params too * Fixed loading every time we switch tabs * Fixed unnecessary list and ! * Fixed reference bug * Inserted cloneDeep to prevent reference bugs * Fixed tests of drag and drop * Fixed flows not being refreshed when uploading * [autofix.ci] apply automated fixes * fixed folders test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/frontend/src/App.tsx | 19 +- .../hooks/use-handle-install.tsx | 7 +- .../src/components/exampleComponent/index.tsx | 10 +- .../components/menuBar/index.tsx | 22 +- .../components/inputFileComponent/index.tsx | 88 +++---- .../components/sideBarFolderButtons/index.tsx | 47 ++-- src/frontend/src/controllers/API/index.ts | 28 +- .../API/queries/_builds/use-delete-builds.ts | 7 +- .../API/queries/_builds/use-get-builds.ts | 3 +- .../queries/flows/use-patch-update-flow.ts | 46 ++++ .../API/queries/flows/use-post-add-flow.ts | 43 +++ .../flows/use-post-download-multiple-flows.ts | 5 +- .../folders/use-post-upload-folders.ts | 5 +- .../src/helpers/create-file-upload.ts | 35 +++ .../src/helpers/get-objects-from-filelist.ts | 9 + src/frontend/src/hooks/flows/use-add-flow.ts | 106 ++++++++ .../src/hooks/flows/use-upload-flow.ts | 98 +++++++ .../components/FileInput/index.tsx | 22 +- .../components/NewFlowCardComponent/index.tsx | 6 +- .../components/undrawCards/index.tsx | 6 +- .../components/PageComponent/index.tsx | 65 +++-- .../components/nodeToolbarComponent/index.tsx | 29 ++- .../hooks/use-handle-duplicate.tsx | 16 +- .../components/componentsComponent/index.tsx | 7 +- .../components/emptyComponent/index.tsx | 5 - .../MainPage/hooks/use-dropdown-options.tsx | 16 +- .../pages/MainPage/hooks/use-on-file-drop.tsx | 69 +---- .../pages/MainPage/pages/mainPage/index.tsx | 2 - src/frontend/src/stores/flowStore.ts | 14 +- src/frontend/src/stores/flowsManagerStore.ts | 244 +----------------- .../src/types/zustand/flowsManager/index.ts | 25 +- src/frontend/src/utils/reactflowUtils.ts | 2 +- .../tests/end-to-end/dragAndDrop.spec.ts | 2 +- src/frontend/tests/end-to-end/folders.spec.ts | 4 +- 34 files changed, 543 insertions(+), 569 deletions(-) create mode 100644 src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts create mode 100644 src/frontend/src/controllers/API/queries/flows/use-post-add-flow.ts create mode 100644 src/frontend/src/helpers/create-file-upload.ts create mode 100644 src/frontend/src/helpers/get-objects-from-filelist.ts create mode 100644 src/frontend/src/hooks/flows/use-add-flow.ts create mode 100644 src/frontend/src/hooks/flows/use-upload-flow.ts diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 0d7bdb72e..ddccd55cc 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -23,12 +23,12 @@ import { useGetVersionQuery } from "./controllers/API/queries/version"; import { setupAxiosDefaults } from "./controllers/API/utils"; import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path"; import Router from "./routes"; -import { Case } from "./shared/components/caseComponent"; import useAlertStore from "./stores/alertStore"; import useAuthStore from "./stores/authStore"; import { useDarkStore } from "./stores/darkStore"; import useFlowsManagerStore from "./stores/flowsManagerStore"; import { useFolderStore } from "./stores/foldersStore"; +import { cn } from "./utils/utils"; export default function App() { useTrackLastVisitedPath(); @@ -164,15 +164,16 @@ export default function App() { > } - -
- -
-
+
+ +
- - - +
diff --git a/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx b/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx index 4c407349d..991b21c6f 100644 --- a/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx +++ b/src/frontend/src/components/cardComponent/hooks/use-handle-install.tsx @@ -1,6 +1,5 @@ -import { useState } from "react"; +import useAddFlow from "@/hooks/flows/use-add-flow"; import { getComponent } from "../../../controllers/API"; -import useFlowsManagerStore from "../../../stores/flowsManagerStore"; import { storeComponent } from "../../../types/store"; import cloneFlowWithParent from "../../../utils/storeUtils"; @@ -14,7 +13,7 @@ const useInstallComponent = ( setSuccessData: (value: { title: string }) => void, setErrorData: (value: { title: string; list: string[] }) => void, ) => { - const addFlow = useFlowsManagerStore((state) => state.addFlow); + const addFlow = useAddFlow(); const handleInstall = () => { const temp = downloadsCount; @@ -24,7 +23,7 @@ const useInstallComponent = ( getComponent(data.id) .then((res) => { const newFlow = cloneFlowWithParent(res, res.id, data.is_component); - addFlow(true, newFlow) + addFlow({ flow: newFlow }) .then((id) => { setSuccessData({ title: `${name} ${isStore ? "Downloaded" : "Installed"} Successfully.`, diff --git a/src/frontend/src/components/exampleComponent/index.tsx b/src/frontend/src/components/exampleComponent/index.tsx index 02af6b9bc..b251c9476 100644 --- a/src/frontend/src/components/exampleComponent/index.tsx +++ b/src/frontend/src/components/exampleComponent/index.tsx @@ -1,5 +1,6 @@ +import useAddFlow from "@/hooks/flows/use-add-flow"; +import emojiRegex from "emoji-regex"; import { useNavigate } from "react-router-dom"; -import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { FlowType } from "../../types/flow"; import { updateIds } from "../../utils/reactflowUtils"; import { cn } from "../../utils/utils"; @@ -20,10 +21,9 @@ export default function CollectionCardComponent({ flow: FlowType; authorized?: boolean; }) { - const addFlow = useFlowsManagerStore((state) => state.addFlow); + const addFlow = useAddFlow(); const navigate = useNavigate(); - const emojiRegex = /\p{Emoji}/u; - const isEmoji = (str: string) => emojiRegex.test(str); + const isEmoji = (str: string) => emojiRegex().test(str); return ( { updateIds(flow.data!); - addFlow(true, flow).then((id) => { + addFlow({ flow }).then((id) => { navigate("/flow/" + id); }); }} diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index a6e6c30d8..b7475e6d5 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -7,6 +7,8 @@ import { DropdownMenuTrigger, } from "../../../ui/dropdown-menu"; +import useAddFlow from "@/hooks/flows/use-add-flow"; +import useUploadFlow from "@/hooks/flows/use-upload-flow"; import { useNavigate } from "react-router-dom"; import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants"; import { SAVED_HOVER } from "../../../../constants/constants"; @@ -26,7 +28,7 @@ import { Button } from "../../../ui/button"; export const MenuBar = ({}: {}): JSX.Element => { const shortcuts = useShortcutsStore((state) => state.shortcuts); - const addFlow = useFlowsManagerStore((state) => state.addFlow); + const addFlow = useAddFlow(); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const setErrorData = useAlertStore((state) => state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); @@ -40,14 +42,14 @@ export const MenuBar = ({}: {}): JSX.Element => { const saveLoading = useFlowsManagerStore((state) => state.saveLoading); const [openSettings, setOpenSettings] = useState(false); const [openLogs, setOpenLogs] = useState(false); - const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); + const uploadFlow = useUploadFlow(); const navigate = useNavigate(); const isBuilding = useFlowStore((state) => state.isBuilding); const getTypes = useTypesStore((state) => state.getTypes); function handleAddFlow() { try { - addFlow(true).then((id) => { + addFlow().then((id) => { navigate("/flow/" + id); }); } catch (err) { @@ -125,14 +127,18 @@ export const MenuBar = ({}: {}): JSX.Element => { { - uploadFlow({ newProject: false, isComponent: false }).catch( - (error) => { + uploadFlow({ position: { x: 300, y: 100 } }) + .then(() => { + setSuccessData({ + title: "Uploaded successfully", + }); + }) + .catch((error) => { setErrorData({ title: UPLOAD_ERROR_ALERT, - list: [error], + list: [(error as Error).message], }); - }, - ); + }); }} > diff --git a/src/frontend/src/components/inputFileComponent/index.tsx b/src/frontend/src/components/inputFileComponent/index.tsx index c8c860a06..e2d0bf5cc 100644 --- a/src/frontend/src/components/inputFileComponent/index.tsx +++ b/src/frontend/src/components/inputFileComponent/index.tsx @@ -1,5 +1,6 @@ import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file"; -import { useEffect, useState } from "react"; +import { createFileUpload } from "@/helpers/create-file-upload"; +import { useEffect } from "react"; import { CONSOLE_ERROR_MSG, INVALID_FILE_ALERT, @@ -19,7 +20,6 @@ export default function InputFileComponent({ id, }: FileComponentType): JSX.Element { const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); - const [loading, setLoading] = useState(false); const setErrorData = useAlertStore((state) => state.setErrorData); // Clear component state @@ -39,58 +39,42 @@ export default function InputFileComponent({ return false; } - const { mutate } = usePostUploadFile(); + const { mutate, isPending } = usePostUploadFile(); const handleButtonClick = (): void => { - // Create a file input element - const input = document.createElement("input"); - document.body.appendChild(input); - input.type = "file"; - input.accept = fileTypes?.join(","); - input.style.display = "none"; // Hidden from view - input.multiple = false; // Allow only one file selection - const onChangeFile = (event: Event): void => { - setLoading(true); + createFileUpload({ multiple: false, accept: fileTypes?.join(",") }).then( + (files) => { + const file = files[0]; + if (file) { + if (checkFileType(file.name)) { + // Upload the file + mutate( + { file, id: currentFlowId }, + { + onSuccess: (data) => { + // Get the file name from the response + const { file_path } = data; - // Get the selected file - const file = (event.target as HTMLInputElement).files?.[0]; - - // Check if the file type is correct - if (file && checkFileType(file.name)) { - // Upload the file - mutate( - { file, id: currentFlowId }, - { - onSuccess: (data) => { - // Get the file name from the response - const { file_path } = data; - - // sets the value that goes to the backend - // Update the state and on with the name of the file - // sets the value to the user - handleOnNewValue({ value: file.name, file_path }); - setLoading(false); - }, - onError: () => { - console.error(CONSOLE_ERROR_MSG); - setLoading(false); - }, - }, - ); - } else { - // Show an error if the file type is not allowed - setErrorData({ - title: INVALID_FILE_ALERT, - list: fileTypes, - }); - setLoading(false); - } - }; - - input.addEventListener("change", onChangeFile); - - // Trigger the file selection dialog - input.click(); + // sets the value that goes to the backend + // Update the state and on with the name of the file + // sets the value to the user + handleOnNewValue({ value: file.name, file_path }); + }, + onError: () => { + console.error(CONSOLE_ERROR_MSG); + }, + }, + ); + } else { + // Show an error if the file type is not allowed + setErrorData({ + title: INVALID_FILE_ALERT, + list: fileTypes, + }); + } + } + }, + ); }; return ( @@ -114,7 +98,7 @@ export default function InputFileComponent({ unstyled className="inline-flex items-center justify-center" onClick={handleButtonClick} - loading={loading} + loading={isPending} disabled={disabled} > state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); const getFoldersApi = useFolderStore((state) => state.getFoldersApi); + const uploadFlow = useUploadFlow(); const handleFolderChange = () => { getFolderById(folderId); @@ -68,46 +72,41 @@ const SideBarFoldersButtonsComponent = ({ const { mutate } = usePostUploadFolders(); const handleUploadFlowsToFolder = () => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".json"; - input.click(); - - input.onchange = (event: Event) => { - if ( - (event.target as HTMLInputElement).files![0].type === "application/json" - ) { - const file = (event.target as HTMLInputElement).files![0]; - const formData = new FormData(); - formData.append("file", file); - file.text().then(async (text) => { - const data = JSON.parse(text); - if (data.data?.nodes) { - await useFlowsManagerStore.getState().addFlow(true, data); + createFileUpload().then((files: File[]) => { + getObjectsFromFilelist(files).then((objects) => { + if (objects.every((flow) => flow.data?.nodes)) { + uploadFlow({ files }).then(() => { getFolderById(folderId); - } else { + setSuccessData({ + title: "Uploaded successfully", + }); + }); + } else { + files.forEach((folder) => { + const formData = new FormData(); + formData.append("file", folder); mutate( { formData }, { onSuccess: () => { - getFolderById(folderId); + getFoldersApi(true); setSuccessData({ - title: "Uploaded successfully", + title: "Folder uploaded successfully.", }); }, onError: (err) => { console.log(err); setErrorData({ title: `Error on upload`, - list: [err["response"]["data"]], + list: [err["response"]["data"]["message"]], }); }, }, ); - } - }); - } - }; + }); + } + }); + }); }; const { mutate: mutateDownloadFolder } = useGetDownloadFolders(); diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 9a7edc2ad..01213333c 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -262,32 +262,6 @@ export async function getFlowStylesFromDatabase() { } } -/** - * Saves a new flow style to the database. - * - * @param {FlowStyleType} flowStyle - The flow style data to save. - * @returns {Promise} The saved flow style data. - * @throws Will throw an error if saving fails. - */ -export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) { - try { - const response = await api.post(`${BASE_URL_API}flow_styles/`, flowStyle, { - headers: { - accept: "application/json", - "Content-Type": "application/json", - }, - }); - - if (response.status !== 201) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return response?.data; - } catch (error) { - console.error(error); - throw error; - } -} - /** * Fetches the version of the API. * @@ -508,7 +482,7 @@ export async function getStoreComponents({ limit = 9999999, is_component = null, sort = "-count(liked_by)", - tags = [] || null, + tags = [], liked = null, isPrivate = null, search = null, diff --git a/src/frontend/src/controllers/API/queries/_builds/use-delete-builds.ts b/src/frontend/src/controllers/API/queries/_builds/use-delete-builds.ts index 9ca111171..4ab6522e0 100644 --- a/src/frontend/src/controllers/API/queries/_builds/use-delete-builds.ts +++ b/src/frontend/src/controllers/API/queries/_builds/use-delete-builds.ts @@ -8,9 +8,10 @@ interface IDeleteBuilds { } // add types for error handling and success -export const useDeleteBuilds: useMutationFunctionType = ( - options, -) => { +export const useDeleteBuilds: useMutationFunctionType< + undefined, + IDeleteBuilds +> = (options) => { const { mutate } = UseRequestProcessor(); const deleteBuildsFn = async (payload: IDeleteBuilds): Promise => { diff --git a/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts b/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts index eeaa34ef4..facc45c4c 100644 --- a/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts +++ b/src/frontend/src/controllers/API/queries/_builds/use-get-builds.ts @@ -1,8 +1,6 @@ import useFlowStore from "@/stores/flowStore"; import useFlowsManagerStore from "@/stores/flowsManagerStore"; import { FlowPoolType } from "@/types/zustand/flow"; -import { cleanEdges } from "@/utils/reactflowUtils"; -import { getInputsAndOutputs } from "@/utils/storeUtils"; import { keepPreviousData } from "@tanstack/react-query"; import { AxiosResponse } from "axios"; import { useQueryFunctionType } from "../../../../types/api"; @@ -52,6 +50,7 @@ export const useGetBuildsQuery: useQueryFunctionType< const queryResult = query(["useGetBuildsQuery"], responseFn, { placeholderData: keepPreviousData, + refetchOnWindowFocus: false, }); return queryResult; diff --git a/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts b/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts new file mode 100644 index 000000000..d9551823e --- /dev/null +++ b/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts @@ -0,0 +1,46 @@ +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { ReactFlowJsonObject } from "reactflow"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface IPatchUpdateFlow { + name: string; + data: ReactFlowJsonObject; + description: string; + folder_id: string; + endpoint_name: string; +} + +interface IPatchUpdateFlowParams { + id: string; +} + +export const usePatchUpdateFlow: useMutationFunctionType< + IPatchUpdateFlowParams, + IPatchUpdateFlow +> = (params, options?) => { + const { mutate } = UseRequestProcessor(); + + const PatchUpdateFlowFn = async (payload: IPatchUpdateFlow): Promise => { + const response = await api.patch(`${getURL("FLOWS")}/${params}`, { + name: payload.name, + data: payload.data, + description: payload.description, + folder_id: payload.folder_id === "" ? null : payload.folder_id, + endpoint_name: payload.endpoint_name, + }); + + return response.data; + }; + + const mutation: UseMutationResult = + mutate( + ["usePatchUpdateFlow", { id: params.id }], + PatchUpdateFlowFn, + options, + ); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/flows/use-post-add-flow.ts b/src/frontend/src/controllers/API/queries/flows/use-post-add-flow.ts new file mode 100644 index 000000000..4eb53be06 --- /dev/null +++ b/src/frontend/src/controllers/API/queries/flows/use-post-add-flow.ts @@ -0,0 +1,43 @@ +import { useMutationFunctionType } from "@/types/api"; +import { UseMutationResult } from "@tanstack/react-query"; +import { ReactFlowJsonObject } from "reactflow"; +import { api } from "../../api"; +import { getURL } from "../../helpers/constants"; +import { UseRequestProcessor } from "../../services/request-processor"; + +interface IPostAddFlow { + name: string; + data: ReactFlowJsonObject; + description: string; + is_component: boolean; + folder_id: string; + endpoint_name: string | undefined; +} + +export const usePostAddFlow: useMutationFunctionType< + undefined, + IPostAddFlow +> = (options?) => { + const { mutate } = UseRequestProcessor(); + + const postAddFlowFn = async (payload: IPostAddFlow): Promise => { + const response = await api.post(`${getURL("FLOWS")}/`, { + name: payload.name, + data: payload.data, + description: payload.description, + is_component: payload.is_component, + folder_id: payload.folder_id || null, + endpoint_name: payload.endpoint_name || null, + }); + + return response.data; + }; + + const mutation: UseMutationResult = mutate( + ["usePostAddFlow"], + postAddFlowFn, + options, + ); + + return mutation; +}; diff --git a/src/frontend/src/controllers/API/queries/flows/use-post-download-multiple-flows.ts b/src/frontend/src/controllers/API/queries/flows/use-post-download-multiple-flows.ts index 1ef801124..da5841f7b 100644 --- a/src/frontend/src/controllers/API/queries/flows/use-post-download-multiple-flows.ts +++ b/src/frontend/src/controllers/API/queries/flows/use-post-download-multiple-flows.ts @@ -32,10 +32,7 @@ export const usePostDownloadMultipleFlows: useMutationFunctionType< IPostDownloadMultipleFlows > = mutate( ["usePostDownloadMultipleFlows"], - async (payload: IPostDownloadMultipleFlows) => { - const res = await postDownloadMultipleFlowsFn(payload); - return res; - }, + postDownloadMultipleFlowsFn, options, ); diff --git a/src/frontend/src/controllers/API/queries/folders/use-post-upload-folders.ts b/src/frontend/src/controllers/API/queries/folders/use-post-upload-folders.ts index 4a2397a61..8c244bffc 100644 --- a/src/frontend/src/controllers/API/queries/folders/use-post-upload-folders.ts +++ b/src/frontend/src/controllers/API/queries/folders/use-post-upload-folders.ts @@ -1,6 +1,4 @@ -import useFlowsManagerStore from "@/stores/flowsManagerStore"; -import { useFolderStore } from "@/stores/foldersStore"; -import { Users, useMutationFunctionType } from "@/types/api"; +import { useMutationFunctionType } from "@/types/api"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; @@ -22,7 +20,6 @@ export const usePostUploadFolders: useMutationFunctionType< `${getURL("FOLDERS")}/upload/`, payload.formData, ); - await useFolderStore.getState().getFoldersApi(true); return res.data; }; diff --git a/src/frontend/src/helpers/create-file-upload.ts b/src/frontend/src/helpers/create-file-upload.ts new file mode 100644 index 000000000..b11363ce5 --- /dev/null +++ b/src/frontend/src/helpers/create-file-upload.ts @@ -0,0 +1,35 @@ +export async function createFileUpload(props?: { + accept?: string; + multiple?: boolean; +}): Promise { + let lock = false; + return new Promise((resolve) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = props?.accept ?? ".json"; + input.multiple = props?.multiple ?? true; + input.style.display = "none"; + // add a change event listener to the file input + input.onchange = async (e: Event) => { + lock = true; + resolve(Array.from((e.target as HTMLInputElement).files!)); + document.body.removeChild(input); + }; + window.addEventListener( + "focus", + () => { + setTimeout(() => { + if (!lock) { + resolve([]); + document.body.removeChild(input); + } + }, 300); + }, + { once: true }, + ); + // add the input element to the body to ensure it is part of the DOM + document.body.appendChild(input); + // trigger the file input click event to open the file dialog + input.click(); + }); +} diff --git a/src/frontend/src/helpers/get-objects-from-filelist.ts b/src/frontend/src/helpers/get-objects-from-filelist.ts new file mode 100644 index 000000000..3cc486286 --- /dev/null +++ b/src/frontend/src/helpers/get-objects-from-filelist.ts @@ -0,0 +1,9 @@ +export async function getObjectsFromFilelist(files: File[]): Promise { + let objects: T[] = []; + for (const file of files) { + let text = await file.text(); + let fileData = await JSON.parse(text); + objects.push(fileData as T); + } + return objects; +} diff --git a/src/frontend/src/hooks/flows/use-add-flow.ts b/src/frontend/src/hooks/flows/use-add-flow.ts new file mode 100644 index 000000000..451648f7b --- /dev/null +++ b/src/frontend/src/hooks/flows/use-add-flow.ts @@ -0,0 +1,106 @@ +import { usePostAddFlow } from "@/controllers/API/queries/flows/use-post-add-flow"; +import useAlertStore from "@/stores/alertStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import { useFolderStore } from "@/stores/foldersStore"; +import { useGlobalVariablesStore } from "@/stores/globalVariablesStore/globalVariables"; +import { useTypesStore } from "@/stores/typesStore"; +import { FlowType } from "@/types/flow"; +import { + addVersionToDuplicates, + createNewFlow, + extractFieldsFromComponenents, + processDataFromFlow, + processFlows, + updateGroupRecursion, +} from "@/utils/reactflowUtils"; +import { cloneDeep } from "lodash"; + +const useAddFlow = () => { + const unavaliableFields = useGlobalVariablesStore( + (state) => state.unavailableFields, + ); + const globalVariablesEntries = useGlobalVariablesStore( + (state) => state.globalVariablesEntries, + ); + const flows = useFlowsManagerStore((state) => state.flows); + const setFlows = useFlowsManagerStore((state) => state.setFlows); + const deleteComponent = useFlowsManagerStore( + (state) => state.deleteComponent, + ); + const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading); + + const { mutate: postAddFlow } = usePostAddFlow(); + + const addFlow = async (params?: { flow?: FlowType; override?: boolean }) => { + return new Promise(async (resolve, reject) => { + const flow = cloneDeep(params?.flow) ?? undefined; + let flowData = flow + ? processDataFromFlow(flow) + : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; + flowData?.nodes.forEach((node) => { + updateGroupRecursion( + node, + flowData?.edges, + unavaliableFields, + globalVariablesEntries, + ); + }); + // Create a new flow with a default name if no flow is provided. + const folder_id = useFolderStore.getState().folderUrl; + const my_collection_id = useFolderStore.getState().myCollectionId; + + if (params?.override && flow) { + await deleteComponent(flow.name); + } + const newFlow = createNewFlow( + flowData!, + folder_id || my_collection_id!, + flow, + ); + + const newName = addVersionToDuplicates(newFlow, flows); + newFlow.name = newName; + newFlow.folder_id = useFolderStore.getState().folderUrl; + + postAddFlow(newFlow, { + onSuccess: ({ id }) => { + newFlow.id = id; + // Add the new flow to the list of flows. + const { data, flows: myFlows } = processFlows([newFlow, ...flows]); + setFlows(myFlows); + useTypesStore.setState((state) => ({ + data: { ...state.data, ["saved_components"]: data }, + ComponentFields: extractFieldsFromComponenents({ + ...state.data, + ["saved_components"]: data, + }), + })); + setIsLoading(false); + resolve(id); + }, + onError: (error) => { + if (error.response?.data?.detail) { + useAlertStore.getState().setErrorData({ + title: "Could not load flows from database", + list: [error.response?.data?.detail], + }); + } else { + useAlertStore.getState().setErrorData({ + title: "Could not load flows from database", + list: [ + error.message ?? + "An unexpected error occurred, please try again", + ], + }); + } + setIsLoading(false); + reject(error); // Re-throw the error so the caller can handle it if needed}, + }, + }); + }); + }; + + return addFlow; +}; + +export default useAddFlow; diff --git a/src/frontend/src/hooks/flows/use-upload-flow.ts b/src/frontend/src/hooks/flows/use-upload-flow.ts new file mode 100644 index 000000000..4466500bd --- /dev/null +++ b/src/frontend/src/hooks/flows/use-upload-flow.ts @@ -0,0 +1,98 @@ +import { createFileUpload } from "@/helpers/create-file-upload"; +import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist"; +import useFlowStore from "@/stores/flowStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import { FlowType } from "@/types/flow"; +import useAddFlow from "./use-add-flow"; + +const useUploadFlow = () => { + const addFlow = useAddFlow(); + const paste = useFlowStore((state) => state.paste); + const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows); + + const getFlowsFromFiles = async ({ + files, + }: { + files: File[]; + }): Promise => { + const objectList = await getObjectsFromFilelist(files); + const flows: FlowType[] = []; + objectList.forEach((object) => { + if (object.flows) { + object.flows.forEach((flow: FlowType) => { + flows.push(flow); + }); + } else { + flows.push(object as FlowType); + } + }); + return flows; + }; + + const getFlowsToUpload = async ({ + files, + }: { + files?: File[]; + }): Promise => { + if (!files) { + files = await createFileUpload(); + } + if (!files.every((file) => file.type === "application/json")) { + throw new Error("Invalid file type"); + } + return await getFlowsFromFiles({ + files, + }); + }; + + const uploadFlow = async ({ + files, + isComponent, + position, + }: { + files?: File[]; + isComponent?: boolean; + position?: { x: number; y: number }; + }): Promise => { + try { + let flows = await getFlowsToUpload({ files }); + if ( + isComponent !== undefined && + flows.every( + (fileData) => + (!fileData.is_component && isComponent === true) || + (fileData.is_component !== undefined && + fileData.is_component !== isComponent), + ) + ) { + throw new Error( + "You cannot upload a component as a flow or vice versa", + ); + } else { + let currentPosition = position; + for (const flow of flows) { + if (flow.data) { + if (currentPosition) { + paste(flow.data, currentPosition); + currentPosition = { + x: currentPosition.x + 50, + y: currentPosition.y + 50, + }; + } else { + await addFlow({ flow }); + } + } else { + throw new Error("Invalid flow data"); + } + } + refreshFlows(); + } + } catch (e) { + throw e; + } + }; + + return uploadFlow; +}; + +export default useUploadFlow; diff --git a/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx b/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx index d8a06c913..2a362a7f1 100644 --- a/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx +++ b/src/frontend/src/modals/IOModal/components/IOFieldView/components/FileInput/index.tsx @@ -1,10 +1,13 @@ import { Button } from "../../../../../../components/ui/button"; import { usePostUploadFile } from "@/controllers/API/queries/files/use-post-upload-file"; +import { createFileUpload } from "@/helpers/create-file-upload"; import { useEffect, useState } from "react"; import IconComponent from "../../../../../../components/genericIconComponent"; -import { BASE_URL_API } from "../../../../../../constants/constants"; -import { uploadFile } from "../../../../../../controllers/API"; +import { + ALLOWED_IMAGE_INPUT_EXTENSIONS, + BASE_URL_API, +} from "../../../../../../constants/constants"; import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore"; import { IOFileInputProps } from "../../../../../../types/components"; @@ -99,18 +102,11 @@ export default function IOFileInput({ field, updateValue }: IOFileInputProps) { }; const handleButtonClick = (): void => { + createFileUpload({ + multiple: false, + accept: ALLOWED_IMAGE_INPUT_EXTENSIONS.join(","), + }).then((files) => upload(files[0])); // Create a file input element - const input = document.createElement("input"); - input.type = "file"; - input.style.display = "none"; // Hidden from view - input.multiple = false; // Allow only one file selection - input.onchange = (event: Event): void => { - // Get the selected file - const file = (event.target as HTMLInputElement).files?.[0]; - upload(file); - }; - // Trigger the file selection dialog - input.click(); }; return ( diff --git a/src/frontend/src/modals/newFlowModal/components/NewFlowCardComponent/index.tsx b/src/frontend/src/modals/newFlowModal/components/NewFlowCardComponent/index.tsx index 5d4a60385..b7a0d491e 100644 --- a/src/frontend/src/modals/newFlowModal/components/NewFlowCardComponent/index.tsx +++ b/src/frontend/src/modals/newFlowModal/components/NewFlowCardComponent/index.tsx @@ -1,3 +1,4 @@ +import useAddFlow from "@/hooks/flows/use-add-flow"; import { useLocation, useNavigate } from "react-router-dom"; import { Card, @@ -5,11 +6,10 @@ import { CardDescription, CardTitle, } from "../../../../components/ui/card"; -import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import { useFolderStore } from "../../../../stores/foldersStore"; export default function NewFlowCardComponent() { - const addFlow = useFlowsManagerStore((state) => state.addFlow); + const addFlow = useAddFlow(); const navigate = useNavigate(); const location = useLocation(); const folderId = location?.state?.folderId; @@ -18,7 +18,7 @@ export default function NewFlowCardComponent() { return ( { - addFlow(true).then((id) => { + addFlow().then((id) => { setFolderUrl(folderId ?? ""); navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`); }); diff --git a/src/frontend/src/modals/newFlowModal/components/undrawCards/index.tsx b/src/frontend/src/modals/newFlowModal/components/undrawCards/index.tsx index 1c87af469..03c415801 100644 --- a/src/frontend/src/modals/newFlowModal/components/undrawCards/index.tsx +++ b/src/frontend/src/modals/newFlowModal/components/undrawCards/index.tsx @@ -10,13 +10,13 @@ import APIRequest from "../../../../assets/undraw_real_time_analytics_re_yliv.sv import BasicPrompt from "../../../../assets/undraw_short_bio_re_fmx0.svg?react"; import TransferFiles from "../../../../assets/undraw_transfer_files_re_a2a9.svg?react"; +import useAddFlow from "@/hooks/flows/use-add-flow"; import { Card, CardContent, CardDescription, CardTitle, } from "../../../../components/ui/card"; -import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import { useFolderStore } from "../../../../stores/foldersStore"; import { UndrawCardComponentProps } from "../../../../types/components"; import { updateIds } from "../../../../utils/reactflowUtils"; @@ -24,7 +24,7 @@ import { updateIds } from "../../../../utils/reactflowUtils"; export default function UndrawCardComponent({ flow, }: UndrawCardComponentProps): JSX.Element { - const addFlow = useFlowsManagerStore((state) => state.addFlow); + const addFlow = useAddFlow(); const navigate = useNavigate(); const location = useLocation(); const folderId = location?.state?.folderId; @@ -142,7 +142,7 @@ export default function UndrawCardComponent({ { updateIds(flow.data!); - addFlow(true, flow).then((id) => { + addFlow({ flow }).then((id) => { setFolderUrl(folderId ?? ""); navigate(`/flow/${id}/folder/${folderIdUrl}`); }); diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 70f546546..cddddeb05 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -1,4 +1,6 @@ +import LoadingComponent from "@/components/loadingComponent"; import { useGetBuildsQuery } from "@/controllers/API/queries/_builds"; +import useUploadFlow from "@/hooks/flows/use-upload-flow"; import { getInputsAndOutputs } from "@/utils/storeUtils"; import _, { cloneDeep } from "lodash"; import { @@ -62,7 +64,7 @@ export default function Page({ flow: FlowType; view?: boolean; }): JSX.Element { - const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); + const uploadFlow = useUploadFlow(); const autoSaveCurrentFlow = useFlowsManagerStore( (state) => state.autoSaveCurrentFlow, ); @@ -70,9 +72,6 @@ export default function Page({ const templates = useTypesStore((state) => state.templates); const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const reactFlowWrapper = useRef(null); - const [showCanvas, setSHowCanvas] = useState( - Object.keys(templates).length > 0 && Object.keys(types).length > 0, - ); const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); const setReactFlowInstance = useFlowStore( @@ -166,7 +165,12 @@ export default function Page({ } }, [currentFlowId, reactFlowInstance]); - const { isFetching } = useGetBuildsQuery({}); + const { isFetching, refetch } = useGetBuildsQuery({}); + + const showCanvas = + Object.keys(templates).length > 0 && + Object.keys(types).length > 0 && + !isFetching; useEffect(() => { if (!isFetching) { @@ -187,13 +191,14 @@ export default function Page({ "Components created before Langflow 1.0 may be unstable. Ensure components are up to date.", }); } - }, []); + }, [currentFlowId]); useEffect(() => { + refetch(); return () => { cleanFlow(); }; - }, []); + }, [currentFlowId]); function handleUndo(e: KeyboardEvent) { if (!isWrappedWithClass(e, "noflow")) { @@ -315,12 +320,6 @@ export default function Page({ //@ts-ignore useHotkeys("delete", handleDelete); - useEffect(() => { - setSHowCanvas( - Object.keys(templates).length > 0 && Object.keys(types).length > 0, - ); - }, [templates, types]); - const onConnectMod = useCallback( (params: Connection) => { takeSnapshot(); @@ -387,28 +386,24 @@ export default function Page({ ); } else if (event.dataTransfer.types.some((types) => types === "Files")) { takeSnapshot(); - if (event.dataTransfer.files.item(0)!.type === "application/json") { - const position = { - x: event.clientX, - y: event.clientY, - }; - uploadFlow({ - newProject: false, - isComponent: false, - file: event.dataTransfer.files.item(0)!, - position: position, - }).catch((error) => { - setErrorData({ - title: UPLOAD_ERROR_ALERT, - list: [error], - }); - }); - } else { + const position = { + x: event.clientX, + y: event.clientY, + }; + uploadFlow({ + files: Array.from(event.dataTransfer.files!), + position: position, + }).catch((error) => { setErrorData({ - title: WRONG_FILE_ERROR_ALERT, - list: [UPLOAD_ALERT_LIST], + title: UPLOAD_ERROR_ALERT, + list: [(error as Error).message], }); - } + }); + } else { + setErrorData({ + title: WRONG_FILE_ERROR_ALERT, + list: [UPLOAD_ALERT_LIST], + }); } }, // Specify dependencies for useCallback @@ -533,7 +528,9 @@ export default function Page({ ) : ( - <> +
+ +
)} ); diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 6cc4bb090..d2c80c02d 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -1,6 +1,7 @@ import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value"; import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; import { usePostRetrieveVertexOrder } from "@/controllers/API/queries/vertex"; +import useAddFlow from "@/hooks/flows/use-add-flow"; import { APIClassType } from "@/types/api"; import _, { cloneDeep } from "lodash"; import { useEffect, useState } from "react"; @@ -82,6 +83,8 @@ export default function NodeToolbarComponent({ const shortcuts = useShortcutsStore((state) => state.shortcuts); const unselectAll = useFlowStore((state) => state.unselectAll); const currentFlow = useFlowsManagerStore((state) => state.currentFlow); + const addFlow = useAddFlow(); + function handleMinimizeWShortcut(e: KeyboardEvent) { if (isWrappedWithClass(e, "noflow")) return; e.preventDefault(); @@ -134,7 +137,10 @@ export default function NodeToolbarComponent({ return; } if (hasCode && !isSaved) { - saveComponent(cloneDeep(data), false); + addFlow({ + flow: flowComponent, + override: false, + }); setSuccessData({ title: `${data.id} saved successfully` }); return; } @@ -208,7 +214,6 @@ export default function NodeToolbarComponent({ const setNodes = useFlowStore((state) => state.setNodes); const setEdges = useFlowStore((state) => state.setEdges); - const saveComponent = useFlowsManagerStore((state) => state.saveComponent); const getNodePosition = useFlowStore((state) => state.getNodePosition); const flows = useFlowsManagerStore((state) => state.flows); const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot); @@ -257,7 +262,10 @@ export default function NodeToolbarComponent({ if (isSaved) { return setShowOverrideModal(true); } - saveComponent(cloneDeep(data), false); + addFlow({ + flow: flowComponent, + override: false, + }); break; case "freeze": setNode(data.id, (old) => ({ @@ -291,7 +299,10 @@ export default function NodeToolbarComponent({ downloadNode(flowComponent!); break; case "SaveAll": - saveComponent(cloneDeep(data), false); + addFlow({ + flow: flowComponent, + override: false, + }); break; case "documentation": if (data.node?.documentation) openInNewTab(data.node?.documentation); @@ -695,12 +706,18 @@ export default function NodeToolbarComponent({ icon={"SaveAll"} index={6} onConfirm={(index, user) => { - saveComponent(cloneDeep(data), true); + addFlow({ + flow: flowComponent, + override: true, + }); setSuccessData({ title: `${data.id} successfully overridden!` }); }} onClose={setShowOverrideModal} onCancel={() => { - saveComponent(cloneDeep(data), false); + addFlow({ + flow: flowComponent, + override: true, + }); setSuccessData({ title: "New component successfully saved!" }); }} > diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx index d0ae38e4f..fb31922f9 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/hooks/use-handle-duplicate.tsx @@ -1,16 +1,8 @@ +import useAddFlow from "@/hooks/flows/use-add-flow"; import { useCallback } from "react"; -import { XYPosition } from "reactflow"; -import { FlowType } from "../../../../../types/flow"; const useDuplicateFlows = ( selectedFlowsComponentsCards: string[], - addFlow: ( - newProject: boolean, - flow?: FlowType, - override?: boolean, - position?: XYPosition, - fromDragAndDrop?: boolean, - ) => Promise, allFlows: any[], resetFilter: () => void, getFoldersApi: ( @@ -27,13 +19,11 @@ const useDuplicateFlows = ( handleSelectAll: (select: boolean) => void, cardTypes: string, ) => { + const addFlow = useAddFlow(); const handleDuplicate = useCallback(() => { Promise.all( selectedFlowsComponentsCards.map((selectedFlow) => - addFlow( - true, - allFlows.find((flow) => flow.id === selectedFlow), - ), + addFlow({ flow: allFlows.find((flow) => flow.id === selectedFlow) }), ), ).then(() => { resetFilter(); diff --git a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx index 6a7b98e0f..b7884cf88 100644 --- a/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx +++ b/src/frontend/src/pages/MainPage/components/componentsComponent/index.tsx @@ -17,7 +17,6 @@ import { sortFlows } from "../../utils/sort-flows"; import EmptyComponent from "../emptyComponent"; import HeaderComponent from "../headerComponent"; import CollectionCard from "./components/collectionCard"; -import useDeleteMultipleFlows from "./hooks/use-delete-multiple"; import useDescriptionModal from "./hooks/use-description-modal"; import useFilteredFlows from "./hooks/use-filtered-flows"; import useDuplicateFlows from "./hooks/use-handle-duplicate"; @@ -30,8 +29,6 @@ export default function ComponentsComponent({ }: { type?: string; }) { - const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); - const removeFlow = useFlowsManagerStore((state) => state.removeFlow); const isLoading = useFlowsManagerStore((state) => state.isLoading); const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows); const allFlows = useFlowsManagerStore((state) => state.allFlows); @@ -55,7 +52,7 @@ export default function ComponentsComponent({ (state) => state.selectedFlowsComponentsCards, ); - const [handleFileDrop] = useFileDrop(uploadFlow, type)!; + const handleFileDrop = useFileDrop(type); const [pageSize, setPageSize] = useState(20); const [pageIndex, setPageIndex] = useState(1); const location = useLocation(); @@ -71,7 +68,6 @@ export default function ComponentsComponent({ const myCollectionId = useFolderStore((state) => state.myCollectionId); const getFoldersApi = useFolderStore((state) => state.getFoldersApi); const setFolderUrl = useFolderStore((state) => state.setFolderUrl); - const addFlow = useFlowsManagerStore((state) => state.addFlow); const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders); const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder); @@ -114,7 +110,6 @@ export default function ComponentsComponent({ const { handleDuplicate } = useDuplicateFlows( selectedFlowsComponentsCards, - addFlow, allFlows, resetFilter, getFoldersApi, diff --git a/src/frontend/src/pages/MainPage/components/emptyComponent/index.tsx b/src/frontend/src/pages/MainPage/components/emptyComponent/index.tsx index ef1b406d6..5008aaedb 100644 --- a/src/frontend/src/pages/MainPage/components/emptyComponent/index.tsx +++ b/src/frontend/src/pages/MainPage/components/emptyComponent/index.tsx @@ -1,14 +1,9 @@ import { useState } from "react"; -import { useNavigate } from "react-router-dom"; import NewFlowModal from "../../../../modals/newFlowModal"; -import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; type EmptyComponentProps = {}; const EmptyComponent = ({}: EmptyComponentProps) => { - const addFlow = useFlowsManagerStore((state) => state.addFlow); - const navigate = useNavigate(); - const [openModal, setOpenModal] = useState(false); return ( diff --git a/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx b/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx index 92957d295..cb2f0e705 100644 --- a/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx +++ b/src/frontend/src/pages/MainPage/hooks/use-dropdown-options.tsx @@ -1,31 +1,19 @@ -import { XYPosition } from "reactflow"; +import useUploadFlow from "@/hooks/flows/use-upload-flow"; import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants"; import useAlertStore from "../../../stores/alertStore"; const useDropdownOptions = ({ - uploadFlow, navigate, is_component, }: { - uploadFlow: ({ - newProject, - file, - isComponent, - position, - }: { - newProject: boolean; - file?: File; - isComponent: boolean | null; - position?: XYPosition; - }) => Promise; navigate: (url: string) => void; is_component: boolean; }) => { const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); + const uploadFlow = useUploadFlow(); const handleImportFromJSON = () => { uploadFlow({ - newProject: true, isComponent: is_component, }) .then((id) => { diff --git a/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx b/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx index e47e13013..6cf01f580 100644 --- a/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx +++ b/src/frontend/src/pages/MainPage/hooks/use-on-file-drop.tsx @@ -1,69 +1,27 @@ +import useUploadFlow from "@/hooks/flows/use-upload-flow"; import { useLocation } from "react-router-dom"; -import { XYPosition } from "reactflow"; -import { - CONSOLE_ERROR_MSG, - UPLOAD_ALERT_LIST, - WRONG_FILE_ERROR_ALERT, -} from "../../../constants/alerts_constants"; +import { CONSOLE_ERROR_MSG } from "../../../constants/alerts_constants"; import useAlertStore from "../../../stores/alertStore"; import { useFolderStore } from "../../../stores/foldersStore"; -const useFileDrop = ( - uploadFlow: ({ - newProject, - file, - isComponent, - position, - }: { - newProject: boolean; - file?: File; - isComponent: boolean | null; - position?: XYPosition; - }) => Promise, - type, -) => { +const useFileDrop = (type?: string) => { const location = useLocation(); const setSuccessData = useAlertStore((state) => state.setSuccessData); const setErrorData = useAlertStore((state) => state.setErrorData); const getFolderById = useFolderStore((state) => state.getFolderById); const folderId = location?.state?.folderId; const myCollectionId = useFolderStore((state) => state.myCollectionId); + const uploadFlow = useUploadFlow(); const handleFileDrop = (e) => { e.preventDefault(); - if (e.dataTransfer.types.some((type) => type === "Files")) { - const files: FileList = e.dataTransfer.files; - - const uploadPromises: Promise[] = []; - - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (file.type === "application/json") { - const reader = new FileReader(); - const FileReaderPromise: Promise = new Promise( - (resolve, reject) => { - reader.onload = (event) => { - const fileContent = event.target!.result; - const fileContentJson = JSON.parse(fileContent as string); - const is_component = fileContentJson.is_component; - uploadFlow({ - newProject: true, - file: file, - isComponent: type === "all" ? null : type === "component", - }) - .then((_) => resolve()) - .catch((error) => { - reject(error); - }); - }; - reader.readAsText(file); - }, - ); - uploadPromises.push(FileReaderPromise); - } - } - - Promise.all(uploadPromises) + if (e.dataTransfer.types.every((type) => type === "Files")) { + const files: File[] = Array.from(e.dataTransfer.files); + uploadFlow({ + files, + isComponent: + type === "component" ? true : type === "flow" ? false : undefined, + }) .then(() => { setSuccessData({ title: `All files uploaded successfully`, @@ -71,14 +29,15 @@ const useFileDrop = ( getFolderById(folderId ? folderId : myCollectionId); }) .catch((error) => { + console.log(error); setErrorData({ title: CONSOLE_ERROR_MSG, - list: [error], + list: [(error as Error).message], }); }); } }; - return [handleFileDrop]; + return handleFileDrop; }; export default useFileDrop; diff --git a/src/frontend/src/pages/MainPage/pages/mainPage/index.tsx b/src/frontend/src/pages/MainPage/pages/mainPage/index.tsx index a2bd6100b..98d26452c 100644 --- a/src/frontend/src/pages/MainPage/pages/mainPage/index.tsx +++ b/src/frontend/src/pages/MainPage/pages/mainPage/index.tsx @@ -16,7 +16,6 @@ import useDropdownOptions from "../../hooks/use-dropdown-options"; import { getFolderById } from "../../services"; export default function HomePage(): JSX.Element { - const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const setCurrentFlowId = useFlowsManagerStore( (state) => state.setCurrentFlowId, ); @@ -41,7 +40,6 @@ export default function HomePage(): JSX.Element { }, [pathname]); const dropdownOptions = useDropdownOptions({ - uploadFlow, navigate, is_component, }); diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 7f0c68040..489cfb1ea 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -294,11 +294,17 @@ const useFlowStore = create((set, get) => ({ selection.nodes.some((node) => node.data.type === "ChatInput") && checkChatInput(get().nodes) ) { - useAlertStore.getState().setErrorData({ - title: "Error pasting components", - list: ["You can only have one ChatInput component in the flow"], + useAlertStore.getState().setNoticeData({ + title: "You can only have one Chat Input component in a flow.", }); - return; + selection.nodes = selection.nodes.filter( + (node) => node.data.type !== "ChatInput", + ); + selection.edges = selection.edges.filter( + (edge) => + selection.nodes.some((node) => edge.source === node.id) && + selection.nodes.some((node) => edge.target === node.id), + ); } if (selection.nodes) { if (checkOldComponents({ nodes: selection.nodes ?? [] })) { diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 04c32444f..e47d815a2 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -1,45 +1,29 @@ -import { brokenEdgeMessage } from "@/utils/utils"; import { AxiosError } from "axios"; import { cloneDeep } from "lodash"; import pDebounce from "p-debounce"; -import { Edge, Node, Viewport, XYPosition } from "reactflow"; +import { Edge, Node, Viewport } from "reactflow"; import { create } from "zustand"; -import { - BROKEN_EDGES_WARNING, - SAVE_DEBOUNCE_TIME, -} from "../constants/constants"; +import { SAVE_DEBOUNCE_TIME } from "../constants/constants"; import { deleteFlowFromDatabase, multipleDeleteFlowsComponents, readFlowsFromDatabase, - saveFlowToDatabase, updateFlowInDatabase, - uploadFlowsToDatabase, } from "../controllers/API"; -import { FlowType, NodeDataType } from "../types/flow"; +import { FlowType } from "../types/flow"; import { FlowsManagerStoreType, UseUndoRedoOptions, } from "../types/zustand/flowsManager"; import { - addVersionToDuplicates, - createFlowComponent, - createNewFlow, - detectBrokenEdgesEdges, extractFieldsFromComponenents, - processDataFromFlow, processFlows, - updateGroupRecursion, } from "../utils/reactflowUtils"; import useAlertStore from "./alertStore"; -import { useDarkStore } from "./darkStore"; import useFlowStore from "./flowStore"; import { useFolderStore } from "./foldersStore"; -import { useGlobalVariablesStore } from "./globalVariablesStore/globalVariables"; import { useTypesStore } from "./typesStore"; -let saveTimeoutId: NodeJS.Timeout | null = null; - const defaultOptions: UseUndoRedoOptions = { maxHistorySize: 100, enableShortcuts: true, @@ -179,148 +163,6 @@ const useFlowsManagerStore = create((set, get) => ({ }); }); }, SAVE_DEBOUNCE_TIME), - 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(); - }); - }, - addFlow: async ( - newProject: Boolean, - flow?: FlowType, - override?: boolean, - position?: XYPosition, - fromDragAndDrop?: boolean, - ): Promise => { - let flowData = flow - ? processDataFromFlow(flow) - : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } }; - flowData?.nodes.forEach((node) => { - updateGroupRecursion( - node, - flowData?.edges, - useGlobalVariablesStore.getState().unavailableFields, - useGlobalVariablesStore.getState().globalVariablesEntries, - ); - }); - if (newProject) { - // Create a new flow with a default name if no flow is provided. - const folder_id = useFolderStore.getState().folderUrl; - const my_collection_id = useFolderStore.getState().myCollectionId; - - if (override) { - get().deleteComponent(flow!.name); - const newFlow = createNewFlow( - flowData!, - flow!, - folder_id || my_collection_id!, - ); - 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 }, - ComponentFields: extractFieldsFromComponenents({ - ...state.data, - ["saved_components"]: data, - }), - })); - }, 200); - // addFlowToLocalState(newFlow); - return; - } - const newFlow = createNewFlow( - flowData!, - flow!, - folder_id || my_collection_id!, - ); - - const newName = addVersionToDuplicates(newFlow, get().flows); - - newFlow.name = newName; - newFlow.folder_id = useFolderStore.getState().folderUrl; - - 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 }, - ComponentFields: extractFieldsFromComponenents({ - ...state.data, - ["saved_components"]: data, - }), - })); - - // Return the id - return id; - } catch (error: any) { - if (error.response?.data?.detail) { - useAlertStore.getState().setErrorData({ - title: "Could not load flows from database", - list: [error.response?.data?.detail], - }); - } else { - useAlertStore.getState().setErrorData({ - title: "Could not load flows from database", - list: [ - error.message ?? "An unexpected error occurred, please try again", - ], - }); - } - throw error; // Re-throw the error so the caller can handle it if needed - } - } else { - let brokenEdges = detectBrokenEdgesEdges( - flow!.data!.nodes, - flow!.data!.edges, - ); - if (brokenEdges.length > 0) { - useAlertStore.getState().setErrorData({ - title: BROKEN_EDGES_WARNING, - list: brokenEdges.map((edge) => brokenEdgeMessage(edge)), - }); - } - useFlowStore - .getState() - .paste( - { nodes: flow!.data!.nodes, edges: flow!.data!.edges }, - position ?? { x: 10, y: 10 }, - ); - } - }, removeFlow: async (id: string | string[]) => { return new Promise((resolve, reject) => { if (Array.isArray(id)) { @@ -381,86 +223,6 @@ const useFlowsManagerStore = create((set, get) => ({ } }); }, - uploadFlow: async ({ - newProject, - file, - isComponent, - position = { x: 10, y: 10 }, - }: { - newProject: boolean; - file?: File; - isComponent: boolean | null; - 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 && - isComponent !== null && - ((!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, - true, - ); - 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, - ); - }, takeSnapshot: () => { const currentFlowId = get().currentFlowId; // push the current graph to the past state diff --git a/src/frontend/src/types/zustand/flowsManager/index.ts b/src/frontend/src/types/zustand/flowsManager/index.ts index 234e76ba0..70d7c01c4 100644 --- a/src/frontend/src/types/zustand/flowsManager/index.ts +++ b/src/frontend/src/types/zustand/flowsManager/index.ts @@ -1,4 +1,4 @@ -import { Edge, Node, Viewport, XYPosition } from "reactflow"; +import { Edge, Node, Viewport } from "reactflow"; import { FlowType } from "../../flow"; export type FlowsManagerStoreType = { @@ -30,31 +30,8 @@ export type FlowsManagerStoreType = { edges: Edge[], viewport: Viewport, ) => void; - uploadFlows: () => Promise; - uploadFlow: ({ - newProject, - file, - isComponent, - position, - }: { - newProject: boolean; - file?: File; - isComponent: boolean | null; - position?: XYPosition; - }) => Promise; - addFlow: ( - newProject: boolean, - flow?: FlowType, - override?: boolean, - position?: XYPosition, - fromDragAndDrop?: boolean, - ) => Promise; deleteComponent: (key: string) => Promise; removeFlow: (id: string | string[]) => Promise; - saveComponent: ( - component: any, - override: boolean, - ) => Promise; undo: () => void; redo: () => void; takeSnapshot: () => void; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 3c2089f10..58541adb5 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -1527,8 +1527,8 @@ export function getRandomDescription(): string { export const createNewFlow = ( flowData: ReactFlowJsonObject, - flow: FlowType, folderId: string, + flow?: FlowType, ) => { return { description: flow?.description ?? getRandomDescription(), diff --git a/src/frontend/tests/end-to-end/dragAndDrop.spec.ts b/src/frontend/tests/end-to-end/dragAndDrop.spec.ts index a1d04ade1..d9b98190d 100644 --- a/src/frontend/tests/end-to-end/dragAndDrop.spec.ts +++ b/src/frontend/tests/end-to-end/dragAndDrop.spec.ts @@ -43,7 +43,7 @@ test.describe("drag and drop test", () => { // Now dispatch await page.dispatchEvent( - '//*[@id="root"]/div/div[1]/div[2]/div[3]/div/div', + '//*[@id="root"]/div/div[2]/div[2]/div[3]/div', "drop", { dataTransfer, diff --git a/src/frontend/tests/end-to-end/folders.spec.ts b/src/frontend/tests/end-to-end/folders.spec.ts index f7e442587..a45bf5dc3 100644 --- a/src/frontend/tests/end-to-end/folders.spec.ts +++ b/src/frontend/tests/end-to-end/folders.spec.ts @@ -77,7 +77,7 @@ test("add folder by drag and drop", async ({ page }) => { // Wait for the target element to be available before evaluation await page.waitForSelector( - '//*[@id="root"]/div/div[1]/div[2]/div[3]/aside/nav/div/div[2]', + '//*[@id="root"]/div/div[2]/div[2]/div[3]/aside/nav/div/div[2]', ); // Create the DataTransfer and File @@ -93,7 +93,7 @@ test("add folder by drag and drop", async ({ page }) => { // Now dispatch await page.dispatchEvent( - '//*[@id="root"]/div/div[1]/div[2]/div[3]/aside/nav/div/div[2]', + '//*[@id="root"]/div/div[2]/div[2]/div[3]/aside/nav/div/div[2]', "drop", { dataTransfer,