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,