mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
fix: adds loading state to the responses download button (#6352)
This commit is contained in:
@@ -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(<CustomFilter survey={mockSurvey} />);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
const [isFilterDropDownOpen, setIsFilterDropDownOpen] = useState<boolean>(false);
|
||||
const [hoveredRange, setHoveredRange] = useState<DateRange | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(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();
|
||||
}}>
|
||||
<DropdownMenuTrigger asChild className="focus:bg-muted cursor-pointer outline-none">
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={cn(
|
||||
"focus:bg-muted cursor-pointer outline-none",
|
||||
isDownloading && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
disabled={isDownloading}
|
||||
data-testid="fb__custom-filter-download-responses-button">
|
||||
<div className="min-w-auto h-auto rounded-md border border-slate-200 bg-white p-3 hover:border-slate-300 sm:flex sm:px-6 sm:py-3">
|
||||
<div className="hidden w-full items-center justify-between sm:flex">
|
||||
<span className="text-sm text-slate-700">{t("common.download")}</span>
|
||||
<ArrowDownToLineIcon className="ml-2 h-4 w-4" />
|
||||
{isDownloading ? (
|
||||
<Loader2Icon className="ml-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<ArrowDownToLineIcon className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
<DownloadIcon className="block h-4 sm:hidden" />
|
||||
</div>
|
||||
@@ -398,26 +412,30 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
|
||||
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.ALL, "csv");
|
||||
data-testid="fb__custom-filter-download-all-csv"
|
||||
onClick={async () => {
|
||||
await handleDownloadResponses(FilterDownload.ALL, "csv");
|
||||
}}>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.all_responses_csv")}</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.ALL, "xlsx");
|
||||
data-testid="fb__custom-filter-download-all-xlsx"
|
||||
onClick={async () => {
|
||||
await handleDownloadResponses(FilterDownload.ALL, "xlsx");
|
||||
}}>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.all_responses_excel")}</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.FILTER, "csv");
|
||||
data-testid="fb__custom-filter-download-filtered-csv"
|
||||
onClick={async () => {
|
||||
await handleDownloadResponses(FilterDownload.FILTER, "csv");
|
||||
}}>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.filtered_responses_csv")}</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleDowndloadResponses(FilterDownload.FILTER, "xlsx");
|
||||
data-testid="fb__custom-filter-download-filtered-xlsx"
|
||||
onClick={async () => {
|
||||
await handleDownloadResponses(FilterDownload.FILTER, "xlsx");
|
||||
}}>
|
||||
<p className="text-slate-700">{t("environments.surveys.summary.filtered_responses_excel")}</p>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -17,7 +17,7 @@ interface DataTableToolbarProps<T> {
|
||||
deleteRowsAction: (rowIds: string[]) => void;
|
||||
type: "response" | "contact";
|
||||
deleteAction: (id: string) => Promise<void>;
|
||||
downloadRowsAction?: (rowIds: string[], format: string) => void;
|
||||
downloadRowsAction?: (rowIds: string[], format: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DataTableToolbar = <T,>({
|
||||
|
||||
@@ -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<T> {
|
||||
deleteRowsAction: (rowId: string[]) => void;
|
||||
type: "response" | "contact";
|
||||
deleteAction: (id: string) => Promise<void>;
|
||||
downloadRowsAction?: (rowIds: string[], format: string) => void;
|
||||
downloadRowsAction?: (rowIds: string[], format: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SelectedRowSettings = <T,>({
|
||||
@@ -32,6 +33,7 @@ export const SelectedRowSettings = <T,>({
|
||||
}: SelectedRowSettingsProps<T>) => {
|
||||
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 = <T,>({
|
||||
|
||||
// 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 = <T,>({
|
||||
<Separator />
|
||||
{downloadRowsAction && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
className={cn(isDownloading && "cursor-not-allowed opacity-50")}
|
||||
disabled={isDownloading}>
|
||||
<Button variant="outline" size="sm" className="h-6 gap-1 border-none px-2">
|
||||
{isDownloading ? <Loader2Icon className="h-4 w-4 animate-spin" /> : <ArrowDownToLineIcon />}
|
||||
{t("common.download")}
|
||||
<ArrowDownToLineIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
|
||||
Reference in New Issue
Block a user