chore: removed old booker and make new booker as a default (#10053)

* removed old booker and make new booker as a default

* fixes merge conflict

* fixed tests

* fixed tests for old-booker

* fixed typo in @calcom/lib/defaultEvents.ts

---------

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
pull/9684/head^2
René Müller 2023-07-11 13:31:55 +02:00 committed by GitHub
parent a2e0d44fb8
commit b364a85ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 322 additions and 3347 deletions

View File

@ -214,9 +214,3 @@ PROJECT_ID_VERCEL=
TEAM_ID_VERCEL=
# Get it from: https://vercel.com/account/tokens
AUTH_BEARER_TOKEN_VERCEL=
#Enables New booker for Embed only
NEW_BOOKER_ENABLED_FOR_EMBED=0
#Enables New booker for All but Embed requests
NEW_BOOKER_ENABLED_FOR_NON_EMBED=0

View File

@ -1,53 +0,0 @@
import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui";
import { Link } from "@calcom/ui/components/icon";
import type { Props } from "./pages/AvailabilityPage";
const excludeNullValues = (value: unknown) => !!value;
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale();
const renderLocations = locations.map((location, index) => {
const eventLocationType = getEventLocationType(location.type);
if (!eventLocationType) {
// It's possible that the location app got uninstalled
return null;
}
if (eventLocationType.variable === "hostDefault") {
return null;
}
const translatedLocation = getTranslatedLocation(location, eventLocationType, t);
return (
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<Link className="text-default ml-[2px] h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
)}
<Tooltip content={translatedLocation}>
<p className="line-clamp-1">{translatedLocation}</p>
</Tooltip>
</div>
);
});
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
return filteredLocations.length ? (
<div className="text-default mr-6 flex w-full flex-col space-y-4 break-words text-sm">
{filteredLocations}
</div>
) : null;
}

View File

@ -1,162 +0,0 @@
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { Badge } from "@calcom/ui";
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import { UserAvatars } from "@components/booking/UserAvatars";
import EventTypeDescriptionSafeHTML from "@components/eventtype/EventTypeDescriptionSafeHTML";
import type { AvailabilityPageProps } from "../../pages/[user]/[type]";
import type { BookPageProps } from "../../pages/[user]/book";
import type { DynamicAvailabilityPageProps } from "../../pages/d/[link]/[slug]";
import type { HashLinkPageProps } from "../../pages/d/[link]/book";
import type { AvailabilityTeamPageProps } from "../../pages/team/[slug]/[type]";
import type { TeamBookingPageProps } from "../../pages/team/[slug]/book";
import { AvailableEventLocations } from "./AvailableEventLocations";
interface Props {
profile:
| AvailabilityPageProps["profile"]
| HashLinkPageProps["profile"]
| TeamBookingPageProps["profile"]
| BookPageProps["profile"]
| AvailabilityTeamPageProps["profile"]
| DynamicAvailabilityPageProps["profile"];
eventType:
| AvailabilityPageProps["eventType"]
| HashLinkPageProps["eventType"]
| TeamBookingPageProps["eventType"]
| BookPageProps["eventType"]
| AvailabilityTeamPageProps["eventType"]
| DynamicAvailabilityPageProps["eventType"];
isBookingPage?: boolean;
children: ReactNode;
isMobile?: boolean;
rescheduleUid?: string;
}
const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (
!duration ||
isNaN(Number(duration)) ||
(eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration.includes(Number(duration)))
) {
setDuration(eventType.length);
}
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
let requiresConfirmation = eventType?.requiresConfirmation;
let requiresConfirmationText = t("requires_confirmation");
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
if (rcThreshold) {
if (isBookingPage) {
if (dayjs(bookingDate).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) {
requiresConfirmation = false;
}
} else {
requiresConfirmationText = t("requires_confirmation_threshold", {
...rcThreshold,
unit: rcThreshold.unit.slice(0, -1),
});
}
}
return (
<>
<UserAvatars
profile={profile}
users={eventType.users}
showMembers={eventType.schedulingType !== SchedulingType.ROUND_ROBIN}
size="sm"
truncateAfter={3}
/>
<h2 className="text-default mb-2 mt-1 break-words text-sm font-medium ">
{eventType.team?.parent?.name} {profile.name}
</h2>
<h1 className="font-cal text-emphasis mb-6 break-words text-2xl font-semibold leading-none">
{eventType.title}
</h1>
<div className=" text-default flex flex-col space-y-4 text-sm font-medium">
{eventType?.description && (
<div
className={classNames(
"scroll-bar scrollbar-track-w-20 -mx-5 flex max-h-[180px] overflow-y-scroll px-5 ",
isBookingPage && "text-default text-sm font-medium"
)}>
{/* TODO: Fix colors when token is introdcued to DS */}
<div className="max-w-full flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
<EventTypeDescriptionSafeHTML eventType={eventType} />
</div>
</div>
)}
{requiresConfirmation && (
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
<div>
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
)}
<AvailableEventLocations
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
/>
<div
className={classNames(
"flex flex-nowrap text-sm font-medium",
isBookingPage && "text-default",
!eventType.metadata?.multipleDuration && "items-center"
)}>
<Clock
className={classNames(
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
{eventType.metadata?.multipleDuration !== undefined ? (
!isBookingPage ? (
<ul className="-mt-1 flex flex-wrap gap-1">
{eventType.metadata.multipleDuration.map((dur, idx) => (
<li key={idx}>
<Badge
variant="gray"
className={classNames(
duration === dur.toString()
? "bg-emphasis border-emphasis text-emphasis "
: "bg-subtle text-default border-transparent ",
"cursor-pointer border"
)}
onClick={() => {
setDuration(dur);
}}>
{dur} {t("minute_timeUnit")}
</Badge>
</li>
))}
</ul>
) : (
`${duration} ${t("minutes")}`
)
) : (
`${eventType.length} ${t("minutes")}`
)}
</div>
{children}
</div>
</>
);
};
export default BookingDescription;

View File

@ -1,298 +0,0 @@
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedStyles,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import notEmpty from "@calcom/lib/notEmpty";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import BookingDescription from "@components/booking/BookingDescription";
import { SlotPicker } from "@components/booking/SlotPicker";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/*const SlotPicker = dynamic(() => import("../SlotPicker").then((mod) => mod.SlotPicker), {
ssr: false,
loading: () => <div className="mt-8 px-4 pb-4 sm:mt-0 sm:p-4 md:min-w-[300px] md:px-5 lg:min-w-[455px]" />,
});*/
const TimezoneDropdown = dynamic(() => import("../TimezoneDropdown").then((mod) => mod.TimezoneDropdown), {
ssr: false,
});
const dateQuerySchema = z.object({
rescheduleUid: z.string().optional().default(""),
date: z.string().optional().default(""),
timeZone: z.string().optional().default(""),
seatReferenceUid: z.string().optional(),
});
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor: string; darkBrandColor: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const router = useRouter();
const isEmbed = useIsEmbed(restProps.isEmbed);
const query = dateQuerySchema.parse(router.query);
const { rescheduleUid } = query;
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const { t, i18n } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const isBackgroundTransparent = useIsBackgroundTransparent();
const [timeZone, setTimeZone] = useState<string>();
const [timeFormat, setTimeFormat] = useState<TimeFormat>(detectBrowserTimeFormat);
const onTimeFormatChange = (is24Hours: boolean) => {
setTimeFormat(is24Hours ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR);
setIs24hClockInLocalStorage(is24Hours);
};
useEffect(() => {
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
}, []);
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
/*
const telemetry = useTelemetry();
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
);
}
}, [telemetry]); */
const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
const timezoneDropdown = useMemo(
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
[timeZone]
);
const paymentAppData = getPaymentAppData(eventType);
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
const { data: bookingAttendees } = trpc.viewer.bookings.getBookingAttendees.useQuery(
{
seatReferenceUid: rescheduleUid,
},
{
enabled: !!(rescheduleUid && eventType.seatsPerTimeSlot),
}
);
return (
<>
<HeadSeo
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
meeting={{
title: eventType.title,
profile: { name: `${profile.name}`, image: profile.image },
users: [
...(eventType.users || []).map((user) => ({
name: `${user.name}`,
username: `${user.username}`,
})),
],
}}
nextSeoProps={{
nofollow: eventType.hidden,
noindex: eventType.hidden,
}}
isBrandingHidden={restProps.isBrandingHidden}
/>
<BookingPageTagManager eventType={eventType} />
<div>
<main
className={classNames(
"flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("bg-subtle dark:bg-default mx-auto my-0 ease-in-out md:my-24")
)}>
<div>
<div
style={availabilityDatePickerEmbedStyles}
className={classNames(
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
"border-booker md:border-booker-width md:rounded-md",
isEmbed && "mx-auto"
)}>
<div className="md:flex">
{showEventTypeDetails && (
<div
className={classNames(
" border-subtle flex flex-col p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{rescheduleUid && eventType.seatsPerTimeSlot && bookingAttendees && (
<div
className={classNames(
"flex flex-nowrap items-center text-sm font-medium",
" text-default",
"ltr:mr-[10px] rtl:ml-[10px]"
)}>
<User
className={classNames(
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
"mt-[2px]"
)}
/>{" "}
{t("event_type_seats", { numberOfSeats: bookingAttendees })}
</div>
)}
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<RefreshCcw className="float-left ml-[2px] mt-[7px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="-ml-2 mb-1 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<NumberInput
defaultValue={eventType.recurringEvent.count}
min="1"
max={eventType.recurringEvent.count}
isFullWidth={false}
className="me-2 inline w-16"
onChange={(event) => {
const count =
eventType?.recurringEvent?.count &&
parseInt(event?.target.value) > eventType?.recurringEvent?.count
? eventType.recurringEvent.count
: parseInt(event?.target.value);
setRecurringEventCount(count);
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
</div>
)}
{paymentAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<CreditCard className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{paymentAppData.paymentOption === "HOLD" ? (
<>
{t("no_show_fee_amount", {
amount: paymentAppData.price / 100.0,
formatParams: { amount: { currency: paymentAppData.currency } },
})}
</>
) : (
<>
{new Intl.NumberFormat(i18n.language, {
style: "currency",
currency: paymentAppData.currency,
}).format(paymentAppData.price / 100)}
</>
)}
</p>
)}
{timezoneDropdown}
</BookingDescription>
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-default"
data-testid="former_time_p_desktop">
{t("former_time")}
</p>
<p className="text-subtle line-through ">
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-subtle" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)*/}
</div>
)}
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
? ([
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].indexOf(profile.weekStart) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
: profile.weekStart /* Allows providing weekStart as number */
}
eventType={eventType}
timeFormat={timeFormat}
onTimeFormatChange={onTimeFormatChange}
timeZone={timeZone}
users={userList}
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
bookingAttendees={bookingAttendees || undefined}
recurringEventCount={recurringEventCount}
/>
</div>
</div>
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
{!restProps.isBrandingHidden && !isEmbed && <PoweredBy />}
</div>
</main>
</div>
<Toaster position="bottom-right" />
</>
);
};
export default AvailabilityPage;

