perf: caching + modularize data fetching for team booking pages (#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
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
|
||||
// Coupled with `getCachedTeamData` in `queries.ts`
|
||||
export async function revalidateTeamDataCache({
|
||||
teamSlug,
|
||||
orgSlug,
|
||||
}: {
|
||||
teamSlug: string;
|
||||
orgSlug: string | null;
|
||||
}) {
|
||||
revalidateTag(`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`);
|
||||
}
|
||||
|
||||
// Coupled with `getCachedTeamEventType` in `queries.ts`
|
||||
export async function revalidateTeamEventTypeCache({
|
||||
teamSlug,
|
||||
meetingSlug,
|
||||
orgSlug,
|
||||
}: {
|
||||
teamSlug: string;
|
||||
meetingSlug: string;
|
||||
orgSlug: string | null;
|
||||
}) {
|
||||
revalidateTag(`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import type { PageProps, SearchParams } from "app/_types";
|
||||
import { generateMeetingMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
|
||||
import { buildLegacyCtx, decodeParams } from "@lib/buildLegacyCtx";
|
||||
@@ -13,7 +14,20 @@ import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
|
||||
import LegacyPage from "~/team/type-view";
|
||||
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||
|
||||
import CachedTeamBooker, { generateMetadata as generateCachedMetadata } from "./pageWithCachedData";
|
||||
|
||||
async function isCachedTeamBookingEnabled(searchParams: SearchParams): Promise<boolean> {
|
||||
const featuresRepository = new FeaturesRepository();
|
||||
const isGloballyEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
||||
"team-booking-page-cache"
|
||||
);
|
||||
return isGloballyEnabled && searchParams.experimentalTeamBookingPageCache === "true";
|
||||
}
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
||||
return await generateCachedMetadata({ params, searchParams });
|
||||
}
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
@@ -53,6 +67,10 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
if (await isCachedTeamBookingEnabled(await searchParams)) {
|
||||
return await CachedTeamBooker({ params, searchParams });
|
||||
}
|
||||
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
@@ -70,4 +88,5 @@ const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
|
||||
return <LegacyPage {...props} />;
|
||||
};
|
||||
|
||||
export default ServerPage;
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { CustomI18nProvider } from "app/CustomI18nProvider";
|
||||
import type { PageProps, Params } from "app/_types";
|
||||
import { generateMeetingMetadata } from "app/_utils";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getBookingForReschedule, type GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getOrgFullOrigin, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
|
||||
import type { TeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import { shouldHideBrandingForTeamEvent } from "@calcom/lib/hideBranding";
|
||||
import { loadTranslations } from "@calcom/lib/server/i18n";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { BookingStatus, RedirectType } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyCtx, buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
|
||||
|
||||
import CachedClientView, { type TeamBookingPageProps } from "~/team/type-view-cached";
|
||||
|
||||
import { getCachedTeamData, getEnrichedEventType, getCRMData, shouldUseApiV2ForTeamSlots } from "./queries";
|
||||
|
||||
const paramsSchema = z.object({
|
||||
slug: z.string().transform((s) => slugify(s)),
|
||||
type: z.string().transform((s) => slugify(s)),
|
||||
});
|
||||
|
||||
const _getTeamMetadataForBooking = (teamData: NonNullable<TeamData>, eventTypeId: number) => {
|
||||
const organizationSettings = getOrganizationSEOSettings(teamData);
|
||||
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
|
||||
|
||||
return {
|
||||
orgBannerUrl: teamData.parent?.bannerUrl ?? "",
|
||||
hideBranding: shouldHideBrandingForTeamEvent({
|
||||
eventTypeId,
|
||||
team: teamData,
|
||||
}),
|
||||
isSEOIndexable: allowSEOIndexing,
|
||||
};
|
||||
};
|
||||
|
||||
async function _getOrgContext(params: Params) {
|
||||
const result = paramsSchema.safeParse({
|
||||
slug: params?.slug,
|
||||
type: params?.type,
|
||||
});
|
||||
|
||||
if (!result.success) return notFound(); // should never happen
|
||||
|
||||
const { slug: teamSlug, type: meetingSlug } = result.data;
|
||||
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(
|
||||
buildLegacyRequest(await headers(), await cookies()),
|
||||
params?.orgSlug ?? undefined
|
||||
);
|
||||
|
||||
return {
|
||||
currentOrgDomain,
|
||||
isValidOrgDomain,
|
||||
teamSlug,
|
||||
meetingSlug,
|
||||
};
|
||||
}
|
||||
|
||||
const _getMultipleDurationValue = (
|
||||
multipleDurationConfig: number[] | undefined,
|
||||
queryDuration: string | string[] | null | undefined,
|
||||
defaultValue: number
|
||||
) => {
|
||||
if (!multipleDurationConfig) return null;
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
||||
|
||||
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
||||
if (!teamData) return {}; // should never happen
|
||||
|
||||
const enrichedEventType = await getEnrichedEventType({
|
||||
teamSlug,
|
||||
meetingSlug,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: (await searchParams).orgRedirection === "true",
|
||||
});
|
||||
if (!enrichedEventType) return {}; // should never happen
|
||||
|
||||
const title = enrichedEventType.title;
|
||||
const profileName = enrichedEventType.profile.name ?? "";
|
||||
const profileImage = enrichedEventType.profile.image;
|
||||
|
||||
const meeting = {
|
||||
title,
|
||||
profile: { name: profileName, image: profileImage },
|
||||
users: [
|
||||
...(enrichedEventType?.subsetOfUsers || []).map((user) => ({
|
||||
name: `${user.name}`,
|
||||
username: `${user.username}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
const { hideBranding, isSEOIndexable } = _getTeamMetadataForBooking(teamData, enrichedEventType.id);
|
||||
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
() => `${title} | ${profileName}`,
|
||||
() => title,
|
||||
hideBranding,
|
||||
getOrgFullOrigin(enrichedEventType.entity.orgSlug ?? null),
|
||||
`/team/${teamSlug}/${meetingSlug}`
|
||||
);
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
robots: {
|
||||
follow: !(enrichedEventType.hidden || !isSEOIndexable),
|
||||
index: !(enrichedEventType.hidden || !isSEOIndexable),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const CachedTeamBooker = async ({ params, searchParams }: PageProps) => {
|
||||
const { currentOrgDomain, isValidOrgDomain, teamSlug, meetingSlug } = await _getOrgContext(await params);
|
||||
const isOrgContext = currentOrgDomain && isValidOrgDomain;
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
|
||||
// Handle org redirects for non-org contexts
|
||||
if (!isOrgContext) {
|
||||
const redirectResult = await getTemporaryOrgRedirect({
|
||||
slugs: teamSlug,
|
||||
redirectType: RedirectType.Team,
|
||||
eventTypeSlug: meetingSlug,
|
||||
currentQuery: legacyCtx.query,
|
||||
});
|
||||
if (redirectResult) return redirect(redirectResult.redirect.destination);
|
||||
}
|
||||
|
||||
const teamData = await getCachedTeamData(teamSlug, currentOrgDomain);
|
||||
|
||||
if (!teamData) return notFound();
|
||||
|
||||
const enrichedEventType = await getEnrichedEventType({
|
||||
teamSlug,
|
||||
meetingSlug,
|
||||
orgSlug: isValidOrgDomain ? currentOrgDomain : null,
|
||||
fromRedirectOfNonOrgLink: legacyCtx.query.orgRedirection === "true",
|
||||
});
|
||||
if (!enrichedEventType) return notFound();
|
||||
|
||||
// Handle rescheduling
|
||||
const { rescheduleUid } = legacyCtx.query;
|
||||
let bookingForReschedule: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
const session = await getServerSession({ req: legacyCtx.req });
|
||||
bookingForReschedule = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
|
||||
if (enrichedEventType.disableRescheduling) return redirect(`/booking/${rescheduleUid}`);
|
||||
|
||||
if (
|
||||
bookingForReschedule?.status === BookingStatus.CANCELLED &&
|
||||
legacyCtx.query.allowRescheduleForCancelledBooking !== "true" &&
|
||||
!enrichedEventType.allowReschedulingCancelledBookings
|
||||
) {
|
||||
// redirecting to the same booking page without `rescheduleUid` search param
|
||||
return redirect(`/team/${teamSlug}/${meetingSlug}`);
|
||||
}
|
||||
}
|
||||
|
||||
const [crmData, useApiV2] = await Promise.all([
|
||||
getCRMData(legacyCtx.query, {
|
||||
id: enrichedEventType.id,
|
||||
isInstantEvent: enrichedEventType.isInstantEvent,
|
||||
schedulingType: enrichedEventType.schedulingType,
|
||||
metadata: enrichedEventType.metadata,
|
||||
length: enrichedEventType.length,
|
||||
}),
|
||||
shouldUseApiV2ForTeamSlots(teamData.id),
|
||||
]);
|
||||
|
||||
const props: TeamBookingPageProps = {
|
||||
..._getTeamMetadataForBooking(teamData, enrichedEventType.id),
|
||||
...crmData,
|
||||
useApiV2,
|
||||
isInstantMeeting: legacyCtx.query.isInstantMeeting === "true",
|
||||
eventSlug: meetingSlug,
|
||||
username: teamSlug,
|
||||
eventData: {
|
||||
...enrichedEventType,
|
||||
},
|
||||
entity: { ...enrichedEventType.entity },
|
||||
bookingData: bookingForReschedule,
|
||||
isTeamEvent: true,
|
||||
durationConfig: enrichedEventType.metadata?.multipleDuration,
|
||||
duration: _getMultipleDurationValue(
|
||||
enrichedEventType.metadata?.multipleDuration,
|
||||
legacyCtx.query.duration,
|
||||
enrichedEventType.length
|
||||
),
|
||||
};
|
||||
const Booker = <CachedClientView {...props} />;
|
||||
|
||||
const eventLocale = enrichedEventType.interfaceLanguage;
|
||||
if (eventLocale) {
|
||||
const ns = "common";
|
||||
const translations = await loadTranslations(eventLocale, ns);
|
||||
return (
|
||||
<CustomI18nProvider translations={translations} locale={eventLocale} ns={ns}>
|
||||
{Booker}
|
||||
</CustomI18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return Booker;
|
||||
};
|
||||
|
||||
export default CachedTeamBooker;
|
||||
@@ -0,0 +1,155 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
import { getTeamData } from "@calcom/features/ee/teams/lib/getTeamData";
|
||||
import {
|
||||
getEventTypeHosts,
|
||||
getProfileFromEvent,
|
||||
getUsersFromEvent,
|
||||
processEventDataShared,
|
||||
} from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import { getTeamEventType } from "@calcom/features/eventtypes/lib/getTeamEventType";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { NEXTJS_CACHE_TTL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { UserRepository } from "@calcom/lib/server/repository/user";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export async function getCachedTeamData(teamSlug: string, orgSlug: string | null) {
|
||||
return unstable_cache(async () => getTeamData(teamSlug, orgSlug), ["team-data", teamSlug, orgSlug ?? ""], {
|
||||
revalidate: NEXTJS_CACHE_TTL,
|
||||
tags: [`team:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}`],
|
||||
})();
|
||||
}
|
||||
|
||||
export async function getCachedTeamEventType(teamSlug: string, meetingSlug: string, orgSlug: string | null) {
|
||||
return unstable_cache(
|
||||
async () => getTeamEventType(teamSlug, meetingSlug, orgSlug),
|
||||
["team-event-type", teamSlug, meetingSlug, orgSlug ?? ""],
|
||||
{
|
||||
revalidate: NEXTJS_CACHE_TTL,
|
||||
tags: [`event-type:${orgSlug ? `${orgSlug}:` : ""}${teamSlug}:${meetingSlug}`],
|
||||
}
|
||||
)();
|
||||
}
|
||||
|
||||
export async function getEnrichedEventType({
|
||||
teamSlug,
|
||||
meetingSlug,
|
||||
orgSlug,
|
||||
fromRedirectOfNonOrgLink,
|
||||
}: {
|
||||
teamSlug: string;
|
||||
meetingSlug: string;
|
||||
orgSlug: string | null;
|
||||
fromRedirectOfNonOrgLink: boolean;
|
||||
}) {
|
||||
const [teamData, eventType] = await Promise.all([
|
||||
getCachedTeamData(teamSlug, orgSlug),
|
||||
getCachedTeamEventType(teamSlug, meetingSlug, orgSlug),
|
||||
]);
|
||||
|
||||
if (!teamData || !eventType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { subsetOfHosts, hosts } = await getEventTypeHosts({
|
||||
hosts: eventType.hosts,
|
||||
prisma,
|
||||
});
|
||||
|
||||
const enrichedOwner = eventType.owner
|
||||
? await new UserRepository(prisma).enrichUserWithItsProfile({
|
||||
user: eventType.owner,
|
||||
})
|
||||
: null;
|
||||
const users =
|
||||
(await getUsersFromEvent({ ...eventType, owner: enrichedOwner, subsetOfHosts, hosts }, prisma)) ?? [];
|
||||
const name = teamData.parent?.name ?? teamData.name ?? null;
|
||||
const isUnpublished = teamData.parent ? !teamData.parent.slug : !teamData.slug;
|
||||
|
||||
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata);
|
||||
|
||||
const eventDataShared = await processEventDataShared({
|
||||
eventData: eventType,
|
||||
metadata: eventMetaData,
|
||||
prisma,
|
||||
});
|
||||
|
||||
return {
|
||||
...eventDataShared,
|
||||
owner: enrichedOwner,
|
||||
subsetOfHosts,
|
||||
hosts,
|
||||
profile: getProfileFromEvent({ ...eventType, owner: enrichedOwner, subsetOfHosts, hosts }),
|
||||
subsetOfUsers: users,
|
||||
users,
|
||||
entity: {
|
||||
fromRedirectOfNonOrgLink,
|
||||
considerUnpublished: isUnpublished && !fromRedirectOfNonOrgLink,
|
||||
orgSlug,
|
||||
teamSlug: teamData.slug ?? null,
|
||||
name,
|
||||
hideProfileLink: false,
|
||||
logoUrl: teamData.parent
|
||||
? getPlaceholderAvatar(teamData.parent.logoUrl, teamData.parent.name)
|
||||
: getPlaceholderAvatar(teamData.logoUrl, teamData.name),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function shouldUseApiV2ForTeamSlots(teamId: number): Promise<boolean> {
|
||||
const featureRepo = new FeaturesRepository();
|
||||
const teamHasApiV2Route = await featureRepo.checkIfTeamHasFeature(teamId, "use-api-v2-for-team-slots");
|
||||
const useApiV2 = teamHasApiV2Route && Boolean(process.env.NEXT_PUBLIC_API_V2_URL);
|
||||
|
||||
return useApiV2;
|
||||
}
|
||||
|
||||
export async function getCRMData(
|
||||
query: GetServerSidePropsContext["query"],
|
||||
eventData: {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
}
|
||||
) {
|
||||
const crmContactOwnerEmail = query["cal.crmContactOwnerEmail"];
|
||||
const crmContactOwnerRecordType = query["cal.crmContactOwnerRecordType"];
|
||||
const crmAppSlugParam = query["cal.crmAppSlug"];
|
||||
|
||||
let teamMemberEmail = Array.isArray(crmContactOwnerEmail) ? crmContactOwnerEmail[0] : crmContactOwnerEmail;
|
||||
let crmOwnerRecordType = Array.isArray(crmContactOwnerRecordType)
|
||||
? crmContactOwnerRecordType[0]
|
||||
: crmContactOwnerRecordType;
|
||||
let crmAppSlug = Array.isArray(crmAppSlugParam) ? crmAppSlugParam[0] : crmAppSlugParam;
|
||||
|
||||
if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) {
|
||||
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
|
||||
"@calcom/lib/server/getTeamMemberEmailFromCrm"
|
||||
);
|
||||
const {
|
||||
email,
|
||||
recordType,
|
||||
crmAppSlug: crmAppSlugQuery,
|
||||
} = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query,
|
||||
eventData,
|
||||
});
|
||||
|
||||
teamMemberEmail = email ?? undefined;
|
||||
crmOwnerRecordType = recordType ?? undefined;
|
||||
crmAppSlug = crmAppSlugQuery ?? undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { BookerWebWrapper, type BookerWebWrapperAtomProps } from "@calcom/atoms/booker";
|
||||
import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
|
||||
|
||||
import BookingPageErrorBoundary from "@components/error/BookingPageErrorBoundary";
|
||||
|
||||
export type TeamBookingPageProps = Omit<BookerWebWrapperAtomProps, "eventData"> &
|
||||
Required<Pick<BookerWebWrapperAtomProps, "eventData">> & { isEmbed?: boolean };
|
||||
|
||||
export default function TeamBookingPage(props: TeamBookingPageProps) {
|
||||
return (
|
||||
<BookingPageErrorBoundary>
|
||||
<main className={getBookerWrapperClasses({ isEmbed: !!props.isEmbed })}>
|
||||
<BookerWebWrapper {...props} />
|
||||
</main>
|
||||
</BookingPageErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { TextField } from "@calcom/ui/components/form";
|
||||
import { ImageUploader } from "@calcom/ui/components/image-uploader";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
|
||||
|
||||
import { subdomainSuffix } from "../../../organizations/lib/orgDomains";
|
||||
@@ -66,6 +67,13 @@ const OtherTeamProfileView = () => {
|
||||
},
|
||||
async onSuccess() {
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
if (team?.slug) {
|
||||
// Org admins editing another team's profile should purge the cached team data
|
||||
revalidateTeamDataCache({
|
||||
teamSlug: team.slug,
|
||||
orgSlug: team.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
showToast(t("your_team_updated_successfully"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
|
||||
import RoundRobinResetInterval from "./RoundRobinResetInterval";
|
||||
import RoundRobinTimestampBasis from "./RoundRobinTimestampBasis";
|
||||
@@ -38,6 +39,13 @@ const RoundRobinSettings = ({ team }: RoundRobinSettingsProps) => {
|
||||
},
|
||||
async onSuccess() {
|
||||
await utils.viewer.teams.get.invalidate();
|
||||
if (team?.slug) {
|
||||
// Rounb robin reset interval / basis governs host selection logic on the booking page.
|
||||
revalidateTeamDataCache({
|
||||
teamSlug: team.slug,
|
||||
orgSlug: team.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
showToast(t("round_robin_settings_updated_successfully"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export type TeamData = Awaited<ReturnType<typeof getTeamData>>;
|
||||
|
||||
export async function getTeamData(teamSlug: string, orgSlug: string | null) {
|
||||
return await prisma.team.findFirst({
|
||||
where: {
|
||||
...getSlugOrRequestedSlug(teamSlug),
|
||||
parent: orgSlug ? getSlugOrRequestedSlug(orgSlug) : null,
|
||||
},
|
||||
orderBy: {
|
||||
slug: { sort: "asc", nulls: "last" },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
isPrivate: true,
|
||||
hideBranding: true,
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
bannerUrl: true,
|
||||
logoUrl: true,
|
||||
hideBranding: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
theme: true,
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
allowSEOIndexing: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { Button } from "@calcom/ui/components/button";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
|
||||
import ThemeLabel from "../../../settings/ThemeLabel";
|
||||
|
||||
@@ -71,6 +72,14 @@ const ProfileView = ({ team }: ProfileViewProps) => {
|
||||
}
|
||||
|
||||
showToast(t("your_team_updated_successfully"), "success");
|
||||
if (res?.slug) {
|
||||
// Appearance changes (theme, colours, branding toggles) are read on the team booking page through
|
||||
// `getCachedTeamData` in `queries.ts`.
|
||||
await revalidateTeamDataCache({
|
||||
teamSlug: res?.slug,
|
||||
orgSlug: team?.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from "@calcom/ui/components/skeleton";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
import { revalidateEventTypesList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/event-types/actions";
|
||||
import { revalidateTeamsList } from "@calcom/web/app/(use-page-wrapper)/(main-nav)/teams/actions";
|
||||
|
||||
@@ -283,6 +284,14 @@ const TeamProfileForm = ({ team, teamId }: TeamProfileFormProps) => {
|
||||
// TODO: Not all changes require list invalidation
|
||||
await utils.viewer.teams.list.invalidate();
|
||||
revalidateTeamsList();
|
||||
|
||||
if (res?.slug) {
|
||||
await revalidateTeamDataCache({
|
||||
teamSlug: res.slug,
|
||||
orgSlug: team?.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
showToast(t("your_team_updated_successfully"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Form } from "@calcom/ui/components/form";
|
||||
import { SettingsToggle } from "@calcom/ui/components/form";
|
||||
import { CheckboxField } from "@calcom/ui/components/form";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamDataCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
|
||||
import DisableTeamImpersonation from "../components/DisableTeamImpersonation";
|
||||
import { default as InternalNotePresetsView } from "../components/InternalNotePresetsView";
|
||||
@@ -57,6 +58,13 @@ const BookingLimitsView = ({ team }: ProfileViewProps) => {
|
||||
includeManagedEventsInLimits: res.includeManagedEventsInLimits,
|
||||
});
|
||||
}
|
||||
if (team?.slug) {
|
||||
// Booking limits are enforced during slot generation, which relies on the same cached team data.
|
||||
revalidateTeamDataCache({
|
||||
teamSlug: team.slug,
|
||||
orgSlug: team.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
showToast(t("booking_limits_updated_successfully"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ const userSelect = {
|
||||
defaultScheduleId: true,
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
const getPublicEventSelect = (fetchAllUsers: boolean) => {
|
||||
export const getPublicEventSelect = (fetchAllUsers: boolean) => {
|
||||
return {
|
||||
id: true,
|
||||
title: true,
|
||||
@@ -222,6 +222,33 @@ function isAvailableInTimeSlot(
|
||||
}
|
||||
|
||||
export type PublicEventType = Awaited<ReturnType<typeof getPublicEvent>>;
|
||||
|
||||
export async function getEventTypeHosts({
|
||||
hosts,
|
||||
fetchAllUsers = false,
|
||||
prisma,
|
||||
}: {
|
||||
hosts: Prisma.EventTypeGetPayload<{ select: ReturnType<typeof getPublicEventSelect> }>["hosts"];
|
||||
fetchAllUsers?: boolean;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
const usersAsHosts = hosts.map((host) => host.user);
|
||||
|
||||
// Enrich users in a single batch call
|
||||
const enrichedUsers = await new UserRepository(prisma).enrichUsersWithTheirProfiles(usersAsHosts);
|
||||
|
||||
// Map enriched users back to the hosts
|
||||
const enrichedHosts = hosts.map((host, index) => ({
|
||||
...host,
|
||||
user: enrichedUsers[index],
|
||||
}));
|
||||
|
||||
return {
|
||||
subsetOfHosts: enrichedHosts,
|
||||
hosts: fetchAllUsers ? enrichedHosts : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Convert it to accept a single parameter with structured data
|
||||
export const getPublicEvent = async (
|
||||
username: string,
|
||||
@@ -535,6 +562,296 @@ export const getPublicEvent = async (
|
||||
};
|
||||
};
|
||||
|
||||
// A cleaner version of `getPublicEvent` that uses various helper functions like `processEventDataShared` and `getEventTypeHosts`
|
||||
// There is still a lot of room for improvement, but it's a good starting point.
|
||||
// For example, currently we have logic for all user / team / org / platform logic in this function.
|
||||
// We should break this down into more dedicated functions.
|
||||
const getPublicEventRefactored = async (
|
||||
username: string,
|
||||
eventSlug: string,
|
||||
isTeamEvent: boolean | undefined,
|
||||
org: string | null,
|
||||
prisma: PrismaClient,
|
||||
fromRedirectOfNonOrgLink: boolean,
|
||||
currentUserId?: number,
|
||||
fetchAllUsers = false
|
||||
) => {
|
||||
const usernameList = getUsernameList(username);
|
||||
const orgQuery = org ? getSlugOrRequestedSlug(org) : null;
|
||||
// In case of dynamic group event, we fetch user's data and use the default event.
|
||||
if (usernameList.length > 1) {
|
||||
const usersInOrgContext = await new UserRepository(prisma).findUsersByUsername({
|
||||
usernameList,
|
||||
orgSlug: org,
|
||||
});
|
||||
const users = usersInOrgContext;
|
||||
|
||||
const defaultEvent = getDefaultEvent(eventSlug);
|
||||
let locations = defaultEvent.locations ? (defaultEvent.locations as LocationObject[]) : [];
|
||||
|
||||
// Get the preferred location type from the first user
|
||||
const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata || {});
|
||||
const preferedLocationType = firstUsersMetadata?.defaultConferencingApp;
|
||||
|
||||
if (preferedLocationType?.appSlug) {
|
||||
const foundApp = getAppFromSlug(preferedLocationType.appSlug);
|
||||
const appType = foundApp?.appData?.location?.type;
|
||||
if (appType) {
|
||||
// Replace the location with the preferred location type
|
||||
// This will still be default to daily if the app is not found
|
||||
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
|
||||
}
|
||||
}
|
||||
|
||||
const defaultEventBookerLayouts = {
|
||||
enabledLayouts: [...bookerLayoutOptions],
|
||||
defaultLayout: BookerLayouts.MONTH_VIEW,
|
||||
} as BookerLayoutSettings;
|
||||
const disableBookingTitle = !defaultEvent.isDynamic;
|
||||
const unPublishedOrgUser = users.find((user) => user.profile?.organization?.slug === null);
|
||||
|
||||
let orgDetails: Pick<Team, "logoUrl" | "name"> | undefined;
|
||||
if (org) {
|
||||
orgDetails = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
slug: org,
|
||||
},
|
||||
select: {
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultEvent,
|
||||
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
|
||||
// Clears meta data since we don't want to send this in the public api.
|
||||
subsetOfUsers: users.map((user) => ({
|
||||
...user,
|
||||
metadata: undefined,
|
||||
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
|
||||
})),
|
||||
users: fetchAllUsers
|
||||
? users.map((user) => ({
|
||||
...user,
|
||||
metadata: undefined,
|
||||
bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null),
|
||||
}))
|
||||
: undefined,
|
||||
locations: privacyFilteredLocations(locations),
|
||||
profile: {
|
||||
weekStart: users[0].weekStart,
|
||||
brandColor: users[0].brandColor,
|
||||
darkBrandColor: users[0].darkBrandColor,
|
||||
theme: null,
|
||||
bookerLayouts: bookerLayoutsSchema.parse(
|
||||
firstUsersMetadata?.defaultBookerLayouts || defaultEventBookerLayouts
|
||||
),
|
||||
...(orgDetails
|
||||
? {
|
||||
image: getPlaceholderAvatar(orgDetails?.logoUrl, orgDetails?.name),
|
||||
name: orgDetails?.name,
|
||||
username: org,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
entity: {
|
||||
considerUnpublished: !fromRedirectOfNonOrgLink && unPublishedOrgUser !== undefined,
|
||||
fromRedirectOfNonOrgLink,
|
||||
orgSlug: org,
|
||||
name: unPublishedOrgUser?.profile?.organization?.name ?? null,
|
||||
teamSlug: null,
|
||||
logoUrl: null,
|
||||
hideProfileLink: false,
|
||||
},
|
||||
isInstantEvent: false,
|
||||
instantMeetingParameters: [],
|
||||
showInstantEventConnectNowModal: false,
|
||||
autoTranslateDescriptionEnabled: false,
|
||||
fieldTranslations: [],
|
||||
};
|
||||
}
|
||||
|
||||
const usersOrTeamQuery = isTeamEvent
|
||||
? {
|
||||
team: {
|
||||
...getSlugOrRequestedSlug(username),
|
||||
parent: orgQuery,
|
||||
},
|
||||
}
|
||||
: {
|
||||
users: {
|
||||
some: {
|
||||
...(orgQuery
|
||||
? {
|
||||
profiles: {
|
||||
some: {
|
||||
organization: orgQuery,
|
||||
username: username,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
username,
|
||||
profiles: { none: {} },
|
||||
}),
|
||||
},
|
||||
},
|
||||
team: null,
|
||||
};
|
||||
|
||||
// In case it's not a group event, it's either a single user or a team, and we query that data.
|
||||
let event = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: eventSlug,
|
||||
...usersOrTeamQuery,
|
||||
},
|
||||
select: getPublicEventSelect(fetchAllUsers),
|
||||
});
|
||||
|
||||
// If no event was found, check for platform org user event
|
||||
if (!event && !orgQuery) {
|
||||
event = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: eventSlug,
|
||||
users: {
|
||||
some: {
|
||||
username,
|
||||
isPlatformManaged: false,
|
||||
profiles: {
|
||||
some: {
|
||||
organization: {
|
||||
isPlatform: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: getPublicEventSelect(fetchAllUsers),
|
||||
});
|
||||
}
|
||||
|
||||
if (!event) return null;
|
||||
|
||||
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
|
||||
const teamMetadata = teamMetadataSchema.parse(event.team?.metadata || {});
|
||||
const { subsetOfHosts, hosts } = await getEventTypeHosts({
|
||||
hosts: event.hosts,
|
||||
fetchAllUsers,
|
||||
prisma,
|
||||
});
|
||||
|
||||
const eventWithUserProfiles = {
|
||||
...event,
|
||||
owner: event.owner
|
||||
? await new UserRepository(prisma).enrichUserWithItsProfile({
|
||||
user: event.owner,
|
||||
})
|
||||
: null,
|
||||
subsetOfHosts,
|
||||
hosts,
|
||||
};
|
||||
|
||||
let users =
|
||||
(await getUsersFromEvent(eventWithUserProfiles, prisma)) ||
|
||||
(await getOwnerFromUsersArray(prisma, event.id));
|
||||
|
||||
if (users === null) {
|
||||
throw new Error(`EventType ${event.id} has no owner or users.`);
|
||||
}
|
||||
//In case the event schedule is not defined ,use the event owner's default schedule
|
||||
if (!eventWithUserProfiles.schedule && eventWithUserProfiles.owner?.defaultScheduleId) {
|
||||
const eventOwnerDefaultSchedule = await prisma.schedule.findUnique({
|
||||
where: {
|
||||
id: eventWithUserProfiles.owner?.defaultScheduleId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
},
|
||||
});
|
||||
eventWithUserProfiles.schedule = eventOwnerDefaultSchedule;
|
||||
}
|
||||
|
||||
let orgDetails: Pick<Team, "logoUrl" | "name"> | undefined | null;
|
||||
if (org) {
|
||||
orgDetails = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: org,
|
||||
parentId: null,
|
||||
},
|
||||
select: {
|
||||
logoUrl: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const isTeamAdminOrOwner = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: currentUserId ?? -1,
|
||||
teamId: event.teamId ?? -1,
|
||||
accepted: true,
|
||||
role: { in: ["ADMIN", "OWNER"] },
|
||||
},
|
||||
});
|
||||
|
||||
const isOrgAdminOrOwner = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: currentUserId ?? -1,
|
||||
teamId: event.team?.parentId ?? -1,
|
||||
accepted: true,
|
||||
role: { in: ["ADMIN", "OWNER"] },
|
||||
},
|
||||
});
|
||||
|
||||
if (event.team?.isPrivate && !isTeamAdminOrOwner && !isOrgAdminOrOwner) {
|
||||
users = [];
|
||||
}
|
||||
|
||||
const eventDataShared = await processEventDataShared({
|
||||
eventData: event,
|
||||
metadata: eventMetaData,
|
||||
prisma,
|
||||
});
|
||||
|
||||
return {
|
||||
...eventDataShared,
|
||||
// getPublicEvent-specific overrides
|
||||
owner: eventWithUserProfiles.owner,
|
||||
subsetOfHosts,
|
||||
hosts,
|
||||
// Sets user data on profile object for easier access
|
||||
profile: getProfileFromEvent(eventWithUserProfiles),
|
||||
subsetOfUsers: users,
|
||||
users: fetchAllUsers ? users : undefined,
|
||||
entity: {
|
||||
fromRedirectOfNonOrgLink,
|
||||
considerUnpublished:
|
||||
!fromRedirectOfNonOrgLink &&
|
||||
(eventWithUserProfiles.team?.slug === null ||
|
||||
eventWithUserProfiles.owner?.profile?.organization?.slug === null ||
|
||||
eventWithUserProfiles.team?.parent?.slug === null),
|
||||
orgSlug: org,
|
||||
teamSlug: (eventWithUserProfiles.team?.slug || teamMetadata?.requestedSlug) ?? null,
|
||||
name:
|
||||
(eventWithUserProfiles.owner?.profile?.organization?.name ||
|
||||
eventWithUserProfiles.team?.parent?.name ||
|
||||
eventWithUserProfiles.team?.name) ??
|
||||
null,
|
||||
hideProfileLink: eventWithUserProfiles.team?.hideTeamProfileLink ?? false,
|
||||
...(orgDetails
|
||||
? {
|
||||
logoUrl: getPlaceholderAvatar(orgDetails?.logoUrl, orgDetails?.name),
|
||||
name: orgDetails?.name,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const eventData = {
|
||||
select: getPublicEventSelect(true),
|
||||
} satisfies Prisma.EventTypeArgs;
|
||||
@@ -546,7 +863,7 @@ type GetProfileFromEventInput = Omit<Event, "hosts"> & {
|
||||
subsetOfHosts: Event["hosts"];
|
||||
};
|
||||
|
||||
function getProfileFromEvent(event: GetProfileFromEventInput) {
|
||||
export function getProfileFromEvent(event: GetProfileFromEventInput) {
|
||||
const { team, subsetOfHosts: hosts, owner } = event;
|
||||
const nonTeamprofile = hosts?.[0]?.user || owner;
|
||||
const profile = team || nonTeamprofile;
|
||||
@@ -576,7 +893,7 @@ function getProfileFromEvent(event: GetProfileFromEventInput) {
|
||||
};
|
||||
}
|
||||
|
||||
async function getUsersFromEvent(
|
||||
export async function getUsersFromEvent(
|
||||
event: Omit<Event, "owner" | "hosts"> & {
|
||||
owner:
|
||||
| (Event["owner"] & {
|
||||
@@ -673,3 +990,39 @@ function mapHostsToUsers(host: {
|
||||
profile: host.user.profile,
|
||||
};
|
||||
}
|
||||
|
||||
export const processEventDataShared = async ({
|
||||
eventData,
|
||||
metadata,
|
||||
prisma,
|
||||
}: {
|
||||
eventData: Prisma.EventTypeGetPayload<{ select: ReturnType<typeof getPublicEventSelect> }>;
|
||||
metadata: ReturnType<typeof eventTypeMetaDataSchemaWithTypedApps.parse>;
|
||||
prisma: PrismaClient;
|
||||
}) => {
|
||||
let showInstantEventConnectNowModal = eventData.isInstantEvent ?? false;
|
||||
if (eventData.isInstantEvent && eventData.instantMeetingSchedule?.id) {
|
||||
const { id, timeZone } = eventData.instantMeetingSchedule;
|
||||
showInstantEventConnectNowModal = await isCurrentlyAvailable({
|
||||
prisma,
|
||||
instantMeetingScheduleId: id,
|
||||
availabilityTimezone: timeZone ?? "Europe/London",
|
||||
length: eventData.length,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...eventData,
|
||||
bookerLayouts: bookerLayoutsSchema.parse(metadata?.bookerLayouts || null),
|
||||
description: markdownToSafeHTML(eventData.description),
|
||||
metadata,
|
||||
customInputs: customInputSchema.array().parse(eventData.customInputs || []),
|
||||
locations: privacyFilteredLocations((eventData.locations || []) as LocationObject[]),
|
||||
bookingFields: getBookingFieldsWithSystemFields(eventData),
|
||||
recurringEvent: isRecurringEvent(eventData.recurringEvent)
|
||||
? parseRecurringEvent(eventData.recurringEvent)
|
||||
: null,
|
||||
isDynamic: false,
|
||||
showInstantEventConnectNowModal,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { getPublicEventSelect } from "./getPublicEvent";
|
||||
|
||||
export async function getTeamEventType(teamSlug: string, meetingSlug: string, orgSlug: string | null) {
|
||||
return await prisma.eventType.findFirst({
|
||||
where: {
|
||||
team: {
|
||||
...getSlugOrRequestedSlug(teamSlug),
|
||||
parent: orgSlug ? getSlugOrRequestedSlug(orgSlug) : null,
|
||||
},
|
||||
OR: [{ slug: meetingSlug }, { slug: { startsWith: `${meetingSlug}-team-id-` } }],
|
||||
},
|
||||
// IMPORTANT:
|
||||
// This ensures that the queried event type has everything expected in Booker
|
||||
select: getPublicEventSelect(false),
|
||||
orderBy: {
|
||||
slug: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export type AppFlags = {
|
||||
"use-api-v2-for-team-slots": boolean;
|
||||
pbac: boolean;
|
||||
"restriction-schedule": boolean;
|
||||
"team-booking-page-cache": boolean;
|
||||
};
|
||||
|
||||
export type TeamFeatures = Record<keyof AppFlags, boolean>;
|
||||
|
||||
@@ -22,6 +22,7 @@ const initialData: AppFlags = {
|
||||
"use-api-v2-for-team-slots": false,
|
||||
pbac: false,
|
||||
"restriction-schedule": false,
|
||||
"team-booking-page-cache": false,
|
||||
};
|
||||
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
|
||||
@@ -227,3 +227,5 @@ export const CAL_VIDEO_MEETING_LINK_FOR_TESTING = process.env.CAL_VIDEO_MEETING_
|
||||
|
||||
export const IS_SMS_CREDITS_ENABLED = !!process.env.NEXT_PUBLIC_STRIPE_CREDITS_PRICE_ID;
|
||||
export const DATABASE_CHUNK_SIZE = parseInt(process.env.DATABASE_CHUNK_SIZE || "25", 10);
|
||||
|
||||
export const NEXTJS_CACHE_TTL = 3600; // 1 hour
|
||||
|
||||
@@ -26,7 +26,7 @@ import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type BookerWebWrapperAtomProps = BookerProps & {
|
||||
export type BookerWebWrapperAtomProps = BookerProps & {
|
||||
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { BookerWebWrapper } from "./BookerWebWrapper";
|
||||
export type { BookerWebWrapperAtomProps } from "./BookerWebWrapper";
|
||||
|
||||
@@ -20,6 +20,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { revalidateTeamEventTypeCache } from "@calcom/web/app/(booking-page-wrapper)/team/[slug]/[type]/actions";
|
||||
import { revalidateEventTypeEditPage } from "@calcom/web/app/(use-page-wrapper)/event-types/[type]/actions";
|
||||
|
||||
import { TRPCClientError } from "@trpc/react-query";
|
||||
@@ -138,6 +139,15 @@ const EventTypeWeb = ({ id, ...rest }: EventTypeSetupProps & { id: number }) =>
|
||||
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
|
||||
form.reset(currentValues);
|
||||
revalidateEventTypeEditPage(eventType.id);
|
||||
if (eventType.team?.slug) {
|
||||
// When an event-type is updated,
|
||||
// guests could still hit a stale cache and see the old page.
|
||||
revalidateTeamEventTypeCache({
|
||||
teamSlug: eventType.team.slug,
|
||||
meetingSlug: eventType.slug,
|
||||
orgSlug: eventType.team.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
@@ -311,6 +321,15 @@ const EventTypeWeb = ({ id, ...rest }: EventTypeSetupProps & { id: number }) =>
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
if (team?.slug) {
|
||||
// When a team event-type is deleted,
|
||||
// guests could still hit a stale cache and see the old page.
|
||||
revalidateTeamEventTypeCache({
|
||||
teamSlug: team.slug,
|
||||
meetingSlug: eventType.slug,
|
||||
orgSlug: team.parent?.slug ?? null,
|
||||
});
|
||||
}
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
isTeamEventTypeDeleted.current = true;
|
||||
appRouter.push("/event-types");
|
||||
|
||||
Reference in New Issue
Block a user