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:
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user