fix: apply standard pagination to /insights/routing (#19950)

* fix: apply standard pagination to /insights/routing

* fix type error

* max limit
This commit is contained in:
Eunjae Lee
2025-03-11 16:51:11 +00:00
committed by GitHub
parent d0ca0dab53
commit eafb61f515
5 changed files with 70 additions and 93 deletions
@@ -1,6 +1,5 @@
"use client";
import { keepPreviousData } from "@tanstack/react-query";
import {
useReactTable,
getCoreRowModel,
@@ -55,34 +54,25 @@ export function RoutingFormResponsesTable() {
const getInsightsFacetedUniqueValues = useInsightsFacetedUniqueValues({ headers, userId, teamId, isAll });
const { sorting, updateFilter } = useDataTable();
const { sorting, limit, offset, updateFilter } = useDataTable();
const { data, fetchNextPage, isFetching, isPending, hasNextPage, isLoading } =
trpc.viewer.insights.routingFormResponses.useInfiniteQuery(
{
teamId,
startDate,
endDate,
userId,
memberUserIds,
isAll,
routingFormId,
columnFilters,
sorting,
limit: 30,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
placeholderData: keepPreviousData,
trpc: {
context: { skipBatch: true },
},
}
);
const { data, isFetching, isPending, isLoading } = trpc.viewer.insights.routingFormResponses.useQuery({
teamId,
startDate,
endDate,
userId,
memberUserIds,
isAll,
routingFormId,
columnFilters,
sorting,
limit,
offset,
});
const processedData = useMemo(() => {
if (!isHeadersSuccess) return [];
return (data?.pages?.flatMap((page) => page.data) ?? []) as RoutingFormTableRow[];
if (!isHeadersSuccess || !data) return [];
return data.data as RoutingFormTableRow[];
}, [data, isHeadersSuccess]);
const columns = useInsightsColumns({ headers, isHeadersSuccess });
@@ -118,7 +108,7 @@ export function RoutingFormResponsesTable() {
}
}, [table, getInsightsFacetedUniqueValues, routingFormId]);
if ((isHeadersLoading && !headers) || ((isFetching || isLoading) && !data)) {
if (isHeadersLoading && !headers) {
return <DataTableSkeleton columns={4} columnWidths={[200, 200, 250, 250]} />;
}
@@ -128,10 +118,9 @@ export function RoutingFormResponsesTable() {
<DataTableWrapper
table={table}
isPending={isPending}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
isFetching={isFetching}
paginationMode="infinite"
paginationMode="standard"
totalRowCount={data?.total}
LoaderView={<DataTableSkeleton columns={4} columnWidths={[200, 200, 250, 250]} />}
ToolbarLeft={
<>
<OrgTeamsFilter />
@@ -17,10 +17,7 @@ type Props = {
sorting: SortingState;
};
type Batch = {
data: RoutingData[];
nextCursor: number | undefined;
};
const BATCH_SIZE = 100; // Increased batch size for downloads
export const RoutingFormResponsesDownload = ({ sorting }: Props) => {
const { t } = useLocale();
@@ -31,12 +28,12 @@ export const RoutingFormResponsesDownload = ({ sorting }: Props) => {
const utils = trpc.useUtils();
const fetchBatch = async (
cursor: number | undefined = undefined
offset: number
): Promise<{
data: RoutingData[];
nextCursor: number | undefined;
total: number;
}> => {
const { data, nextCursor } = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
const result = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
teamId,
startDate,
endDate,
@@ -46,27 +43,28 @@ export const RoutingFormResponsesDownload = ({ sorting }: Props) => {
routingFormId,
columnFilters,
sorting,
cursor,
limit: BATCH_SIZE,
offset,
});
return {
data,
nextCursor,
};
return result;
};
const handleDownloadClick = async () => {
try {
setIsDownloading(true);
let allData: RoutingData[] = [];
let hasMore = true;
let cursor: number | undefined = undefined;
let offset = 0;
// Fetch data in batches until there's no more data
while (hasMore) {
const result: Batch = await fetchBatch(cursor);
// Get first batch to get total count
const firstBatch = await fetchBatch(0);
allData = [...firstBatch.data];
const totalRecords = firstBatch.total;
// Continue fetching remaining batches
while (allData.length < totalRecords) {
offset += BATCH_SIZE;
const result = await fetchBatch(offset);
allData = [...allData, ...result.data];
hasMore = result.nextCursor !== undefined;
cursor = result.nextCursor;
}
if (allData.length > 0) {
@@ -23,7 +23,7 @@ export const rawDataInputSchema = z.object({
eventTypeId: z.coerce.number().optional().nullable(),
}) satisfies z.ZodType<RawDataInput>;
export type RoutingFormResponsesInput = {
export type RoutingFormStatsInput = {
startDate: string;
endDate: string;
teamId?: number;
@@ -31,13 +31,16 @@ export type RoutingFormResponsesInput = {
memberUserIds?: number[];
isAll: boolean;
routingFormId?: string;
cursor?: number;
limit?: number;
columnFilters: ColumnFilter[];
sorting?: Sorting[];
};
export const routingFormResponsesInputSchema = z.object({
export type RoutingFormResponsesInput = RoutingFormStatsInput & {
offset?: number;
limit?: number;
};
export const routingFormStatsInputSchema = z.object({
startDate: z.string(),
endDate: z.string(),
teamId: z.coerce.number().optional(),
@@ -45,8 +48,12 @@ export const routingFormResponsesInputSchema = z.object({
memberUserIds: z.number().array().optional(),
isAll: z.coerce.boolean(),
routingFormId: z.string().optional(),
cursor: z.number().optional(),
limit: z.number().optional(),
columnFilters: z.array(ZColumnFilter),
sorting: z.array(ZSorting).optional(),
}) satisfies z.ZodType<RoutingFormStatsInput>;
export const routingFormResponsesInputSchema = z.object({
...routingFormStatsInputSchema.shape,
offset: z.number().optional(),
limit: z.number().max(100).optional(),
}) satisfies z.ZodType<RoutingFormResponsesInput>;
@@ -12,10 +12,12 @@ import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-
import dayjs from "@calcom/dayjs";
import { ColumnFilterType } from "@calcom/features/data-table";
import { makeWhereClause, makeOrderBy } from "@calcom/features/data-table/lib/server";
import type { ColumnFilter, TypedColumnFilter } from "@calcom/features/data-table/lib/types";
import type { RoutingFormResponsesInput } from "@calcom/features/insights/server/raw-data.schema";
import type { TypedColumnFilter } from "@calcom/features/data-table/lib/types";
import type {
RoutingFormResponsesInput,
RoutingFormStatsInput,
} from "@calcom/features/insights/server/raw-data.schema";
import { readonlyPrisma as prisma } from "@calcom/prisma";
import type { BookingStatus } from "@calcom/prisma/enums";
import { type ResponseValue, ZResponse } from "../lib/types";
@@ -27,17 +29,8 @@ type RoutingFormInsightsTeamFilter = {
routingFormId?: string | null;
};
type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & {
startDate?: string;
endDate?: string;
memberUserId?: number | null;
searchQuery?: string | null;
bookingStatus?: BookingStatus | "NO_BOOKING" | null;
fieldFilter?: {
fieldId: string;
optionId: string;
} | null;
columnFilters: ColumnFilter[];
type RoutingFormStatsFilter = RoutingFormStatsInput & {
organizationId: number | null;
};
type RoutingFormResponsesFilter = RoutingFormResponsesInput & {
@@ -114,13 +107,11 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
limit,
userId,
memberUserIds,
columnFilters,
sorting,
}: RoutingFormResponsesFilter) {
}: RoutingFormStatsFilter) {
const whereClause = await this.getWhereClauseForRoutingFormResponses({
teamId,
startDate,
@@ -128,8 +119,6 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
limit,
userId,
memberUserIds,
columnFilters,
@@ -288,8 +277,8 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
limit,
offset,
userId,
memberUserIds,
columnFilters,
@@ -302,8 +291,6 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
limit,
userId,
memberUserIds,
columnFilters,
@@ -336,16 +323,14 @@ class RoutingEventsInsights {
},
where: whereClause,
orderBy: sorting && sorting.length > 0 ? makeOrderBy(sorting) : { createdAt: "desc" },
take: limit ? limit + 1 : undefined, // Get one extra item to check if there are more pages
cursor: cursor ? { id: cursor } : undefined,
take: limit,
skip: offset,
});
const [totalResponses, responses] = await Promise.all([totalResponsePromise, responsesPromise]);
const hasNextPage = responses.length > (limit ?? 0);
const responsesToReturn = responses.slice(0, limit ? limit : responses.length);
type Response = Omit<
(typeof responsesToReturn)[number],
(typeof responses)[number],
"response" | "responseLowercase" | "bookingAttendees"
> & {
response: Record<string, ResponseValue>;
@@ -355,8 +340,7 @@ class RoutingEventsInsights {
return {
total: totalResponses,
data: responsesToReturn as Response[],
nextCursor: hasNextPage ? responsesToReturn[responsesToReturn.length - 1].id : undefined,
data: responses as Response[],
};
}
@@ -367,8 +351,8 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
limit,
offset,
userId,
memberUserIds,
columnFilters,
@@ -388,10 +372,10 @@ class RoutingEventsInsights {
isAll,
organizationId,
routingFormId,
cursor,
userId,
memberUserIds,
limit,
offset,
columnFilters,
sorting,
});
@@ -444,7 +428,7 @@ class RoutingEventsInsights {
return {
data: dataWithFlatResponse,
nextCursor: data.nextCursor,
total: data.total,
};
}
@@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs";
import {
rawDataInputSchema,
routingFormResponsesInputSchema,
routingFormStatsInputSchema,
} from "@calcom/features/insights/server/raw-data.schema";
import { randomString } from "@calcom/lib/random";
import type { readonlyPrisma } from "@calcom/prisma";
@@ -1574,7 +1575,7 @@ export const insightsRouter = router({
});
}),
routingFormsByStatus: userBelongsToTeamProcedure
.input(routingFormResponsesInputSchema)
.input(routingFormStatsInputSchema)
.query(async ({ ctx, input }) => {
return await RoutingEventsInsights.getRoutingFormStats({
teamId: input.teamId,
@@ -1583,10 +1584,8 @@ export const insightsRouter = router({
isAll: input.isAll,
organizationId: ctx.user.organizationId ?? null,
routingFormId: input.routingFormId,
cursor: input.cursor,
userId: ctx.user.id,
memberUserIds: input.memberUserIds,
limit: input.limit,
columnFilters: input.columnFilters,
sorting: input.sorting,
});
@@ -1601,10 +1600,10 @@ export const insightsRouter = router({
isAll: input.isAll,
organizationId: ctx.user.organizationId ?? null,
routingFormId: input.routingFormId,
cursor: input.cursor,
userId: ctx.user.id,
memberUserIds: input.memberUserIds,
limit: input.limit,
offset: input.offset,
columnFilters: input.columnFilters,
sorting: input.sorting,
});
@@ -1619,10 +1618,10 @@ export const insightsRouter = router({
isAll: input.isAll,
organizationId: ctx.user.organizationId ?? null,
routingFormId: input.routingFormId,
cursor: input.cursor,
userId: ctx.user.id,
memberUserIds: input.memberUserIds,
limit: input.limit ?? BATCH_SIZE,
offset: input.offset,
columnFilters: input.columnFilters,
sorting: input.sorting,
});