From de70e97940572f0b5844d693d0dcb50de2856c81 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:52:22 +0530 Subject: [PATCH] fix: adds loading state to the responses download button (#6352) --- .../components/CustomFilter.test.tsx | 66 +++++++++++++++ .../[surveyId]/components/CustomFilter.tsx | 82 +++++++++++-------- .../components/data-table-toolbar.tsx | 2 +- .../components/selected-row-settings.tsx | 17 ++-- 4 files changed, 129 insertions(+), 38 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx index 1fa13be6aa..dd34ce26aa 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.test.tsx @@ -188,4 +188,70 @@ describe("CustomFilter", () => { expect(screen.queryByTestId("calendar-mock")).not.toBeInTheDocument(); }); }); + + test("downloading all and filtered responses in csv and xlsx formats", async () => { + const user = userEvent.setup(); + + render(); + + // Mock the action to return undefined data to avoid DOM manipulation + vi.mocked(getResponsesDownloadUrlAction).mockResolvedValue({ + data: undefined, + }); + + // Test CSV download + const downloadButton = screen.getByTestId("fb__custom-filter-download-responses-button"); + await user.click(downloadButton); + const downloadAllCsv = screen.getByTestId("fb__custom-filter-download-all-csv"); + await user.click(downloadAllCsv); + + await waitFor(() => { + expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ + surveyId: "survey-1", + format: "csv", + filterCriteria: {}, + }); + }); + + // Test XLSX download + await user.click(downloadButton); + const downloadAllXlsx = screen.getByTestId("fb__custom-filter-download-all-xlsx"); + await user.click(downloadAllXlsx); + + await waitFor(() => { + expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ + surveyId: "survey-1", + format: "xlsx", + filterCriteria: {}, + }); + }); + + // Test filtered CSV download + await user.click(downloadButton); + const downloadFilteredCsv = screen.getByTestId("fb__custom-filter-download-filtered-csv"); + await user.click(downloadFilteredCsv); + + await waitFor(() => { + expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ + surveyId: "survey-1", + format: "csv", + filterCriteria: {}, + }); + }); + + // Test filtered XLSX download + await user.click(downloadButton); + const downloadFilteredXlsx = screen.getByTestId("fb__custom-filter-download-filtered-xlsx"); + await user.click(downloadFilteredXlsx); + + await waitFor(() => { + expect(getResponsesDownloadUrlAction).toHaveBeenCalledWith({ + surveyId: "survey-1", + format: "xlsx", + filterCriteria: {}, + }); + }); + + vi.restoreAllMocks(); + }); }); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx index 0409c9c98d..9d8acc8c74 100755 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx @@ -15,6 +15,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/modules/ui/components/dropdown-menu"; +import { cn } from "@/modules/ui/lib/utils"; import { TFnType, useTranslate } from "@tolgee/react"; import { differenceInDays, @@ -31,7 +32,7 @@ import { subQuarters, subYears, } from "date-fns"; -import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon } from "lucide-react"; +import { ArrowDownToLineIcon, ChevronDown, ChevronUp, DownloadIcon, Loader2Icon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import toast from "react-hot-toast"; import { TSurvey } from "@formbricks/types/surveys/types"; @@ -135,6 +136,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { const [isDatePickerOpen, setIsDatePickerOpen] = useState(false); const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState(false); const [hoveredRange, setHoveredRange] = useState(null); + const [isDownloading, setIsDownloading] = useState(false); const firstMountRef = useRef(true); @@ -236,28 +238,29 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { setSelectingDate(DateSelected.FROM); }; - const handleDowndloadResponses = async (filter: FilterDownload, filetype: "csv" | "xlsx") => { - try { - const responseFilters = filter === FilterDownload.ALL ? {} : filters; - const responsesDownloadUrlResponse = await getResponsesDownloadUrlAction({ - surveyId: survey.id, - format: filetype, - filterCriteria: responseFilters, - }); - if (responsesDownloadUrlResponse?.data) { - const link = document.createElement("a"); - link.href = responsesDownloadUrlResponse.data; - link.download = ""; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } else { - const errorMessage = getFormattedErrorMessage(responsesDownloadUrlResponse); - toast.error(errorMessage); - } - } catch (error) { - toast.error("Error downloading responses"); + const handleDownloadResponses = async (filter: FilterDownload, filetype: "csv" | "xlsx") => { + const responseFilters = filter === FilterDownload.ALL ? {} : filters; + setIsDownloading(true); + + const responsesDownloadUrlResponse = await getResponsesDownloadUrlAction({ + surveyId: survey.id, + format: filetype, + filterCriteria: responseFilters, + }); + + if (responsesDownloadUrlResponse?.data) { + const link = document.createElement("a"); + link.href = responsesDownloadUrlResponse.data; + link.download = ""; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } else { + const errorMessage = getFormattedErrorMessage(responsesDownloadUrlResponse); + toast.error(errorMessage); } + + setIsDownloading(false); }; useClickOutside(datePickerRef, () => handleDatePickerClose()); @@ -386,11 +389,22 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { onOpenChange={(value) => { value && handleDatePickerClose(); }}> - +
{t("common.download")} - + {isDownloading ? ( + + ) : ( + + )}
@@ -398,26 +412,30 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { { - handleDowndloadResponses(FilterDownload.ALL, "csv"); + data-testid="fb__custom-filter-download-all-csv" + onClick={async () => { + await handleDownloadResponses(FilterDownload.ALL, "csv"); }}>

{t("environments.surveys.summary.all_responses_csv")}

{ - handleDowndloadResponses(FilterDownload.ALL, "xlsx"); + data-testid="fb__custom-filter-download-all-xlsx" + onClick={async () => { + await handleDownloadResponses(FilterDownload.ALL, "xlsx"); }}>

{t("environments.surveys.summary.all_responses_excel")}

{ - handleDowndloadResponses(FilterDownload.FILTER, "csv"); + data-testid="fb__custom-filter-download-filtered-csv" + onClick={async () => { + await handleDownloadResponses(FilterDownload.FILTER, "csv"); }}>

{t("environments.surveys.summary.filtered_responses_csv")}

{ - handleDowndloadResponses(FilterDownload.FILTER, "xlsx"); + data-testid="fb__custom-filter-download-filtered-xlsx" + onClick={async () => { + await handleDownloadResponses(FilterDownload.FILTER, "xlsx"); }}>

{t("environments.surveys.summary.filtered_responses_excel")}

diff --git a/apps/web/modules/ui/components/data-table/components/data-table-toolbar.tsx b/apps/web/modules/ui/components/data-table/components/data-table-toolbar.tsx index 192cb800d5..b17d2278a6 100644 --- a/apps/web/modules/ui/components/data-table/components/data-table-toolbar.tsx +++ b/apps/web/modules/ui/components/data-table/components/data-table-toolbar.tsx @@ -17,7 +17,7 @@ interface DataTableToolbarProps { deleteRowsAction: (rowIds: string[]) => void; type: "response" | "contact"; deleteAction: (id: string) => Promise; - downloadRowsAction?: (rowIds: string[], format: string) => void; + downloadRowsAction?: (rowIds: string[], format: string) => Promise; } export const DataTableToolbar = ({ diff --git a/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx b/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx index e1bc49d5e0..d032122df2 100644 --- a/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx +++ b/apps/web/modules/ui/components/data-table/components/selected-row-settings.tsx @@ -9,9 +9,10 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/modules/ui/components/dropdown-menu"; +import { cn } from "@/modules/ui/lib/utils"; import { Table } from "@tanstack/react-table"; import { useTranslate } from "@tolgee/react"; -import { ArrowDownToLineIcon, Trash2Icon } from "lucide-react"; +import { ArrowDownToLineIcon, Loader2Icon, Trash2Icon } from "lucide-react"; import { useCallback, useState } from "react"; import { toast } from "react-hot-toast"; @@ -20,7 +21,7 @@ interface SelectedRowSettingsProps { deleteRowsAction: (rowId: string[]) => void; type: "response" | "contact"; deleteAction: (id: string) => Promise; - downloadRowsAction?: (rowIds: string[], format: string) => void; + downloadRowsAction?: (rowIds: string[], format: string) => Promise; } export const SelectedRowSettings = ({ @@ -32,6 +33,7 @@ export const SelectedRowSettings = ({ }: SelectedRowSettingsProps) => { const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + const [isDownloading, setIsDownloading] = useState(false); const { t } = useTranslate(); const selectedRowCount = table.getFilteredSelectedRowModel().rows.length; @@ -73,10 +75,12 @@ export const SelectedRowSettings = ({ // Handle download selected rows const handleDownloadSelectedRows = async (format: string) => { + setIsDownloading(true); const rowsToDownload = table.getFilteredSelectedRowModel().rows.map((row) => row.id); if (downloadRowsAction && rowsToDownload.length > 0) { - downloadRowsAction(rowsToDownload, format); + await downloadRowsAction(rowsToDownload, format); } + setIsDownloading(false); }; // Helper component for the separator @@ -112,10 +116,13 @@ export const SelectedRowSettings = ({ {downloadRowsAction && ( - +