fix: cancel download fetch when progress toast is closed (#27152)
* fix: cancel download fetch when progress toast is closed - Create new progress-toast utility using @coss/ui toastManager - Add ToastProvider to app providers - Update download components to use AbortController for cancellation - When cancel button is clicked, abort signal stops the fetch loop Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: track active toasts with Set instead of accessing toastManager.toasts Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: add AnchoredToastProvider to app providers Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: extract common CSV download logic into useCsvDownload hook Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: improve progress toast with i18n support and Progress component Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: replace useCsvDownload hook with DownloadButton component Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: rename DownloadButton to CsvDownloadButton Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * clean up i18n * fix: properly throw AbortError when download is cancelled Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * Revert "fix: properly throw AbortError when download is cancelled" This reverts commit 3c64b848140271e4dcd2e45dba5302eef9ca562c. * fix abort controller * fix layout shift * better handle error * fix: address Cubic AI review feedback - Fix typo 'copmlete' -> 'complete' in download_progress i18n string - Treat null batch as failure during CSV download to prevent partial exports Co-Authored-By: unknown <> * remove redundant try - catch --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
unknown <>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
7ce1ee5cea
commit
fd476bf7f7
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AnchoredToastProvider, ToastProvider } from "@coss/ui/components/toast";
|
||||
import { TrpcProvider } from "app/_trpc/trpc-provider";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
@@ -27,7 +28,11 @@ export function Providers({ isEmbed, children, country }: ProvidersProps) {
|
||||
{!isEmbed && !isBookingPage && <NotificationSoundHandler />}
|
||||
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
|
||||
<CacheProvider>
|
||||
<WebPushProvider>{children}</WebPushProvider>
|
||||
<WebPushProvider>
|
||||
<ToastProvider>
|
||||
<AnchoredToastProvider>{children}</AnchoredToastProvider>
|
||||
</ToastProvider>
|
||||
</WebPushProvider>
|
||||
</CacheProvider>
|
||||
</TrpcProvider>
|
||||
</SessionProvider>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@coss/ui/components/button";
|
||||
import { Group, GroupSeparator, GroupText } from "@coss/ui/components/group";
|
||||
import { Spinner } from "@coss/ui/components/spinner";
|
||||
import { toastManager } from "@coss/ui/components/toast";
|
||||
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "@coss/ui/components/tooltip";
|
||||
import { DownloadIcon, XIcon } from "lucide-react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
|
||||
interface PaginatedResponse<TData> {
|
||||
data: TData[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface CsvDownloadButtonProps<TData, TTransformed = TData> {
|
||||
fetchBatch: (offset: number) => Promise<PaginatedResponse<TData> | null>;
|
||||
transformData?: (data: TData[]) => TTransformed[];
|
||||
filename: string | (() => string);
|
||||
onDownloadStart?: () => void;
|
||||
}
|
||||
|
||||
function wrapWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
if (signal.aborted) {
|
||||
return Promise.reject(new DOMException("Cancelled", "AbortError"));
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const handleAbort = () => {
|
||||
reject(new DOMException("Cancelled", "AbortError"));
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", handleAbort, { once: true });
|
||||
|
||||
promise.then(resolve, reject).finally(() => {
|
||||
signal.removeEventListener("abort", handleAbort);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function CsvDownloadButton<TData, TTransformed = TData>({
|
||||
fetchBatch,
|
||||
transformData,
|
||||
filename,
|
||||
onDownloadStart,
|
||||
}: CsvDownloadButtonProps<TData, TTransformed>) {
|
||||
const { t } = useLocale();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const infoToastIdRef = useRef<string | null>(null);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
if (isDownloading) return;
|
||||
|
||||
onDownloadStart?.();
|
||||
setIsDownloading(true);
|
||||
setProgress(0);
|
||||
abortControllerRef.current = new AbortController();
|
||||
const { signal } = abortControllerRef.current;
|
||||
|
||||
infoToastIdRef.current = toastManager.add({
|
||||
title: t("downloading"),
|
||||
type: "info",
|
||||
});
|
||||
|
||||
try {
|
||||
const firstBatch = await wrapWithAbort(fetchBatch(0), signal);
|
||||
if (signal.aborted) return;
|
||||
if (!firstBatch) {
|
||||
throw new Error("Failed to download data.");
|
||||
}
|
||||
|
||||
let allData = firstBatch.data;
|
||||
const totalRecords = firstBatch.total;
|
||||
|
||||
while (totalRecords > 0 && allData.length < totalRecords && !signal.aborted) {
|
||||
const batch = await wrapWithAbort(fetchBatch(allData.length), signal);
|
||||
if (signal.aborted) return;
|
||||
if (!batch) {
|
||||
throw new Error("Failed to download data.");
|
||||
}
|
||||
allData = [...allData, ...batch.data];
|
||||
|
||||
const currentProgress = Math.min(Math.round((allData.length / totalRecords) * 100), 99);
|
||||
setProgress(currentProgress);
|
||||
}
|
||||
|
||||
if (signal.aborted) return;
|
||||
if (allData.length < totalRecords) {
|
||||
throw new Error("Failed to download data.");
|
||||
}
|
||||
|
||||
setProgress(100);
|
||||
|
||||
if (infoToastIdRef.current) {
|
||||
toastManager.close(infoToastIdRef.current);
|
||||
infoToastIdRef.current = null;
|
||||
}
|
||||
|
||||
const csvData = transformData ? transformData(allData) : allData;
|
||||
const resolvedFilename = typeof filename === "function" ? filename() : filename;
|
||||
downloadAsCsv(csvData as Record<string, unknown>[], resolvedFilename);
|
||||
} catch (err) {
|
||||
if (infoToastIdRef.current) {
|
||||
toastManager.close(infoToastIdRef.current);
|
||||
infoToastIdRef.current = null;
|
||||
}
|
||||
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
toastManager.add({
|
||||
title: t("cancelled"),
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
toastManager.add({
|
||||
title: t("failed_to_download"),
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
setProgress(0);
|
||||
abortControllerRef.current = null;
|
||||
infoToastIdRef.current = null;
|
||||
}
|
||||
}, [isDownloading, fetchBatch, transformData, filename, onDownloadStart, t]);
|
||||
|
||||
function handleCancel() {
|
||||
abortControllerRef.current?.abort();
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delay={0}>
|
||||
<div className="inline-grid">
|
||||
<div className={isDownloading ? "col-start-1 row-start-1 invisible" : "col-start-1 row-start-1"}>
|
||||
<Button onClick={handleDownload} variant="outline">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
{t("download")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={isDownloading ? "col-start-1 row-start-1" : "col-start-1 row-start-1 invisible"}>
|
||||
<Group>
|
||||
<GroupText aria-live="polite" className="cursor-default gap-2" role="status">
|
||||
<Spinner />
|
||||
<span aria-hidden="true" className="font-medium text-foreground tabular-nums">
|
||||
{progress.toString().padStart(2, "\u2007")}%
|
||||
</span>
|
||||
<span className="sr-only">
|
||||
{t("downloading")}, {t("download_progress", { progress })}
|
||||
</span>
|
||||
</GroupText>
|
||||
<GroupSeparator />
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
aria-label={t("cancel_download")}
|
||||
onClick={handleCancel}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
/>
|
||||
}>
|
||||
<XIcon aria-hidden="true" />
|
||||
</TooltipTrigger>
|
||||
<TooltipPopup>{t("cancel")}</TooltipPopup>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { hideProgressToast, showProgressToast, showToast } from "@calcom/ui/components/toast";
|
||||
import { useState } from "react";
|
||||
|
||||
import { CsvDownloadButton } from "@lib/components/CsvDownloadButton";
|
||||
import { useBookingFilters } from "~/bookings/hooks/useBookingFilters";
|
||||
import type { BookingListingStatus } from "../types";
|
||||
|
||||
@@ -38,7 +36,6 @@ function transformBookingToCsv(booking: BookingOutput, t: TranslationFunction) {
|
||||
export function BookingsCsvDownload({ status }: BookingsCsvDownloadProps) {
|
||||
const { t } = useLocale();
|
||||
const { data: user, isPending: isUserPending } = useMeQuery();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { eventTypeIds, teamIds, userIds, dateRange, attendeeName, attendeeEmail, bookingUid } =
|
||||
@@ -51,75 +48,30 @@ export function BookingsCsvDownload({ status }: BookingsCsvDownloadProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fetchBatch = async (offset: number) => {
|
||||
const result = await utils.viewer.bookings.get.fetch({
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
filters: {
|
||||
statuses: [status],
|
||||
eventTypeIds,
|
||||
teamIds,
|
||||
userIds,
|
||||
attendeeName,
|
||||
attendeeEmail,
|
||||
bookingUid,
|
||||
afterStartDate: dateRange?.startDate
|
||||
? dayjs(dateRange?.startDate).startOf("day").toISOString()
|
||||
: undefined,
|
||||
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bookings: result.bookings,
|
||||
totalCount: result.totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
setIsDownloading(true);
|
||||
showProgressToast(0);
|
||||
|
||||
// Fetch first batch to get total count
|
||||
const firstBatch = await fetchBatch(0);
|
||||
let allBookings = firstBatch.bookings;
|
||||
const totalCount = firstBatch.totalCount;
|
||||
|
||||
// Continue fetching remaining batches
|
||||
while (allBookings.length < totalCount) {
|
||||
const offset = allBookings.length;
|
||||
const batch = await fetchBatch(offset);
|
||||
if (batch.bookings.length === 0) break; // Prevent infinite loop if batch returns empty
|
||||
allBookings = [...allBookings, ...batch.bookings];
|
||||
|
||||
const currentProgress = Math.min(Math.round((allBookings.length / totalCount) * 100), 99);
|
||||
showProgressToast(currentProgress);
|
||||
}
|
||||
|
||||
showProgressToast(100);
|
||||
|
||||
// Transform and download
|
||||
const csvData = allBookings.map((booking) => transformBookingToCsv(booking, t));
|
||||
const filename = `${t("bookings").toLowerCase()}-${status}-${dayjs().format("YYYY-MM-DD")}.csv`;
|
||||
downloadAsCsv(csvData, filename);
|
||||
} catch {
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
hideProgressToast();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon="download"
|
||||
loading={isDownloading}
|
||||
onClick={handleDownload}
|
||||
size="sm"
|
||||
className="h-full">
|
||||
{t("download")}
|
||||
</Button>
|
||||
<CsvDownloadButton
|
||||
fetchBatch={async (offset) => {
|
||||
const result = await utils.viewer.bookings.get.fetch({
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
filters: {
|
||||
statuses: [status],
|
||||
eventTypeIds,
|
||||
teamIds,
|
||||
userIds,
|
||||
attendeeName,
|
||||
attendeeEmail,
|
||||
bookingUid,
|
||||
afterStartDate: dateRange?.startDate
|
||||
? dayjs(dateRange?.startDate).startOf("day").toISOString()
|
||||
: undefined,
|
||||
beforeEndDate: dateRange?.endDate ? dayjs(dateRange?.endDate).endOf("day").toISOString() : undefined,
|
||||
},
|
||||
});
|
||||
return { data: result.bookings, total: result.totalCount };
|
||||
}}
|
||||
transformData={(bookings) => bookings.map((booking) => transformBookingToCsv(booking, t))}
|
||||
filename={`${t("bookings").toLowerCase()}-${status}-${dayjs().format("YYYY-MM-DD")}.csv`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,112 +1,41 @@
|
||||
import { useState } from "react";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
import { showToast, showProgressToast, hideProgressToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import { useInsightsBookingParameters } from "../../../hooks/useInsightsBookingParameters";
|
||||
import { extractDateRangeFromColumnFilters } from "@calcom/features/insights/lib/bookingUtils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { CsvDownloadButton } from "@lib/components/CsvDownloadButton";
|
||||
import posthog from "posthog-js";
|
||||
import { useInsightsBookingParameters } from "../../../hooks/useInsightsBookingParameters";
|
||||
|
||||
type RawData = RouterOutputs["viewer"]["insights"]["rawData"]["data"][number];
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
const Download = () => {
|
||||
const { t } = useLocale();
|
||||
const insightsBookingParams = useInsightsBookingParameters();
|
||||
const { startDate, endDate } = extractDateRangeFromColumnFilters(insightsBookingParams.columnFilters);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
type PaginatedResponse = {
|
||||
data: RawData[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
const fetchBatch = async (offset: number): Promise<PaginatedResponse | null> => {
|
||||
try {
|
||||
const result = await utils.viewer.insights.rawData.fetch({
|
||||
...insightsBookingParams,
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
});
|
||||
|
||||
if (result && "data" in result && "total" in result) {
|
||||
return result as PaginatedResponse;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadClick = async () => {
|
||||
try {
|
||||
posthog.capture("insights_bookings_download_clicked", { teamId: insightsBookingParams.selectedTeamId });
|
||||
setIsDownloading(true);
|
||||
showProgressToast(0);
|
||||
let allData: RawData[] = [];
|
||||
let offset = 0;
|
||||
|
||||
// Get first batch to get total count
|
||||
const firstBatch = await fetchBatch(0);
|
||||
if (!firstBatch) return;
|
||||
|
||||
allData = firstBatch.data;
|
||||
const totalRecords = firstBatch.total;
|
||||
|
||||
// Continue fetching remaining batches
|
||||
while (totalRecords > 0 && allData.length < totalRecords) {
|
||||
offset += BATCH_SIZE;
|
||||
const result = await fetchBatch(offset);
|
||||
if (!result) break;
|
||||
allData = [...allData, ...result.data];
|
||||
|
||||
const currentProgress = Math.min(Math.round((allData.length / totalRecords) * 100), 99);
|
||||
showProgressToast(currentProgress);
|
||||
}
|
||||
|
||||
if (allData.length >= totalRecords) {
|
||||
showProgressToast(100); // Set to 100% before actual download
|
||||
const filename = `Insights-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(endDate).format(
|
||||
"YYYY-MM-DD"
|
||||
)}.csv`;
|
||||
downloadAsCsv(allData as Record<string, unknown>[], filename);
|
||||
}
|
||||
} catch {
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
hideProgressToast(); // Reset progress
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
EndIcon="file-down"
|
||||
color="secondary"
|
||||
loading={isDownloading}
|
||||
className="h-full self-end sm:self-baseline">
|
||||
{t("download")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownItem onClick={handleDownloadClick}>{t("as_csv")}</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<CsvDownloadButton<RawData>
|
||||
fetchBatch={async (offset) => {
|
||||
const result = await utils.viewer.insights.rawData.fetch({
|
||||
...insightsBookingParams,
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
});
|
||||
|
||||
if (result && "data" in result && "total" in result) {
|
||||
return { data: result.data as RawData[], total: result.total };
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
filename={`Insights-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(endDate).format("YYYY-MM-DD")}.csv`}
|
||||
onDownloadStart={() => {
|
||||
posthog.capture("insights_bookings_download_clicked", {
|
||||
teamId: insightsBookingParams.selectedTeamId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+15
-85
@@ -1,103 +1,33 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { SortingState } from "@calcom/features/data-table";
|
||||
import { useInsightsRoutingParameters } from "@calcom/web/modules/insights/hooks/useInsightsRoutingParameters";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
import { showToast, showProgressToast, hideProgressToast } from "@calcom/ui/components/toast";
|
||||
import { useInsightsRoutingParameters } from "@calcom/web/modules/insights/hooks/useInsightsRoutingParameters";
|
||||
|
||||
type RoutingData = RouterOutputs["viewer"]["insights"]["routingFormResponsesForDownload"]["data"][number];
|
||||
import { CsvDownloadButton } from "@lib/components/CsvDownloadButton";
|
||||
|
||||
type Props = {
|
||||
sorting: SortingState;
|
||||
};
|
||||
|
||||
const BATCH_SIZE = 100; // Increased batch size for downloads
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export const RoutingFormResponsesDownload = ({ sorting }: Props) => {
|
||||
const { t } = useLocale();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const insightsRoutingParameters = useInsightsRoutingParameters();
|
||||
const { startDate, endDate } = insightsRoutingParameters;
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const fetchBatch = async (
|
||||
offset: number
|
||||
): Promise<{
|
||||
data: RoutingData[];
|
||||
total: number;
|
||||
}> => {
|
||||
const result = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
|
||||
...insightsRoutingParameters,
|
||||
sorting,
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleDownloadClick = async () => {
|
||||
try {
|
||||
setIsDownloading(true);
|
||||
showProgressToast(0); // Reset progress
|
||||
let allData: RoutingData[] = [];
|
||||
let offset = 0;
|
||||
|
||||
// Get first batch to get total count
|
||||
const firstBatch = await fetchBatch(0);
|
||||
allData = [...firstBatch.data];
|
||||
const totalRecords = firstBatch.total;
|
||||
|
||||
// Continue fetching remaining batches
|
||||
while (totalRecords > 0 && allData.length < totalRecords) {
|
||||
offset += BATCH_SIZE;
|
||||
const result = await fetchBatch(offset);
|
||||
allData = [...allData, ...result.data];
|
||||
|
||||
const currentProgress = Math.min(Math.round((allData.length / totalRecords) * 100), 99);
|
||||
showProgressToast(currentProgress);
|
||||
}
|
||||
|
||||
if (allData.length >= totalRecords) {
|
||||
showProgressToast(100); // Set to 100% before actual download
|
||||
const filename = `RoutingFormResponses-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(
|
||||
endDate
|
||||
).format("YYYY-MM-DD")}.csv`;
|
||||
downloadAsCsv(allData as Record<string, unknown>[], filename);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(t("error_downloading_data"), "error");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
hideProgressToast(); // Reset progress
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
EndIcon="file-down"
|
||||
color="secondary"
|
||||
className="self-end sm:self-baseline"
|
||||
loading={isDownloading}>
|
||||
{t("download")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownItem onClick={handleDownloadClick}>{t("as_csv")}</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
<CsvDownloadButton
|
||||
fetchBatch={async (offset) => {
|
||||
const result = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
|
||||
...insightsRoutingParameters,
|
||||
sorting,
|
||||
limit: BATCH_SIZE,
|
||||
offset,
|
||||
});
|
||||
return result;
|
||||
}}
|
||||
filename={`RoutingFormResponses-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(endDate).format("YYYY-MM-DD")}.csv`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { clearFilters, applySelectFilter } from "./filter-helpers";
|
||||
import { expect } from "@playwright/test";
|
||||
import { applySelectFilter, clearFilters } from "./filter-helpers";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { createAllPermissionsArray, enablePBACForTeam } from "./lib/test-helpers/pbac";
|
||||
|
||||
@@ -224,17 +222,13 @@ test.describe("Insights", async () => {
|
||||
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
|
||||
const downloadButton = page.getByRole("button", { name: "Download" });
|
||||
|
||||
// Expect download button to be visible
|
||||
await expect(page.locator("text=Download")).toBeVisible();
|
||||
|
||||
// Click on Download button
|
||||
await page.getByText("Download").click();
|
||||
|
||||
// Expect as csv option to be visible
|
||||
await expect(page.locator("text=as CSV")).toBeVisible();
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
await page.getByText("as CSV").click();
|
||||
await downloadButton.click();
|
||||
const download = await downloadPromise;
|
||||
|
||||
// Wait for the download process to complete and save the downloaded file somewhere.
|
||||
|
||||
@@ -4508,5 +4508,8 @@
|
||||
"error_enabling_feature": "Error enabling feature. Please try again.",
|
||||
"set_organizer_as_contact_owner": "Set booking organizer as contact owner",
|
||||
"overwrite_existing_contact_owner": "Overwrite existing contact owner",
|
||||
"failed_to_download": "Failed to download",
|
||||
"cancel_download": "Cancel Download",
|
||||
"download_progress": "{{progress}}% complete",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS":"↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user