feat: change download flows to download as .zip file (#2870)

* 📝 (flows.py): Add endpoint to download multiple flows as a zip file
📝 (constants.ts): Add FLOWS constant for API endpoint
📝 (index.ts): Add use-post-download-multiple-flows query function
📝 (use-post-download-multiple-flows.ts): Implement function to download multiple flows as a zip file
📝 (index.tsx): Import usePostDownloadMultipleFlows in componentsComponent
📝 (index.tsx): Implement handleExport function to download multiple flows as a zip file
📝 (index.tsx): Update handleExport function to handle downloading and saving the zip file
📝 (headerComponent.tsx): Add shouldSelectAll state and setShouldSelectAll function to manage select all functionality

* formatting file

* 📝 (flows.py): Remove download_file endpoint and refactor download_multiple_file to support downloading multiple flows as a zip file
📝 (index.tsx): Refactor export functionality to handle exporting multiple flows as a zip file or a single flow as a JSON file
🔧 (use-post-download-multiple-flows.ts): Update API endpoint for downloading flows
🔧 (index.tsx): Remove unused imports and functions related to exporting flows
🔧 (reactflowUtils.ts): Remove unused downloadFlows function and related imports

* [autofix.ci] apply automated fixes

* 📝 (flows.py): remove unnecessary whitespace and improve code readability by aligning function parameters in download_multiple_file function

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2024-07-23 11:41:55 -03:00 • committed by GitHub
commit 61265cc327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 170 additions and 129 deletions

View file

@ -13,6 +13,8 @@ export const URLs = {
AUTOLOGIN: "auto_login",
REFRESH: "refresh",
BUILD: `build`,
CUSTOM_COMPONENT: `custom_component`,
FLOWS: `flows`,
FOLDERS: `folders`,
} as const;

View file

@ -197,19 +197,6 @@ export async function readFlowsFromDatabase() {
}
}
export async function downloadFlowsFromDatabase() {
try {
const response = await api.get(`${BASE_URL_API}flows/download/`);
if (response && response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
export async function uploadFlowsToDatabase(flows: FormData) {
try {
const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);

View file

@ -0,0 +1 @@
export * from "./use-post-download-multiple-flows";

View file

@ -0,0 +1,43 @@
import { useMutationFunctionType } from "@/types/api";
import { UseMutationResult } from "@tanstack/react-query";
import { api } from "../../api";
import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor";
interface IPostDownloadMultipleFlows {
flow_ids: string[];
}
export const usePostDownloadMultipleFlows: useMutationFunctionType<
undefined,
IPostDownloadMultipleFlows
> = (options?) => {
const { mutate } = UseRequestProcessor();
const postDownloadMultipleFlowsFn = async (
payload: IPostDownloadMultipleFlows,
): Promise<any> => {
const response = await api.post<any>(
`${getURL("FLOWS")}/download/`,
payload.flow_ids,
{ responseType: "blob" },
);
return response.data;
};
const mutation: UseMutationResult<
IPostDownloadMultipleFlows,
any,
IPostDownloadMultipleFlows
> = mutate(
["usePostDownloadMultipleFlows"],
async (payload: IPostDownloadMultipleFlows) => {
const res = await postDownloadMultipleFlowsFn(payload);
return res;
},
options,
);
return mutation;
};

View file

@ -1,54 +0,0 @@
import { useCallback } from "react";
import { FlowType } from "../../../../../types/flow";
const useExportFlows = (
selectedFlowsComponentsCards: string[],
allFlows: Array<FlowType>,
downloadFlow: (flow: any, name: string, description: string) => void,
removeApiKeys: (flow: any) => any,
version: string,
setSuccessData: (data: { title: string }) => void,
setSelectedFlowsComponentsCards: (
selectedFlowsComponentsCards: string[],
) => void,
handleSelectAll: (select: boolean) => void,
cardTypes: string,
) => {
const handleExport = useCallback(() => {
selectedFlowsComponentsCards.forEach((selectedFlowId) => {
const selectedFlow = allFlows.find((flow) => flow.id === selectedFlowId);
if (selectedFlow) {
downloadFlow(
removeApiKeys({
id: selectedFlow.id,
data: selectedFlow.data,
description: selectedFlow.description,
name: selectedFlow.name,
last_tested_version: version,
endpoint_name: selectedFlow.endpoint_name,
is_component: false,
}),
selectedFlow.name,
selectedFlow.description,
);
}
});
setSuccessData({ title: `${cardTypes} exported successfully` });
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
}, [
selectedFlowsComponentsCards,
allFlows,
downloadFlow,
removeApiKeys,
version,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
]);
return { handleExport };
};
export default useExportFlows;

View file

@ -1,18 +1,15 @@
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
import useAlertStore from "../../../../stores/alertStore";
import { useDarkStore } from "../../../../stores/darkStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { FlowType } from "../../../../types/flow";
import { downloadFlow, removeApiKeys } from "../../../../utils/reactflowUtils";
import useFileDrop from "../../hooks/use-on-file-drop";
import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows";
@ -23,7 +20,6 @@ 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";
import useExportFlows from "./hooks/use-handle-export";
import useSelectAll from "./hooks/use-handle-select-all";
import useSelectOptionsChange from "./hooks/use-select-options-change";
import useSelectedFlows from "./hooks/use-selected-flows";
@ -78,6 +74,8 @@ export default function ComponentsComponent({
const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders);
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
const [shouldSelectAll, setShouldSelectAll] = useState(true);
const cardTypes = useMemo(() => {
if (window.location.pathname.includes("components")) {
return "Components";
@ -128,19 +126,59 @@ export default function ComponentsComponent({
cardTypes,
);
const version = useDarkStore((state) => state.version);
const { mutate: mutateDownloadMultipleFlows } =
usePostDownloadMultipleFlows();
const { handleExport } = useExportFlows(
selectedFlowsComponentsCards,
allFlows,
downloadFlow,
removeApiKeys,
version,
setSuccessData,
setSelectedFlowsComponentsCards,
handleSelectAll,
cardTypes,
);
const handleExport = () => {
mutateDownloadMultipleFlows(
{
flow_ids: selectedFlowsComponentsCards,
},
{
onSuccess: (data) => {
const selectedFlow = allFlows.find(
(flow) => flow.id === selectedFlowsComponentsCards[0],
);
const blobType =
selectedFlowsComponentsCards.length > 1
? "application/zip"
: "application/json";
const fileNameSuffix =
selectedFlowsComponentsCards.length > 1
? "_langflow_flows.zip"
: `${selectedFlow!.name}.json`;
const blob = new Blob([data], { type: blobType });
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
let current_time = new Date().toISOString().replace(/[:.]/g, "");
current_time = current_time
.replace(/-/g, "")
.replace(/T/g, "")
.replace(/Z/g, "");
link.download =
selectedFlowsComponentsCards.length > 1
? `${current_time}${fileNameSuffix}`
: `${fileNameSuffix}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setSuccessData({ title: `${cardTypes} exported successfully` });
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
setShouldSelectAll(true);
},
},
);
};
const { handleSelectOptionsChange } = useSelectOptionsChange(
selectedFlowsComponentsCards,
@ -184,6 +222,8 @@ export default function ComponentsComponent({
<div className="flex w-full gap-4 pb-5">
{allFlows?.length > 0 && (
<HeaderComponent
shouldSelectAll={shouldSelectAll}
setShouldSelectAll={setShouldSelectAll}
handleDelete={() => handleSelectOptionsChange("delete")}
handleSelectAll={handleSelectAll}
handleDuplicate={() => handleSelectOptionsChange("duplicate")}

View file

@ -11,6 +11,8 @@ type HeaderComponentProps = {
handleDuplicate: () => void;
handleExport: () => void;
disableFunctions: boolean;
setShouldSelectAll: (select) => void;
shouldSelectAll: boolean;
};
const HeaderComponent = ({
@ -19,9 +21,9 @@ const HeaderComponent = ({
handleDuplicate,
handleExport,
disableFunctions,
setShouldSelectAll,
shouldSelectAll,
}: HeaderComponentProps) => {
const [shouldSelectAll, setShouldSelectAll] = useState(true);
const handleClick = () => {
handleSelectAll(shouldSelectAll);
setShouldSelectAll((prevState) => !prevState);

View file

@ -17,7 +17,6 @@ import {
SUCCESS_BUILD,
specialCharsRegex,
} from "../constants/constants";
import { downloadFlowsFromDatabase } from "../controllers/API";
import { DESCRIPTIONS } from "../flow_constants";
import {
APIClassType,
@ -1423,22 +1422,6 @@ export function downloadFlow(
link.click();
}
export function downloadFlows() {
downloadFlowsFromDatabase().then((flows) => {
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify(flows),
)}`;
// create a link element and set its properties
const link = document.createElement("a");
link.href = jsonString;
link.download = `flows.json`;
// simulate a click on the link element to trigger the download
link.click();
});
}
export function getRandomElement<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
}