Skip to content

Commit c4eda5f

Browse files
authored
perf: caching + modularize data fetching for team booking pages (calcom#22172)
* wip * wip * wip * refactor * refactor * refactor * refactor * refactor * fix * better * fix * fix * use notFound * refactor * wip * refactor * wip * refactor * wip * wip * finalize * wip * fix * lots of refactors * better code * clean up * fix * fix type checks * finalize * select more fields * select more fields * fix * fix * fix * fix * better * better * fix * fix * fix * fix * add back comment * fix * better * fix * refactor * fix * fix type check * add comment * fix * wip * wip * add more comment * refactor * rename * refactors * fix reschedule logic * remove hard-coded true from isCachedEnabled * better * use revalidateTeamDataCache in team profile update * fix invalidation * better comment * refactors * add better comments * add revalidations * add comments * fix * remove hard code * fix * update tags * use features package * fix type check
1 parent e6da5c8 commit c4eda5f

19 files changed

Lines changed: 932 additions & 5 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use server";
2+
3+
import { revalidateTag } from "next/cache";
4+
5+
// Coupled with `getCachedTeamData` in `queries.ts`
6+
export async function revalidateTeamDataCache({
7+
teamSlug,
8+
orgSlug,
9+
}: {
10+
teamSlug: string;
11+
orgSlug: string | null;
12+
}) {
13+
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`);
14+
}
15+
16+
// Coupled with `getCachedTeamEventType` in `queries.ts`
17+
export async function revalidateTeamEventTypeCache({
18+
teamSlug,
19+
meetingSlug,
20+
orgSlug,
21+
}: {
22+
teamSlug: string;
23+
meetingSlug: string;
24+
orgSlug: string | null;
25+
}) {
26+
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`);
27+
}

apps/web/app/(booking-page-wrapper)/team/[slug]/[type]/page.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { CustomI18nProvider } from "app/CustomI18nProvider";
22
import { withAppDirSsr } from "app/WithAppDirSsr";
3-
import type { PageProps } from "app/_types";
3+
import type { PageProps, SearchParams } from "app/_types";
44
import { generateMeetingMetadata } from "app/_utils";
55
import { cookies, headers } from "next/headers";
66

77
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
8+
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
89
import { loadTranslations } from "@calcom/lib/server/i18n";
910

1011
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
@@ -13,7 +14,20 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
1314
import LegacyPage from "~/team/type-view";
1415
import type { PageProps as LegacyPageProps } from "~/team/type-view";
1516

17+
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
18+
19+
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
20+
const featuresRepository = new FeaturesRepository();
21+
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
22+
"team-booking-page-cache"
23+
);
24+
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
25+
}
26+
1627
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
28+
if (await isCachedTeamBookingEnabled(await searchParams)) {
29+
return await generateCachedMetadata({ params, searchParams });
30+
}
1731
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
1832
const props = await getData(legacyCtx);
1933
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
@@ -53,6 +67,10 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
5367
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
5468

