From ea34545d721a06593cca2cf837635f9a36843bf9 Mon Sep 17 00:00:00 2001 From: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Date: Wed, 15 Feb 2023 15:42:49 +0000 Subject: [PATCH] Handle booking flow for dynamic default events (#7094) * Handle booking flow for default events * Cleanup * Update packages/lib/defaultEvents.ts * Fix naming conventions * Fix prettier error --------- Co-authored-by: zomars --- apps/web/pages/[user].tsx | 9 +++++ apps/web/pages/[user]/[type].tsx | 35 +++++++++++++++- apps/web/pages/[user]/book.tsx | 40 ++++++++++++++++++- packages/app-store/utils.ts | 4 ++ .../features/bookings/lib/handleNewBooking.ts | 30 +++++++++++--- 5 files changed, 109 insertions(+), 9 deletions(-) diff --git a/apps/web/pages/[user].tsx b/apps/web/pages/[user].tsx index d49b012828..cbeb12b59c 100644 --- a/apps/web/pages/[user].tsx +++ b/apps/web/pages/[user].tsx @@ -291,6 +291,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => } const isDynamicGroup = users.length > 1; + if (isDynamicGroup) { + // sort and be in the same order as usernameList so first user is the first user in the list + 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; + }); + } + const dynamicNames = isDynamicGroup ? users.map((user) => { return user.name || ""; diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 8175f7021f..998f4d0967 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -3,13 +3,18 @@ import { GetStaticPaths, GetStaticPropsContext } from "next"; import { z } from "zod"; import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations"; +import { getAppFromSlug } from "@calcom/app-store/utils"; import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; import prisma from "@calcom/prisma"; import { User } from "@calcom/prisma/client"; -import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { + EventTypeMetaDataSchema, + teamMetadataSchema, + userMetadata as userMetadataSchema, +} from "@calcom/prisma/zod-utils"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -215,6 +220,7 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) { darkBrandColor: true, defaultScheduleId: true, allowDynamicBooking: true, + metadata: true, away: true, schedules: { select: { @@ -233,7 +239,32 @@ async function getDynamicGroupPageProps(context: GetStaticPropsContext) { }; } - const locations = eventType.locations ? (eventType.locations as LocationObject[]) : []; + // 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 locations = eventType.locations ? (eventType.locations as LocationObject[]) : []; + + // 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[]; + } + } + const eventTypeObject = Object.assign({}, eventType, { metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}), recurringEvent: parseRecurringEvent(eventType.recurringEvent), diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index cd52165c8c..81e24e6387 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -1,6 +1,7 @@ import { GetServerSidePropsContext } from "next"; import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations"; +import { getAppFromSlug } from "@calcom/app-store/utils"; import { parseRecurringEvent } from "@calcom/lib"; import { getDefaultEvent, @@ -11,7 +12,11 @@ import { import { useLocale } from "@calcom/lib/hooks/useLocale"; import { bookEventTypeSelect } from "@calcom/prisma"; import prisma from "@calcom/prisma"; -import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; +import { + customInputSchema, + EventTypeMetaDataSchema, + userMetadata as userMetadataSchema, +} from "@calcom/prisma/zod-utils"; import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull"; import getBooking, { GetBookingType } from "@lib/getBooking"; @@ -85,6 +90,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { darkBrandColor: true, allowDynamicBooking: true, away: true, + metadata: true, }, }); @@ -117,8 +123,38 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), }; - const eventTypeObject = [eventType].map((e) => { + 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, diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 3125a677d7..2e5de4e8db 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -188,6 +188,10 @@ export function getAppType(name: string): string { return "Unknown"; } +export function getAppFromSlug(slug: string | undefined): AppMeta | undefined { + return ALL_APPS.find((app) => app.slug === slug); +} + export const getEventTypeAppData = ( eventType: Pick, "price" | "currency" | "metadata">, appId: T, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 9477e47f27..9791a682b2 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -19,7 +19,7 @@ import { metadata as GoogleMeetMetadata } from "@calcom/app-store/googlevideo/_m import { getLocationValueForDB, LocationObject } from "@calcom/app-store/locations"; import { MeetLocationType } from "@calcom/app-store/locations"; import { handleEthSignature } from "@calcom/app-store/rainbow/utils/ethereum"; -import { EventTypeAppsList, getEventTypeAppData } from "@calcom/app-store/utils"; +import { EventTypeAppsList, getAppFromSlug, getEventTypeAppData } from "@calcom/app-store/utils"; import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler"; import EventManager from "@calcom/core/EventManager"; import { getEventName } from "@calcom/core/event"; @@ -50,6 +50,7 @@ import { customInputSchema, EventTypeMetaDataSchema, extendedBookingCreateBody, + userMetadata as userMetadataSchema, } from "@calcom/prisma/zod-utils"; import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime"; import type { AdditionalInformation, AppsStatus, CalendarEvent } from "@calcom/types/Calendar"; @@ -376,7 +377,10 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { in: dynamicUserList, }, }, - ...userSelect, + select: { + ...userSelect.select, + metadata: true, + }, }) : !!eventType.hosts?.length ? eventType.hosts.map(({ user, isFixed }) => ({ @@ -385,7 +389,10 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { })) : eventType.users; // loadUsers allows type inferring - let users: (Awaited>[number] & { isFixed?: boolean })[] = await loadUsers(); + let users: (Awaited>[number] & { + isFixed?: boolean; + metadata?: Prisma.JsonValue; + })[] = await loadUsers(); const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking); if (!isDynamicAllowed && !eventTypeId) { @@ -497,8 +504,21 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { const seed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}:${new Date().getTime()}`; const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL)); - const bookingLocation = getLocationValueForDB(reqBody.location, eventType.locations); + let locationBodyString = reqBody.location; + let defaultLocationUrl = undefined; + if (dynamicUserList.length > 1) { + users = users.sort((a, b) => { + const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0; + const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0; + return aIndex - bIndex; + }); + const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata); + const app = getAppFromSlug(firstUsersMetadata?.defaultConferencingApp?.appSlug); + locationBodyString = app?.appData?.location?.type || locationBodyString; + defaultLocationUrl = firstUsersMetadata?.defaultConferencingApp?.appLink; + } + const bookingLocation = getLocationValueForDB(locationBodyString, eventType.locations); const customInputs = {} as NonNullable; const teamMemberPromises = @@ -1040,7 +1060,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { metadata.conferenceData = results[0].createdEvent?.conferenceData; metadata.entryPoints = results[0].createdEvent?.entryPoints; handleAppsStatus(results, booking); - videoCallUrl = metadata.hangoutLink || videoCallUrl; + videoCallUrl = metadata.hangoutLink || defaultLocationUrl || videoCallUrl; } if (noEmail !== true) { await sendScheduledEmails({