diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts index d19febf2df..acd4a2918f 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_04_15/outputs/get-event-type-public.output.ts @@ -399,8 +399,8 @@ class PublicEventTypeOutput { workflows!: any[]; @IsArray() - @ApiProperty() - hosts!: any[]; + @ApiPropertyOptional() + hosts?: any[]; @ValidateNested() @Type(() => Owner) @@ -427,8 +427,8 @@ class PublicEventTypeOutput { @IsArray() @ValidateNested({ each: true }) @Type(() => User) - @ApiProperty({ type: [User] }) - users!: User[]; + @ApiPropertyOptional({ type: [User] }) + users?: User[]; @IsObject() @ApiProperty({ type: Object }) diff --git a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts index fd5d405460..6e705590ac 100644 --- a/apps/web/server/lib/[user]/[type]/getServerSideProps.ts +++ b/apps/web/server/lib/[user]/[type]/getServerSideProps.ts @@ -20,9 +20,9 @@ type Props = { eventData: Omit< Pick< NonNullable>>, - "id" | "length" | "metadata" | "entity" | "profile" | "title" | "users" | "hidden" + "id" | "length" | "metadata" | "entity" | "profile" | "title" | "subsetOfUsers" | "hidden" >, - "profile" | "users" + "profile" | "subsetOfUsers" > & { profile: { image: string | undefined; @@ -163,7 +163,10 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) { username: eventData.profile.username ?? null, }, title: eventData.title, - users: eventData.users.map((user) => ({ username: user.username ?? "", name: user.name ?? "" })), + users: eventData.subsetOfUsers.map((user) => ({ + username: user.username ?? "", + name: user.name ?? "", + })), hidden: eventData.hidden, }, user: usernames.join("+"), @@ -259,7 +262,10 @@ async function getUserPageProps(context: GetServerSidePropsContext) { username: eventData.profile.username ?? null, }, title: eventData.title, - users: eventData.users.map((user) => ({ username: user.username ?? "", name: user.name ?? "" })), + users: eventData.subsetOfUsers.map((user) => ({ + username: user.username ?? "", + name: user.name ?? "", + })), hidden: eventData.hidden, }, user: username, diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx index e1b3be2f5f..54d78ae62f 100644 --- a/packages/features/bookings/Booker/components/DatePicker.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -18,7 +18,7 @@ export const DatePicker = ({ scrollToTimeSlots, }: { event: { - data?: { users: Pick[] } | null; + data?: { subsetOfUsers: Pick[] } | null; }; schedule: useScheduleForEventReturnType; classNames?: { @@ -85,7 +85,7 @@ export const DatePicker = ({ locale={i18n.language} browsingDate={month ? dayjs(month) : undefined} selected={dayjs(selectedDate)} - weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)} + weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)} slots={schedule?.data?.slots} scrollToTimeSlots={scrollToTimeSlots} /> diff --git a/packages/features/bookings/Booker/components/EventMeta.tsx b/packages/features/bookings/Booker/components/EventMeta.tsx index 601317a104..4b3b97da55 100644 --- a/packages/features/bookings/Booker/components/EventMeta.tsx +++ b/packages/features/bookings/Booker/components/EventMeta.tsx @@ -55,7 +55,7 @@ export const EventMeta = ({ | "lockTimeZoneToggleOnBookingPage" | "schedule" | "seatsPerTimeSlot" - | "users" + | "subsetOfUsers" | "length" | "schedulingType" | "profile" @@ -150,7 +150,7 @@ export const EventMeta = ({ diff --git a/packages/features/bookings/Booker/components/InstantBooking.tsx b/packages/features/bookings/Booker/components/InstantBooking.tsx index 6ff912428a..08b84b0bce 100644 --- a/packages/features/bookings/Booker/components/InstantBooking.tsx +++ b/packages/features/bookings/Booker/components/InstantBooking.tsx @@ -6,7 +6,7 @@ import { Button, UserAvatarGroupWithOrg } from "@calcom/ui"; interface IInstantBookingProps { onConnectNow: () => void; event: Pick & { - users: (Pick & { bookerUrl: string })[]; + subsetOfUsers: (Pick & { bookerUrl: string })[]; }; } @@ -25,7 +25,7 @@ export const InstantBooking = ({ onConnectNow, event }: IInstantBookingProps) => name: event.entity.name || "", logoUrl: event.entity.logoUrl ?? null, }} - users={event.users.slice(0, 2)} + users={event.subsetOfUsers.slice(0, 2)} disableHref />
diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index cafb2c6740..bb4f6258c4 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -28,7 +28,7 @@ export interface IUseBookings { BookerEvent, | "id" | "slug" - | "hosts" + | "subsetOfHosts" | "requiresConfirmation" | "isDynamic" | "metadata" @@ -38,8 +38,8 @@ export interface IUseBookings { | "recurringEvent" | "schedulingType" > & { - users: Pick< - BookerEvent["users"][number], + subsetOfUsers: Pick< + BookerEvent["subsetOfUsers"][number], "name" | "username" | "avatarUrl" | "weekStart" | "profile" | "bookerUrl" >[]; }) @@ -187,9 +187,9 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb const { uid, paymentUid } = booking; const fullName = getFullName(bookingForm.getValues("responses.name")); - const users = !!event.data?.hosts?.length - ? event.data?.hosts.map((host) => host.user) - : event.data?.users; + const users = !!event.data?.subsetOfHosts?.length + ? event.data?.subsetOfHosts.map((host) => host.user) + : event.data?.subsetOfUsers; const validDuration = event.data?.isDynamic ? duration || event.data?.length diff --git a/packages/features/bookings/components/event-meta/Members.tsx b/packages/features/bookings/components/event-meta/Members.tsx index ad2c0207f1..1aecca0cb6 100644 --- a/packages/features/bookings/components/event-meta/Members.tsx +++ b/packages/features/bookings/components/event-meta/Members.tsx @@ -14,7 +14,7 @@ export interface EventMembersProps { * In case of Round Robin type, members aren't shown. */ schedulingType: BookerEvent["schedulingType"]; - users: BookerEvent["users"]; + users: BookerEvent["subsetOfUsers"]; profile: BookerEvent["profile"]; entity: BookerEvent["entity"]; } diff --git a/packages/features/bookings/types.ts b/packages/features/bookings/types.ts index f8c124bdca..86aed0cc98 100644 --- a/packages/features/bookings/types.ts +++ b/packages/features/bookings/types.ts @@ -15,7 +15,7 @@ export type BookerEventQuery = { }; type BookerEventUser = Pick< - PublicEvent["users"][number], + PublicEvent["subsetOfUsers"][number], "name" | "username" | "avatarUrl" | "weekStart" | "profile" > & { metadata?: undefined; @@ -47,14 +47,17 @@ export type BookerEvent = Pick< | "description" | "forwardParamsSuccessRedirect" | "successRedirectUrl" - | "hosts" + | "subsetOfHosts" | "bookingFields" | "seatsShowAvailabilityCount" | "isInstantEvent" | "instantMeetingParameters" | "fieldTranslations" | "autoTranslateDescriptionEnabled" -> & { users: BookerEventUser[]; showInstantEventConnectNowModal: boolean } & { profile: BookerEventProfile }; +> & { + subsetOfUsers: BookerEventUser[]; + showInstantEventConnectNowModal: boolean; +} & { profile: BookerEventProfile }; export type ValidationErrors = { key: FieldPath; error: ErrorOption }[]; diff --git a/packages/features/embed/Embed.tsx b/packages/features/embed/Embed.tsx index 136717b632..d447e7ee90 100644 --- a/packages/features/embed/Embed.tsx +++ b/packages/features/embed/Embed.tsx @@ -280,7 +280,7 @@ const EmailEmbed = ({ locale={i18n.language} browsingDate={month ? dayjs(month) : undefined} selected={dayjs(selectedDate)} - weekStart={weekdayToWeekIndex(event?.data?.users?.[0]?.weekStart)} + weekStart={weekdayToWeekIndex(event?.data?.subsetOfUsers?.[0]?.weekStart)} eventSlug={eventType?.slug} /> diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index d3fa3c5f92..40dec3be52 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -50,98 +50,100 @@ const userSelect = Prisma.validator()({ defaultScheduleId: true, }); -const publicEventSelect = Prisma.validator()({ - id: true, - title: true, - description: true, - eventName: true, - slug: true, - isInstantEvent: true, - instantMeetingParameters: true, - aiPhoneCallConfig: true, - schedulingType: true, - length: true, - locations: true, - customInputs: true, - disableGuests: true, - metadata: true, - lockTimeZoneToggleOnBookingPage: true, - requiresConfirmation: true, - autoTranslateDescriptionEnabled: true, - fieldTranslations: { - select: { - translatedText: true, - targetLocale: true, - field: true, - }, - }, - requiresBookerEmailVerification: true, - recurringEvent: true, - price: true, - currency: true, - seatsPerTimeSlot: true, - seatsShowAvailabilityCount: true, - bookingFields: true, - teamId: true, - team: { - select: { - parentId: true, - metadata: true, - brandColor: true, - darkBrandColor: true, - slug: true, - name: true, - logoUrl: true, - theme: true, - parent: { - select: { - slug: true, - name: true, - bannerUrl: true, - logoUrl: true, - }, +const getPublicEventSelect = (fetchAllUsers: boolean) => { + return Prisma.validator()({ + id: true, + title: true, + description: true, + eventName: true, + slug: true, + isInstantEvent: true, + instantMeetingParameters: true, + aiPhoneCallConfig: true, + schedulingType: true, + length: true, + locations: true, + customInputs: true, + disableGuests: true, + metadata: true, + lockTimeZoneToggleOnBookingPage: true, + requiresConfirmation: true, + autoTranslateDescriptionEnabled: true, + fieldTranslations: { + select: { + translatedText: true, + targetLocale: true, + field: true, }, - isPrivate: true, }, - }, - successRedirectUrl: true, - forwardParamsSuccessRedirect: true, - workflows: { - include: { - workflow: { - include: { - steps: true, + requiresBookerEmailVerification: true, + recurringEvent: true, + price: true, + currency: true, + seatsPerTimeSlot: true, + seatsShowAvailabilityCount: true, + bookingFields: true, + teamId: true, + team: { + select: { + parentId: true, + metadata: true, + brandColor: true, + darkBrandColor: true, + slug: true, + name: true, + logoUrl: true, + theme: true, + parent: { + select: { + slug: true, + name: true, + bannerUrl: true, + logoUrl: true, + }, + }, + isPrivate: true, + }, + }, + successRedirectUrl: true, + forwardParamsSuccessRedirect: true, + workflows: { + include: { + workflow: { + include: { + steps: true, + }, }, }, }, - }, - hosts: { - select: { - user: { - select: userSelect, + hosts: { + select: { + user: { + select: userSelect, + }, + }, + ...(fetchAllUsers ? {} : { take: 3 }), + }, + owner: { + select: userSelect, + }, + schedule: { + select: { + id: true, + timeZone: true, }, }, - }, - owner: { - select: userSelect, - }, - schedule: { - select: { - id: true, - timeZone: true, + instantMeetingSchedule: { + select: { + id: true, + timeZone: true, + }, }, - }, - instantMeetingSchedule: { - select: { - id: true, - timeZone: true, - }, - }, - - hidden: true, - assignAllTeamMembers: true, - rescheduleWithSameRoundRobinHost: true, -}); + hidden: true, + assignAllTeamMembers: true, + rescheduleWithSameRoundRobinHost: true, + }); +}; export async function isCurrentlyAvailable({ prisma, @@ -217,7 +219,8 @@ export const getPublicEvent = async ( org: string | null, prisma: PrismaClient, fromRedirectOfNonOrgLink: boolean, - currentUserId?: number + currentUserId?: number, + fetchAllUsers = false ) => { const usernameList = getUsernameList(username); const orgQuery = org ? getSlugOrRequestedSlug(org) : null; @@ -270,11 +273,18 @@ export const getPublicEvent = async ( ...defaultEvent, bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }), // Clears meta data since we don't want to send this in the public api. - users: users.map((user) => ({ + subsetOfUsers: users.map((user) => ({ ...user, metadata: undefined, bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null), })), + users: fetchAllUsers + ? users.map((user) => ({ + ...user, + metadata: undefined, + bookerUrl: getBookerBaseUrlSync(user.profile?.organization?.slug ?? null), + })) + : undefined, locations: privacyFilteredLocations(locations), profile: { weekStart: users[0].weekStart, @@ -342,7 +352,7 @@ export const getPublicEvent = async ( slug: eventSlug, ...usersOrTeamQuery, }, - select: publicEventSelect, + select: getPublicEventSelect(fetchAllUsers), }); // If no event was found, check for platform org user event @@ -362,7 +372,7 @@ export const getPublicEvent = async ( }, }, }, - select: publicEventSelect, + select: getPublicEventSelect(fetchAllUsers), }); } @@ -388,7 +398,8 @@ export const getPublicEvent = async ( user: event.owner, }) : null, - hosts: hosts, + subsetOfHosts: hosts, + hosts: fetchAllUsers ? hosts : undefined, }; let users = @@ -471,7 +482,8 @@ export const getPublicEvent = async ( : null, // Sets user data on profile object for easier access profile: getProfileFromEvent(eventWithUserProfiles), - users, + subsetOfUsers: users, + users: fetchAllUsers ? users : undefined, entity: { fromRedirectOfNonOrgLink, considerUnpublished: @@ -493,7 +505,6 @@ export const getPublicEvent = async ( } : {}), }, - isDynamic: false, isInstantEvent: eventWithUserProfiles.isInstantEvent, showInstantEventConnectNowModal, @@ -504,13 +515,18 @@ export const getPublicEvent = async ( }; const eventData = Prisma.validator()({ - select: publicEventSelect, + select: getPublicEventSelect(true), }); type Event = Prisma.EventTypeGetPayload; -function getProfileFromEvent(event: Event) { - const { team, hosts, owner } = event; +type GetProfileFromEventInput = Omit & { + hosts?: Event["hosts"]; + subsetOfHosts: Event["hosts"]; +}; + +function getProfileFromEvent(event: GetProfileFromEventInput) { + const { team, subsetOfHosts: hosts, owner } = event; const nonTeamprofile = hosts?.[0]?.user || owner; const profile = team || nonTeamprofile; if (!profile) throw new Error("Event has no owner"); @@ -538,6 +554,7 @@ function getProfileFromEvent(event: Event) { ), }; } + async function getUsersFromEvent( event: Omit & { owner: @@ -545,7 +562,12 @@ async function getUsersFromEvent( profile: UserProfile; }) | null; - hosts: (Omit & { + hosts?: (Omit & { + user: Event["hosts"][number]["user"] & { + profile: UserProfile; + }; + })[]; + subsetOfHosts: (Omit & { user: Event["hosts"][number]["user"] & { profile: UserProfile; }; @@ -553,11 +575,12 @@ async function getUsersFromEvent( }, prisma: PrismaClient ) { - const { team, hosts, owner, id } = event; + const { team, hosts, subsetOfHosts, owner, id } = event; if (team) { + const eventHosts = !!hosts?.length ? hosts : subsetOfHosts; // getOwnerFromUsersArray is used here for backward compatibility when team event type has users[] but not hosts[] - return hosts.length - ? hosts.filter((host) => host.user.username).map(mapHostsToUsers) + return eventHosts.length + ? eventHosts.filter((host) => host.user.username).map(mapHostsToUsers) : (await getOwnerFromUsersArray(prisma, id)) ?? []; } if (!owner) { diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 9aeddef0b3..93b55cd67b 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -109,6 +109,7 @@ const commons = { workflows: [], users: [user], hosts: [], + subsetOfHosts: [], metadata: EventTypeMetaDataSchema.parse({}), bookingFields: [], assignAllTeamMembers: false, diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index 46a0fa0edd..8ec701941c 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -31,7 +31,9 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); - const event = useEvent({ fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink }); + const event = useEvent({ + fromRedirectOfNonOrgLink: props.entity.fromRedirectOfNonOrgLink, + }); const bookerLayout = useBookerLayout(event.data); const selectedDate = searchParams?.get("date");