View File

@ -1,704 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import type { EventLocationType } from "@calcom/app-store/locations";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import type { LocationObject } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { createBooking, createRecurringBooking } from "@calcom/features/bookings/lib";
import { useTimePreferences } from "@calcom/features/bookings/lib";
import {
getBookingFieldsWithSystemFields,
SystemField,
} from "@calcom/features/bookings/lib/getBookingFields";
import getBookingResponsesSchema, {
getBookingResponsesPartialSchema,
} from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import classNames from "@calcom/lib/classNames";
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { parseDate, parseDateTimeWithTimeZone, parseRecurringDates } from "@calcom/lib/parse-dates";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc";
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
import BookingDescription from "@components/booking/BookingDescription";
import type { BookPageProps } from "../../../pages/[user]/book";
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
/** These are like 40kb that not every user needs */
const BookingDescriptionPayment = dynamic(
() => import("@components/booking/BookingDescriptionPayment")
) as unknown as typeof import("@components/booking/BookingDescriptionPayment").default;
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
type BookingPageProps = BookPageProps | TeamBookingPageProps | HashLinkPageProps;
const BookingFields = ({
fields,
locations,
rescheduleUid,
isDynamicGroupBooking,
}: {
fields: BookingPageProps["eventType"]["bookingFields"];
locations: LocationObject[];
rescheduleUid?: string;
isDynamicGroupBooking: boolean;
}) => {
const { t } = useLocale();
const { watch, setValue } = useFormContext();
const locationResponse = watch("responses.location");
const currentView = rescheduleUid ? "reschedule" : "";
return (
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
<div>
{fields.map((field, index) => {
// During reschedule by default all system fields are readOnly. Make them editable on case by case basis.
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
let readOnly =
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
let noLabel = false;
let hidden = !!field.hidden;
const fieldViews = field.views;
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
return null;
}
if (field.name === SystemField.Enum.rescheduleReason) {
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
readOnly = false;
}
if (field.name === SystemField.Enum.smsReminderNumber) {
// `smsReminderNumber` and location.optionValue when location.value===phone are the same data point. We should solve it in a better way in the Form Builder itself.
// I think we should have a way to connect 2 fields together and have them share the same value in Form Builder
if (locationResponse?.value === "phone") {
setValue(`responses.${SystemField.Enum.smsReminderNumber}`, locationResponse?.optionValue);
// Just don't render the field now, as the value is already connected to attendee phone location
return null;
}
// `smsReminderNumber` can be edited during reschedule even though it's a system field
readOnly = false;
}
if (field.name === SystemField.Enum.guests) {
// No matter what user configured for Guests field, we don't show it for dynamic group booking as that doesn't support guests
hidden = isDynamicGroupBooking ? true : !!field.hidden;
}
// We don't show `notes` field during reschedule
if (
(field.name === SystemField.Enum.notes || field.name === SystemField.Enum.guests) &&
!!rescheduleUid
) {
return null;
}
// Dynamically populate location field options
if (field.name === SystemField.Enum.location && field.type === "radioInput") {
if (!field.optionsInputs) {
throw new Error("radioInput must have optionsInputs");
}
const optionsInputs = field.optionsInputs;
// TODO: Instead of `getLocationOptionsForSelect` options should be retrieved from dataStore[field.getOptionsAt]. It would make it agnostic of the `name` of the field.
const options = getLocationOptionsForSelect(locations, t);
options.forEach((option) => {
const optionInput = optionsInputs[option.value as keyof typeof optionsInputs];
if (optionInput) {
optionInput.placeholder = option.inputPlaceholder;
}
});
field.options = options.filter(
(location): location is NonNullable<(typeof options)[number]> => !!location
);
// If we have only one option and it has an input, we don't show the field label because Option name acts as label.
// e.g. If it's just Attendee Phone Number option then we don't show `Location` label
if (field.options.length === 1) {
if (field.optionsInputs[field.options[0].value]) {
noLabel = true;
} else {
// If there's only one option and it doesn't have an input, we don't show the field at all because it's visible in the left side bar
hidden = true;
}
}
}
const label = noLabel ? "" : field.label || t(field.defaultLabel || "");
const placeholder = field.placeholder || t(field.defaultPlaceholder || "");
return (
<FormBuilderField
className="mb-4"
field={{ ...field, label, placeholder, hidden }}
readOnly={readOnly}
key={index}
/>
);
})}
</div>
);
};
const routerQuerySchema = z
.object({
timeFormat: z.nativeEnum(TimeFormat),
rescheduleUid: z.string().optional(),
date: z
.string()
.optional()
.transform((date) => {
if (date === undefined) {
return null;
}
return date;
}),
})
.passthrough();
const BookingPage = ({
eventType,
booking,
currentSlotBooking,
profile,
isDynamicGroupBooking,
recurringEventCount,
hasHashedBookingLink,
hashedLink,
...restProps
}: BookingPageProps) => {
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const { date: queryDate } = useRouterQuery("date");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
const { data: session } = useSession();
const isBackgroundTransparent = useIsBackgroundTransparent();
const telemetry = useTelemetry();
const { timezone } = useTimePreferences();
const reserveSlot = () => {
if (queryDuration) {
reserveSlotMutation.mutate({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
bookingUid: currentSlotBooking?.uid,
});
}
};
// Define duration now that we support multiple duration eventTypes
let duration = eventType.length;
if (
queryDuration &&
!isNaN(Number(queryDuration)) &&
eventType.metadata?.multipleDuration &&
eventType.metadata?.multipleDuration.includes(Number(queryDuration))
) {
duration = Number(queryDuration);
}
useEffect(() => {
/* if (top !== window) {
//page_view will be collected automatically by _middleware.ts
telemetry.event(
telemetryEventTypes.embedView,
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
);
} */
reserveSlot();
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
return () => {
clearInterval(interval);
removeSelectedSlotMarkMutation.mutate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const mutation = useMutation(createBooking, {
onSuccess: async (responseData) => {
const { uid } = responseData;
if ("paymentUid" in responseData && !!responseData.paymentUid) {
return await router.push(
createPaymentLink({
paymentUid: responseData.paymentUid,
date,
name: bookingForm.getValues("responses.name"),
email: bookingForm.getValues("responses.email"),
absolute: false,
})
);
}
const query = {
isSuccessBookingPage: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
seatReferenceUid: "seatReferenceUid" in responseData ? responseData.seatReferenceUid : null,
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
const recurringMutation = useMutation(createRecurringBooking, {
onSuccess: async (responseData = []) => {
const { uid } = responseData[0] || {};
const query = {
isSuccessBookingPage: true,
allRemainingBookings: true,
email: bookingForm.getValues("responses.email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
};
return bookingSuccessRedirect({
router,
successRedirectUrl: eventType.successRedirectUrl,
query,
bookingUid: uid,
});
},
});
const {
data: { timeFormat, rescheduleUid, date },
} = useTypedQuery(routerQuerySchema);
useTheme(profile.theme);
useBrandColors({
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
});
const querySchema = getBookingResponsesPartialSchema({
eventType: {
bookingFields: getBookingFieldsWithSystemFields(eventType),
},
view: rescheduleUid ? "reschedule" : "booking",
});
const parsedQuery = querySchema.parse({
...router.query,
// `guest` because we need to support legacy URL with `guest` query param support
// `guests` because the `name` of the corresponding bookingField is `guests`
guests: router.query.guests || router.query.guest,
});
// it would be nice if Prisma at some point in the future allowed for Json<Location>; as of now this is not the case.
const locations: LocationObject[] = useMemo(
() => (eventType.locations as LocationObject[]) || [],
[eventType.locations]
);
const [isClientTimezoneAvailable, setIsClientTimezoneAvailable] = useState(false);
useEffect(() => {
// THis is to fix hydration error that comes because of different timezone on server and client
setIsClientTimezoneAvailable(true);
}, []);
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
// There should only exists one default userData variable for primaryAttendee.
const defaultUserValues = {
email: rescheduleUid ? booking?.attendees[0].email : parsedQuery["email"],
name: rescheduleUid ? booking?.attendees[0].name : parsedQuery["name"],
};
const defaultValues = () => {
if (!rescheduleUid) {
const defaults = {
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
};
const responses = eventType.bookingFields.reduce((responses, field) => {
return {
...responses,
[field.name]: parsedQuery[field.name],
};
}, {});
defaults.responses = {
...responses,
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
};
return defaults;
}
if (!booking || !booking.attendees.length) {
return {};
}
const primaryAttendee = booking.attendees[0];
if (!primaryAttendee) {
return {};
}
const defaults = {
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
};
const responses = eventType.bookingFields.reduce((responses, field) => {
return {
...responses,
[field.name]: booking.responses[field.name],
};
}, {});
defaults.responses = {
...responses,
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
};
return defaults;
};
const bookingFormSchema = z
.object({
responses: getBookingResponsesSchema({
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
view: rescheduleUid ? "reschedule" : "booking",
}),
})
.passthrough();
type BookingFormValues = {
locationType?: EventLocationType["type"];
responses: z.infer<typeof bookingFormSchema>["responses"];
};
const bookingForm = useForm<BookingFormValues>({
defaultValues: defaultValues(),
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
});
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
selectedTimeFormat: timeFormat,
},
i18n.language
);
}
const bookEvent = (bookingValues: BookingFormValues) => {
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
);
// "metadata" is a reserved key to allow for connecting external users without relying on the email address.
// <...url>&metadata[user_id]=123 will be send as a custom input field as the hidden type.
// @TODO: move to metadata
const metadata = Object.keys(router.query)
.filter((key) => key.startsWith("metadata"))
.reduce(
(metadata, key) => ({
...metadata,
[key.substring("metadata[".length, key.length - 1)]: router.query[key],
}),
{}
);
if (recurringDates.length) {
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
const recurringBookings = recurringDates.map((recurringDate) => ({
...bookingValues,
start: dayjs(recurringDate).utc().format(),
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
recurringEventId,
// Added to track down the number of actual occurrences selected by the user
recurringCount: recurringDates.length,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
}));
recurringMutation.mutate(recurringBookings);
} else {
mutation.mutate({
...bookingValues,
start: dayjs(date).utc().format(),
end: dayjs(date).utc().add(duration, "minute").format(),
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
timeZone: timeZone(),
language: i18n.language,
rescheduleUid,
bookingUid: (router.query.bookingUid as string) || booking?.uid,
user: router.query.user,
metadata,
hasHashedBookingLink,
hashedLink,
seatReferenceUid: router.query.seatReferenceUid as string,
});
}
};
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
return (
<>
<Head>
<title>
{rescheduleUid
? t("booking_reschedule_confirmation", {
eventTypeTitle: eventType.title,
profileName: profile.name,
})
: t("booking_confirmation", {
eventTypeTitle: eventType.title,
profileName: profile.name,
})}{" "}
| {APP_NAME}
</title>
<link rel="icon" href="/favico.ico" />
</Head>
<BookingPageTagManager eventType={eventType} />
<main
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl"
)}>
<div
className={classNames(
"main",
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
"border-booker sm:border-booker-width rounded-md"
)}>
<div className="sm:flex">
{showEventTypeDetails && (
<div className="sm:border-subtle text-default flex flex-col px-6 pb-0 pt-6 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
<BookingDescriptionPayment eventType={eventType} t={t} i18n={i18n} />
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="text-default items-start text-sm font-medium">
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
recurringEvent: eventType.recurringEvent,
recurringCount: recurringEventCount,
})}
</p>
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDate(date, i18n.language, { selectedTimeFormat: timeFormat })}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
return <p key={key}>{timeFormatted}</p>;
})}
{!rescheduleUid && eventType?.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className=" text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
)}
</div>
</div>
{booking?.startTime && rescheduleUid && (
<div>
<p className="mb-2 mt-8 text-sm " data-testid="former_time_p">
{t("former_time")}
</p>
<p className="line-through ">
<Calendar className="-mt-1 ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDateTimeWithTimeZone(booking.startTime, i18n.language, timezone, {
selectedTimeFormat: timeFormat,
})}
</p>
</div>
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<User
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: currentSlotBooking &&
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
} mb-2 font-medium`}>
{currentSlotBooking
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot}{" "}
{t("seats_available", {
count: currentSlotBooking
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
: eventType.seatsPerTimeSlot,
})}
</p>
</div>
)}
</BookingDescription>
</div>
)}
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
<Form form={bookingForm} noValidate handleSubmit={bookEvent}>
<BookingFields
isDynamicGroupBooking={isDynamicGroupBooking}
fields={eventType.bookingFields}
locations={locations}
rescheduleUid={rescheduleUid}
/>
<div
className={classNames(
"flex justify-end space-x-2 rtl:space-x-reverse",
// HACK: If the last field is guests, we need to move Cancel, Submit buttons up because "Add Guests" being present on the left and the buttons on the right, spacing is not required
eventType.bookingFields[eventType.bookingFields.length - 1].name ===
SystemField.Enum.guests
? "-mt-4"
: ""
)}>
<Button color="minimal" type="button" onClick={() => router.back()}>
{t("cancel")}
</Button>
<Button
type="submit"
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
</Button>
</div>
</Form>
{mutation.isError || recurringMutation.isError ? (
<ErrorMessage error={mutation.error || recurringMutation.error} />
) : null}
</div>
</div>
</div>
</main>
<Toaster position="bottom-right" />
</>
);
};
export default BookingPage;
function ErrorMessage({ error }: { error: unknown }) {
const { t } = useLocale();
const { query: { rescheduleUid } = {} } = useRouter();
const router = useRouter();
return (
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
<div className="flex">
<div className="flex-shrink-0">
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
</div>
<div className="ms-3">
<p className="text-sm text-blue-700">
{rescheduleUid ? t("reschedule_fail") : t("booking_fail")}{" "}
{error instanceof HttpError || error instanceof Error ? (
<>
{t("can_you_try_again")}{" "}
<span className="cursor-pointer underline" onClick={() => router.back()}>
{t("go_back")}
</span>
.
</> /* t(error.message) */
) : (
"Unknown error"
)}
</p>
</div>
</div>
</div>
);
}

View File

@ -10,10 +10,6 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
const middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
const requestHeaders = new Headers(req.headers);
/**
* We are using env variable to toggle new-booker because using flags would be an unnecessary delay for booking pages
* Also, we can't easily identify the booker page requests here(to just fetch the flags for those requests)
*/
if (isIpInBanlist(req) && url.pathname !== "/api/nope") {
// DDOS Prevention: Immediately end request with no response - Avoids a redirect as well initiated by NextAuth on invalid callback

View File

@ -5,11 +5,6 @@ const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
const { i18n } = require("./next-i18next.config");
const {
userTypeRoutePath,
teamTypeRoutePath,
privateLinkRoutePath,
embedUserTypeRoutePath,
embedTeamTypeRoutePath,
orgHostPath,
orgUserRoutePath,
orgUserTypeRoutePath,
@ -285,85 +280,12 @@ const nextConfig = {
},
],
// Keep cookie based booker enabled just in case we disable new-booker globally
...[
{
source: userTypeRoutePath,
destination: "/new-booker/:user/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: teamTypeRoutePath,
destination: "/new-booker/team/:slug/:type",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: privateLinkRoutePath,
destination: "/new-booker/d/:link/:slug",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
],
// Keep cookie based booker enabled to test new-booker embed in production
...[
{
source: embedUserTypeRoutePath,
destination: "/new-booker/:user/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
{
source: embedTeamTypeRoutePath,
destination: "/new-booker/team/:slug/:type/embed",
has: [{ type: "cookie", key: "new-booker-enabled" }],
},
],
/* TODO: have these files being served from another deployment or CDN {
source: "/embed/embed.js",
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
}, */
/**
* Enables new booker using cookie. It works even if NEW_BOOKER_ENABLED_FOR_NON_EMBED, NEW_BOOKER_ENABLED_FOR_EMBED are disabled
*/
];
// Enable New Booker for all Embed Requests
if (process.env.NEW_BOOKER_ENABLED_FOR_EMBED === "1") {
console.log("Enabling New Booker for Embed");
afterFiles.push(
...[
{
source: embedUserTypeRoutePath,
destination: "/new-booker/:user/:type/embed",
},
{
source: embedTeamTypeRoutePath,
destination: "/new-booker/team/:slug/:type/embed",
},
]
);
}
// Enable New Booker for All but embed Requests
if (process.env.NEW_BOOKER_ENABLED_FOR_NON_EMBED === "1") {
console.log("Enabling New Booker for Non-Embed");
afterFiles.push(
...[
{
source: userTypeRoutePath,
destination: "/new-booker/:user/:type",
},
{
source: teamTypeRoutePath,
destination: "/new-booker/team/:slug/:type",
},
{
source: privateLinkRoutePath,
destination: "/new-booker/d/:link/:slug",
},
]
);
}
return {
beforeFiles,
afterFiles,

View File

@ -1,281 +1,60 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { classNames } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { User } from "@calcom/prisma/client";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export type AvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: AvailabilityPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamic && !props.profile.allowDynamicBooking ? (
<div className="dark:bg-darkgray-50 h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
</div>
</div>
</div>
</main>
</div>
) : !props.isValidOrgDomain && props.organizationContext ? (
<div className="dark:bg-darkgray-50 h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_belongs_organization")}</p>
</div>
</div>
</div>
</main>
</div>
) : (
<AvailabilityPage {...props} />
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({ type: z.string(), user: z.string() });
async function getUserPageProps(context: GetServerSidePropsContext) {
// load server side dependencies
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const { EventTypeMetaDataSchema, teamMetadataSchema } = await import("@calcom/prisma/zod-utils");
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const ssr = await ssrInit(context);
const { type: slug, user: username } = paramsSchema.parse(context.query);
const user = await prisma.user.findFirst({
where: {
/** TODO: We should standarize this */
username: username.toLowerCase().replace(/( |%20)/g, "+"),
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
id: true,
username: true,
away: true,
name: true,
hideBranding: true,
timeZone: true,
theme: true,
weekStart: true,
brandColor: true,
darkBrandColor: true,
metadata: true,
organizationId: true,
eventTypes: {
where: {
// Many-to-many relationship causes inclusion of the team events - cool -
// but to prevent these from being selected, make sure the teamId is NULL.
AND: [{ slug }, { teamId: null }],
},
select: {
title: true,
slug: true,
hidden: true,
recurringEvent: true,
length: true,
locations: true,
id: true,
description: true,
price: true,
currency: true,
requiresConfirmation: true,
schedulingType: true,
metadata: true,
seatsPerTimeSlot: true,
team: {
select: {
logo: true,
parent: {
select: {
logo: true,
name: true,
},
},
},
},
},
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
teams: {
include: {
team: true,
},
},
},
});
if (!user || !user.eventTypes.length) return { notFound: true };
const [eventType]: ((typeof user.eventTypes)[number] & {
users: Pick<User, "name" | "username" | "hideBranding" | "timeZone">[];
})[] = [
{
...user.eventTypes[0],
users: [
{
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
timeZone: user.timeZone,
},
],
},
];
if (!eventType) return { notFound: true };
//TODO: Use zodSchema to verify it instead of using Type Assertion
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
});
// Check if the user you are logging into has any active teams or premium user name
const hasActiveTeam =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
const hasPremiumUserName = hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
return {
props: {
eventType: eventTypeObject,
profile: {
...eventType.users[0],
theme: user.theme,
allowDynamicBooking: false,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
slug: `${user.username}/${eventType.slug}`,
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
},
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: user.username,
organizationContext: user?.organizationId !== null,
away: user?.away,
isDynamic: false,
trpcState: ssr.dehydrate(),
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
isBrandingHidden: isBrandingHidden(user.hideBranding, hasActiveTeam || hasPremiumUserName),
},
};
}
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
// load server side dependencies
const { getDefaultEvent, getGroupName, getUsernameList } = await import("@calcom/lib/defaultEvents");
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
const { EventTypeMetaDataSchema, userMetadata: userMetadataSchema } = await import(
"@calcom/prisma/zod-utils"
);
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { getAppFromSlug } = await import("@calcom/app-store/utils");
const { type: typeParam, user: userParam } = paramsSchema.parse(context.params);
const usernameList = getUsernameList(userParam);
const length = parseInt(typeParam);
const eventType = getDefaultEvent("" + length);
const users = await prisma.user.findMany({
where: {
username: {
in: usernameList,
in: usernames,
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
startTime: true,
endTime: true,
timeZone: true,
weekStart: true,
availability: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
metadata: true,
organizationId: true,
away: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
},
},
theme: true,
},
});
@ -285,86 +64,101 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
};
}
// sort and be in the same order as usernameList so first user is the first user in the list
let sortedUsers: typeof users = [];
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
let locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username: usernames.join("+"), eventSlug: slug });
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[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 prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
if (!eventData) {
return {
notFound: true,
};
}
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: users.map((user) => {
return {
name: user.name,
username: user.username,
hideBranding: user.hideBranding,
timeZone: user.timeZone,
};
}),
});
const dynamicNames = users.map((user) => {
return user.name || "";
});
const profile = {
name: getGroupName(dynamicNames),
image: null,
slug: "" + length,
theme: null as string | null,
weekStart: "Sunday",
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => {
return !user.allowDynamicBooking;
}),
};
return {
props: {
eventType: eventTypeObject,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: null,
isDynamic: true,
booking,
user: usernames.join("+"),
slug,
away: false,
organizationContext: !users.some((user) => user.organizationId === null),
trpcState: ssr.dehydrate(),
isValidOrgDomain: orgDomainConfig(context.req.headers.host ?? ""),
isBrandingHidden: false, // I think we should always show branding for dynamic groups - saves us checking every single user
isBrandingHidden: false,
themeBasis: null,
},
};
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { user: userParam } = paramsSchema.parse(context.params);
// dynamic groups are not generated at build time, but otherwise are probably cached until infinity.
const isDynamicGroup = getUsernameList(userParam).length > 1;
if (isDynamicGroup) {
return await getDynamicGroupPageProps(context);
} else {
return await getUserPageProps(context);
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
},
};
}
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
user: z.string().transform((s) => getUsernameList(s)),
});
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { user } = paramsSchema.parse(context.params);
const isDynamicGroup = user.length > 1;
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
};

View File

@ -1,19 +1,11 @@
import type { GetServerSidePropsContext } from "next";
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssrResponse = await _getServerSideProps(context);
if (ssrResponse.notFound) {
return ssrResponse;
}
return {
...ssrResponse,
props: {
...ssrResponse.props,
isEmbed: true,
},
};
};
// Somehow these types don't accept the {notFound: true} return type.
// Probably still need to fix this. I don't know why this isn't allowed yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,309 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getAppFromSlug } from "@calcom/app-store/utils";
import dayjs from "@calcom/dayjs";
import getBooking from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import {
getDefaultEvent,
getDynamicEventName,
getGroupName,
getUsernameList,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
import {
customInputSchema,
EventTypeMetaDataSchema,
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {
const { t } = useLocale();
return props.away ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
😴{" " + t("user_away")}
</h2>
<p className="mx-auto max-w-md">{t("user_away_description")}</p>
</div>
</div>
</div>
</main>
</div>
) : props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? (
<div className="dark:bg-inverted h-screen">
<main className="mx-auto max-w-3xl px-4 py-24">
<div className="space-y-6" data-testid="event-types">
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-default mb-2 text-3xl">
{" " + t("unavailable")}
</h2>
<p className="mx-auto max-w-md">{t("user_dynamic_booking_disabled")}</p>
</div>
</div>
</div>
</main>
</div>
) : (
<BookingPage {...props} />
);
}
Book.isBookingPage = true;
Book.PageWrapper = PageWrapper;
const querySchema = z.object({
bookingUid: z.string().optional(),
count: z.coerce.number().optional(),
embed: z.string().optional(),
rescheduleUid: z.string().optional(),
slug: z.string().optional(),
/** This is the event "type" ID */
type: z.coerce.number().optional(),
user: z.string(),
seatReferenceUid: z.string().optional(),
date: z.string().optional(),
duration: z.coerce.number().optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const query = querySchema.parse(context.query);
const usernameList = getUsernameList(query.user);
const eventTypeSlug = query.slug;
const recurringEventCountQuery = query.count;
const users = await prisma.user.findMany({
where: {
username: {
in: usernameList,
},
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
away: true,
metadata: true,
},
});
if (!users.length) return { notFound: true };
const [user] = users;
const isDynamicGroupBooking = users.length > 1 && !!eventTypeSlug;
// Dynamic Group link doesn't need a type but it must have a slug
if ((!isDynamicGroupBooking && !query.type) || (users.length > 1 && !eventTypeSlug)) {
return { notFound: true };
}
const eventTypeRaw = isDynamicGroupBooking
? getDefaultEvent(eventTypeSlug)
: await prisma.eventType.findUnique({
where: {
id: query.type,
},
select: {
...bookEventTypeSelect,
},
});
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
bookingFields: getBookingFieldsWithSystemFields(eventTypeRaw),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
};
const getLocations = () => {
let locations = eventTypeRaw.locations || [];
if (!isDynamicGroupBooking) return locations;
let sortedUsers: typeof users = [];
// sort and be in the same order as usernameList so first user is the first user in the list
if (users.length > 1) {
sortedUsers = users.sort((a, b) => {
const aIndex = (a.username && usernameList.indexOf(a.username)) || 0;
const bIndex = (b.username && usernameList.indexOf(b.username)) || 0;
return aIndex - bIndex;
});
}
// Get the prefered location type from the first user
const firstUsersMetadata = userMetadataSchema.parse(sortedUsers[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 prefered location type
// This will still be default to daily if the app is not found
locations = [{ type: appType, link: preferedLocationType.appLink }] as LocationObject[];
}
}
return locations;
};
const eventTypeObject = [eventType].map((e) => {
let locations = getLocations();
locations = privacyFilteredLocations(locations as LocationObject[]);
return {
...e,
locations: locations,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
schedulingType: null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: users.map((u) => ({
id: u.id,
name: u.name,
username: u.username,
avatar: u.avatar,
image: u.avatar,
slug: u.username,
theme: u.theme,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
// If rescheduleUid and event has seats lets convert Uid to bookingUid
let rescheduleUid = query.rescheduleUid;
const rescheduleEventTypeHasSeats = query.rescheduleUid && eventTypeRaw.seatsPerTimeSlot;
let attendeeEmail: string;
let bookingUidWithSeats: string | null = null;
if (rescheduleEventTypeHasSeats) {
const bookingSeat = await prisma.bookingSeat.findFirst({
where: {
referenceUid: query.rescheduleUid,
},
select: {
id: true,
attendee: true,
booking: {
select: {
uid: true,
},
},
},
});
if (bookingSeat) {
rescheduleUid = bookingSeat.booking.uid;
attendeeEmail = bookingSeat.attendee.email;
}
}
if (query.duration) {
// If it's not reschedule but event Type has seats we should obtain
// the bookingUid regardless and use it to get the booking
const currentSeats = await prisma.booking.findFirst({
where: {
eventTypeId: eventTypeRaw.id,
startTime: dayjs(query.date).toISOString(),
endTime: dayjs(query.date).add(query.duration, "minutes").toISOString(),
},
select: {
uid: true,
},
});
if (currentSeats && currentSeats) {
bookingUidWithSeats = currentSeats.uid;
}
}
let booking: GetBookingType | null = null;
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
}
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
const currentAttendee = booking?.attendees.find((attendee) => {
return attendee.email === attendeeEmail;
});
if (currentAttendee) {
booking.attendees = [currentAttendee] || [];
}
}
const dynamicNames = isDynamicGroupBooking ? users.map((user) => user.name || "") : [];
const profile = isDynamicGroupBooking
? {
name: getGroupName(dynamicNames),
image: null,
slug: eventTypeSlug,
theme: null,
brandColor: "",
darkBrandColor: "",
allowDynamicBooking: !users.some((user) => !user.allowDynamicBooking),
eventName: getDynamicEventName(dynamicNames, eventTypeSlug),
}
: {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
eventName: null,
};
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventType.recurringEvent?.count &&
recurringEventCountQuery &&
(recurringEventCountQuery <= eventType.recurringEvent.count
? recurringEventCountQuery
: eventType.recurringEvent.count)) ||
null;
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
return {
props: {
away: user.away,
profile,
// Dynamic group has no theme preference right now. It uses system theme.
themeBasis: isDynamicGroupBooking ? null : user.username,
eventType: eventTypeObject,
booking,
currentSlotBooking: currentSlotBooking,
recurringEventCount,
trpcState: ssr.dehydrate(),
isDynamicGroupBooking,
hasHashedBookingLink: false,
hashedLink: null,
isEmbed: !!query.embed,
},
};
}

View File

@ -1,26 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { defaultResponder } from "@calcom/lib/server";
const newBookerSchema = z.object({
status: z.enum(["enable", "disable"]),
});
/**
* Very basic temporary api route to enable/disable new booker access.
*/
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { status } = newBookerSchema.parse(req.query);
if (status === "enable") {
const expires = new Date();
expires.setFullYear(expires.getFullYear() + 1);
res.setHeader("Set-Cookie", `new-booker-enabled=true; path=/; expires=${expires.toUTCString()}`);
} else {
res.setHeader("Set-Cookie", "new-booker-enabled=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
}
res.send({ status: 200, body: `Done ${status}` });
}
export default defaultResponder(handler);

View File

@ -1,42 +1,50 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: DynamicAvailabilityPageProps) {
return <AvailabilityPage {...props} />;
export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
/>
</main>
);
}
Type.isBookingPage = true;
Type.PageWrapper = PageWrapper;
const querySchema = z.object({
link: z.string().optional().default(""),
slug: z.string().optional().default(""),
date: z.union([z.string(), z.null()]).optional().default(null),
});
async function getUserPageProps(context: GetServerSidePropsContext) {
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const { link, slug, date } = querySchema.parse(context.query);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
@ -45,132 +53,87 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
select: {
eventTypeId: true,
eventType: {
select: availiblityPageEventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
} as {
notFound: true;
};
if (hashedLink?.eventType.slug !== slug)
return {
notFound: true,
} as {
notFound: true;
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
startTime: true,
endTime: true,
timeZone: true,
weekStart: true,
availability: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
defaultScheduleId: true,
allowDynamicBooking: true,
away: true,
schedules: {
select: {
availability: true,
timeZone: true,
id: true,
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
theme: true,
},
});
if (!users || !users.length) {
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return {
notFound: true,
} as {
notFound: true;
};
}
const locations = hashedLink.eventType.locations
? (hashedLink.eventType.locations as LocationObject[])
: [];
const eventTypeObject = Object.assign({}, hashedLink.eventType, {
metadata: EventTypeMetaDataSchema.parse(hashedLink.eventType.metadata || {}),
recurringEvent: parseRecurringEvent(hashedLink.eventType.recurringEvent),
periodStartDate: hashedLink.eventType.periodStartDate?.toString() ?? null,
periodEndDate: hashedLink.eventType.periodEndDate?.toString() ?? null,
slug,
locations: privacyFilteredLocations(locations),
users: users.map((u) => ({
name: u.name,
username: u.username,
hideBranding: u.hideBranding,
timeZone: u.timeZone,
})),
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
const [user] = users;
if (!user) {
return {
notFound: true,
};
}
const schedule = {
...user.schedules.filter(
(schedule) => !user.defaultScheduleId || schedule.id === user.defaultScheduleId
)[0],
};
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
const timeZone = schedule.timeZone || user.timeZone;
const isTeamEvent = !!hashedLink.eventType?.team?.id;
const workingHours = getWorkingHours(
{
timeZone,
},
schedule.availability || user.availability
);
eventTypeObject.schedule = null;
eventTypeObject.availability = [];
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent });
const booking: GetBookingType | null = null;
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
weekStart: user.weekStart,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
};
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
away: user.away,
themeBasis: user.username,
isDynamicGroup: false,
profile,
date,
eventType: eventTypeObject,
workingHours,
trpcState: ssr.dehydrate(),
previousPage: context.req.headers.referer ?? null,
booking,
users: [user.username],
isBrandingHidden: user.hideBranding,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
},
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
return await getUserPageProps(context);
};

View File

@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[slug]";
export { default } from "../[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,137 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {
return <BookingPage {...props} />;
}
Book.isBookingPage = true;
Book.PageWrapper = PageWrapper;
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const link = asStringOrThrow(context.query.link as string);
const recurringEventCountQuery = asStringOrNull(context.query.count);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: bookEventTypeSelect,
},
},
});
const userId = hashedLink?.eventType.userId || hashedLink?.eventType.users[0]?.id;
if (!userId)
return {
notFound: true,
};
const users = await prisma.user.findMany({
where: {
id: userId,
},
select: {
id: true,
username: true,
name: true,
email: true,
bio: true,
avatar: true,
theme: true,
brandColor: true,
darkBrandColor: true,
allowDynamicBooking: true,
},
});
if (!users.length) return { notFound: true };
const [user] = users;
const eventTypeRaw = hashedLink?.eventType;
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata || {}),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
bookingFields: eventTypeBookingFields.parse(eventTypeRaw.bookingFields || []),
};
const eventTypeObject = [eventType].map((e) => {
return {
...e,
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
schedulingType: null,
users: users.map((u) => ({
id: u.id,
name: u.name,
username: u.username,
avatar: u.avatar,
image: u.avatar,
slug: u.username,
theme: u.theme,
email: u.email,
brandColor: u.brandColor,
darkBrandColor: u.darkBrandColor,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
const profile = {
name: user.name || user.username,
image: user.avatar,
slug: user.username,
theme: user.theme,
brandColor: user.brandColor,
darkBrandColor: user.darkBrandColor,
eventName: null,
};
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventTypeObject.recurringEvent?.count &&
recurringEventCountQuery &&
(parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count
? parseInt(recurringEventCountQuery)
: eventType.recurringEvent?.count)) ||
null;
return {
props: {
profile,
themeBasis: user.username,
eventType: eventTypeObject,
booking: null,
currentSlotBooking: null,
trpcState: ssr.dehydrate(),
recurringEventCount,
isDynamicGroupBooking: false,
hasHashedBookingLink: true,
hashedLink: link,
isEmbed: typeof context.query.embed === "string",
},
};
}

View File

@ -1,164 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { classNames } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const users = await prisma.user.findMany({
where: {
username: {
in: usernames,
},
},
select: {
allowDynamicBooking: true,
},
});
if (!users.length) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username: usernames.join("+"), eventSlug: slug });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
user: usernames.join("+"),
slug,
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
themeBasis: null,
},
};
}
async function getUserPageProps(context: GetServerSidePropsContext) {
const { user: usernames, type: slug } = paramsSchema.parse(context.params);
const username = usernames[0];
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
themeBasis: username,
},
};
}
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
user: z.string().transform((s) => getUsernameList(s)),
});
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { user } = paramsSchema.parse(context.params);
const isDynamicGroup = user.length > 1;
return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
};

View File

@ -1,11 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// Somehow these types don't accept the {notFound: true} return type.
// Probably still need to fix this. I don't know why this isn't allowed yet.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../[user]";
export { default } from "../../[user]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,139 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden, isTeamEvent }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent={isTeamEvent}
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
async function getUserPageProps(context: GetServerSidePropsContext) {
const { link, slug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req.headers.host ?? "");
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const hashedLink = await prisma.hashedLink.findUnique({
where: {
link,
},
select: {
eventTypeId: true,
eventType: {
select: {
users: {
select: {
username: true,
},
},
team: {
select: {
id: true,
},
},
},
},
},
});
const username = hashedLink?.eventType.users[0]?.username;
if (!hashedLink || !username) {
return {
notFound: true,
};
}
const user = await prisma.user.findFirst({
where: {
username,
organization: isValidOrgDomain
? {
slug: currentOrgDomain,
}
: null,
},
select: {
away: true,
hideBranding: true,
},
});
if (!user) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
const isTeamEvent = !!hashedLink.eventType?.team?.id;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent });
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: user?.away,
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
// Sending the team event from the server, because this template file
// is reused for both team and user events.
isTeamEvent,
},
};
}
const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
// Booker page fetches a tiny bit of data server side, to determine early
// whether the page should show an away state or dynamic booking not allowed.
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
return await getUserPageProps(context);
};

View File

@ -1,104 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { classNames } from "@calcom/lib";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent
/>
</main>
);
}
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
slug: z.string().transform((s) => slugify(s)),
});
// Booker page fetches a tiny bit of data server side:
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const team = await prisma.team.findFirst({
where: {
slug: teamSlug,
},
select: {
id: true,
hideBranding: true,
},
});
if (!team) {
return {
notFound: true,
};
}
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username: teamSlug,
eventSlug: meetingSlug,
isTeamEvent: true,
});
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
booking,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
themeBasis: null,
},
};
};

View File

@ -1,9 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,7 +0,0 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../../../team/[slug]";
export { default } from "../../../team/[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -4,10 +4,10 @@ import prisma from "@calcom/prisma";
import PageWrapper from "@components/PageWrapper";
import type { PageProps as UserTypePageProps } from "../../../new-booker/[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../new-booker/[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../new-booker/team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../new-booker/team/[slug]/[type]";
import type { PageProps as UserTypePageProps } from "../../../[user]/[type]";
import UserTypePage, { getServerSideProps as GSSUserTypePage } from "../../../[user]/[type]";
import TeamTypePage, { getServerSideProps as GSSTeamTypePage } from "../../../team/[slug]/[type]";
import type { PageProps as TeamTypePageProps } from "../../../team/[slug]/[type]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const team = await prisma.team.findFirst({

View File

@ -6,7 +6,7 @@ import PageWrapper from "@components/PageWrapper";
import type { UserPageProps } from "../../../[user]";
import UserPage, { getServerSideProps as GSSUserPage } from "../../../[user]";
import type { TeamPageProps } from "../../../team/[slug]";
import type { PageProps as TeamPageProps } from "../../../team/[slug]";
import TeamPage, { getServerSideProps as GSSTeamPage } from "../../../team/[slug]";
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {

View File

@ -28,9 +28,9 @@ import Team from "@components/team/screens/Team";
import { ssrInit } from "@server/lib/ssr";
export type TeamPageProps = inferSSRProps<typeof getServerSideProps>;
export type PageProps = inferSSRProps<typeof getServerSideProps>;
function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }: TeamPageProps) {
function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }: PageProps) {
useTheme(team.theme);
const showMembers = useToggleQuery("members");
const { t } = useLocale();
@ -65,7 +65,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
}
// slug is a route parameter, we don't want to forward it to the next route
const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = router.query;
const { slug: _slug, ...queryParamsToForward } = router.query;
const EventTypes = () => (
<ul className="border-subtle rounded-md border">
@ -153,9 +153,9 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
<div className="text-muted dark:text-inverted p-8 text-center">
<h2 className="font-cal dark:text-inverted text-emphasis600 mb-2 text-3xl">
{" " + t("org_no_teams_yet")}
{" " + t("no_teams_yet")}
</h2>
<p className="mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
<p className="mx-auto max-w-md">{t("no_teams_yet_description")}</p>
</div>
</div>
</div>
@ -187,7 +187,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
{!isBioEmpty && (
<>
<div
className="text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
className=" text-subtle break-words text-sm [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{ __html: team.safeBio }}
/>
</>
@ -197,26 +197,21 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<SubTeams />
) : (
<>
{(showMembers.isOn || !team.eventTypes.length) &&
(team.isPrivate ? (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
</div>
) : (
<Team team={team} />
))}
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes />
{!(team.hideBookATeamMember || team.isPrivate) && (
{!team.hideBookATeamMember && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="bg-subtle text-subtle px-2 text-sm">{t("or")}</span>
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
{t("or")}
</span>
</div>
</div>

View File

@ -1,214 +1,104 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { Booker } from "@calcom/atoms";
import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
import { getBookingByUidOrRescheduleUid } from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import getBooking from "@calcom/features/bookings/lib/get-booking";
import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { classNames } from "@calcom/lib";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
import PageWrapper from "@components/PageWrapper";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssgInit } from "@server/lib/ssg";
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export type AvailabilityTeamPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function TeamType(props: AvailabilityTeamPageProps) {
return <AvailabilityPage {...props} />;
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
const isEmbed = typeof window !== "undefined" && window?.isEmbed?.();
return (
<main className={classNames("flex h-full items-center justify-center", !isEmbed && "min-h-[100dvh]")}>
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
isTeamEvent
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
isTeamEvent
/>
</main>
);
}
TeamType.isBookingPage = true;
TeamType.PageWrapper = PageWrapper;
Type.PageWrapper = PageWrapper;
const paramsSchema = z.object({
type: z.string().transform((s) => slugify(s)),
slug: z.string().transform((s) => slugify(s)),
});
// Booker page fetches a tiny bit of data server side:
// 1. Check if team exists, to show 404
// 2. If rescheduling, get the booking details
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const slugParam = asStringOrNull(context.query.slug);
const typeParam = asStringOrNull(context.query.type);
const dateParam = asStringOrNull(context.query.date);
const rescheduleUid = asStringOrNull(context.query.rescheduleUid);
const ssg = await ssgInit(context);
if (!slugParam || !typeParam) {
throw new Error(`File is not named [idOrSlug]/[user]`);
}
const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
const { rescheduleUid } = context.query;
const { ssrInit } = await import("@server/lib/ssr");
const ssr = await ssrInit(context);
const team = await prisma.team.findFirst({
where: {
slug: slugParam,
slug: teamSlug,
},
select: {
id: true,
name: true,
slug: true,
logo: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
theme: true,
eventTypes: {
where: {
slug: typeParam,
},
select: {
id: true,
slug: true,
hidden: true,
hosts: {
select: {
isFixed: true,
user: {
select: {
id: true,
name: true,
username: true,
timeZone: true,
hideBranding: true,
brandColor: true,
darkBrandColor: true,
},
},
},
},
title: true,
availability: true,
description: true,
length: true,
disableGuests: true,
schedulingType: true,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
recurringEvent: true,
requiresConfirmation: true,
locations: true,
price: true,
currency: true,
timeZone: true,
slotInterval: true,
metadata: true,
seatsPerTimeSlot: true,
bookingFields: true,
customInputs: true,
schedule: {
select: {
timeZone: true,
availability: true,
},
},
workflows: {
select: {
workflow: {
select: {
id: true,
steps: true,
},
},
},
},
team: {
select: {
members: {
where: {
role: "OWNER",
},
select: {
user: {
select: {
weekStart: true,
},
},
},
},
parent: {
select: {
logo: true,
name: true,
},
},
},
},
},
},
},
});
if (!team || team.eventTypes.length != 1) {
if (!team) {
return {
notFound: true,
} as {
notFound: true;
};
}
const [eventType] = team.eventTypes;
const timeZone = eventType.schedule?.timeZone || eventType.timeZone || undefined;
const workingHours = getWorkingHours(
{
timeZone,
},
eventType.schedule?.availability || eventType.availability
);
eventType.schedule = null;
const locations = eventType.locations ? (eventType.locations as LocationObject[]) : [];
const eventTypeObject = Object.assign({}, eventType, {
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
locations: privacyFilteredLocations(locations),
users: eventType.hosts.map(({ user: { name, username, hideBranding, timeZone } }) => ({
name,
username,
hideBranding,
timeZone,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
});
eventTypeObject.availability = [];
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBooking(prisma, rescheduleUid);
booking = await getBookingByUidOrRescheduleUid(`${rescheduleUid}`);
}
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
// We use this to both prefetch the query on the server,
// as well as to check if the event exist, so we c an show a 404 otherwise.
const eventData = await ssr.viewer.public.event.fetch({
username: teamSlug,
eventSlug: meetingSlug,
isTeamEvent: true,
});
if (!eventData) {
return {
notFound: true,
};
}
return {
props: {
profile: {
name: team.name || team.slug,
slug: team.slug,
image: team.logo,
theme: team.theme,
weekStart: weekStart ?? "Sunday",
brandColor: team.brandColor,
darkBrandColor: team.darkBrandColor,
},
themeBasis: team.slug,
date: dateParam,
eventType: eventTypeObject,
workingHours,
previousPage: context.req.headers.referer ?? null,
booking,
trpcState: ssg.dehydrate(),
isBrandingHidden: team.hideBranding,
away: false,
user: teamSlug,
teamId: team.id,
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
themeBasis: null,
},
};
};

View File

@ -4,4 +4,6 @@ import { getServerSideProps as _getServerSideProps } from "../[type]";
export { default } from "../[type]";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -1,173 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { z } from "zod";
import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import getBooking from "@calcom/features/bookings/lib/get-booking";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type TeamBookingPageProps = inferSSRProps<typeof getServerSideProps>;
export default function TeamBookingPage(props: TeamBookingPageProps) {
return <BookingPage {...props} />;
}
TeamBookingPage.isBookingPage = true;
TeamBookingPage.PageWrapper = PageWrapper;
const querySchema = z.object({
rescheduleUid: z.string().optional(),
bookingUid: z.string().optional(),
});
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const eventTypeId = parseInt(asStringOrThrow(context.query.type));
const recurringEventCountQuery = asStringOrNull(context.query.count);
if (typeof eventTypeId !== "number" || eventTypeId % 1 !== 0) {
return {
notFound: true,
} as const;
}
const eventTypeRaw = await prisma.eventType.findUnique({
where: {
id: eventTypeId,
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
locations: true,
customInputs: true,
periodType: true,
periodDays: true,
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
recurringEvent: true,
requiresConfirmation: true,
disableGuests: true,
price: true,
currency: true,
metadata: true,
seatsPerTimeSlot: true,
schedulingType: true,
bookingFields: true,
successRedirectUrl: true,
workflows: {
include: {
workflow: {
include: {
steps: true,
},
},
},
},
team: {
select: {
slug: true,
name: true,
logo: true,
theme: true,
brandColor: true,
darkBrandColor: true,
parent: {
select: {
logo: true,
name: true,
},
},
},
},
users: {
select: {
id: true,
username: true,
avatar: true,
name: true,
},
},
},
});
if (!eventTypeRaw) return { notFound: true };
const eventType = {
...eventTypeRaw,
//TODO: Use zodSchema to verify it instead of using Type Assertion
locations: privacyFilteredLocations((eventTypeRaw.locations || []) as LocationObject[]),
recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
bookingFields: eventTypeBookingFields.parse(eventTypeRaw.bookingFields || []),
};
const eventTypeObject = [eventType].map((e) => {
return {
...e,
metadata: EventTypeMetaDataSchema.parse(e.metadata || {}),
bookingFields: getBookingFieldsWithSystemFields(eventType),
periodStartDate: e.periodStartDate?.toString() ?? null,
periodEndDate: e.periodEndDate?.toString() ?? null,
customInputs: customInputSchema.array().parse(e.customInputs || []),
users: eventType.users.map((u) => ({
id: u.id,
name: u.name,
username: u.username,
avatar: u.avatar,
image: u.avatar,
slug: u.username,
})),
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
};
})[0];
let booking: GetBookingType | null = null;
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
if (rescheduleUid || bookingUid) {
booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
}
// Checking if number of recurring event ocurrances is valid against event type configuration
const recurringEventCount =
(eventType.recurringEvent?.count &&
recurringEventCountQuery &&
(parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count
? parseInt(recurringEventCountQuery)
: eventType.recurringEvent.count)) ||
null;
return {
props: {
trpcState: ssr.dehydrate(),
profile: {
...eventTypeObject.team,
// FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking.
slug: "team/" + eventTypeObject.slug,
image: eventTypeObject.team?.logo || null,
eventName: null,
},
themeBasis: eventTypeObject.team?.slug,
eventType: eventTypeObject,
recurringEventCount,
booking,
currentSlotBooking: null,
isDynamicGroupBooking: false,
hasHashedBookingLink: false,
hashedLink: null,
isEmbed: typeof context.query.embed === "string",
},
};
}

View File

@ -1,7 +1,7 @@
import withEmbedSsr from "@lib/withEmbedSsr";
import { getServerSideProps as _getServerSideProps } from "../[slug]";
import { getServerSideProps as _getServerSideProps } from "../../team/[slug]";
export { default } from "../[slug]";
export { default } from "../../team/[slug]";
export const getServerSideProps = withEmbedSsr(_getServerSideProps);

View File

@ -22,14 +22,6 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
// [^/]+ makes the RegExp match the full path, it seems like a partial match doesn't work.
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
const afterFilesRewriteExcludePages = pages;
exports.userTypeRoutePath = `/:user((?!${afterFilesRewriteExcludePages.join(
"/|"
)})[^/]*)/:type((?!book$)[^/]+)`;
exports.teamTypeRoutePath = "/team/:slug/:type((?!book$)[^/]+)";
exports.privateLinkRoutePath = "/d/:link/:slug((?!book$)[^/]+)";
exports.embedUserTypeRoutePath = `/:user((?!${afterFilesRewriteExcludePages.join("/|")})[^/]*)/:type/embed`;
exports.embedTeamTypeRoutePath = "/team/:slug/:type/embed";
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
));

View File

@ -1,7 +1,6 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
import {
bookFirstEvent,
bookOptinEvent,
@ -15,7 +14,7 @@ import {
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users }) => users.deleteAll());
testBothBookers.describe("free user", (bookerVariant) => {
test.describe("free user", () => {
test.beforeEach(async ({ page, users }) => {
const free = await users.create();
await page.goto(`/${free.username}`);
@ -27,17 +26,6 @@ testBothBookers.describe("free user", (bookerVariant) => {
await selectFirstAvailableTimeSlotNextMonth(page);
// Kept in if statement here, since it's only temporary
// until the old booker isn't used anymore, and I wanted
// to change the test as little as possible.
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant !== "new-booker") {
// Navigate to book page
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
// save booking url
const bookingUrl: string = page.url();
@ -58,7 +46,7 @@ testBothBookers.describe("free user", (bookerVariant) => {
});
});
testBothBookers.describe("pro user", () => {
test.describe("pro user", () => {
test.beforeEach(async ({ page, users }) => {
const pro = await users.create();
await page.goto(`/${pro.username}`);

View File

@ -8,7 +8,6 @@ import { BookingStatus } from "@calcom/prisma/enums";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
import {
bookTimeSlot,
createNewSeatedEventType,
@ -56,7 +55,7 @@ async function createUserWithSeatedEventAndAttendees(
return { user, eventType, booking };
}
testBothBookers.describe("Booking with Seats", (bookerVariant) => {
test.describe("Booking with Seats", () => {
test("User can create a seated event (2 seats as example)", async ({ users, page }) => {
const user = await users.create({ name: "Seated event" });
await user.apiLogin();
@ -85,16 +84,6 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
await page.goto(`/${user.username}/${slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// Kept in if statement here, since it's only temporary
// until the old booker isn't used anymore, and I wanted
// to change the test as little as possible.
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant === "old-booker") {
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
const bookingUrl = page.url();
await test.step("Attendee #1 can book a seated event time slot", async () => {
await page.goto(bookingUrl);
@ -187,7 +176,7 @@ testBothBookers.describe("Booking with Seats", (bookerVariant) => {
});
});
testBothBookers.describe("Reschedule for booking with seats", () => {
test.describe("Reschedule for booking with seats", () => {
test("Should reschedule booking with seats", async ({ page, users, bookings }) => {
const { booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: `first+seats-${uuid()}@cal.com`, timeZone: "Europe/Berlin" },

View File

@ -4,13 +4,12 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { randomString } from "@calcom/lib/random";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
import { bookTimeSlot, createNewEventType, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.describe("Event Types tests", () => {
testBothBookers.describe("user", (bookerVariant) => {
test.describe("user", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
@ -143,17 +142,6 @@ test.describe("Event Types tests", () => {
await selectFirstAvailableTimeSlotNextMonth(page);
// Navigate to book page
// Kept in if statement here, since it's only temporary
// until the old booker isn't used anymore, and I wanted
// to change the test as little as possible.
// eslint-disable-next-line playwright/no-conditional-in-test
if (bookerVariant === "old-booker") {
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
for (const location of locationData) {
await page.locator(`span:has-text("${location}")`).click();
}

View File

@ -1,31 +0,0 @@
import { test } from "./fixtures";
export type BookerVariants = "new-booker" | "old-booker";
const bookerVariants = ["new-booker", "old-booker"];
/**
* Small wrapper around test.describe().
* When using testbothBookers.describe() instead of test.describe(), this will run the specified
* tests twice. One with the old booker, and one with the new booker. It will also add the booker variant
* name to the test name for easier debugging.
* Finally it also adds a parameter bookerVariant to your testBothBooker.describe() callback, which
* can be used to do any conditional rendering in the test for a specific booker variant (should be as little
* as possible).
*
* See apps/web/playwright/booking-pages.e2e.ts for an example.
*/
export const testBothBookers = {
describe: (testName: string, testFn: (bookerVariant: BookerVariants) => void) => {
bookerVariants.forEach((bookerVariant) => {
test.describe(`${testName} -- ${bookerVariant}`, () => {
if (bookerVariant === "new-booker") {
test.beforeEach(({ context }) => {
context.addCookies([{ name: "new-booker-enabled", value: "true", url: "http://localhost:3000" }]);
});
}
testFn(bookerVariant as BookerVariants);
});
});
},
};

View File

@ -7,8 +7,6 @@ import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
import type { BookerVariants } from "./lib/new-booker";
import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
async function getLabelText(field: Locator) {
@ -21,7 +19,7 @@ test.describe("Manage Booking Questions", () => {
await users.deleteAll();
});
testBothBookers.describe("For User EventType", (bookerVariant) => {
test.describe("For User EventType", () => {
test("Do a booking with a user added question and verify a few thing in b/w", async ({
page,
users,
@ -40,11 +38,11 @@ test.describe("Manage Booking Questions", () => {
await firstEventTypeElement.click();
});
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver, bookerVariant);
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
});
});
testBothBookers.describe("For Team EventType", (bookerVariant) => {
test.describe("For Team EventType", () => {
test("Do a booking with a user added question and verify a few thing in b/w", async ({
page,
users,
@ -76,7 +74,7 @@ test.describe("Manage Booking Questions", () => {
await firstEventTypeElement.click();
});
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver, bookerVariant);
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
});
});
});
@ -90,8 +88,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestList: (import("http").IncomingMessage & { body?: any })[];
url: string;
},
bookerVariant: BookerVariants
}
) {
await page.click('[href$="tabName=advanced"]');
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
@ -106,7 +103,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
},
});
await doOnFreshPreview(page, context, bookerVariant, async (page) => {
await doOnFreshPreview(page, context, async (page) => {
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
const userFieldLocator = allFieldsLocator.nth(5);
@ -122,7 +119,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
name: "how_are_you",
page,
});
await doOnFreshPreview(page, context, bookerVariant, async (page) => {
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeHidden();
});
@ -136,7 +133,7 @@ async function runTestStepsCommonForTeamAndUserEventType(
});
await test.step('Try to book without providing "How are you?" response', async () => {
await doOnFreshPreview(page, context, bookerVariant, async (page) => {
await doOnFreshPreview(page, context, async (page) => {
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expectErrorToBeThereFor({ page, name: "how_are_you" });
});
@ -155,7 +152,6 @@ async function runTestStepsCommonForTeamAndUserEventType(
return await doOnFreshPreview(
page,
context,
bookerVariant,
async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeVisible();
@ -324,11 +320,10 @@ async function expectErrorToBeThereFor({ page, name }: { page: Page; name: strin
async function doOnFreshPreview(
page: Page,
context: PlaywrightTestArgs["context"],
bookerVariant: BookerVariants,
callback: (page: Page) => Promise<void>,
persistTab = false
) {
const previewTabPage = await openBookingFormInPreviewTab(context, page, bookerVariant);
const previewTabPage = await openBookingFormInPreviewTab(context, page);
await callback(previewTabPage);
if (!persistTab) {
await previewTabPage.close();
@ -377,39 +372,19 @@ async function createAndLoginUserWithEventTypes({
return user;
}
async function rescheduleFromTheLinkOnPage({
page,
bookerVariant,
}: {
page: Page;
bookerVariant?: BookerVariants;
}) {
async function rescheduleFromTheLinkOnPage({ page }: { page: Page }) {
await page.locator('[data-testid="reschedule-link"]').click();
await page.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(page);
if (bookerVariant === "old-booker") {
await page.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
await page.click('[data-testid="confirm-reschedule-button"]');
}
async function openBookingFormInPreviewTab(
context: PlaywrightTestArgs["context"],
page: Page,
bookerVariant: BookerVariants
) {
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
const previewTabPromise = context.waitForEvent("page");
await page.locator('[data-testid="preview-button"]').click();
const previewTabPage = await previewTabPromise;
await previewTabPage.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(previewTabPage);
if (bookerVariant === "old-booker") {
await previewTabPage.waitForURL((url) => {
return url.pathname.endsWith("/book");
});
}
return previewTabPage;
}

View File

@ -4,7 +4,6 @@ import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import { testBothBookers } from "./lib/new-booker";
import { selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
const IS_STRIPE_ENABLED = !!(
@ -17,7 +16,7 @@ test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
testBothBookers.describe("Reschedule Tests", async () => {
test.describe("Reschedule Tests", async () => {
test("Should do a booking request reschedule from /bookings", async ({ page, users, bookings }) => {
const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@ -4,11 +4,6 @@ const { getSubdomainRegExp } = require("../../getSubdomainRegExp");
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { match, pathToRegexp } = require("next/dist/compiled/path-to-regexp");
type MatcherRes = (path: string) => { params: Record<string, string> };
let userTypeRouteMatch: MatcherRes;
let teamTypeRouteMatch: MatcherRes;
let privateLinkRouteMatch: MatcherRes;
let embedUserTypeRouteMatch: MatcherRes;
let embedTeamTypeRouteMatch: MatcherRes;
let orgUserTypeRouteMatch: MatcherRes;
let orgUserRouteMatch: MatcherRes;
@ -17,160 +12,22 @@ beforeAll(async () => {
//@ts-ignore
process.env.NEXT_PUBLIC_WEBAPP_URL = "http://example.com";
const {
userTypeRoutePath,
teamTypeRoutePath,
privateLinkRoutePath,
embedUserTypeRoutePath,
embedTeamTypeRoutePath,
orgUserRoutePath,
orgUserTypeRoutePath,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require("../../pagesAndRewritePaths");
userTypeRouteMatch = match(userTypeRoutePath);
teamTypeRouteMatch = match(teamTypeRoutePath);
privateLinkRouteMatch = match(privateLinkRoutePath);
embedUserTypeRouteMatch = match(embedUserTypeRoutePath);
embedTeamTypeRouteMatch = match(embedTeamTypeRoutePath);
orgUserTypeRouteMatch = match(orgUserTypeRoutePath);
orgUserRouteMatch = match(orgUserRoutePath);
console.log({
regExps: {
userTypeRouteMatch: pathToRegexp(userTypeRoutePath),
teamTypeRouteMatch: pathToRegexp(teamTypeRoutePath),
privateLinkRouteMatch: pathToRegexp(privateLinkRoutePath),
embedUserTypeRouteMatch: pathToRegexp(embedUserTypeRoutePath),
embedTeamTypeRouteMatch: pathToRegexp(embedTeamTypeRoutePath),
orgUserTypeRouteMatch: pathToRegexp(orgUserTypeRoutePath),
orgUserRouteMatch: pathToRegexp(orgUserRoutePath),
},
});
});
describe("next.config.js - RegExp", () => {
it("Booking Urls", async () => {
expect(userTypeRouteMatch("/free/30")?.params).toContain({
user: "free",
type: "30",
});
// Edgecase of username starting with team also works
expect(userTypeRouteMatch("/teampro/30")?.params).toContain({
user: "teampro",
type: "30",
});
// Edgecase of username starting with team also works
expect(userTypeRouteMatch("/workflowteam/30")?.params).toContain({
user: "workflowteam",
type: "30",
});
expect(userTypeRouteMatch("/teampro+pro/30")?.params).toContain({
user: "teampro+pro",
type: "30",
});
expect(userTypeRouteMatch("/teampro+pro/book")).toEqual(false);
// Because /book doesn't have a corresponding new-booker route.
expect(userTypeRouteMatch("/free/book")).toEqual(false);
// Because /booked is a normal event name
expect(userTypeRouteMatch("/free/booked")?.params).toEqual({
user: "free",
type: "booked",
});
expect(embedUserTypeRouteMatch("/free/30/embed")?.params).toContain({
user: "free",
type: "30",
});
// Edgecase of username starting with team also works
expect(embedUserTypeRouteMatch("/teampro/30/embed")?.params).toContain({
user: "teampro",
type: "30",
});
expect(teamTypeRouteMatch("/team/seeded/30")?.params).toContain({
slug: "seeded",
type: "30",
});
// Because /book doesn't have a corresponding new-booker route.
expect(teamTypeRouteMatch("/team/seeded/book")).toEqual(false);
expect(teamTypeRouteMatch("/team/seeded/30/embed")).toEqual(false);
expect(embedTeamTypeRouteMatch("/team/seeded/30/embed")?.params).toContain({
slug: "seeded",
type: "30",
});
expect(
privateLinkRouteMatch("/d/3v4s321CXRJZx5TFxkpPvd/30min")?.params
).toContain({
link: "3v4s321CXRJZx5TFxkpPvd",
slug: "30min",
});
expect(
privateLinkRouteMatch("/d/3v4s321CXRJZx5TFxkpPvd/30min")?.params
).toContain({
link: "3v4s321CXRJZx5TFxkpPvd",
slug: "30min",
});
// Because /book doesn't have a corresponding new-booker route.
expect(privateLinkRouteMatch("/d/3v4s321CXRJZx5TFxkpPvd/book")).toEqual(
false
);
});
it("Non booking Urls", () => {
expect(userTypeRouteMatch("/404/")).toEqual(false);
expect(teamTypeRouteMatch("/404/")).toEqual(false);
expect(userTypeRouteMatch("/404/30")).toEqual(false);
expect(teamTypeRouteMatch("/404/30")).toEqual(false);
expect(userTypeRouteMatch("/api")).toEqual(false);
expect(teamTypeRouteMatch("/api")).toEqual(false);
expect(userTypeRouteMatch("/api/30")).toEqual(false);
expect(teamTypeRouteMatch("/api/30")).toEqual(false);
expect(userTypeRouteMatch("/workflows/30")).toEqual(false);
expect(teamTypeRouteMatch("/workflows/30")).toEqual(false);
expect(userTypeRouteMatch("/event-types/30")).toEqual(false);
expect(teamTypeRouteMatch("/event-types/30")).toEqual(false);
expect(userTypeRouteMatch("/teams/1")).toEqual(false);
expect(teamTypeRouteMatch("/teams/1")).toEqual(false);
expect(userTypeRouteMatch("/teams")).toEqual(false);
expect(teamTypeRouteMatch("/teams")).toEqual(false);
// Note that even though it matches /embed/embed.js, but it's served from /public and the regexes are in afterEach, it won't hit the flow.
// expect(userTypeRouteRegExp('/embed/embed.js')).toEqual(false)
// expect(teamTypeRouteRegExp('/embed/embed.js')).toEqual(false)
});
});
describe("next.config.js - Org Rewrite", () => {
const orgHostRegExp = (subdomainRegExp: string) =>
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.

View File

@ -101,7 +101,7 @@ async function selectFirstAvailableTimeSlotNextMonth(frame: Frame, page: Page) {
await frame.click('[data-testid="time"]');
}
export async function bookFirstEvent(username: string, frame: Frame, page: Page, bookerVariant: string) {
export async function bookFirstEvent(username: string, frame: Frame, page: Page) {
// Click first event type on Profile Page
await frame.click('[data-testid="event-type-link"]');
await frame.waitForURL((url) => {
@ -125,11 +125,6 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page,
// Remove /embed from the end if present.
const eventSlug = new URL(frame.url()).pathname.replace(/\/embed$/, "");
await selectFirstAvailableTimeSlotNextMonth(frame, page);
if (bookerVariant !== "new-booker") {
await frame.waitForURL((url) => {
return url.pathname.includes(`/${username}/book`);
});
}
// expect(await page.screenshot()).toMatchSnapshot("booking-page.png");
// --- fill form
await frame.fill('[name="name"]', "Embed User");
@ -152,13 +147,8 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page,
return booking;
}
export async function rescheduleEvent(username: string, frame: Frame, page: Page, bookerVariant: string) {
export async function rescheduleEvent(username: string, frame: Frame, page: Page) {
await selectFirstAvailableTimeSlotNextMonth(frame, page);
if (bookerVariant !== "new-booker") {
await frame.waitForURL((url: { pathname: string | string[] }) => {
return url.pathname.includes(`/${username}/book`);
});
}
// --- fill form
await frame.press('[name="email"]', "Enter");
await frame.click("[data-testid=confirm-reschedule-button]");

View File

@ -3,7 +3,6 @@ import { expect } from "@playwright/test";
import { test } from "@calcom/web/playwright/lib/fixtures";
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { testBothBookers } from "@calcom/web/playwright/lib/new-booker";
import {
todo,
@ -18,12 +17,10 @@ async function bookFirstFreeUserEventThroughEmbed({
addEmbedListeners,
page,
getActionFiredDetails,
bookerVariant,
}: {
addEmbedListeners: Fixtures["addEmbedListeners"];
page: Page;
getActionFiredDetails: Fixtures["getActionFiredDetails"];
bookerVariant: string;
}) {
const embedButtonLocator = page.locator('[data-cal-link="free"]').first();
await page.goto("/");
@ -43,11 +40,11 @@ async function bookFirstFreeUserEventThroughEmbed({
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
const booking = await bookFirstEvent("free", embedIframe, page, bookerVariant);
const booking = await bookFirstEvent("free", embedIframe, page);
return booking;
}
testBothBookers.describe("Popup Tests", (bookerVariant) => {
test.describe("Popup Tests", () => {
test.afterEach(async () => {
await deleteAllBookingsByEmail("embed-user@example.com");
});
@ -76,7 +73,7 @@ testBothBookers.describe("Popup Tests", (bookerVariant) => {
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
const { uid: bookingId } = await bookFirstEvent("free", embedIframe, page, bookerVariant);
const { uid: bookingId } = await bookFirstEvent("free", embedIframe, page);
const booking = await getBooking(bookingId);
expect(booking.attendees.length).toBe(1);
@ -89,7 +86,6 @@ testBothBookers.describe("Popup Tests", (bookerVariant) => {
page,
addEmbedListeners,
getActionFiredDetails,
bookerVariant,
});
});
@ -102,7 +98,7 @@ testBothBookers.describe("Popup Tests", (bookerVariant) => {
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
await rescheduleEvent("free", embedIframe, page, bookerVariant);
await rescheduleEvent("free", embedIframe, page);
});
});
@ -126,7 +122,7 @@ testBothBookers.describe("Popup Tests", (bookerVariant) => {
throw new Error("Embed iframe not found");
}
const { uid: bookingId } = await bookFirstEvent("pro", embedIframe, page, bookerVariant);
const { uid: bookingId } = await bookFirstEvent("pro", embedIframe, page);
const booking = await getBooking(bookingId);
expect(booking.attendees.length).toBe(3);

View File

@ -1,11 +1,10 @@
import { expect } from "@playwright/test";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { testBothBookers } from "@calcom/web/playwright/lib/new-booker";
import { bookFirstEvent, deleteAllBookingsByEmail, getEmbedIframe, todo } from "../lib/testUtils";
testBothBookers.describe("Inline Iframe", (bookerVariant) => {
test.describe("Inline Iframe", () => {
test("Inline Iframe - Configured with Dark Theme", async ({
page,
getActionFiredDetails,
@ -26,7 +25,7 @@ testBothBookers.describe("Inline Iframe", (bookerVariant) => {
if (!embedIframe) {
throw new Error("Embed iframe not found");
}
await bookFirstEvent("pro", embedIframe, page, bookerVariant);
await bookFirstEvent("pro", embedIframe, page);
await deleteAllBookingsByEmail("embed-user@example.com");
});

View File

@ -1,9 +1,8 @@
import { expect } from "@playwright/test";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { testBothBookers } from "@calcom/web/playwright/lib/new-booker";
testBothBookers.describe("Preview", () => {
test.describe("Preview", () => {
test("Preview - embed-core should load", async ({ page }) => {
await page.goto("http://localhost:3000/embed/preview.html");
const libraryLoaded = await page.evaluate(() => {

View File

@ -2,9 +2,8 @@ import { expect } from "@playwright/test";
import { getEmbedIframe } from "@calcom/embed-core/playwright/lib/testUtils";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { testBothBookers } from "@calcom/web/playwright/lib/new-booker";
testBothBookers.describe("Inline Embed", () => {
test.describe("Inline Embed", () => {
test("should verify that the iframe got created with correct URL", async ({
page,
getActionFiredDetails,

View File

@ -94,7 +94,6 @@ const commons = {
parentId: null,
owner: null,
workflows: [],
parentId: null,
users: [user],
hosts: [],
metadata: EventTypeMetaDataSchema.parse({}),

View File

@ -214,8 +214,6 @@
"LARK_OPEN_VERIFICATION_TOKEN",
"MS_GRAPH_CLIENT_ID",
"MS_GRAPH_CLIENT_SECRET",
"NEW_BOOKER_ENABLED_FOR_EMBED",
"NEW_BOOKER_ENABLED_FOR_NON_EMBED",
"NEXT_PUBLIC_API_URL",
"NEXT_PUBLIC_APP_NAME",
"NEXT_PUBLIC_AUTH_URL",