diff --git a/apps/web/components/booking/AvailableTimes.tsx b/apps/web/components/booking/AvailableTimes.tsx index f64672627c..08dbac1cc2 100644 --- a/apps/web/components/booking/AvailableTimes.tsx +++ b/apps/web/components/booking/AvailableTimes.tsx @@ -20,6 +20,7 @@ type AvailableTimesProps = { afterBufferTime: number; eventTypeId: number; eventLength: number; + eventTypeSlug: string; slotInterval: number | null; date: Dayjs; users: { @@ -32,6 +33,7 @@ const AvailableTimes: FC = ({ date, eventLength, eventTypeId, + eventTypeSlug, slotInterval, minimumBookingNotice, timeFormat, @@ -86,6 +88,7 @@ const AvailableTimes: FC = ({ ...router.query, date: slot.time.format(), type: eventTypeId, + slug: eventTypeSlug, }, }; diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index b927eb3cc4..7205db3369 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -260,6 +260,7 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage timeFormat={timeFormat} minimumBookingNotice={eventType.minimumBookingNotice} eventTypeId={eventType.id} + eventTypeSlug={eventType.slug} slotInterval={eventType.slotInterval} eventLength={eventType.length} date={selectedDate} diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 08557ace73..fc0dda4ba5 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -12,6 +12,7 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { createPaymentLink } from "@calcom/stripe/client"; import { Button } from "@calcom/ui/Button"; @@ -20,7 +21,6 @@ import { EmailInput, Form } from "@calcom/ui/form/fields"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; -import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; @@ -55,7 +55,13 @@ type BookingFormValues = { }; }; -const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPageProps) => { +const BookingPage = ({ + eventType, + booking, + profile, + isDynamicGroupBooking, + locationLabels, +}: BookingPageProps) => { const { t, i18n } = useLocale(); const router = useRouter(); const { contracts } = useContracts(); @@ -99,6 +105,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag query: { date, type: eventType.id, + eventSlug: eventType.slug, user: profile.slug, reschedule: !!rescheduleUid, name: attendees[0].name, @@ -160,7 +167,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag return { name: primaryAttendee.name || "", email: primaryAttendee.email || "", - guests: booking.attendees.slice(1).map((attendee) => attendee.email), + guests: !isDynamicGroupBooking ? booking.attendees.slice(1).map((attendee) => attendee.email) : [], }; }; @@ -241,6 +248,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag start: dayjs(date).format(), end: dayjs(date).add(eventType.length, "minute").format(), eventTypeId: eventType.id, + eventTypeSlug: eventType.slug, timeZone: timeZone(), language: i18n.language, rescheduleUid, diff --git a/apps/web/lib/types/booking.ts b/apps/web/lib/types/booking.ts index 165c061e98..061226ea2c 100644 --- a/apps/web/lib/types/booking.ts +++ b/apps/web/lib/types/booking.ts @@ -13,6 +13,7 @@ export type BookingCreateBody = { userSignature: unknown; }; eventTypeId: number; + eventTypeSlug: string; guests?: string[]; location: string; name: string; diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index 912f725ed2..6c9cb35ee0 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -1,5 +1,6 @@ import { ArrowRightIcon } from "@heroicons/react/outline"; import { BadgeCheckIcon } from "@heroicons/react/solid"; +import { UserPlan } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; import dynamic from "next/dynamic"; import Link from "next/link"; @@ -9,6 +10,11 @@ import { Toaster } from "react-hot-toast"; import { JSONObject } from "superjson/dist/types"; import { sdkActionManager, useEmbedStyles } from "@calcom/embed-core"; +import defaultEvents, { + getDynamicEventDescription, + getUsernameList, + getUsernameSlugLink, +} from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; @@ -16,6 +22,7 @@ import useTheme from "@lib/hooks/useTheme"; import prisma from "@lib/prisma"; import { inferSSRProps } from "@lib/types/inferSSRProps"; +import AvatarGroup from "@components/ui/AvatarGroup"; import { AvatarSSR } from "@components/ui/AvatarSSR"; import { ssrInit } from "@server/lib/ssr"; @@ -29,10 +36,66 @@ interface EvtsToVerify { } export default function User(props: inferSSRProps) { - const { Theme } = useTheme(props.user.theme); - const { user, eventTypes } = props; + const { users } = props; + const [user] = users; //To be used when we only have a single user, not dynamic group + const { Theme } = useTheme(user.theme); const { t } = useLocale(); const router = useRouter(); + const isSingleUser = props.users.length === 1; + const isDynamicGroup = props.users.length > 1; + const dynamicUsernames = isDynamicGroup + ? props.users.map((user) => { + return user.username || ""; + }) + : []; + const eventTypes = isDynamicGroup + ? defaultEvents.map((event) => { + event.description = getDynamicEventDescription(dynamicUsernames, event.slug); + return event; + }) + : props.eventTypes; + const groupEventTypes = props.users.some((user) => { + return !user.allowDynamicBooking; + }) ? ( +
+
+
+

{" " + t("unavailable")}

+

{t("user_dynamic_booking_disabled")}

+
+
+
+ ) : ( + + ); const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem"); const query = { ...router.query }; delete query.user; // So it doesn't display in the Link (and make tests fail) @@ -51,16 +114,18 @@ export default function User(props: inferSSRProps) { />
-
- -

- {nameOrUsername} - {user.verified && ( - - )} -

-

{user.bio}

-
+ {isSingleUser && ( // When we deal with a single user, not dynamic group +
+ +

+ {nameOrUsername} + {user.verified && ( + + )} +

+

{user.bio}

+
+ )}
{user.away ? (
@@ -71,6 +136,8 @@ export default function User(props: inferSSRProps) {

{t("user_away_description")}

+ ) : isDynamicGroup ? ( //When we deal with dynamic group (users > 1) + groupEventTypes ) : ( eventTypes.map((type) => (
) { ); } -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const ssr = await ssrInit(context); - const crypto = require("crypto"); - - const username = (context.query.user as string).toLowerCase(); - const dataFetchStart = Date.now(); - const user = await prisma.user.findUnique({ - where: { - username: username.toLowerCase(), - }, - select: { - id: true, - username: true, - email: true, - name: true, - bio: true, - avatar: true, - theme: true, - plan: true, - away: true, - verified: true, - }, - }); - - if (!user) { - return { - notFound: true, - }; - } - - const credentials = await prisma.credential.findMany({ - where: { - userId: user.id, - }, - select: { - id: true, - type: true, - key: true, - }, - }); - - const web3Credentials = credentials.find((credential) => credential.type.includes("_web3")); - - const eventTypesWithHidden = await prisma.eventType.findMany({ +const getEventTypesWithHiddenFromDB = async (userId: number, plan: UserPlan) => { + return await prisma.eventType.findMany({ where: { AND: [ { @@ -188,12 +213,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => { OR: [ { - userId: user.id, + userId, }, { users: { some: { - id: user.id, + id: userId, }, }, }, @@ -221,8 +246,63 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => currency: true, metadata: true, }, - take: user.plan === "FREE" ? 1 : undefined, + take: plan === UserPlan.FREE ? 1 : undefined, }); +}; + +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const ssr = await ssrInit(context); + const crypto = require("crypto"); + + const usernameList = getUsernameList(context.query.user as string); + const dataFetchStart = Date.now(); + const users = await prisma.user.findMany({ + where: { + username: { + in: usernameList, + }, + }, + select: { + id: true, + username: true, + email: true, + name: true, + bio: true, + avatar: true, + theme: true, + plan: true, + away: true, + verified: true, + allowDynamicBooking: true, + }, + }); + + if (!users.length) { + return { + notFound: true, + }; + } + + const isDynamicGroup = users.length > 1; + + const [user] = users; //to be used when dealing with single user, not dynamic group + const usersIds = users.map((user) => user.id); + const credentials = await prisma.credential.findMany({ + where: { + userId: { + in: usersIds, + }, + }, + select: { + id: true, + type: true, + key: true, + }, + }); + + const web3Credentials = credentials.find((credential) => credential.type.includes("_web3")); + + const eventTypesWithHidden = isDynamicGroup ? [] : await getEventTypesWithHiddenFromDB(user.id, user.plan); const dataFetchEnd = Date.now(); if (context.query.log === "1") { context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`); @@ -240,8 +320,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => return { props: { + users, user: { - ...user, emailMd5: crypto.createHash("md5").update(user.email).digest("hex"), }, eventTypes, diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 9748dfb6c1..238a4220fa 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -1,7 +1,11 @@ import { Prisma } from "@prisma/client"; +import { UserPlan } from "@prisma/client"; import { GetServerSidePropsContext } from "next"; import { JSONObject } from "superjson/dist/types"; +import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + import { asStringOrNull } from "@lib/asStringOrNull"; import { getWorkingHours } from "@lib/availability"; import prisma from "@lib/prisma"; @@ -14,13 +18,33 @@ import { ssrInit } from "@server/lib/ssr"; export type AvailabilityPageProps = inferSSRProps; export default function Type(props: AvailabilityPageProps) { - return ; + const { t } = useLocale(); + return props.isDynamicGroup && !props.profile.allowDynamicBooking ? ( +
+
+
+
+
+

+ {" " + t("unavailable")} +

+

{t("user_dynamic_booking_disabled")}

+
+
+
+
+
+ ) : ( + + ); } export const getServerSideProps = async (context: GetServerSidePropsContext) => { const ssr = await ssrInit(context); // get query params and typecast them to string // (would be even better to assert them instead of typecasting) + const usernameList = getUsernameList(context.query.user as string); + const userParam = asStringOrNull(context.query.user); const typeParam = asStringOrNull(context.query.type); const dateParam = asStringOrNull(context.query.date); @@ -49,6 +73,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => timeZone: true, }, }, + hidden: true, + slug: true, minimumBookingNotice: true, beforeEventBuffer: true, afterEventBuffer: true, @@ -67,9 +93,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }, }); - const user = await prisma.user.findUnique({ + const users = await prisma.user.findMany({ where: { - username: userParam.toLowerCase(), + username: { + in: usernameList, + }, }, select: { id: true, @@ -87,6 +115,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => brandColor: true, darkBrandColor: true, defaultScheduleId: true, + allowDynamicBooking: true, schedules: { select: { availability: true, @@ -112,13 +141,16 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }, }); - if (!user) { + if (!users) { return { notFound: true, }; } + const [user] = users; //to be used when dealing with single user, not dynamic group + const isSingleUser = users.length === 1; + const isDynamicGroup = users.length > 1; - if (user.eventTypes.length !== 1) { + if (isSingleUser && user.eventTypes.length !== 1) { const eventTypeBackwardsCompat = await prisma.eventType.findFirst({ where: { AND: [ @@ -150,10 +182,24 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => user.eventTypes.push(eventTypeBackwardsCompat); } - const [eventType] = user.eventTypes; + let [eventType] = user.eventTypes; - // check this is the first event - if (user.plan === "FREE") { + if (isDynamicGroup) { + eventType = getDefaultEvent(typeParam); + eventType["users"] = users.map((user) => { + return { + avatar: user.avatar as string, + name: user.name as string, + username: user.username as string, + hideBranding: user.hideBranding, + plan: user.plan, + timeZone: user.timeZone as string, + }; + }); + } + + // check this is the first event for free user + if (isSingleUser && user.plan === UserPlan.FREE) { const firstEventType = await prisma.eventType.findFirst({ where: { OR: [ @@ -202,21 +248,35 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => )[0], }; - const timeZone = schedule.timeZone || eventType.timeZone || user.timeZone; + const timeZone = isDynamicGroup ? undefined : schedule.timeZone || eventType.timeZone || user.timeZone; const workingHours = getWorkingHours( { timeZone, }, - schedule.availability || (eventType.availability.length ? eventType.availability : user.availability) + isDynamicGroup + ? eventType.availability || undefined + : schedule.availability || (eventType.availability.length ? eventType.availability : user.availability) ); - eventTypeObject.schedule = null; eventTypeObject.availability = []; - return { - props: { - profile: { + const profile = isDynamicGroup + ? { + name: getGroupName(usernameList), + image: null, + slug: typeParam, + theme: null, + weekStart: "Sunday", + brandColor: "", + darkBrandColor: "", + allowDynamicBooking: users.some((user) => { + return !user.allowDynamicBooking; + }) + ? false + : true, + } + : { name: user.name || user.username, image: user.avatar, slug: user.username, @@ -224,7 +284,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => weekStart: user.weekStart, brandColor: user.brandColor, darkBrandColor: user.darkBrandColor, - }, + }; + + return { + props: { + isDynamicGroup, + profile, plan: user.plan, date: dateParam, eventType: eventTypeObject, diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index 3e8bd5b6f7..e05cd1ca30 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -6,6 +6,8 @@ import { GetServerSidePropsContext } from "next"; import { JSONObject } from "superjson/dist/types"; import { getLocationLabels } from "@calcom/app-store/utils"; +import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { asStringOrThrow } from "@lib/asStringOrNull"; import prisma from "@lib/prisma"; @@ -22,14 +24,36 @@ dayjs.extend(timezone); export type BookPageProps = inferSSRProps; export default function Book(props: BookPageProps) { - return ; + const { t } = useLocale(); + return props.isDynamicGroupBooking && !props.profile.allowDynamicBooking ? ( +
+
+
+
+
+

+ {" " + t("unavailable")} +

+

{t("user_dynamic_booking_disabled")}

+
+
+
+
+
+ ) : ( + + ); } export async function getServerSideProps(context: GetServerSidePropsContext) { const ssr = await ssrInit(context); - const user = await prisma.user.findUnique({ + const usernameList = getUsernameList(asStringOrThrow(context.query.user as string)); + const eventTypeSlug = context.query.slug as string; + const users = await prisma.user.findMany({ where: { - username: asStringOrThrow(context.query.user).toLowerCase(), + username: { + in: usernameList, + }, }, select: { id: true, @@ -41,50 +65,56 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { theme: true, brandColor: true, darkBrandColor: true, + allowDynamicBooking: true, }, }); - if (!user) return { notFound: true }; - - const eventTypeRaw = await prisma.eventType.findUnique({ - where: { - id: parseInt(asStringOrThrow(context.query.type)), - }, - select: { - id: true, - title: true, - slug: true, - description: true, - length: true, - locations: true, - customInputs: true, - periodType: true, - periodDays: true, - periodStartDate: true, - periodEndDate: true, - metadata: true, - periodCountCalendarDays: true, - price: true, - currency: true, - disableGuests: true, - users: { - select: { - username: true, - name: true, - email: true, - bio: true, - avatar: true, - theme: true, - }, - }, - }, - }); + if (!users.length) return { notFound: true }; + const [user] = users; + const eventTypeRaw = + usernameList.length > 1 + ? getDefaultEvent(eventTypeSlug) + : await prisma.eventType.findUnique({ + where: { + id: parseInt(asStringOrThrow(context.query.type)), + }, + select: { + id: true, + title: true, + slug: true, + description: true, + length: true, + locations: true, + customInputs: true, + periodType: true, + periodDays: true, + periodStartDate: true, + periodEndDate: true, + metadata: true, + periodCountCalendarDays: true, + price: true, + currency: true, + disableGuests: true, + users: { + select: { + username: true, + name: true, + email: true, + bio: true, + avatar: true, + theme: true, + }, + }, + }, + }); if (!eventTypeRaw) return { notFound: true }; const credentials = await prisma.credential.findMany({ where: { - userId: user.id, + userId: { + in: users.map((user) => user.id), + }, }, select: { id: true, @@ -136,22 +166,41 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { booking = await getBooking(); } + const isDynamicGroupBooking = users.length > 1; + + const profile = isDynamicGroupBooking + ? { + name: getGroupName(usernameList), + image: null, + slug: eventTypeSlug, + theme: null, + brandColor: "", + darkBrandColor: "", + allowDynamicBooking: users.some((user) => { + return !user.allowDynamicBooking; + }) + ? false + : true, + } + : { + name: user.name || user.username, + image: user.avatar, + slug: user.username, + theme: user.theme, + brandColor: user.brandColor, + darkBrandColor: user.darkBrandColor, + }; + const t = await getTranslation(context.locale ?? "en", "common"); return { props: { locationLabels: getLocationLabels(t), - profile: { - slug: user.username, - name: user.name, - image: user.avatar, - theme: user.theme, - brandColor: user.brandColor, - darkBrandColor: user.darkBrandColor, - }, + profile, eventType: eventTypeObject, booking, trpcState: ssr.dehydrate(), + isDynamicGroupBooking, }, }; } diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index ebf85cad8e..eed093b996 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -12,6 +12,7 @@ import { v5 as uuidv5 } from "uuid"; import { getBusyCalendarTimes } from "@calcom/core/CalendarManager"; import EventManager from "@calcom/core/EventManager"; import { getBusyVideoTimes } from "@calcom/core/videoClient"; +import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import logger from "@calcom/lib/logger"; import notEmpty from "@calcom/lib/notEmpty"; @@ -181,30 +182,8 @@ const getUserNameWithBookingCounts = async (eventTypeId: number, selectedUserNam return userNamesWithBookingCounts; }; -type User = Prisma.UserGetPayload; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const reqBody = req.body as BookingCreateBody; - const eventTypeId = reqBody.eventTypeId; - const tAttendees = await getTranslation(reqBody.language ?? "en", "common"); - const tGuests = await getTranslation("en", "common"); - log.debug(`Booking eventType ${eventTypeId} started`); - - const isTimeInPast = (time: string): boolean => { - return dayjs(time).isBefore(new Date(), "day"); - }; - - if (isTimeInPast(reqBody.start)) { - const error = { - errorCode: "BookingDateInPast", - message: "Attempting to create a meeting in the past.", - }; - - log.error(`Booking ${eventTypeId} failed`, error); - return res.status(400).json(error); - } - - const eventType = await prisma.eventType.findUnique({ +const getEventTypesFromDB = async (eventTypeId: number) => { + return await prisma.eventType.findUnique({ rejectOnNotFound: true, where: { id: eventTypeId, @@ -235,10 +214,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) hideCalendarNotes: true, }, }); +}; +type User = Prisma.UserGetPayload; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const reqBody = req.body as BookingCreateBody; + + // handle dynamic user + const dynamicUserList = getUsernameList(reqBody.user as string); + const eventTypeSlug = reqBody.eventTypeSlug; + const eventTypeId = reqBody.eventTypeId; + const tAttendees = await getTranslation(reqBody.language ?? "en", "common"); + const tGuests = await getTranslation("en", "common"); + log.debug(`Booking eventType ${eventTypeId} started`); + + const isTimeInPast = (time: string): boolean => { + return dayjs(time).isBefore(new Date(), "day"); + }; + + if (isTimeInPast(reqBody.start)) { + const error = { + errorCode: "BookingDateInPast", + message: "Attempting to create a meeting in the past.", + }; + + log.error(`Booking ${eventTypeId} failed`, error); + return res.status(400).json(error); + } + + const eventType = !eventTypeId ? getDefaultEvent(eventTypeSlug) : await getEventTypesFromDB(eventTypeId); if (!eventType) return res.status(404).json({ message: "eventType.notFound" }); - let users = eventType.users; + let users = !eventTypeId + ? await prisma.user.findMany({ + where: { + username: { + in: dynamicUserList, + }, + }, + ...userSelect, + }) + : eventType.users; /* If this event was pre-relationship migration */ if (!users.length && eventType.userId) { @@ -340,7 +357,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, attendees: attendeesList, location: reqBody.location, // Will be processed by the EventManager later. - /** For team events, we will need to handle each member destinationCalendar eventually */ + /** For team events & dynamic collective events, we will need to handle each member destinationCalendar eventually */ destinationCalendar: eventType.destinationCalendar || users[0].destinationCalendar, hideCalendarNotes: eventType.hideCalendarNotes, }; @@ -362,6 +379,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) await verifyAccount(web3Details.userSignature, web3Details.userWallet); } + const eventTypeRel = !eventTypeId + ? {} + : { + connect: { + id: eventTypeId, + }, + }; + + const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null; + const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null; + return prisma.booking.create({ include: { user: { @@ -377,11 +405,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) description: evt.description, confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid, location: evt.location, - eventType: { - connect: { - id: eventTypeId, - }, - }, + eventType: eventTypeRel, attendees: { createMany: { data: evt.attendees.map((attendee) => { @@ -397,6 +421,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }), }, }, + dynamicEventSlugRef, + dynamicGroupSlugRef, user: { connect: { id: users[0].id, diff --git a/apps/web/pages/reschedule/[uid].tsx b/apps/web/pages/reschedule/[uid].tsx index 27eca3ada1..8057376091 100644 --- a/apps/web/pages/reschedule/[uid].tsx +++ b/apps/web/pages/reschedule/[uid].tsx @@ -1,5 +1,7 @@ import { GetServerSidePropsContext } from "next"; +import { getDefaultEvent } from "@calcom/lib/defaultEvents"; + import { asStringOrUndefined } from "@lib/asStringOrNull"; import prisma from "@lib/prisma"; @@ -30,6 +32,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, }, }, + dynamicEventSlugRef: true, + dynamicGroupSlugRef: true, user: true, title: true, description: true, @@ -38,17 +42,19 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { attendees: true, }, }); + const dynamicEventSlugRef = booking?.dynamicEventSlugRef || ""; + if (!booking?.eventType && !booking?.dynamicEventSlugRef) throw Error("This booking doesn't exists"); - if (!booking?.eventType) throw Error("This booking doesn't exists"); - - const eventType = booking.eventType; + const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef); const eventPage = (eventType.team ? "team/" + eventType.team.slug + : dynamicEventSlugRef + ? booking.dynamicGroupSlugRef : booking.user?.username || "rick") /* This shouldn't happen */ + "/" + - booking.eventType.slug; + eventType?.slug; return { redirect: { diff --git a/apps/web/pages/settings/profile.tsx b/apps/web/pages/settings/profile.tsx index 9b90cb4b03..9754222a91 100644 --- a/apps/web/pages/settings/profile.tsx +++ b/apps/web/pages/settings/profile.tsx @@ -29,6 +29,7 @@ import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; import Avatar from "@components/ui/Avatar"; import Badge from "@components/ui/Badge"; +import InfoBadge from "@components/ui/InfoBadge"; import ColorPicker from "@components/ui/colorpicker"; import { UpgradeToProDialog } from "../../components/UpgradeToProDialog"; @@ -126,6 +127,7 @@ function SettingsView(props: ComponentProps & { localeProp: str const descriptionRef = useRef(null!); const avatarRef = useRef(null!); const hideBrandingRef = useRef(null!); + const allowDynamicGroupBookingRef = useRef(null!); const [selectedTheme, setSelectedTheme] = useState(); const [selectedTimeFormat, setSelectedTimeFormat] = useState({ value: props.user.timeFormat || 12, @@ -168,6 +170,7 @@ function SettingsView(props: ComponentProps & { localeProp: str const enteredTimeZone = typeof selectedTimeZone === "string" ? selectedTimeZone : selectedTimeZone.value; const enteredWeekStartDay = selectedWeekStartDay.value; const enteredHideBranding = hideBrandingRef.current.checked; + const enteredAllowDynamicGroupBooking = allowDynamicGroupBookingRef.current.checked; const enteredLanguage = selectedLanguage.value; const enteredTimeFormat = selectedTimeFormat.value; @@ -182,6 +185,7 @@ function SettingsView(props: ComponentProps & { localeProp: str timeZone: enteredTimeZone, weekStart: asStringOrUndefined(enteredWeekStartDay), hideBranding: enteredHideBranding, + allowDynamicBooking: enteredAllowDynamicGroupBooking, theme: asStringOrNull(selectedTheme?.value), brandColor: enteredBrandColor, darkBrandColor: enteredDarkBrandColor, @@ -363,6 +367,25 @@ function SettingsView(props: ComponentProps & { localeProp: str />
+
+
+ +
+
+ +
+