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>
This commit is contained in:
Lucas Oliveira 2024-08-06 18:52:47 -03:00 committed by GitHub
commit 4d66bf351d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 543 additions and 569 deletions

View file

@ -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() {
></FetchErrorComponent>
}
<Case condition={isLoadingApplication}>
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
</Case>
<div
className={cn(
"loading-page-panel absolute left-0 top-0 z-[999]",
isLoadingApplication ? "" : "hidden",
)}
>
<LoadingComponent remSize={50} />
</div>
<Case condition={!isLoadingApplication}>
<Router />
</Case>
<Router />
</>
</ErrorBoundary>
<div></div>

View file

@ -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.`,

View file

@ -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 (
<Card
@ -77,7 +77,7 @@ export default function CollectionCardComponent({
<Button
onClick={() => {
updateIds(flow.data!);
addFlow(true, flow).then((id) => {
addFlow({ flow }).then((id) => {
navigate("/flow/" + id);
});
}}

View file

@ -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 => {
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
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],
});
},
);
});
}}
>
<IconComponent name="FileUp" className="header-menu-options" />

View file

@ -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}
>
<IconComponent

View file

@ -4,6 +4,9 @@ import {
usePostUploadFolders,
} from "@/controllers/API/queries/folders";
import { useGetDownloadFolders } from "@/controllers/API/queries/folders/use-get-download-folders";
import { createFileUpload } from "@/helpers/create-file-upload";
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
@ -55,6 +58,7 @@ const SideBarFoldersButtonsComponent = ({
const setErrorData = useAlertStore((state) => 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<any>(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();

View file

@ -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<any>} 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,

View file

@ -8,9 +8,10 @@ interface IDeleteBuilds {
}
// add types for error handling and success
export const useDeleteBuilds: useMutationFunctionType<undefined,IDeleteBuilds> = (
options,
) => {
export const useDeleteBuilds: useMutationFunctionType<
undefined,
IDeleteBuilds
> = (options) => {
const { mutate } = UseRequestProcessor();
const deleteBuildsFn = async (payload: IDeleteBuilds): Promise<any> => {

View file

@ -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;

View file

@ -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<any> => {
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<IPatchUpdateFlow, any, IPatchUpdateFlow> =
mutate(
["usePatchUpdateFlow", { id: params.id }],
PatchUpdateFlowFn,
options,
);
return mutation;
};

View file

@ -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<any> => {
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<IPostAddFlow, any, IPostAddFlow> = mutate(
["usePostAddFlow"],
postAddFlowFn,
options,
);
return mutation;
};

View file

@ -32,10 +32,7 @@ export const usePostDownloadMultipleFlows: useMutationFunctionType<
IPostDownloadMultipleFlows
> = mutate(
["usePostDownloadMultipleFlows"],
async (payload: IPostDownloadMultipleFlows) => {
const res = await postDownloadMultipleFlowsFn(payload);
return res;
},
postDownloadMultipleFlowsFn,
options,
);

View file

@ -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;
};

View file

@ -0,0 +1,35 @@
export async function createFileUpload(props?: {
accept?: string;
multiple?: boolean;
}): Promise<File[]> {
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();
});
}

View file

@ -0,0 +1,9 @@
export async function getObjectsFromFilelist<T>(files: File[]): Promise<T[]> {
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;
}

View file

@ -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;

View file

@ -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<FlowType[]> => {
const objectList = await getObjectsFromFilelist<any>(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<FlowType[]> => {
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<void> => {
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;

View file

@ -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 (

View file

@ -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 (
<Card
onClick={() => {
addFlow(true).then((id) => {
addFlow().then((id) => {
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
});

View file

@ -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({
<Card
onClick={() => {
updateIds(flow.data!);
addFlow(true, flow).then((id) => {
addFlow({ flow }).then((id) => {
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}/folder/${folderIdUrl}`);
});

View file

@ -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<HTMLDivElement>(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({
</ReactFlow>
</div>
) : (
<></>
<div className="flex h-full w-full items-center justify-center">
<LoadingComponent remSize={30} />
</div>
)}
</div>
);

View file

@ -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!" });
}}
>

View file

@ -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<string | undefined>,
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();

View file

@ -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,

View file

@ -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 (

View file

@ -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<string | never>;
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) => {

View file

@ -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<string | never>,
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<any>[] = [];
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type === "application/json") {
const reader = new FileReader();
const FileReaderPromise: Promise<void> = 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;

View file

@ -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,
});

View file

@ -294,11 +294,17 @@ const useFlowStore = create<FlowStoreType>((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 ?? [] })) {

View file

@ -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<FlowsManagerStoreType>((set, get) => ({
});
});
}, SAVE_DEBOUNCE_TIME),
uploadFlows: () => {
return new Promise<void>((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<string | undefined> => {
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<void>((resolve, reject) => {
if (Array.isArray(id)) {
@ -381,86 +223,6 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
}
});
},
uploadFlow: async ({
newProject,
file,
isComponent,
position = { x: 10, y: 10 },
}: {
newProject: boolean;
file?: File;
isComponent: boolean | null;
position?: XYPosition;
}): Promise<string | never> => {
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

View file

@ -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<void>;
uploadFlow: ({
newProject,
file,
isComponent,
position,
}: {
newProject: boolean;
file?: File;
isComponent: boolean | null;
position?: XYPosition;
}) => Promise<string | never>;
addFlow: (
newProject: boolean,
flow?: FlowType,
override?: boolean,
position?: XYPosition,
fromDragAndDrop?: boolean,
) => Promise<string | undefined>;
deleteComponent: (key: string) => Promise<void>;
removeFlow: (id: string | string[]) => Promise<void>;
saveComponent: (
component: any,
override: boolean,
) => Promise<string | undefined>;
undo: () => void;
redo: () => void;
takeSnapshot: () => void;

View file

@ -1527,8 +1527,8 @@ export function getRandomDescription(): string {
export const createNewFlow = (
flowData: ReactFlowJsonObject,
flow: FlowType,
folderId: string,
flow?: FlowType,
) => {
return {
description: flow?.description ?? getRandomDescription(),

View file

@ -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,

View file

@ -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,