5569
const ServerPage = async ({ params, searchParams }: PageProps) => {
70+
if (await isCachedTeamBookingEnabled(await searchParams)) {
71+
return await CachedTeamBooker({ params, searchParams });
72+
}
73+
5674
const props = await getData(
5775
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
5876
);
@@ -70,4 +88,5 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
7088

7189
return <LegacyPage {...props} />;
7290
};
91+
7392
export default ServerPage;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { CustomI18nProvider } from "app/CustomI18nProvider";
2+
import type { PageProps, Params } from "app/_types";
3+
import { generateMeetingMetadata } from "app/_utils";
4+
import { headers, cookies } from "next/headers";
5+
import { notFound, redirect } from "next/navigation";
6+
import { z } from "zod";
7+
8+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
9+
import { getBookingForReschedule, type GetBookingType } from "@calcom/features/bookings/lib/get-booking";
10+
import { getOrgFullOrigin, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
11+
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
12+
import type { TeamData } from "@calcom/features/ee/teams/lib/getTeamData";
13+
import { shouldHideBrandingForTeamEvent } from "@calcom/lib/hideBranding";
14+
import { loadTranslations } from "@calcom/lib/server/i18n";
15+
import slugify from "@calcom/lib/slugify";
16+
import { BookingStatus, RedirectType } from "@calcom/prisma/enums";
17+
18+
import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx";
19+
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
20+
21+
import CachedClientView, { type TeamBookingPageProps } from "~/team/type-view-cached";
22+
23+
import { getCachedTeamData, getEnrichedEventType, getCRMData, shouldUseApiV2ForTeamSlots } from "./queries";
24+
25+
const paramsSchema = z.object({
26+
slug: z.string().transform((s) => slugify(s)),
27+
type: z.string().transform((s) => slugify(s)),
28+
});
29+
30+
const _getTeamMetadataForBooking = (teamData: NonNullable<TeamData>, eventTypeId: number) => {
31+
const organizationSettings = getOrganizationSEOSettings(teamData);
32+
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
33+
34+
return {
35+
orgBannerUrl: teamData.parent?.bannerUrl ?? "",
36+
hideBranding: shouldHideBrandingForTeamEvent({
37+
eventTypeId,
38+
team: teamData,
39+
}),
40+
isSEOIndexable: allowSEOIndexing,
41+
};
42+
};
43+
44+
async function _getOrgContext(params: Params) {
45+
const result = paramsSchema.safeParse({
46+
slug: params?.slug,
47+
type: params?.type,
48+
});
49+
50+
if (!result.success) return notFound(); // should never happen
51+
52+
const { slug: teamSlug, type: meetingSlug } = result.data;
53+
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
54+
buildLegacyRequest(await headers(), await cookies()),
55+
params?.orgSlug ?? undefined
56+
);
57+
58+
return {
59+
currentOrgDomain,
60+
isValidOrgDomain,
61+
teamSlug,
62+
meetingSlug,
63+
};
64+
}
65+
66+
const _getMultipleDurationValue = (
67+
multipleDurationConfig: number[] | undefined,
68+
queryDuration: string | string[] | null | undefined,
69+
defaultValue: number
70+
) => {
71+
if (!multipleDurationConfig) return null;
72+
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
73+
return defaultValue;
74+
};
75+
76+
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
77+
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
78+
79+
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
80+
if (!teamData) return {}; // should never happen
81+
82+
const enrichedEventType = await getEnrichedEventType({
83+
teamSlug,
84+
meetingSlug,
85+
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
86+
fromRedirectOfNonOrgLink: (await searchParams).orgRedirection === "true",
87+
});
88+
if (!enrichedEventType) return {}; // should never happen
89+
90+
const title = enrichedEventType.title;
91+
const profileName = enrichedEventType.profile.name ?? "";
92+
const profileImage = enrichedEventType.profile.image;
93+
94+
const meeting = {
95+
title,
96+
profile: { name: profileName, image: profileImage },
97+
users: [
98+
...(enrichedEventType?.subsetOfUsers || []).map((user) => ({
99+
name: `${user.name}`,
100+
username: `${user.username}`,
101+
})),
102+
],
103+
};
104+
105+
const { hideBranding, isSEOIndexable } = _getTeamMetadataForBooking(teamData, enrichedEventType.id);
106+
107+
const metadata = await generateMeetingMetadata(
108+
meeting,
109+
() => `${title} | ${profileName}`,
110+
() => title,
111+
hideBranding,
112+
getOrgFullOrigin(enrichedEventType.entity.orgSlug ?? null),
113+
`/team/${teamSlug}/${meetingSlug}`
114+
);
115+
116+
return {
117+
...metadata,
118+
robots: {
119+
follow: !(enrichedEventType.hidden || !isSEOIndexable),
120+
index: !(enrichedEventType.hidden || !isSEOIndexable),
121+
},
122+
};
123+
};
124+
125+
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
126+
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
127+
const isOrgContext = currentOrgDomain && isValidOrgDomain;
128+
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
129+
130+
// Handle org redirects for non-org contexts
131+
if (!isOrgContext) {
132+
const redirectResult = await getTemporaryOrgRedirect({
133+
slugs: teamSlug,
134+
redirectType: RedirectType.Team,
135+
eventTypeSlug: meetingSlug,
136+
currentQuery: legacyCtx.query,
137+
});
138+
if (redirectResult) return redirect(redirectResult.redirect.destination);
139+
}
140+
141+
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
142+
143+
if (!teamData) return notFound();
144+
145+
const enrichedEventType = await getEnrichedEventType({
146+
teamSlug,
147+
meetingSlug,
148+
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
149+
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
150+
});
151+
if (!enrichedEventType) return notFound();
152+
153+
// Handle rescheduling
154+
const { rescheduleUid } = legacyCtx.query;
155+
let bookingForReschedule: GetBookingType | null = null;
156+
if (rescheduleUid) {
157+
const session = await getServerSession({ req: legacyCtx.req });
158+
bookingForReschedule = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
159+
if (enrichedEventType.disableRescheduling) return redirect(`/booking/${rescheduleUid}`);
160+
161+
if (
162+
bookingForReschedule?.status === BookingStatus.CANCELLED &&
163+
legacyCtx.query.allowRescheduleForCancelledBooking !== "true" &&
164+
!enrichedEventType.allowReschedulingCancelledBookings
165+
) {
166+
// redirecting to the same booking page without `rescheduleUid` search param
167+
return redirect(`/team/${teamSlug}/${meetingSlug}`);
168+
}
169+
}
170+
171+
const [crmData, useApiV2] = await Promise.all([
172+
getCRMData(legacyCtx.query, {
173+
id: enrichedEventType.id,
174+
isInstantEvent: enrichedEventType.isInstantEvent,
175+
schedulingType: enrichedEventType.schedulingType,
176+
metadata: enrichedEventType.metadata,
177+
length: enrichedEventType.length,
178+
}),
179+
shouldUseApiV2ForTeamSlots(teamData.id),
180+
]);
181+
182+
const props: TeamBookingPageProps = {
183+
..._getTeamMetadataForBooking(teamData, enrichedEventType.id),
184+
...crmData,
185+
useApiV2,
186+
isInstantMeeting: legacyCtx.query.isInstantMeeting === "true",
187+
eventSlug: meetingSlug,
188+
username: teamSlug,
189+
eventData: {
190+
...enrichedEventType,
191+
},
192+
entity: { ...enrichedEventType.entity },
193+
bookingData: bookingForReschedule,
194+
isTeamEvent: true,
195+
durationConfig: enrichedEventType.metadata?.multipleDuration,
196+
duration: _getMultipleDurationValue(
197+
enrichedEventType.metadata?.multipleDuration,
198+
legacyCtx.query.duration,
199+
enrichedEventType.length
200+
),
201+
};
202+
const Booker = <CachedClientView {...props} />;
203+
204+
const eventLocale = enrichedEventType.interfaceLanguage;
205+
if (eventLocale) {
206+
const ns = "common";
207+
const translations = await loadTranslations(eventLocale, ns);
208+
return (
209+
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
210+
{Booker}
211+
</CustomI18nProvider>
212+
);
213+
}
214+
215+
return Booker;
216+
};
217+
218+
export default CachedTeamBooker;

0 commit comments

Comments
 (0)