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:
parent
2db87e89bf
commit
61265cc327
10 changed files with 170 additions and 129 deletions
|
|
@ -1,13 +1,17 @@
|
||||||
|
import io
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List
|
from typing import List
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
import orjson
|
import orjson
|
||||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from sqlmodel import Session, col, select
|
from sqlmodel import Session, and_, col, select
|
||||||
|
|
||||||
from langflow.api.utils import remove_api_keys, validate_is_component
|
from langflow.api.utils import remove_api_keys, validate_is_component
|
||||||
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
from langflow.api.v1.schemas import FlowListCreate, FlowListRead
|
||||||
|
|
@ -314,18 +318,6 @@ async def upload_file(
|
||||||
return response_list
|
return response_list
|
||||||
|
|
||||||
|
|
||||||
@router.get("/download/", response_model=FlowListRead, status_code=200)
|
|
||||||
async def download_file(
|
|
||||||
*,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
settings_service: "SettingsService" = Depends(get_settings_service),
|
|
||||||
current_user: User = Depends(get_current_active_user),
|
|
||||||
):
|
|
||||||
"""Download all flows as a file."""
|
|
||||||
flows = read_flows(current_user=current_user, session=session, settings_service=settings_service)
|
|
||||||
return FlowListRead(flows=flows)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/")
|
@router.delete("/")
|
||||||
async def delete_multiple_flows(
|
async def delete_multiple_flows(
|
||||||
flow_ids: List[UUID], user: User = Depends(get_current_active_user), db: Session = Depends(get_session)
|
flow_ids: List[UUID], user: User = Depends(get_current_active_user), db: Session = Depends(get_session)
|
||||||
|
|
@ -350,3 +342,46 @@ async def delete_multiple_flows(
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/download/", status_code=200)
|
||||||
|
async def download_multiple_file(
|
||||||
|
flow_ids: List[UUID],
|
||||||
|
user: User = Depends(get_current_active_user),
|
||||||
|
db: Session = Depends(get_session),
|
||||||
|
):
|
||||||
|
"""Download all flows as a zip file."""
|
||||||
|
flows = db.exec(select(Flow).where(and_(Flow.user_id == user.id, Flow.id.in_(flow_ids)))).all() # type: ignore
|
||||||
|
|
||||||
|
if not flows:
|
||||||
|
raise HTTPException(status_code=404, detail="No flows found.")
|
||||||
|
|
||||||
|
flows_without_api_keys = [remove_api_keys(flow.model_dump()) for flow in flows]
|
||||||
|
|
||||||
|
if len(flows_without_api_keys) > 1:
|
||||||
|
# Create a byte stream to hold the ZIP file
|
||||||
|
zip_stream = io.BytesIO()
|
||||||
|
|
||||||
|
# Create a ZIP file
|
||||||
|
with zipfile.ZipFile(zip_stream, "w") as zip_file:
|
||||||
|
for flow in flows_without_api_keys:
|
||||||
|
# Convert the flow object to JSON
|
||||||
|
flow_json = json.dumps(jsonable_encoder(flow))
|
||||||
|
|
||||||
|
# Write the JSON to the ZIP file
|
||||||
|
zip_file.writestr(f"{flow['name']}.json", flow_json)
|
||||||
|
|
||||||
|
# Seek to the beginning of the byte stream
|
||||||
|
zip_stream.seek(0)
|
||||||
|
|
||||||
|
# Generate the filename with the current datetime
|
||||||
|
current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"{current_time}_langflow_flows.zip"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
zip_stream,
|
||||||
|
media_type="application/x-zip-compressed",
|
||||||
|
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return FlowListRead(flows=flows_without_api_keys)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import json
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
|
@ -197,26 +198,27 @@ def test_download_file(
|
||||||
)
|
)
|
||||||
db_manager = get_db_service()
|
db_manager = get_db_service()
|
||||||
with session_getter(db_manager) as session:
|
with session_getter(db_manager) as session:
|
||||||
|
saved_flows = []
|
||||||
for flow in flow_list.flows:
|
for flow in flow_list.flows:
|
||||||
flow.user_id = active_user.id
|
flow.user_id = active_user.id
|
||||||
db_flow = Flow.model_validate(flow, from_attributes=True)
|
db_flow = Flow.model_validate(flow, from_attributes=True)
|
||||||
session.add(db_flow)
|
session.add(db_flow)
|
||||||
|
saved_flows.append(db_flow)
|
||||||
session.commit()
|
session.commit()
|
||||||
# Make request to endpoint
|
# Make request to endpoint inside the session context
|
||||||
response = client.get("api/v1/flows/download/", headers=logged_in_headers)
|
flow_ids = [str(db_flow.id) for db_flow in saved_flows] # Convert UUIDs to strings
|
||||||
|
flow_ids_json = json.dumps(flow_ids)
|
||||||
|
response = client.post(
|
||||||
|
"api/v1/flows/download/",
|
||||||
|
data=flow_ids_json,
|
||||||
|
headers={**logged_in_headers, "Content-Type": "application/json"},
|
||||||
|
)
|
||||||
# Check response status code
|
# Check response status code
|
||||||
assert response.status_code == 200, response.json()
|
assert response.status_code == 200, response.json()
|
||||||
# Check response data
|
# Check response data
|
||||||
response_data = response.json()["flows"]
|
# Since the endpoint now returns a zip file, we need to check the content type and the filename in the headers
|
||||||
starter_projects = load_starter_projects()
|
assert response.headers["Content-Type"] == "application/x-zip-compressed"
|
||||||
number_of_projects = len(starter_projects) + len(flow_list.flows)
|
assert "attachment; filename=" in response.headers["Content-Disposition"]
|
||||||
assert len(response_data) == number_of_projects, response_data
|
|
||||||
assert response_data[0]["name"] == "Flow 1"
|
|
||||||
assert response_data[0]["description"] == "description"
|
|
||||||
assert response_data[0]["data"] == data
|
|
||||||
assert response_data[1]["name"] == "Flow 2"
|
|
||||||
assert response_data[1]["description"] == "description"
|
|
||||||
assert response_data[1]["data"] == data
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_flow_with_invalid_data(client: TestClient, active_user, logged_in_headers):
|
def test_create_flow_with_invalid_data(client: TestClient, active_user, logged_in_headers):
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ export const URLs = {
|
||||||
AUTOLOGIN: "auto_login",
|
AUTOLOGIN: "auto_login",
|
||||||
REFRESH: "refresh",
|
REFRESH: "refresh",
|
||||||
BUILD: `build`,
|
BUILD: `build`,
|
||||||
|
CUSTOM_COMPONENT: `custom_component`,
|
||||||
|
FLOWS: `flows`,
|
||||||
FOLDERS: `folders`,
|
FOLDERS: `folders`,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
export async function uploadFlowsToDatabase(flows: FormData) {
|
||||||
try {
|
try {
|
||||||
const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);
|
const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);
|
||||||
|
|
|
||||||
1
src/frontend/src/controllers/API/queries/flows/index.ts
Normal file
1
src/frontend/src/controllers/API/queries/flows/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./use-post-download-multiple-flows";
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
|
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
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 CardsWrapComponent from "../../../../components/cardsWrapComponent";
|
||||||
import IconComponent from "../../../../components/genericIconComponent";
|
|
||||||
import PaginatorComponent from "../../../../components/paginatorComponent";
|
import PaginatorComponent from "../../../../components/paginatorComponent";
|
||||||
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
|
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
|
||||||
import { Button } from "../../../../components/ui/button";
|
|
||||||
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
|
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
|
||||||
import useAlertStore from "../../../../stores/alertStore";
|
import useAlertStore from "../../../../stores/alertStore";
|
||||||
import { useDarkStore } from "../../../../stores/darkStore";
|
|
||||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||||
import { useFolderStore } from "../../../../stores/foldersStore";
|
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||||
import { FlowType } from "../../../../types/flow";
|
import { FlowType } from "../../../../types/flow";
|
||||||
import { downloadFlow, removeApiKeys } from "../../../../utils/reactflowUtils";
|
|
||||||
import useFileDrop from "../../hooks/use-on-file-drop";
|
import useFileDrop from "../../hooks/use-on-file-drop";
|
||||||
import { getNameByType } from "../../utils/get-name-by-type";
|
import { getNameByType } from "../../utils/get-name-by-type";
|
||||||
import { sortFlows } from "../../utils/sort-flows";
|
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 useDescriptionModal from "./hooks/use-description-modal";
|
||||||
import useFilteredFlows from "./hooks/use-filtered-flows";
|
import useFilteredFlows from "./hooks/use-filtered-flows";
|
||||||
import useDuplicateFlows from "./hooks/use-handle-duplicate";
|
import useDuplicateFlows from "./hooks/use-handle-duplicate";
|
||||||
import useExportFlows from "./hooks/use-handle-export";
|
|
||||||
import useSelectAll from "./hooks/use-handle-select-all";
|
import useSelectAll from "./hooks/use-handle-select-all";
|
||||||
import useSelectOptionsChange from "./hooks/use-select-options-change";
|
import useSelectOptionsChange from "./hooks/use-select-options-change";
|
||||||
import useSelectedFlows from "./hooks/use-selected-flows";
|
import useSelectedFlows from "./hooks/use-selected-flows";
|
||||||
|
|
@ -78,6 +74,8 @@ export default function ComponentsComponent({
|
||||||
const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders);
|
const isLoadingFolders = useFolderStore((state) => state.isLoadingFolders);
|
||||||
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
|
const setSelectedFolder = useFolderStore((state) => state.setSelectedFolder);
|
||||||
|
|
||||||
|
const [shouldSelectAll, setShouldSelectAll] = useState(true);
|
||||||
|
|
||||||
const cardTypes = useMemo(() => {
|
const cardTypes = useMemo(() => {
|
||||||
if (window.location.pathname.includes("components")) {
|
if (window.location.pathname.includes("components")) {
|
||||||
return "Components";
|
return "Components";
|
||||||
|
|
@ -128,19 +126,59 @@ export default function ComponentsComponent({
|
||||||
cardTypes,
|
cardTypes,
|
||||||
);
|
);
|
||||||
|
|
||||||
const version = useDarkStore((state) => state.version);
|
const { mutate: mutateDownloadMultipleFlows } =
|
||||||
|
usePostDownloadMultipleFlows();
|
||||||
|
|
||||||
const { handleExport } = useExportFlows(
|
const handleExport = () => {
|
||||||
selectedFlowsComponentsCards,
|
mutateDownloadMultipleFlows(
|
||||||
allFlows,
|
{
|
||||||
downloadFlow,
|
flow_ids: selectedFlowsComponentsCards,
|
||||||
removeApiKeys,
|
},
|
||||||
version,
|
{
|
||||||
setSuccessData,
|
onSuccess: (data) => {
|
||||||
setSelectedFlowsComponentsCards,
|
const selectedFlow = allFlows.find(
|
||||||
handleSelectAll,
|
(flow) => flow.id === selectedFlowsComponentsCards[0],
|
||||||
cardTypes,
|
);
|
||||||
);
|
|
||||||
|
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(
|
const { handleSelectOptionsChange } = useSelectOptionsChange(
|
||||||
selectedFlowsComponentsCards,
|
selectedFlowsComponentsCards,
|
||||||
|
|
@ -184,6 +222,8 @@ export default function ComponentsComponent({
|
||||||
<div className="flex w-full gap-4 pb-5">
|
<div className="flex w-full gap-4 pb-5">
|
||||||
{allFlows?.length > 0 && (
|
{allFlows?.length > 0 && (
|
||||||
<HeaderComponent
|
<HeaderComponent
|
||||||
|
shouldSelectAll={shouldSelectAll}
|
||||||
|
setShouldSelectAll={setShouldSelectAll}
|
||||||
handleDelete={() => handleSelectOptionsChange("delete")}
|
handleDelete={() => handleSelectOptionsChange("delete")}
|
||||||
handleSelectAll={handleSelectAll}
|
handleSelectAll={handleSelectAll}
|
||||||
handleDuplicate={() => handleSelectOptionsChange("duplicate")}
|
handleDuplicate={() => handleSelectOptionsChange("duplicate")}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ type HeaderComponentProps = {
|
||||||
handleDuplicate: () => void;
|
handleDuplicate: () => void;
|
||||||
handleExport: () => void;
|
handleExport: () => void;
|
||||||
disableFunctions: boolean;
|
disableFunctions: boolean;
|
||||||
|
setShouldSelectAll: (select) => void;
|
||||||
|
shouldSelectAll: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeaderComponent = ({
|
const HeaderComponent = ({
|
||||||
|
|
@ -19,9 +21,9 @@ const HeaderComponent = ({
|
||||||
handleDuplicate,
|
handleDuplicate,
|
||||||
handleExport,
|
handleExport,
|
||||||
disableFunctions,
|
disableFunctions,
|
||||||
|
setShouldSelectAll,
|
||||||
|
shouldSelectAll,
|
||||||
}: HeaderComponentProps) => {
|
}: HeaderComponentProps) => {
|
||||||
const [shouldSelectAll, setShouldSelectAll] = useState(true);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
handleSelectAll(shouldSelectAll);
|
handleSelectAll(shouldSelectAll);
|
||||||
setShouldSelectAll((prevState) => !prevState);
|
setShouldSelectAll((prevState) => !prevState);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import {
|
||||||
SUCCESS_BUILD,
|
SUCCESS_BUILD,
|
||||||
specialCharsRegex,
|
specialCharsRegex,
|
||||||
} from "../constants/constants";
|
} from "../constants/constants";
|
||||||
import { downloadFlowsFromDatabase } from "../controllers/API";
|
|
||||||
import { DESCRIPTIONS } from "../flow_constants";
|
import { DESCRIPTIONS } from "../flow_constants";
|
||||||
import {
|
import {
|
||||||
APIClassType,
|
APIClassType,
|
||||||
|
|
@ -1423,22 +1422,6 @@ export function downloadFlow(
|
||||||
link.click();
|
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 {
|
export function getRandomElement<T>(array: T[]): T {
|
||||||
return array[Math.floor(Math.random() * array.length)];
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue