fix: add Export modal instead of Download in start page, change Download button size (#8095)

* Fix download button size on main page

* Added export modal into main page card dropdown

* added open and setopen as optional

* Add download component directly if its component

* Added success message to exported flow

* Added data test id

* Fixed export tests

* Added downloaded message and data test id

* Fixed bulk actions test
This commit is contained in:
Lucas Oliveira 2025-05-19 16:55:32 -03:00 • committed by GitHub
commit 15ccc886f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 69 additions and 26 deletions

View file

@ -1,5 +1,6 @@
import { track } from "@/customization/utils/analytics"; import { track } from "@/customization/utils/analytics";
import useFlowStore from "@/stores/flowStore"; import useFlowStore from "@/stores/flowStore";
import { FlowType } from "@/types/flow";
import { ReactNode, forwardRef, useEffect, useState } from "react"; import { ReactNode, forwardRef, useEffect, useState } from "react";
import IconComponent from "../../components/common/genericIconComponent"; import IconComponent from "../../components/common/genericIconComponent";
import EditFlowSettings from "../../components/core/editFlowSettingsComponent"; import EditFlowSettings from "../../components/core/editFlowSettingsComponent";
@ -16,11 +17,21 @@ import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
const ExportModal = forwardRef( const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => { (
props: {
children?: ReactNode;
open?: boolean;
setOpen?: (open: boolean) => void;
flowData?: FlowType;
},
ref,
): JSX.Element => {
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setNoticeData = useAlertStore((state) => state.setNoticeData); const setNoticeData = useAlertStore((state) => state.setNoticeData);
const [checked, setChecked] = useState(false); const [checked, setChecked] = useState(false);
const currentFlow = useFlowStore((state) => state.currentFlow); const currentFlowOnPage = useFlowStore((state) => state.currentFlow);
const currentFlow = props.flowData ?? currentFlowOnPage;
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
useEffect(() => { useEffect(() => {
setName(currentFlow?.name ?? ""); setName(currentFlow?.name ?? "");
@ -30,7 +41,13 @@ const ExportModal = forwardRef(
const [description, setDescription] = useState( const [description, setDescription] = useState(
currentFlow?.description ?? "", currentFlow?.description ?? "",
); );
const [open, setOpen] = useState(false);
const [customOpen, customSetOpen] = useState(false);
const [open, setOpen] =
props.open !== undefined && props.setOpen !== undefined
? [props.open, props.setOpen]
: [customOpen, customSetOpen];
return ( return (
<BaseModal <BaseModal
size="smaller-h-full" size="smaller-h-full"
@ -69,12 +86,16 @@ const ExportModal = forwardRef(
}), }),
name!, name!,
description, description,
); ).then(() => {
setSuccessData({
title: "Flow exported successfully",
});
});
setOpen(false); setOpen(false);
track("Flow Exported", { flowId: currentFlow!.id }); track("Flow Exported", { flowId: currentFlow!.id });
}} }}
> >
<BaseModal.Trigger asChild>{props.children}</BaseModal.Trigger> <BaseModal.Trigger asChild>{props.children ?? <></>}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}> <BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
<span className="pr-2">Export</span> <span className="pr-2">Export</span>
<IconComponent <IconComponent
@ -107,7 +128,13 @@ const ExportModal = forwardRef(
</span> </span>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer submit={{ label: "Export", loading: isBuilding }} /> <BaseModal.Footer
submit={{
label: "Export",
loading: isBuilding,
dataTestId: "modal-export-button",
}}
/>
</BaseModal> </BaseModal>
); );
}, },

View file

@ -2,25 +2,24 @@ import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import { FlowType } from "@/types/flow"; import { FlowType } from "@/types/flow";
import { downloadFlow } from "@/utils/reactflowUtils";
import useDuplicateFlow from "../../hooks/use-handle-duplicate"; import useDuplicateFlow from "../../hooks/use-handle-duplicate";
import useSelectOptionsChange from "../../hooks/use-select-options-change"; import useSelectOptionsChange from "../../hooks/use-select-options-change";
type DropdownComponentProps = { type DropdownComponentProps = {
flowData: FlowType; flowData: FlowType;
setOpenDelete: (open: boolean) => void; setOpenDelete: (open: boolean) => void;
handlePlaygroundClick?: () => void; handleExport: () => void;
handleEdit: () => void; handleEdit: () => void;
}; };
const DropdownComponent = ({ const DropdownComponent = ({
flowData, flowData,
setOpenDelete, setOpenDelete,
handleExport,
handleEdit, handleEdit,
}: DropdownComponentProps) => { }: DropdownComponentProps) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData); const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const { handleDuplicate } = useDuplicateFlow({ flow: flowData }); const { handleDuplicate } = useDuplicateFlow({ flow: flowData });
const duplicateFlow = () => { const duplicateFlow = () => {
@ -31,16 +30,12 @@ const DropdownComponent = ({
); );
}; };
const handleExport = () => {
downloadFlow(flowData, flowData.name, flowData.description);
setSuccessData({ title: `${flowData.name} exported successfully` });
};
const { handleSelectOptionsChange } = useSelectOptionsChange( const { handleSelectOptionsChange } = useSelectOptionsChange(
[flowData.id], [flowData.id],
setErrorData, setErrorData,
setOpenDelete, setOpenDelete,
duplicateFlow,
handleExport, handleExport,
duplicateFlow,
handleEdit, handleEdit,
); );
@ -74,7 +69,7 @@ const DropdownComponent = ({
aria-hidden="true" aria-hidden="true"
className="mr-2 h-4 w-4" className="mr-2 h-4 w-4"
/> />
Download Export
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {

View file

@ -82,6 +82,7 @@ const HeaderComponent = ({
const handleDownload = () => { const handleDownload = () => {
downloadFlows({ ids: selectedFlows }); downloadFlows({ ids: selectedFlows });
setSuccessData({ title: "Flows downloaded successfully" });
}; };
const handleDelete = () => { const handleDelete = () => {
@ -190,13 +191,15 @@ const HeaderComponent = ({
<div className="flex items-center"> <div className="flex items-center">
<div <div
className={cn( className={cn(
"-mr-4 flex w-0 items-center gap-2 overflow-hidden opacity-0 transition-all duration-300", "-mr-3 flex w-0 items-center gap-2 overflow-hidden opacity-0 transition-all duration-300",
selectedFlows.length > 0 && "w-36 opacity-100", selectedFlows.length > 0 && "w-36 opacity-100",
)} )}
> >
<Button <Button
variant="outline" variant="outline"
size="iconMd" size="iconMd"
className="h-8 w-8"
data-testid="download-bulk-btn"
onClick={handleDownload} onClick={handleDownload}
loading={isDownloading} loading={isDownloading}
> >

View file

@ -11,10 +11,11 @@ import {
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useDeleteFlow from "@/hooks/flows/use-delete-flow"; import useDeleteFlow from "@/hooks/flows/use-delete-flow";
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal"; import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
import ExportModal from "@/modals/exportModal";
import FlowSettingsModal from "@/modals/flowSettingsModal"; import FlowSettingsModal from "@/modals/flowSettingsModal";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { FlowType } from "@/types/flow"; import { FlowType } from "@/types/flow";
import { downloadFlow } from "@/utils/reactflowUtils";
import { swatchColors } from "@/utils/styleUtils"; import { swatchColors } from "@/utils/styleUtils";
import { cn, getNumberFromString } from "@/utils/utils"; import { cn, getNumberFromString } from "@/utils/utils";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -42,6 +43,7 @@ const ListComponent = ({
const setErrorData = useAlertStore((state) => state.setErrorData); const setErrorData = useAlertStore((state) => state.setErrorData);
const { folderId } = useParams(); const { folderId } = useParams();
const [openSettings, setOpenSettings] = useState(false); const [openSettings, setOpenSettings] = useState(false);
const [openExportModal, setOpenExportModal] = useState(false);
const isComponent = flowData.is_component ?? false; const isComponent = flowData.is_component ?? false;
const { getIcon } = useGetTemplateStyle(flowData); const { getIcon } = useGetTemplateStyle(flowData);
@ -86,6 +88,15 @@ const ListComponent = ({
: getNumberFromString(flowData.gradient ?? flowData.id)) % : getNumberFromString(flowData.gradient ?? flowData.id)) %
swatchColors.length; swatchColors.length;
const handleExport = () => {
if (flowData.is_component) {
downloadFlow(flowData, flowData.name, flowData.description);
setSuccessData({ title: `${flowData.name} exported successfully` });
} else {
setOpenExportModal(true);
}
};
const [icon, setIcon] = useState<string>(""); const [icon, setIcon] = useState<string>("");
useEffect(() => { useEffect(() => {
@ -193,18 +204,15 @@ const ListComponent = ({
<DropdownComponent <DropdownComponent
flowData={flowData} flowData={flowData}
setOpenDelete={setOpenDelete} setOpenDelete={setOpenDelete}
handleExport={handleExport}
handleEdit={() => { handleEdit={() => {
setOpenSettings(true); setOpenSettings(true);
}} }}
handlePlaygroundClick={() => {
// handlePlaygroundClick();
}}
/> />
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</Card> </Card>
{openDelete && ( {openDelete && (
<DeleteConfirmationModal <DeleteConfirmationModal
open={openDelete} open={openDelete}
@ -214,6 +222,11 @@ const ListComponent = ({
note={!flowData.is_component ? "and its message history" : ""} note={!flowData.is_component ? "and its message history" : ""}
/> />
)} )}
<ExportModal
open={openExportModal}
setOpen={setOpenExportModal}
flowData={flowData}
/>
<FlowSettingsModal <FlowSettingsModal
open={openSettings} open={openSettings}
setOpen={setOpenSettings} setOpen={setOpenSettings}

View file

@ -4,8 +4,8 @@ const useSelectOptionsChange = (
selectedFlowsComponentsCards: string[] | undefined, selectedFlowsComponentsCards: string[] | undefined,
setErrorData: (data: { title: string; list: string[] }) => void, setErrorData: (data: { title: string; list: string[] }) => void,
setOpenDelete: (value: boolean) => void, setOpenDelete: (value: boolean) => void,
handleDuplicate: () => void,
handleExport: () => void, handleExport: () => void,
handleDuplicate: () => void,
handleEdit: () => void, handleEdit: () => void,
) => { ) => {
const handleSelectOptionsChange = useCallback( const handleSelectOptionsChange = useCallback(
@ -33,8 +33,8 @@ const useSelectOptionsChange = (
setErrorData, setErrorData,
setOpenDelete, setOpenDelete,
handleDuplicate, handleDuplicate,
handleExport,
handleEdit, handleEdit,
handleExport,
], ],
); );

View file

@ -35,6 +35,9 @@ test(
await page.getByTestId("icon-ChevronLeft").last().click(); await page.getByTestId("icon-ChevronLeft").last().click();
await page.getByTestId("home-dropdown-menu").nth(0).click(); await page.getByTestId("home-dropdown-menu").nth(0).click();
await page.getByTestId("btn-download-json").last().click(); await page.getByTestId("btn-download-json").last().click();
await page.getByText("Export").first().isVisible();
await page.getByTestId("modal-export-button").isVisible();
await page.getByTestId("modal-export-button").click();
await expect(page.getByText(/.*exported successfully/)).toBeVisible({ await expect(page.getByText(/.*exported successfully/)).toBeVisible({
timeout: 10000, timeout: 10000,
}); });
@ -42,6 +45,9 @@ test(
await page.getByText("Flows", { exact: true }).click(); await page.getByText("Flows", { exact: true }).click();
await page.getByTestId("home-dropdown-menu").nth(0).click(); await page.getByTestId("home-dropdown-menu").nth(0).click();
await page.getByTestId("btn-download-json").last().click(); await page.getByTestId("btn-download-json").last().click();
await page.getByText("Export").first().isVisible();
await page.getByTestId("modal-export-button").isVisible();
await page.getByTestId("modal-export-button").click();
await expect(page.getByText(/.*exported successfully/).last()).toBeVisible({ await expect(page.getByText(/.*exported successfully/).last()).toBeVisible({
timeout: 10000, timeout: 10000,
}); });

View file

@ -58,9 +58,8 @@ test(
await expect(secondCheckbox).toBeChecked(); await expect(secondCheckbox).toBeChecked();
await expect(thirdCheckbox).toBeChecked(); await expect(thirdCheckbox).toBeChecked();
// Test bulk download // Test bulk download
await page.getByTestId("home-dropdown-menu").first().click(); await page.getByTestId("download-bulk-btn").last().click();
await page.getByTestId("btn-download-json").last().click(); await expect(page.getByText(/.*downloaded successfully/)).toBeVisible({
await expect(page.getByText(/.*exported successfully/)).toBeVisible({
timeout: 10000, timeout: 10000,
}); });