diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx index d54d3d0f1e..5db4be54d3 100644 --- a/apps/web/modules/event-types/views/event-types-listing-view.tsx +++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx @@ -81,7 +81,7 @@ interface InfiniteEventTypeListProps { group: InfiniteEventTypeGroup; readOnly: boolean; bookerUrl: string | null; - pages: { nextCursor: number | undefined; eventTypes: InfiniteEventType[] }[] | undefined; + pages: { nextCursor: number | null | undefined; eventTypes: InfiniteEventType[] }[] | undefined; lockedByOrg?: boolean; isPending?: boolean; debouncedSearchTerm?: string; @@ -349,9 +349,14 @@ export const InfiniteEventTypeList = ({ group: { teamId: group?.teamId, parentId: group?.parentId }, }, (data) => { + if (!data) return { pages: [], pageParams: [] }; + return { - pageParams: data?.pageParams ?? [], - pages: newOrder, + ...data, + pages: newOrder.map((page) => ({ + ...page, + nextCursor: page.nextCursor ?? undefined, + })), }; } ); diff --git a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts index 107474b9c2..6dc26cf1ff 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts @@ -6,7 +6,6 @@ import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; import { prisma } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma"; -// import { SchedulingType } from "@calcom/prisma/enums"; import type { TrpcSessionUser } from "../../../trpc"; import type { TGetEventTypesFromGroupSchema } from "./getByViewer.schema"; import { mapEventType } from "./util"; @@ -22,8 +21,15 @@ type GetByViewerOptions = { }; type EventType = Awaited>[number]; +type MappedEventType = Awaited>; -export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) => { +export const getEventTypesFromGroup = async ({ + ctx, + input, +}: GetByViewerOptions): Promise<{ + eventTypes: MappedEventType[]; + nextCursor: number | null | undefined; +}> => { await checkRateLimitAndThrowError({ identifier: `eventTypes:getEventTypesFromGroup:${ctx.user.id}`, rateLimitingType: "common", @@ -31,7 +37,7 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) const userProfile = ctx.user.profile; const { group, limit, cursor, filters, searchQuery } = input; - const { teamId, parentId } = group; + const { teamId } = group; const isFilterSet = (filters && hasFilter(filters)) || !!teamId; const isUpIdInFilter = filters?.upIds?.includes(userProfile.upId); @@ -39,6 +45,41 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) const shouldListUserEvents = !isFilterSet || isUpIdInFilter || (isFilterSet && filters?.upIds && !isUpIdInFilter); + const eventTypes: MappedEventType[] = []; + const currentCursor = cursor; + let nextCursor: number | null | undefined = undefined; + let isFetchingForFirstTime = true; + + const fetchAndFilterEventTypes = async () => { + const batch = await fetchEventTypesBatch(ctx, input, shouldListUserEvents, currentCursor, searchQuery); + const filteredBatch = await filterEventTypes(batch.eventTypes, ctx.user.id, shouldListUserEvents, teamId); + eventTypes.push(...filteredBatch); + nextCursor = batch.nextCursor; + }; + + while (eventTypes.length < limit && (nextCursor || isFetchingForFirstTime)) { + await fetchAndFilterEventTypes(); + isFetchingForFirstTime = false; + } + + return { + eventTypes, + nextCursor: nextCursor ?? undefined, + }; +}; + +const fetchEventTypesBatch = async ( + ctx: GetByViewerOptions["ctx"], + input: GetByViewerOptions["input"], + shouldListUserEvents: boolean | undefined, + cursor: TGetEventTypesFromGroupSchema["cursor"], + searchQuery: TGetEventTypesFromGroupSchema["searchQuery"] +) => { + const userProfile = ctx.user.profile; + const { group, limit, filters } = input; + const { teamId, parentId } = group; + const isFilterSet = (filters && hasFilter(filters)) || !!teamId; + const eventTypes: EventType[] = []; if (shouldListUserEvents || !teamId) { @@ -99,8 +140,8 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) eventTypes.push(...teamEventTypes); } - let nextCursor: typeof cursor | undefined = undefined; - if (eventTypes && eventTypes.length > limit) { + let nextCursor: number | null | undefined = undefined; + if (eventTypes.length > limit) { const nextItem = eventTypes.pop(); nextCursor = nextItem?.id; } @@ -108,37 +149,45 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) const mappedEventTypes = await Promise.all(eventTypes.map(mapEventType)); log.info( - "mappedEventTypes before filtering", + "fetchEventTypesBatch", safeStringify({ - input, mappedEventTypes, }) ); - const filteredEventTypes = mappedEventTypes.filter((eventType) => { - const isAChildEvent = eventType.parentId; - if (!isAChildEvent) { + return { eventTypes: mappedEventTypes, nextCursor: nextCursor ?? undefined }; +}; + +const filterEventTypes = async ( + eventTypes: MappedEventType[], + userId: number, + shouldListUserEvents: boolean | undefined, + teamId: number | null | undefined +) => { + const filteredEventTypes = eventTypes.filter((eventType) => { + if (!eventType.parentId) { return true; } // A child event only has one user const childEventAssignee = eventType.users[0]; - if (!childEventAssignee || childEventAssignee.id != ctx.user.id) { + + if (!childEventAssignee || childEventAssignee.id !== userId) { return false; } return true; }); log.info( - "mappedEventTypes after filtering", + "mappedEventTypes before and after filtering", safeStringify({ - input, - filteredEventTypes, + beforeFiltering: eventTypes, + afterFiltering: filteredEventTypes, }) ); const membership = await prisma.membership.findFirst({ where: { - userId: ctx.user.id, + userId, teamId: teamId ?? 0, accepted: true, role: "MEMBER", @@ -158,8 +207,12 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) evType.hosts = []; }); - return { - eventTypes: filteredEventTypes || [], - nextCursor, - }; + log.info( + "filteredEventTypes", + safeStringify({ + filteredEventTypes, + }) + ); + + return filteredEventTypes; };