diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index b5c97a7bbb5518..f5bbb5032dd055 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; import type { getEventLocationValue } from "@calcom/app-store/locations"; -import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/app-store/locations"; +import { getSuccessPageLocationMessage, guessEventLocationTypeSync } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; // TODO: Use browser locale, implement Intl in Dayjs maybe? import "@calcom/dayjs/locales"; @@ -200,7 +200,7 @@ function BookingListItem(booking: BookingItemProps) { t, booking.status ); - const provider = guessEventLocationType(location); + const provider = guessEventLocationTypeSync(location); const isDisabledCancelling = booking.eventType.disableCancelling; const isDisabledRescheduling = booking.eventType.disableRescheduling; diff --git a/apps/web/components/dialog/EditLocationDialog.tsx b/apps/web/components/dialog/EditLocationDialog.tsx index bc94b6a62522de..03b403831275eb 100644 --- a/apps/web/components/dialog/EditLocationDialog.tsx +++ b/apps/web/components/dialog/EditLocationDialog.tsx @@ -7,7 +7,7 @@ import { z } from "zod"; import type { EventLocationType, LocationObject } from "@calcom/app-store/locations"; import { - getEventLocationType, + getEventLocationTypeSync, getHumanReadableLocationValue, getMessageForOrganizer, isAttendeeInputRequired, @@ -168,7 +168,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => { name: "locationAddress", }); - const eventLocationType = getEventLocationType(selectedLocation); + const eventLocationType = getEventLocationTypeSync(selectedLocation); const defaultLocation = defaultValues?.find( (location: { type: EventLocationType["type"]; address?: string }) => { diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index 4f55f9e2a9318e..8367e82c989655 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -11,7 +11,7 @@ import { z } from "zod"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import type { getEventLocationValue } from "@calcom/app-store/locations"; -import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/app-store/locations"; +import { getSuccessPageLocationMessage, guessEventLocationTypeSync } from "@calcom/app-store/locations"; import { getEventTypeAppData } from "@calcom/app-store/utils"; import type { ConfigType } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; @@ -158,11 +158,19 @@ export default function Success(props: PageProps) { const utmParams = bookingInfo.tracking; const [date, setDate] = useState(dayjs.utc(bookingInfo.startTime)); - const calendarLinks = getCalendarLinks({ - booking: bookingWithParsedMetadata, - eventType: eventType, - t, - }); + const [calendarLinks, setCalendarLinks] = useState([]); + + useEffect(() => { + const loadCalendarLinks = async () => { + const links = await getCalendarLinks({ + booking: bookingWithParsedMetadata, + eventType: eventType, + t, + }); + setCalendarLinks(links); + }; + loadCalendarLinks(); + }, [bookingWithParsedMetadata, eventType, t]); // TODO: We could transform the JSX to just iterate over calendarLinks and render a link for each type const icsLink = calendarLinks.find((link) => link.id === CalendarLinkType.ICS)?.link; @@ -353,8 +361,8 @@ export default function Success(props: PageProps) { bookingInfo.status ); - const providerName = guessEventLocationType(location)?.label; - const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label; + const providerName = guessEventLocationTypeSync(location)?.label; + const rescheduleProviderName = guessEventLocationTypeSync(rescheduleLocation)?.label; const isBookingInPast = new Date(bookingInfo.endTime) < new Date(); const isReschedulable = !isCancelled; @@ -415,7 +423,7 @@ export default function Success(props: PageProps) { {!isEmbed && !isFeedbackMode && ( {t("what")}
- {isRoundRobin ? bookingInfo.title : eventName} + {isRoundRobin ? bookingInfo.title : typeof eventName === "string" ? eventName : ""}
{t("when")}
diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 997785645a9bb6..ef488d7e98498a 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -121,7 +121,6 @@ describe("handleChildrenEventTypes", () => { autoTranslateDescriptionEnabled, includeNoShowInRRCalculation, instantMeetingScheduleId, - showOptimizedSlots, ...evType } = mockFindFirstEventType({ id: 123, @@ -183,7 +182,6 @@ describe("handleChildrenEventTypes", () => { assignRRMembersUsingSegment, includeNoShowInRRCalculation, instantMeetingScheduleId, - showOptimizedSlots, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -294,7 +292,6 @@ describe("handleChildrenEventTypes", () => { includeNoShowInRRCalculation, instantMeetingScheduleId, assignRRMembersUsingSegment, - showOptimizedSlots, ...evType } = mockFindFirstEventType({ id: 123, @@ -359,7 +356,6 @@ describe("handleChildrenEventTypes", () => { assignRRMembersUsingSegment, rrSegmentQueryValue, useEventLevelSelectedCalendars, - showOptimizedSlots, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -425,7 +421,6 @@ describe("handleChildrenEventTypes", () => { includeNoShowInRRCalculation, instantMeetingScheduleId, assignRRMembersUsingSegment, - showOptimizedSlots, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -449,7 +444,6 @@ describe("handleChildrenEventTypes", () => { requiresBookerEmailVerification: false, lockTimeZoneToggleOnBookingPage: false, useEventTypeDestinationCalendarEmail: false, - showOptimizedSlots: false, workflows: [], parentId: 1, locations: [], diff --git a/packages/app-store-cli/src/build.ts b/packages/app-store-cli/src/build.ts index e52cc73439ea41..56ed2d179ba80e 100644 --- a/packages/app-store-cli/src/build.ts +++ b/packages/app-store-cli/src/build.ts @@ -467,6 +467,102 @@ function generateFiles() { videoOutput.push(...videoAdapters); } + const calendarMetadataOutput = []; + const calendarMetadataServices = getExportedObject( + "CalendarMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return app.type && app.type.endsWith("_calendar"); + } + ); + calendarMetadataOutput.push(...calendarMetadataServices); + + const paymentMetadataOutput = []; + const paymentMetadataServices = getExportedObject( + "PaymentMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return app.type && app.type.endsWith("_payment"); + } + ); + paymentMetadataOutput.push(...paymentMetadataServices); + + const videoMetadataOutput = []; + const videoMetadataServices = getExportedObject( + "VideoMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return app.type && app.type.endsWith("_video"); + } + ); + videoMetadataOutput.push(...videoMetadataServices); + + const analyticsMetadataOutput = []; + const analyticsMetadataServices = getExportedObject( + "AnalyticsMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return app.type && app.type.endsWith("_analytics"); + } + ); + analyticsMetadataOutput.push(...analyticsMetadataServices); + + const crmMetadataOutput = []; + const crmMetadataServices = getExportedObject( + "CrmMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return app.type && app.type.endsWith("_crm"); + } + ); + crmMetadataOutput.push(...crmMetadataServices); + + const locationMetadataOutput = []; + const locationMetadataServices = getExportedObject( + "LocationMetadataMap", + { + importConfig: { + fileToBeImported: "_metadata.ts", + importName: "default", + }, + lazyImport: true, + }, + (app: App) => { + return !!app.appData?.location; + } + ); + locationMetadataOutput.push(...locationMetadataServices); + const banner = `/** This file is autogenerated using the command \`yarn app-store:build --watch\`. Don't modify this file manually. @@ -484,6 +580,12 @@ function generateFiles() { ["calendar.services.generated.ts", calendarOutput], ["payment.services.generated.ts", paymentOutput], ["video.adapters.generated.ts", videoOutput], + ["calendar.metadata.generated.ts", calendarMetadataOutput], + ["payment.metadata.generated.ts", paymentMetadataOutput], + ["video.metadata.generated.ts", videoMetadataOutput], + ["analytics.metadata.generated.ts", analyticsMetadataOutput], + ["crm.metadata.generated.ts", crmMetadataOutput], + ["location.metadata.generated.ts", locationMetadataOutput], ]; filesToGenerate.forEach(([fileName, output]) => { fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); diff --git a/packages/app-store/BookingPageTagManager.tsx b/packages/app-store/BookingPageTagManager.tsx index 6b3955b9bd065e..28aae05eadee42 100644 --- a/packages/app-store/BookingPageTagManager.tsx +++ b/packages/app-store/BookingPageTagManager.tsx @@ -1,7 +1,7 @@ import Script from "next/script"; +import React from "react"; import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppData"; -import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; import type { Tag } from "@calcom/app-store/types"; import { sdkActionManager } from "@calcom/lib/sdk-event"; import type { AppMeta } from "@calcom/types/App"; @@ -29,30 +29,42 @@ const getPushEventScript = ({ tag, appId }: { tag: Tag; appId: string }) => { }; }; -function getAnalyticsApps(eventType: Parameters[0]) { - return Object.entries(appStoreMetadata).reduce( - (acc, entry) => { - const [appId, app] = entry; - const eventTypeAppData = getEventTypeAppData(eventType, appId as keyof typeof appDataSchemas); +async function getAnalyticsApps(eventType: Parameters[0]) { + const analyticsApps: Record< + string, + { + meta: AnalyticApp; + eventTypeAppData: ReturnType; + } + > = {}; - if (!eventTypeAppData || !app.appData?.tag) { - return acc; - } + const { AnalyticsMetadataMap } = await import("./analytics.metadata.generated"); - acc[appId] = { - meta: app as AnalyticApp, - eventTypeAppData: eventTypeAppData, - }; - return acc; - }, - {} as Record< - string, - { - meta: AnalyticApp; - eventTypeAppData: ReturnType; + for (const [appId, metadataPromise] of Object.entries(AnalyticsMetadataMap)) { + try { + const metadata = await metadataPromise; + const eventTypeAppData = getEventTypeAppData(eventType, appId as keyof typeof appDataSchemas); + if ( + eventTypeAppData && + metadata && + typeof metadata === "object" && + metadata !== null && + "appData" in metadata + ) { + const typedMetadata = metadata as AppMeta; + if (typedMetadata.appData?.tag) { + analyticsApps[appId] = { + meta: typedMetadata as AnalyticApp, + eventTypeAppData, + }; + } } - > - ); + } catch (error) { + console.warn(`Failed to load analytics app ${appId}:`, error); + } + } + + return analyticsApps; } export function handleEvent(event: { detail: Record & { type: string } }) { @@ -94,7 +106,20 @@ export default function BookingPageTagManager({ }: { eventType: Parameters[0]; }) { - const analyticsApps = getAnalyticsApps(eventType); + const [analyticsApps, setAnalyticsApps] = React.useState< + Record< + string, + { + meta: AnalyticApp; + eventTypeAppData: ReturnType; + } + > + >({}); + + React.useEffect(() => { + getAnalyticsApps(eventType).then(setAnalyticsApps); + }, [eventType]); + return ( <> {Object.entries(analyticsApps).map(([appId, { meta: app, eventTypeAppData }]) => { diff --git a/packages/app-store/_appRegistry.ts b/packages/app-store/_appRegistry.ts index 202045df336a07..66894edcc56999 100644 --- a/packages/app-store/_appRegistry.ts +++ b/packages/app-store/_appRegistry.ts @@ -1,5 +1,5 @@ import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; -import { getAppFromSlug } from "@calcom/app-store/utils"; +import { getAppFromSlugSync } from "@calcom/app-store/utils"; import getInstallCountPerApp from "@calcom/lib/apps/getInstallCountPerApp"; import { getAllDelegationCredentialsForUser } from "@calcom/lib/delegationCredential/server"; import type { UserAdminTeams } from "@calcom/lib/server/repository/user"; @@ -121,7 +121,7 @@ export async function getAppRegistryWithCredentials(userId: number, userAdminTea (dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency ); // If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app. - const dependencyName = getAppFromSlug(dependency)?.name; + const dependencyName = getAppFromSlugSync(dependency)?.name; return { name: dependencyName, installed: dependencyInstalled }; }); } diff --git a/packages/app-store/_utils/setDefaultConferencingApp.ts b/packages/app-store/_utils/setDefaultConferencingApp.ts index deeac5f2b90c0a..b22f8ffaa04ccb 100644 --- a/packages/app-store/_utils/setDefaultConferencingApp.ts +++ b/packages/app-store/_utils/setDefaultConferencingApp.ts @@ -1,5 +1,5 @@ import type { LocationObject } from "@calcom/app-store/locations"; -import { getAppFromSlug } from "@calcom/app-store/utils"; +import { getAppFromSlugSync } from "@calcom/app-store/utils"; import { getBulkUserEventTypes } from "@calcom/lib/event-types/getBulkEventTypes"; import prisma from "@calcom/prisma"; import { userMetadata } from "@calcom/prisma/zod-utils"; @@ -7,7 +7,7 @@ import { userMetadata } from "@calcom/prisma/zod-utils"; const setDefaultConferencingApp = async (userId: number, appSlug: string) => { const eventTypes = await getBulkUserEventTypes(userId); const eventTypeIds = eventTypes.eventTypes.map((item) => item.id); - const foundApp = getAppFromSlug(appSlug); + const foundApp = getAppFromSlugSync(appSlug); const appType = foundApp?.appData?.location?.type; if (!appType) { diff --git a/packages/app-store/alby/minimal-metadata.ts b/packages/app-store/alby/minimal-metadata.ts new file mode 100644 index 00000000000000..fd16bc73c16f49 --- /dev/null +++ b/packages/app-store/alby/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "alby", + name: "alby", + type: "alby_payment", +}; diff --git a/packages/app-store/amie/minimal-metadata.ts b/packages/app-store/amie/minimal-metadata.ts new file mode 100644 index 00000000000000..c6886482eb33b1 --- /dev/null +++ b/packages/app-store/amie/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "amie", + name: "amie", + type: "amie_other", +}; diff --git a/packages/app-store/analytics.metadata.generated.ts b/packages/app-store/analytics.metadata.generated.ts new file mode 100644 index 00000000000000..af75741893ffe4 --- /dev/null +++ b/packages/app-store/analytics.metadata.generated.ts @@ -0,0 +1,5 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const AnalyticsMetadataMap = {}; diff --git a/packages/app-store/app.metadata.generated.ts b/packages/app-store/app.metadata.generated.ts new file mode 100644 index 00000000000000..20cf43374cdd2a --- /dev/null +++ b/packages/app-store/app.metadata.generated.ts @@ -0,0 +1,113 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const AppMetadataMap = { + alby: import("./alby/minimal-metadata"), + amie: import("./amie/minimal-metadata"), + applecalendar: import("./applecalendar/minimal-metadata"), + attio: import("./attio/minimal-metadata"), + autocheckin: import("./autocheckin/minimal-metadata"), + "baa-for-hipaa": import("./baa-for-hipaa/minimal-metadata"), + basecamp3: import("./basecamp3/minimal-metadata"), + bolna: import("./bolna/minimal-metadata"), + btcpayserver: import("./btcpayserver/minimal-metadata"), + caldavcalendar: import("./caldavcalendar/minimal-metadata"), + campfire: import("./campfire/minimal-metadata"), + chatbase: import("./chatbase/minimal-metadata"), + clic: import("./clic/minimal-metadata"), + closecom: import("./closecom/minimal-metadata"), + cron: import("./cron/minimal-metadata"), + dailyvideo: import("./dailyvideo/minimal-metadata"), + deel: import("./deel/minimal-metadata"), + demodesk: import("./demodesk/minimal-metadata"), + dialpad: import("./dialpad/minimal-metadata"), + discord: import("./discord/minimal-metadata"), + dub: import("./dub/minimal-metadata"), + eightxeight: import("./eightxeight/minimal-metadata"), + "element-call": import("./element-call/minimal-metadata"), + elevenlabs: import("./elevenlabs/minimal-metadata"), + exchange2013calendar: import("./exchange2013calendar/minimal-metadata"), + exchange2016calendar: import("./exchange2016calendar/minimal-metadata"), + exchangecalendar: import("./exchangecalendar/minimal-metadata"), + facetime: import("./facetime/minimal-metadata"), + fathom: import("./fathom/minimal-metadata"), + feishucalendar: import("./feishucalendar/minimal-metadata"), + ga4: import("./ga4/minimal-metadata"), + giphy: import("./giphy/minimal-metadata"), + googlecalendar: import("./googlecalendar/minimal-metadata"), + googlevideo: import("./googlevideo/minimal-metadata"), + granola: import("./granola/minimal-metadata"), + "greetmate-ai": import("./greetmate-ai/minimal-metadata"), + gtm: import("./gtm/minimal-metadata"), + hitpay: import("./hitpay/minimal-metadata"), + "horizon-workrooms": import("./horizon-workrooms/minimal-metadata"), + hubspot: import("./hubspot/minimal-metadata"), + huddle01video: import("./huddle01video/minimal-metadata"), + "ics-feedcalendar": import("./ics-feedcalendar/minimal-metadata"), + insihts: import("./insihts/minimal-metadata"), + intercom: import("./intercom/minimal-metadata"), + jelly: import("./jelly/minimal-metadata"), + jitsivideo: import("./jitsivideo/minimal-metadata"), + larkcalendar: import("./larkcalendar/minimal-metadata"), + lindy: import("./lindy/minimal-metadata"), + linear: import("./linear/minimal-metadata"), + make: import("./make/minimal-metadata"), + matomo: import("./matomo/minimal-metadata"), + metapixel: import("./metapixel/minimal-metadata"), + "millis-ai": import("./millis-ai/minimal-metadata"), + mirotalk: import("./mirotalk/minimal-metadata"), + "mock-payment-app": import("./mock-payment-app/minimal-metadata"), + monobot: import("./monobot/minimal-metadata"), + n8n: import("./n8n/minimal-metadata"), + nextcloudtalk: import("./nextcloudtalk/minimal-metadata"), + office365calendar: import("./office365calendar/minimal-metadata"), + office365video: import("./office365video/minimal-metadata"), + paypal: import("./paypal/minimal-metadata"), + ping: import("./ping/minimal-metadata"), + pipedream: import("./pipedream/minimal-metadata"), + "pipedrive-crm": import("./pipedrive-crm/minimal-metadata"), + plausible: import("./plausible/minimal-metadata"), + posthog: import("./posthog/minimal-metadata"), + qr_code: import("./qr_code/minimal-metadata"), + raycast: import("./raycast/minimal-metadata"), + "retell-ai": import("./retell-ai/minimal-metadata"), + riverside: import("./riverside/minimal-metadata"), + roam: import("./roam/minimal-metadata"), + "routing-forms": import("./routing-forms/minimal-metadata"), + salesforce: import("./salesforce/minimal-metadata"), + salesroom: import("./salesroom/minimal-metadata"), + sendgrid: import("./sendgrid/minimal-metadata"), + shimmervideo: import("./shimmervideo/minimal-metadata"), + signal: import("./signal/minimal-metadata"), + sirius_video: import("./sirius_video/minimal-metadata"), + skype: import("./skype/minimal-metadata"), + stripepayment: import("./stripepayment/minimal-metadata"), + sylapsvideo: import("./sylapsvideo/minimal-metadata"), + synthflow: import("./synthflow/minimal-metadata"), + tandemvideo: import("./tandemvideo/minimal-metadata"), + telegram: import("./telegram/minimal-metadata"), + telli: import("./telli/minimal-metadata"), + basic: import("./templates/basic/minimal-metadata"), + "booking-pages-tag": import("./templates/booking-pages-tag/minimal-metadata"), + "event-type-app-card": import("./templates/event-type-app-card/minimal-metadata"), + "event-type-location-video-static": import("./templates/event-type-location-video-static/minimal-metadata"), + "general-app-settings": import("./templates/general-app-settings/minimal-metadata"), + "link-as-an-app": import("./templates/link-as-an-app/minimal-metadata"), + tests: import("./tests/minimal-metadata"), + twipla: import("./twipla/minimal-metadata"), + umami: import("./umami/minimal-metadata"), + vimcal: import("./vimcal/minimal-metadata"), + vital: import("./vital/minimal-metadata"), + weather_in_your_calendar: import("./weather_in_your_calendar/minimal-metadata"), + webex: import("./webex/minimal-metadata"), + whatsapp: import("./whatsapp/minimal-metadata"), + whereby: import("./whereby/minimal-metadata"), + wipemycalother: import("./wipemycalother/minimal-metadata"), + wordpress: import("./wordpress/minimal-metadata"), + zapier: import("./zapier/minimal-metadata"), + "zoho-bigin": import("./zoho-bigin/minimal-metadata"), + zohocalendar: import("./zohocalendar/minimal-metadata"), + zohocrm: import("./zohocrm/minimal-metadata"), + zoomvideo: import("./zoomvideo/minimal-metadata"), +}; diff --git a/packages/app-store/app.metadata.utils.generated.ts b/packages/app-store/app.metadata.utils.generated.ts new file mode 100644 index 00000000000000..7a6882c3e29534 --- /dev/null +++ b/packages/app-store/app.metadata.utils.generated.ts @@ -0,0 +1,166 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +import type { AppMeta } from "@calcom/types/App"; + +import { AnalyticsMetadataMap } from "./analytics.metadata.generated"; +import { CalendarMetadataMap } from "./calendar.metadata.generated"; +import { CrmMetadataMap } from "./crm.metadata.generated"; +import { LocationMetadataMap } from "./location.metadata.generated"; +import { PaymentMetadataMap } from "./payment.metadata.generated"; +import { VideoMetadataMap } from "./video.metadata.generated"; + +export async function getAppFromSlug(slug: string | undefined): Promise { + if (!slug) return undefined; + + const allMetadataMaps = [ + CalendarMetadataMap, + PaymentMetadataMap, + VideoMetadataMap, + AnalyticsMetadataMap, + CrmMetadataMap, + LocationMetadataMap, + ]; + + for (const metadataMap of allMetadataMaps) { + for (const [appKey, appImport] of Object.entries(metadataMap)) { + try { + const appMeta = await appImport; + const app = (appMeta as any).metadata; + if (app.slug === slug) { + return app as AppMeta; + } + } catch (error) { + console.warn(`Failed to load app metadata for ${appKey}:`, error); + } + } + } + return undefined; +} + +export async function getAppName(name: string): Promise { + const allMetadataMaps = [ + CalendarMetadataMap, + PaymentMetadataMap, + VideoMetadataMap, + AnalyticsMetadataMap, + CrmMetadataMap, + LocationMetadataMap, + ]; + + for (const metadataMap of allMetadataMaps) { + const appImport = metadataMap[name as keyof typeof metadataMap]; + if (appImport) { + try { + const appMeta = await appImport; + const app = (appMeta as any).metadata; + return app?.name ?? null; + } catch (error) { + console.warn(`Failed to load app metadata for ${name}:`, error); + } + } + } + return null; +} + +export async function getAppType(name: string): Promise { + const allMetadataMaps = [ + CalendarMetadataMap, + PaymentMetadataMap, + VideoMetadataMap, + AnalyticsMetadataMap, + CrmMetadataMap, + LocationMetadataMap, + ]; + + for (const metadataMap of allMetadataMaps) { + const appImport = metadataMap[name as keyof typeof metadataMap]; + if (appImport) { + try { + const appMeta = await appImport; + const app = (appMeta as any).metadata; + const type = app?.type; + + if (type?.endsWith("_calendar")) { + return "Calendar"; + } + if (type?.endsWith("_payment")) { + return "Payment"; + } + if (type?.endsWith("_video")) { + return "Video"; + } + if (type?.endsWith("_analytics")) { + return "Analytics"; + } + if (type?.endsWith("_crm")) { + return "CRM"; + } + return "Unknown"; + } catch (error) { + console.warn(`Failed to load app metadata for ${name}:`, error); + } + } + } + return "Unknown"; +} + +export async function getAppFromLocationValue(type: string): Promise { + for (const [appKey, appImport] of Object.entries(LocationMetadataMap)) { + try { + const appMeta = await appImport; + const app = appMeta.metadata; + if ( + app && + "appData" in app && + app.appData && + "location" in app.appData && + app.appData.location?.type === type + ) { + return app as AppMeta; + } + } catch (error) { + console.warn(`Failed to load app metadata for ${appKey}:`, error); + } + } + return undefined; +} + +export function getAppFromSlugSync(slug: string | undefined): AppMeta | undefined { + if (!slug) return undefined; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { appStoreMetadata } = require("./appStoreMetaData"); + const ALL_APPS = Object.values(appStoreMetadata); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ALL_APPS.find((app: any) => app.slug === slug) as AppMeta | undefined; +} + +export function getAppNameSync(name: string): string | null { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { appStoreMetadata } = require("./appStoreMetaData"); + return appStoreMetadata[name as keyof typeof appStoreMetadata]?.name ?? null; +} + +export function getAppTypeSync(name: string): string { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { appStoreMetadata } = require("./appStoreMetaData"); + const type = appStoreMetadata[name as keyof typeof appStoreMetadata]?.type; + + if (type?.endsWith("_calendar")) { + return "Calendar"; + } + if (type?.endsWith("_payment")) { + return "Payment"; + } + return "Unknown"; +} + +export function getAppFromLocationValueSync(type: string): AppMeta | undefined { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { appStoreMetadata } = require("./appStoreMetaData"); + const ALL_APPS = Object.values(appStoreMetadata); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ALL_APPS.find((app: any) => app?.appData?.location?.type === type) as AppMeta | undefined; +} diff --git a/packages/app-store/applecalendar/minimal-metadata.ts b/packages/app-store/applecalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..66e10507387ef0 --- /dev/null +++ b/packages/app-store/applecalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "apple-calendar", + name: "applecalendar", + type: "apple_calendar", +}; diff --git a/packages/app-store/attio/minimal-metadata.ts b/packages/app-store/attio/minimal-metadata.ts new file mode 100644 index 00000000000000..bde4a9912e6804 --- /dev/null +++ b/packages/app-store/attio/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "attio", + name: "attio", + type: "attio_crm", +}; diff --git a/packages/app-store/autocheckin/minimal-metadata.ts b/packages/app-store/autocheckin/minimal-metadata.ts new file mode 100644 index 00000000000000..5613f4dad51b4f --- /dev/null +++ b/packages/app-store/autocheckin/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "autocheckin", + name: "autocheckin", + type: "autocheckin_automation", +}; diff --git a/packages/app-store/baa-for-hipaa/minimal-metadata.ts b/packages/app-store/baa-for-hipaa/minimal-metadata.ts new file mode 100644 index 00000000000000..abb93d4c1aaff6 --- /dev/null +++ b/packages/app-store/baa-for-hipaa/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "baa-for-hipaa", + name: "baa-for-hipaa", + type: "baa-for-hipaa_other", +}; diff --git a/packages/app-store/basecamp3/minimal-metadata.ts b/packages/app-store/basecamp3/minimal-metadata.ts new file mode 100644 index 00000000000000..6836dfbbef8d76 --- /dev/null +++ b/packages/app-store/basecamp3/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "basecamp3", + name: "basecamp3", + type: "basecamp3_other_calendar", +}; diff --git a/packages/app-store/bolna/minimal-metadata.ts b/packages/app-store/bolna/minimal-metadata.ts new file mode 100644 index 00000000000000..f8d0fd27a2935c --- /dev/null +++ b/packages/app-store/bolna/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "bolna", + name: "bolna", + type: "bolna_automation", +}; diff --git a/packages/app-store/btcpayserver/minimal-metadata.ts b/packages/app-store/btcpayserver/minimal-metadata.ts new file mode 100644 index 00000000000000..6f84ccd7add76d --- /dev/null +++ b/packages/app-store/btcpayserver/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "btcpayserver", + name: "btcpayserver", + type: "btcpayserver_payment", +}; diff --git a/packages/app-store/caldavcalendar/minimal-metadata.ts b/packages/app-store/caldavcalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..eed6cace43cdb1 --- /dev/null +++ b/packages/app-store/caldavcalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "caldav-calendar", + name: "caldavcalendar", + type: "caldav_calendar", +}; diff --git a/packages/app-store/calendar.metadata.generated.ts b/packages/app-store/calendar.metadata.generated.ts new file mode 100644 index 00000000000000..23b84c8aa17a99 --- /dev/null +++ b/packages/app-store/calendar.metadata.generated.ts @@ -0,0 +1,14 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const CalendarMetadataMap = { + applecalendar: import("./applecalendar/_metadata"), + caldavcalendar: import("./caldavcalendar/_metadata"), + exchange2013calendar: import("./exchange2013calendar/_metadata"), + exchange2016calendar: import("./exchange2016calendar/_metadata"), + feishucalendar: import("./feishucalendar/_metadata"), + googlecalendar: import("./googlecalendar/_metadata"), + larkcalendar: import("./larkcalendar/_metadata"), + office365calendar: import("./office365calendar/_metadata"), +}; diff --git a/packages/app-store/campfire/minimal-metadata.ts b/packages/app-store/campfire/minimal-metadata.ts new file mode 100644 index 00000000000000..61c6c6d8f3fe0b --- /dev/null +++ b/packages/app-store/campfire/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "campfire", + name: "campfire", + type: "campfire_video", + appData: { + location: { + type: "integrations:campfire_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/chatbase/minimal-metadata.ts b/packages/app-store/chatbase/minimal-metadata.ts new file mode 100644 index 00000000000000..658258b1d34682 --- /dev/null +++ b/packages/app-store/chatbase/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "chatbase", + name: "chatbase", + type: "chatbase_other", +}; diff --git a/packages/app-store/clic/minimal-metadata.ts b/packages/app-store/clic/minimal-metadata.ts new file mode 100644 index 00000000000000..36f9194b5ae0b6 --- /dev/null +++ b/packages/app-store/clic/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "clic", + name: "clic", + type: "check_in_automation", +}; diff --git a/packages/app-store/closecom/minimal-metadata.ts b/packages/app-store/closecom/minimal-metadata.ts new file mode 100644 index 00000000000000..c7bbd1a520f5a1 --- /dev/null +++ b/packages/app-store/closecom/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "closecom", + name: "closecom", + type: "closecom_crm", +}; diff --git a/packages/app-store/crm.metadata.generated.ts b/packages/app-store/crm.metadata.generated.ts new file mode 100644 index 00000000000000..ad3dffe8870138 --- /dev/null +++ b/packages/app-store/crm.metadata.generated.ts @@ -0,0 +1,7 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const CrmMetadataMap = { + hubspot: import("./hubspot/_metadata"), +}; diff --git a/packages/app-store/cron/minimal-metadata.ts b/packages/app-store/cron/minimal-metadata.ts new file mode 100644 index 00000000000000..9100363689c4f6 --- /dev/null +++ b/packages/app-store/cron/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "cron", + name: "cron", + type: "cron_calendar", +}; diff --git a/packages/app-store/dailyvideo/minimal-metadata.ts b/packages/app-store/dailyvideo/minimal-metadata.ts new file mode 100644 index 00000000000000..e7c84c92d08c8a --- /dev/null +++ b/packages/app-store/dailyvideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "daily-video", + name: "dailyvideo", + type: "daily_video", + appData: { + location: { + type: "integrations:daily", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/deel/minimal-metadata.ts b/packages/app-store/deel/minimal-metadata.ts new file mode 100644 index 00000000000000..b65a2519043c4b --- /dev/null +++ b/packages/app-store/deel/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "deel", + name: "deel", + type: "deel_other", +}; diff --git a/packages/app-store/demodesk/minimal-metadata.ts b/packages/app-store/demodesk/minimal-metadata.ts new file mode 100644 index 00000000000000..12f5144deac256 --- /dev/null +++ b/packages/app-store/demodesk/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "demodesk", + name: "demodesk", + type: "demodesk_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/dialpad/minimal-metadata.ts b/packages/app-store/dialpad/minimal-metadata.ts new file mode 100644 index 00000000000000..e403d48477a06c --- /dev/null +++ b/packages/app-store/dialpad/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "dialpad", + name: "dialpad", + type: "dialpad_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/discord/minimal-metadata.ts b/packages/app-store/discord/minimal-metadata.ts new file mode 100644 index 00000000000000..2173a2b6442e93 --- /dev/null +++ b/packages/app-store/discord/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "discord", + name: "discord", + type: "discord_video", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/dub/minimal-metadata.ts b/packages/app-store/dub/minimal-metadata.ts new file mode 100644 index 00000000000000..8eb71115807391 --- /dev/null +++ b/packages/app-store/dub/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "dub", + name: "dub", + type: "dub_analytics", +}; diff --git a/packages/app-store/eightxeight/minimal-metadata.ts b/packages/app-store/eightxeight/minimal-metadata.ts new file mode 100644 index 00000000000000..e4360475647537 --- /dev/null +++ b/packages/app-store/eightxeight/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "eightxeight", + name: "eightxeight", + type: "eightxeight_video", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/element-call/minimal-metadata.ts b/packages/app-store/element-call/minimal-metadata.ts new file mode 100644 index 00000000000000..ed516a39adf56c --- /dev/null +++ b/packages/app-store/element-call/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "element-call", + name: "element-call", + type: "element-call_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/elevenlabs/minimal-metadata.ts b/packages/app-store/elevenlabs/minimal-metadata.ts new file mode 100644 index 00000000000000..07d683d47d616b --- /dev/null +++ b/packages/app-store/elevenlabs/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "elevenlabs", + name: "elevenlabs", + type: "elevenlabs_automation", +}; diff --git a/packages/app-store/exchange2013calendar/minimal-metadata.ts b/packages/app-store/exchange2013calendar/minimal-metadata.ts new file mode 100644 index 00000000000000..221fcddd338421 --- /dev/null +++ b/packages/app-store/exchange2013calendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "exchange2013-calendar", + name: "exchange2013calendar", + type: "exchange2013_calendar", +}; diff --git a/packages/app-store/exchange2016calendar/minimal-metadata.ts b/packages/app-store/exchange2016calendar/minimal-metadata.ts new file mode 100644 index 00000000000000..d914c248cb428f --- /dev/null +++ b/packages/app-store/exchange2016calendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "exchange2016-calendar", + name: "exchange2016calendar", + type: "exchange2016_calendar", +}; diff --git a/packages/app-store/exchangecalendar/minimal-metadata.ts b/packages/app-store/exchangecalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..d983ba72aa9ff7 --- /dev/null +++ b/packages/app-store/exchangecalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "exchange", + name: "exchangecalendar", + type: "exchange_calendar", +}; diff --git a/packages/app-store/facetime/minimal-metadata.ts b/packages/app-store/facetime/minimal-metadata.ts new file mode 100644 index 00000000000000..95727884e7d14d --- /dev/null +++ b/packages/app-store/facetime/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "facetime", + name: "facetime", + type: "facetime_video", + appData: { + location: { + type: "integrations:facetime_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/fathom/minimal-metadata.ts b/packages/app-store/fathom/minimal-metadata.ts new file mode 100644 index 00000000000000..f67f7dc0a3108e --- /dev/null +++ b/packages/app-store/fathom/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "fathom", + name: "fathom", + type: "fathom_analytics", +}; diff --git a/packages/app-store/feishucalendar/minimal-metadata.ts b/packages/app-store/feishucalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..d626afb0105bdc --- /dev/null +++ b/packages/app-store/feishucalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "feishu-calendar", + name: "feishucalendar", + type: "feishu_calendar", +}; diff --git a/packages/app-store/ga4/minimal-metadata.ts b/packages/app-store/ga4/minimal-metadata.ts new file mode 100644 index 00000000000000..8ed11cddc2426c --- /dev/null +++ b/packages/app-store/ga4/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "ga4", + name: "ga4", + type: "ga4_analytics", +}; diff --git a/packages/app-store/giphy/minimal-metadata.ts b/packages/app-store/giphy/minimal-metadata.ts new file mode 100644 index 00000000000000..c8d725f8b62055 --- /dev/null +++ b/packages/app-store/giphy/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "giphy", + name: "giphy", + type: "giphy_other", +}; diff --git a/packages/app-store/googlecalendar/minimal-metadata.ts b/packages/app-store/googlecalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..0db5a90eebbf36 --- /dev/null +++ b/packages/app-store/googlecalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "google-calendar", + name: "googlecalendar", + type: "google_calendar", +}; diff --git a/packages/app-store/googlevideo/minimal-metadata.ts b/packages/app-store/googlevideo/minimal-metadata.ts new file mode 100644 index 00000000000000..b6d374c2b5be6b --- /dev/null +++ b/packages/app-store/googlevideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "google-meet", + name: "googlevideo", + type: "google_video", + appData: { + location: { + type: "integrations:google:meet", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/granola/minimal-metadata.ts b/packages/app-store/granola/minimal-metadata.ts new file mode 100644 index 00000000000000..90b9cbe696e9e7 --- /dev/null +++ b/packages/app-store/granola/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "granola", + name: "granola", + type: "granola_other", +}; diff --git a/packages/app-store/greetmate-ai/minimal-metadata.ts b/packages/app-store/greetmate-ai/minimal-metadata.ts new file mode 100644 index 00000000000000..61e9b64349da19 --- /dev/null +++ b/packages/app-store/greetmate-ai/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "greetmate-ai", + name: "greetmate-ai", + type: "greetmate-ai_automation", +}; diff --git a/packages/app-store/gtm/minimal-metadata.ts b/packages/app-store/gtm/minimal-metadata.ts new file mode 100644 index 00000000000000..80f48423c36d5b --- /dev/null +++ b/packages/app-store/gtm/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "gtm", + name: "gtm", + type: "gtm_analytics", +}; diff --git a/packages/app-store/hitpay/minimal-metadata.ts b/packages/app-store/hitpay/minimal-metadata.ts new file mode 100644 index 00000000000000..236bdc31d7853b --- /dev/null +++ b/packages/app-store/hitpay/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "hitpay", + name: "hitpay", + type: "hitpay_payment", +}; diff --git a/packages/app-store/horizon-workrooms/minimal-metadata.ts b/packages/app-store/horizon-workrooms/minimal-metadata.ts new file mode 100644 index 00000000000000..a5723855ce9078 --- /dev/null +++ b/packages/app-store/horizon-workrooms/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "horizon-workrooms", + name: "horizon-workrooms", + type: "horizon-workrooms_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/hubspot/minimal-metadata.ts b/packages/app-store/hubspot/minimal-metadata.ts new file mode 100644 index 00000000000000..52a9dd30aa63ff --- /dev/null +++ b/packages/app-store/hubspot/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "hubspot", + name: "hubspot", + type: "hubspot_crm", +}; diff --git a/packages/app-store/huddle01video/minimal-metadata.ts b/packages/app-store/huddle01video/minimal-metadata.ts new file mode 100644 index 00000000000000..a47529a54aa79d --- /dev/null +++ b/packages/app-store/huddle01video/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "huddle01", + name: "huddle01video", + type: "huddle01_video", + appData: { + location: { + type: "integrations:huddle01_video", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/ics-feedcalendar/minimal-metadata.ts b/packages/app-store/ics-feedcalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..b8bcb291e099e6 --- /dev/null +++ b/packages/app-store/ics-feedcalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "ics-feed", + name: "ics-feedcalendar", + type: "ics-feed_calendar", +}; diff --git a/packages/app-store/insihts/minimal-metadata.ts b/packages/app-store/insihts/minimal-metadata.ts new file mode 100644 index 00000000000000..28b53ae2616c84 --- /dev/null +++ b/packages/app-store/insihts/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "insihts", + name: "insihts", + type: "insihts_analytics", +}; diff --git a/packages/app-store/intercom/minimal-metadata.ts b/packages/app-store/intercom/minimal-metadata.ts new file mode 100644 index 00000000000000..987729f79db23a --- /dev/null +++ b/packages/app-store/intercom/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "intercom", + name: "intercom", + type: "intercom_automation", +}; diff --git a/packages/app-store/jelly/minimal-metadata.ts b/packages/app-store/jelly/minimal-metadata.ts new file mode 100644 index 00000000000000..b807a85d8cdb20 --- /dev/null +++ b/packages/app-store/jelly/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "jelly", + name: "jelly", + type: "jelly_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_conferencing", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/jitsivideo/minimal-metadata.ts b/packages/app-store/jitsivideo/minimal-metadata.ts new file mode 100644 index 00000000000000..29d78a15a08c1e --- /dev/null +++ b/packages/app-store/jitsivideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "jitsi", + name: "jitsivideo", + type: "jitsi_video", + appData: { + location: { + type: "integrations:jitsi", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/larkcalendar/minimal-metadata.ts b/packages/app-store/larkcalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..d002268996265f --- /dev/null +++ b/packages/app-store/larkcalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "lark-calendar", + name: "larkcalendar", + type: "lark_calendar", +}; diff --git a/packages/app-store/lindy/minimal-metadata.ts b/packages/app-store/lindy/minimal-metadata.ts new file mode 100644 index 00000000000000..e8b5565369bc41 --- /dev/null +++ b/packages/app-store/lindy/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "lindy", + name: "lindy", + type: "lindy_automation", +}; diff --git a/packages/app-store/linear/minimal-metadata.ts b/packages/app-store/linear/minimal-metadata.ts new file mode 100644 index 00000000000000..91b4dce740985d --- /dev/null +++ b/packages/app-store/linear/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "linear", + name: "linear", + type: "linear_other", +}; diff --git a/packages/app-store/location.metadata.generated.ts b/packages/app-store/location.metadata.generated.ts new file mode 100644 index 00000000000000..8732f6c1ed3ed0 --- /dev/null +++ b/packages/app-store/location.metadata.generated.ts @@ -0,0 +1,13 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const LocationMetadataMap = { + dailyvideo: import("./dailyvideo/_metadata"), + googlevideo: import("./googlevideo/_metadata"), + huddle01video: import("./huddle01video/_metadata"), + jitsivideo: import("./jitsivideo/_metadata"), + office365video: import("./office365video/_metadata"), + tandemvideo: import("./tandemvideo/_metadata"), + zoomvideo: import("./zoomvideo/_metadata"), +}; diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 7e646b7d7c1671..07d0f387018b47 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -4,7 +4,7 @@ import type { TFunction } from "i18next"; import { z } from "zod"; -import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData"; +import { LocationMetadataMap } from "@calcom/app-store/location.metadata.generated"; import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import type { Ensure, Optional } from "@calcom/types/utils"; @@ -229,55 +229,83 @@ export type BookingLocationValue = string; export const AppStoreLocationType: Record = {}; -const locationsFromApps: EventLocationTypeFromApp[] = []; - -for (const [appName, meta] of Object.entries(appStoreMetadata)) { - const location = meta.appData?.location; - if (location) { - // TODO: This template variable replacement should happen once during app-store:build. - for (const [key, value] of Object.entries(location)) { - if (typeof value === "string") { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); - } - } - const newLocation = { - ...location, - messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`, - iconUrl: meta.logo, - // For All event location apps, locationLink is where we store the input - // TODO: locationLink and link seems redundant. We can modify the code to keep just one of them. - variable: location.variable || "locationLink", - defaultValueVariable: location.defaultValueVariable || "link", - }; - - // Static links always require organizer to input - if (newLocation.linkType === "static") { - newLocation.organizerInputType = location.organizerInputType || "text"; - if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) { - // HACK: Translation ends up removing https? if it's in the beginning :( - newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`; +let cachedLocationApps: EventLocationTypeFromApp[] | null = null; + +async function getLocationApps(): Promise { + if (cachedLocationApps) return cachedLocationApps; + + const locationsFromApps: EventLocationTypeFromApp[] = []; + for (const [appName, appImport] of Object.entries(LocationMetadataMap)) { + try { + const appMeta = await appImport; + const meta = (appMeta as any).metadata; + if (meta?.appData?.location) { + const location = meta.appData.location; + // TODO: This template variable replacement should happen once during app-store:build. + for (const [key, value] of Object.entries(location)) { + if (typeof value === "string") { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); + } + } + const newLocation: EventLocationTypeFromApp = { + type: location.type, + label: location.label, + linkType: (location.linkType as "static" | "dynamic") || "static", + messageForOrganizer: (location as any).messageForOrganizer || `Set ${location.label} link`, + iconUrl: meta.logo, + // For All event location apps, locationLink is where we store the input + // TODO: locationLink and link seems redundant. We can modify the code to keep just one of them. + variable: (location as any).variable || "locationLink", + defaultValueVariable: (location as any).defaultValueVariable || "link", + }; + + // Static links always require organizer to input + if (newLocation.linkType === "static") { + (newLocation as any).organizerInputType = (location as any).organizerInputType || "text"; + if ((newLocation as any).organizerInputPlaceholder?.match(/https?:\/\//)) { + // HACK: Translation ends up removing https? if it's in the beginning :( + (newLocation as any).organizerInputPlaceholder = ` ${ + (newLocation as any).organizerInputPlaceholder + }`; + } + } else { + (newLocation as any).organizerInputType = null; + } + + AppStoreLocationType[appName] = newLocation.type; + + locationsFromApps.push(newLocation); } - } else { - newLocation.organizerInputType = null; + } catch (error) { + console.warn(`Failed to load location app ${appName}:`, error); } - - AppStoreLocationType[appName] = newLocation.type; - - locationsFromApps.push({ - ...newLocation, - }); } + + cachedLocationApps = locationsFromApps; + return locationsFromApps; } +const locationsFromApps: EventLocationTypeFromApp[] = []; const locations = [...defaultLocations, ...locationsFromApps]; -export const getLocationFromApp = (locationType: string) => - locationsFromApps.find((l) => l.type === locationType); +export const getLocationFromApp = async (locationType: string) => { + const locationApps = await getLocationApps(); + return locationApps.find((l) => l.type === locationType); +}; // TODO: Rename this to getLocationByType() -export const getEventLocationType = (locationType: string | undefined | null) => +export const getEventLocationType = async (locationType: string | undefined | null) => { + const locationApps = await getLocationApps(); + const allLocations = [...defaultLocations, ...locationApps]; + return allLocations.find((l) => l.type === locationType); +}; + +export const getLocationFromAppSync = (locationType: string) => + locationsFromApps.find((l) => l.type === locationType); + +export const getEventLocationTypeSync = (locationType: string | undefined | null) => locations.find((l) => l.type === locationType); const getStaticLinkLocationByValue = (value: string | undefined | null) => { @@ -292,8 +320,15 @@ const getStaticLinkLocationByValue = (value: string | undefined | null) => { }); }; -export const guessEventLocationType = (locationTypeOrValue: string | undefined | null) => - getEventLocationType(locationTypeOrValue) || getStaticLinkLocationByValue(locationTypeOrValue); +export const guessEventLocationType = async (locationTypeOrValue: string | undefined | null) => { + const eventLocationType = await getEventLocationType(locationTypeOrValue); + return eventLocationType || getStaticLinkLocationByValue(locationTypeOrValue); +}; + +export const guessEventLocationTypeSync = (locationTypeOrValue: string | undefined | null) => { + const eventLocationType = getEventLocationTypeSync(locationTypeOrValue); + return eventLocationType || getStaticLinkLocationByValue(locationTypeOrValue); +}; export const LocationType = { ...DefaultEventLocationTypeEnum, ...AppStoreLocationType }; @@ -301,7 +336,7 @@ type PrivacyFilteredLocationObject = Optional { const locationsAfterPrivacyFilter = locations.map((location) => { - const eventLocationType = getEventLocationType(location.type); + const eventLocationType = getEventLocationTypeSync(location.type); if (!eventLocationType) { logger.debug(`Couldn't find location type. App might be uninstalled: ${location.type} `); } @@ -326,7 +361,7 @@ export const privacyFilteredLocations = (locations: LocationObject[]): PrivacyFi * @returns string */ export const getMessageForOrganizer = (location: string, t: TFunction) => { - const videoLocation = getLocationFromApp(location); + const videoLocation = getLocationFromAppSync(location); const defaultLocation = defaultLocations.find((l) => l.type === location); if (defaultLocation) { return t(defaultLocation.messageForOrganizer); @@ -352,7 +387,7 @@ export const getHumanReadableLocationValue = ( } // Just in case linkValue is a `locationType.type`(for old bookings) - const eventLocationType = getEventLocationType(linkValue); + const eventLocationType = getEventLocationTypeSync(linkValue); const isDefault = eventLocationType?.default; if (eventLocationType) { // If we can find a video location based on linkValue then it means that the linkValue is something like integrations:google-meet and in that case we don't have the meeting URL to show. @@ -364,7 +399,7 @@ export const getHumanReadableLocationValue = ( }; export const locationKeyToString = (location: LocationObject) => { - const eventLocationType = getEventLocationType(location.type); + const eventLocationType = getEventLocationTypeSync(location.type); if (!eventLocationType) { return null; } @@ -398,7 +433,7 @@ export const getLocationValueForDB = ( eventLocations.forEach((location) => { if (location.type === bookingLocationTypeOrValue) { - const eventLocationType = getEventLocationType(bookingLocationTypeOrValue); + const eventLocationType = getEventLocationTypeSync(bookingLocationTypeOrValue); conferenceCredentialId = location.credentialId; if (!eventLocationType) { return; @@ -421,7 +456,7 @@ export const getLocationValueForDB = ( }; export const getEventLocationValue = (eventLocations: LocationObject[], bookingLocation: LocationObject) => { - const eventLocationType = getEventLocationType(bookingLocation?.type); + const eventLocationType = getEventLocationTypeSync(bookingLocation?.type); if (!eventLocationType) { return ""; } @@ -450,7 +485,7 @@ export function getSuccessPageLocationMessage( t: TFunction, bookingStatus?: BookingStatus ) { - const eventLocationType = getEventLocationType(location); + const eventLocationType = getEventLocationTypeSync(location); let locationToDisplay = location; if (eventLocationType && !eventLocationType.default && eventLocationType.linkType === "dynamic") { const isConfirmed = bookingStatus === BookingStatus.ACCEPTED; @@ -468,9 +503,9 @@ export function getSuccessPageLocationMessage( return locationToDisplay; } -export const getTranslatedLocation = ( +export const getTranslatedLocation = async ( location: PrivacyFilteredLocationObject, - eventLocationType: ReturnType, + eventLocationType: Awaited>, t: TFunction ) => { if (!eventLocationType) return null; @@ -484,16 +519,54 @@ export const getTranslatedLocation = ( return translatedLocation; }; -export const getOrganizerInputLocationTypes = () => { +export const getTranslatedLocationSync = ( + location: PrivacyFilteredLocationObject, + eventLocationType: ReturnType, + t: TFunction +) => { + if (!eventLocationType) return null; + const locationKey = z.string().default("").parse(locationKeyToString(location)); + const translatedLocation = location.type.startsWith("integrations:") + ? eventLocationType.label + : translateAbleKeys.includes(locationKey) + ? t(locationKey) + : locationKey; + + return translatedLocation; +}; + +export const getOrganizerInputLocationTypes = async () => { const result: DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"][] = []; + const locationApps = await getLocationApps(); + const allLocations = [...defaultLocations, ...locationApps]; + const organizerInputTypeLocations = allLocations.filter((location) => !!location.organizerInputType); + organizerInputTypeLocations?.forEach((l) => result.push(l.type)); + + return result; +}; + +export const getOrganizerInputLocationTypesSync = () => { + const result: (DefaultEventLocationType["type"] | EventLocationTypeFromApp["type"])[] = []; + const organizerInputTypeLocations = locations.filter((location) => !!location.organizerInputType); organizerInputTypeLocations?.forEach((l) => result.push(l.type)); return result; }; -export const isAttendeeInputRequired = (locationType: string) => { +export const isAttendeeInputRequired = async (locationType: string) => { + const locationApps = await getLocationApps(); + const allLocations = [...defaultLocations, ...locationApps]; + const location = allLocations.find((l) => l.type === locationType); + if (!location) { + // Consider throwing an error here. This shouldn't happen normally. + return false; + } + return location.attendeeInputType; +}; + +export const isAttendeeInputRequiredSync = (locationType: string) => { const location = locations.find((l) => l.type === locationType); if (!location) { // Consider throwing an error here. This shouldn't happen normally. diff --git a/packages/app-store/make/minimal-metadata.ts b/packages/app-store/make/minimal-metadata.ts new file mode 100644 index 00000000000000..0532ed6e7af432 --- /dev/null +++ b/packages/app-store/make/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "make", + name: "make", + type: "make_automation", +}; diff --git a/packages/app-store/matomo/minimal-metadata.ts b/packages/app-store/matomo/minimal-metadata.ts new file mode 100644 index 00000000000000..651c1c2d91f86d --- /dev/null +++ b/packages/app-store/matomo/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "matomo", + name: "matomo", + type: "matomo_analytics", +}; diff --git a/packages/app-store/metapixel/minimal-metadata.ts b/packages/app-store/metapixel/minimal-metadata.ts new file mode 100644 index 00000000000000..db8e8f69b8ec91 --- /dev/null +++ b/packages/app-store/metapixel/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "metapixel", + name: "metapixel", + type: "metapixel_analytics", +}; diff --git a/packages/app-store/millis-ai/minimal-metadata.ts b/packages/app-store/millis-ai/minimal-metadata.ts new file mode 100644 index 00000000000000..fe03d580753048 --- /dev/null +++ b/packages/app-store/millis-ai/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "millis-ai", + name: "millis-ai", + type: "millis-ai_automation", +}; diff --git a/packages/app-store/mirotalk/minimal-metadata.ts b/packages/app-store/mirotalk/minimal-metadata.ts new file mode 100644 index 00000000000000..39603a7ba821ee --- /dev/null +++ b/packages/app-store/mirotalk/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "mirotalk", + name: "mirotalk", + type: "mirotalk_video", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/mock-payment-app/minimal-metadata.ts b/packages/app-store/mock-payment-app/minimal-metadata.ts new file mode 100644 index 00000000000000..9ee95745e4072a --- /dev/null +++ b/packages/app-store/mock-payment-app/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "mock-payment-app", + name: "mock-payment-app", + type: "mock-payment-app_payment", +}; diff --git a/packages/app-store/monobot/minimal-metadata.ts b/packages/app-store/monobot/minimal-metadata.ts new file mode 100644 index 00000000000000..b2f1401a5eb7fd --- /dev/null +++ b/packages/app-store/monobot/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "monobot", + name: "monobot", + type: "monobot_automation", +}; diff --git a/packages/app-store/n8n/minimal-metadata.ts b/packages/app-store/n8n/minimal-metadata.ts new file mode 100644 index 00000000000000..58d6c47d055926 --- /dev/null +++ b/packages/app-store/n8n/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "n8n", + name: "n8n", + type: "n8n_automation", +}; diff --git a/packages/app-store/nextcloudtalk/minimal-metadata.ts b/packages/app-store/nextcloudtalk/minimal-metadata.ts new file mode 100644 index 00000000000000..f81f16253f0a3c --- /dev/null +++ b/packages/app-store/nextcloudtalk/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "nextcloudtalk", + name: "nextcloudtalk", + type: "nextcloudtalk_video", + appData: { + location: { + type: "integrations:{SLUG}_conferencing", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/office365calendar/minimal-metadata.ts b/packages/app-store/office365calendar/minimal-metadata.ts new file mode 100644 index 00000000000000..bbde35b369e6ec --- /dev/null +++ b/packages/app-store/office365calendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "office365-calendar", + name: "office365calendar", + type: "office365_calendar", +}; diff --git a/packages/app-store/office365video/minimal-metadata.ts b/packages/app-store/office365video/minimal-metadata.ts new file mode 100644 index 00000000000000..3fc7b63f37c4c4 --- /dev/null +++ b/packages/app-store/office365video/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "msteams", + name: "office365video", + type: "office365_video", + appData: { + location: { + type: "integrations:office365_video", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/payment.metadata.generated.ts b/packages/app-store/payment.metadata.generated.ts new file mode 100644 index 00000000000000..41f176be6ac85d --- /dev/null +++ b/packages/app-store/payment.metadata.generated.ts @@ -0,0 +1,7 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const PaymentMetadataMap = { + stripepayment: import("./stripepayment/_metadata"), +}; diff --git a/packages/app-store/paypal/minimal-metadata.ts b/packages/app-store/paypal/minimal-metadata.ts new file mode 100644 index 00000000000000..e9f448d794eb7f --- /dev/null +++ b/packages/app-store/paypal/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "paypal", + name: "paypal", + type: "paypal_payment", +}; diff --git a/packages/app-store/ping/minimal-metadata.ts b/packages/app-store/ping/minimal-metadata.ts new file mode 100644 index 00000000000000..b54b96018db3d3 --- /dev/null +++ b/packages/app-store/ping/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "ping", + name: "ping", + type: "ping_video", + appData: { + location: { + type: "integrations:ping_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/pipedream/minimal-metadata.ts b/packages/app-store/pipedream/minimal-metadata.ts new file mode 100644 index 00000000000000..b00d861ca47a5d --- /dev/null +++ b/packages/app-store/pipedream/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "pipedream", + name: "pipedream", + type: "pipedream_automation", +}; diff --git a/packages/app-store/pipedrive-crm/minimal-metadata.ts b/packages/app-store/pipedrive-crm/minimal-metadata.ts new file mode 100644 index 00000000000000..22ddf341da3d57 --- /dev/null +++ b/packages/app-store/pipedrive-crm/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "pipedrive-crm", + name: "pipedrive-crm", + type: "pipedrive-crm_crm", +}; diff --git a/packages/app-store/plausible/minimal-metadata.ts b/packages/app-store/plausible/minimal-metadata.ts new file mode 100644 index 00000000000000..4db3cf322b07a6 --- /dev/null +++ b/packages/app-store/plausible/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "plausible", + name: "plausible", + type: "plausible_analytics", +}; diff --git a/packages/app-store/posthog/minimal-metadata.ts b/packages/app-store/posthog/minimal-metadata.ts new file mode 100644 index 00000000000000..e3a28d15fa62e9 --- /dev/null +++ b/packages/app-store/posthog/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "posthog", + name: "posthog", + type: "posthog_analytics", +}; diff --git a/packages/app-store/qr_code/minimal-metadata.ts b/packages/app-store/qr_code/minimal-metadata.ts new file mode 100644 index 00000000000000..3d32dbc62b435e --- /dev/null +++ b/packages/app-store/qr_code/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "qr_code", + name: "qr_code", + type: "qr_code_other", +}; diff --git a/packages/app-store/raycast/minimal-metadata.ts b/packages/app-store/raycast/minimal-metadata.ts new file mode 100644 index 00000000000000..7040f50c67178c --- /dev/null +++ b/packages/app-store/raycast/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "raycast", + name: "raycast", + type: "raycast_other", +}; diff --git a/packages/app-store/retell-ai/minimal-metadata.ts b/packages/app-store/retell-ai/minimal-metadata.ts new file mode 100644 index 00000000000000..fe9e0d2c704ac2 --- /dev/null +++ b/packages/app-store/retell-ai/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "retell-ai", + name: "retell-ai", + type: "retell-ai_automation", +}; diff --git a/packages/app-store/riverside/minimal-metadata.ts b/packages/app-store/riverside/minimal-metadata.ts new file mode 100644 index 00000000000000..df67243cca537f --- /dev/null +++ b/packages/app-store/riverside/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "riverside", + name: "riverside", + type: "riverside_video", + appData: { + location: { + type: "integrations:riverside_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/roam/minimal-metadata.ts b/packages/app-store/roam/minimal-metadata.ts new file mode 100644 index 00000000000000..665f9e305b4bc8 --- /dev/null +++ b/packages/app-store/roam/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "roam", + name: "roam", + type: "roam_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/routing-forms/minimal-metadata.ts b/packages/app-store/routing-forms/minimal-metadata.ts new file mode 100644 index 00000000000000..ec704384122761 --- /dev/null +++ b/packages/app-store/routing-forms/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "routing-forms", + name: "routing-forms", + type: "routing-forms_other", +}; diff --git a/packages/app-store/salesforce/minimal-metadata.ts b/packages/app-store/salesforce/minimal-metadata.ts new file mode 100644 index 00000000000000..31a9314c7720e4 --- /dev/null +++ b/packages/app-store/salesforce/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "salesforce", + name: "salesforce", + type: "salesforce_crm", +}; diff --git a/packages/app-store/salesroom/minimal-metadata.ts b/packages/app-store/salesroom/minimal-metadata.ts new file mode 100644 index 00000000000000..ca2c3ff9a9dc76 --- /dev/null +++ b/packages/app-store/salesroom/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "salesroom", + name: "salesroom", + type: "salesroom_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/sendgrid/minimal-metadata.ts b/packages/app-store/sendgrid/minimal-metadata.ts new file mode 100644 index 00000000000000..ddefb014ba6c5a --- /dev/null +++ b/packages/app-store/sendgrid/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "sendgrid", + name: "sendgrid", + type: "sendgrid_other_calendar", +}; diff --git a/packages/app-store/shimmervideo/minimal-metadata.ts b/packages/app-store/shimmervideo/minimal-metadata.ts new file mode 100644 index 00000000000000..ab7c1cd9b82f00 --- /dev/null +++ b/packages/app-store/shimmervideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "shimmervideo", + name: "shimmervideo", + type: "shimmer_video", + appData: { + location: { + type: "integrations:shimmer_video", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/signal/minimal-metadata.ts b/packages/app-store/signal/minimal-metadata.ts new file mode 100644 index 00000000000000..2d09d6c806511d --- /dev/null +++ b/packages/app-store/signal/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "signal", + name: "signal", + type: "signal_video", + appData: { + location: { + type: "integrations:signal_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/sirius_video/minimal-metadata.ts b/packages/app-store/sirius_video/minimal-metadata.ts new file mode 100644 index 00000000000000..6e6b207023dad4 --- /dev/null +++ b/packages/app-store/sirius_video/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "sirius_video", + name: "sirius_video", + type: "sirius_video_video", + appData: { + location: { + type: "integrations:sirius_video_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/skype/minimal-metadata.ts b/packages/app-store/skype/minimal-metadata.ts new file mode 100644 index 00000000000000..6461df2a990c3e --- /dev/null +++ b/packages/app-store/skype/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "skype", + name: "skype", + type: "skype_conferencing", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/stripepayment/minimal-metadata.ts b/packages/app-store/stripepayment/minimal-metadata.ts new file mode 100644 index 00000000000000..7ef55135ca27a1 --- /dev/null +++ b/packages/app-store/stripepayment/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "stripe", + name: "stripepayment", + type: "stripe_payment", +}; diff --git a/packages/app-store/sylapsvideo/minimal-metadata.ts b/packages/app-store/sylapsvideo/minimal-metadata.ts new file mode 100644 index 00000000000000..d827f78cb80461 --- /dev/null +++ b/packages/app-store/sylapsvideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "sylapsvideo", + name: "sylapsvideo", + type: "sylaps_video", + appData: { + location: { + type: "integrations:sylaps_video", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/synthflow/minimal-metadata.ts b/packages/app-store/synthflow/minimal-metadata.ts new file mode 100644 index 00000000000000..1609a1dcb01021 --- /dev/null +++ b/packages/app-store/synthflow/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "synthflow", + name: "synthflow", + type: "synthflow_automation", +}; diff --git a/packages/app-store/tandemvideo/minimal-metadata.ts b/packages/app-store/tandemvideo/minimal-metadata.ts new file mode 100644 index 00000000000000..a6298f74ae410a --- /dev/null +++ b/packages/app-store/tandemvideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "tandem", + name: "tandemvideo", + type: "tandem_video", + appData: { + location: { + type: "integrations:tandem", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/telegram/minimal-metadata.ts b/packages/app-store/telegram/minimal-metadata.ts new file mode 100644 index 00000000000000..4ab29ae0fd5bbd --- /dev/null +++ b/packages/app-store/telegram/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "telegram", + name: "telegram", + type: "telegram_video", + appData: { + location: { + type: "integrations:telegram_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/telli/minimal-metadata.ts b/packages/app-store/telli/minimal-metadata.ts new file mode 100644 index 00000000000000..697981c7266510 --- /dev/null +++ b/packages/app-store/telli/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "telli", + name: "telli", + type: "telli_automation", +}; diff --git a/packages/app-store/templates/basic/minimal-metadata.ts b/packages/app-store/templates/basic/minimal-metadata.ts new file mode 100644 index 00000000000000..7bbbe2da2bdd16 --- /dev/null +++ b/packages/app-store/templates/basic/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "basic", + name: "basic", + type: "basic_other", +}; diff --git a/packages/app-store/templates/booking-pages-tag/minimal-metadata.ts b/packages/app-store/templates/booking-pages-tag/minimal-metadata.ts new file mode 100644 index 00000000000000..71d92be6004fbc --- /dev/null +++ b/packages/app-store/templates/booking-pages-tag/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "booking-pages-tag", + name: "booking-pages-tag", + type: "booking-pages-tag_analytics", +}; diff --git a/packages/app-store/templates/event-type-app-card/minimal-metadata.ts b/packages/app-store/templates/event-type-app-card/minimal-metadata.ts new file mode 100644 index 00000000000000..1671df3a391e9a --- /dev/null +++ b/packages/app-store/templates/event-type-app-card/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "event-type-app-card", + name: "event-type-app-card", + type: "event-type-app-card_other", +}; diff --git a/packages/app-store/templates/event-type-location-video-static/minimal-metadata.ts b/packages/app-store/templates/event-type-location-video-static/minimal-metadata.ts new file mode 100644 index 00000000000000..758817cf3c55e6 --- /dev/null +++ b/packages/app-store/templates/event-type-location-video-static/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "event-type-location-video-static", + name: "event-type-location-video-static", + type: "event-type-location-video-static_video", + appData: { + location: { + type: "integrations:{SLUG}_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/templates/general-app-settings/minimal-metadata.ts b/packages/app-store/templates/general-app-settings/minimal-metadata.ts new file mode 100644 index 00000000000000..c25e22005305b5 --- /dev/null +++ b/packages/app-store/templates/general-app-settings/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "general-app-settings", + name: "general-app-settings", + type: "general-app-settings_other", +}; diff --git a/packages/app-store/templates/link-as-an-app/minimal-metadata.ts b/packages/app-store/templates/link-as-an-app/minimal-metadata.ts new file mode 100644 index 00000000000000..ba404a230629d7 --- /dev/null +++ b/packages/app-store/templates/link-as-an-app/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "link-as-an-app", + name: "link-as-an-app", + type: "link-as-an-app_other", +}; diff --git a/packages/app-store/tests/minimal-metadata.ts b/packages/app-store/tests/minimal-metadata.ts new file mode 100644 index 00000000000000..8b335977be7d6b --- /dev/null +++ b/packages/app-store/tests/minimal-metadata.ts @@ -0,0 +1,3 @@ +export const metadata = { + name: "tests", +}; diff --git a/packages/app-store/twipla/minimal-metadata.ts b/packages/app-store/twipla/minimal-metadata.ts new file mode 100644 index 00000000000000..e643f9bc111192 --- /dev/null +++ b/packages/app-store/twipla/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "twipla", + name: "twipla", + type: "twipla_analytics", +}; diff --git a/packages/app-store/umami/minimal-metadata.ts b/packages/app-store/umami/minimal-metadata.ts new file mode 100644 index 00000000000000..1a6acb109e1d7e --- /dev/null +++ b/packages/app-store/umami/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "umami", + name: "umami", + type: "umami_analytics", +}; diff --git a/packages/app-store/utils-hybrid.ts b/packages/app-store/utils-hybrid.ts new file mode 100644 index 00000000000000..1dfb3c7d0eeb4a --- /dev/null +++ b/packages/app-store/utils-hybrid.ts @@ -0,0 +1,199 @@ +import type { AppCategories } from "@prisma/client"; + +import type { EventLocationType } from "@calcom/app-store/locations"; +import prisma from "@calcom/prisma"; +import type { AppMeta } from "@calcom/types/App"; +import type { CredentialForCalendarService } from "@calcom/types/Credential"; + +import { getAppWithMetadata } from "./_appRegistry"; + +export type LocationOption = { + label: string; + value: EventLocationType["type"]; + icon?: string; + disabled?: boolean; +}; + +export type CredentialDataWithTeamName = CredentialForCalendarService & { + team?: { + name: string; + } | null; +}; + +const appMetadataCache = new Map(); +const appSlugToMetadataCache = new Map(); + +export async function getAppFromSlug(slug: string | undefined): Promise { + if (!slug) return undefined; + + if (appSlugToMetadataCache.has(slug)) { + return appSlugToMetadataCache.get(slug); + } + + const metadata = await getAppWithMetadata({ slug }); + if (metadata) { + appSlugToMetadataCache.set(slug, metadata); + } + return metadata || undefined; +} + +export async function getAppName(slug: string): Promise { + const app = await getAppFromSlug(slug); + return app?.name ?? null; +} + +export async function getAppType(slug: string): Promise { + const app = await getAppFromSlug(slug); + if (!app) return "Unknown"; + + const type = app.type; + if (type.endsWith("_calendar")) { + return "Calendar"; + } + if (type.endsWith("_payment")) { + return "Payment"; + } + return "Unknown"; +} + +export async function getAppFromLocationValue(type: string): Promise { + const dbApps = await prisma.app.findMany({ + where: { enabled: true }, + select: { slug: true, dirName: true }, + }); + + for (const dbApp of dbApps) { + const metadata = await getAppWithMetadata({ slug: dbApp.slug }); + if (metadata?.appData?.location?.type === type) { + return metadata; + } + } + + return undefined; +} + +export async function getCalendarApps(): Promise { + const dbApps = await prisma.app.findMany({ + where: { + enabled: true, + categories: { has: "calendar" }, + }, + select: { slug: true, dirName: true }, + }); + + const calendarApps: AppMeta[] = []; + for (const dbApp of dbApps) { + const metadata = await getAppWithMetadata({ slug: dbApp.slug }); + if (metadata) { + calendarApps.push(metadata); + } + } + + return calendarApps; +} + +export async function getPaymentApps(): Promise { + const dbApps = await prisma.app.findMany({ + where: { + enabled: true, + categories: { has: "payment" }, + }, + select: { slug: true, dirName: true }, + }); + + const paymentApps: AppMeta[] = []; + for (const dbApp of dbApps) { + const metadata = await getAppWithMetadata({ slug: dbApp.slug }); + if (metadata) { + paymentApps.push(metadata); + } + } + + return paymentApps; +} + +export async function getAnalyticsApps(): Promise { + const dbApps = await prisma.app.findMany({ + where: { + enabled: true, + categories: { has: "analytics" }, + }, + select: { slug: true, dirName: true }, + }); + + const analyticsApps: AppMeta[] = []; + for (const dbApp of dbApps) { + const metadata = await getAppWithMetadata({ slug: dbApp.slug }); + if (metadata && metadata.appData?.tag) { + analyticsApps.push(metadata); + } + } + + return analyticsApps; +} + +export async function getLocationApps(): Promise { + const dbApps = await prisma.app.findMany({ + where: { enabled: true }, + select: { slug: true, dirName: true }, + }); + + const locationApps: AppMeta[] = []; + for (const dbApp of dbApps) { + const metadata = await getAppWithMetadata({ slug: dbApp.slug }); + if (metadata?.appData?.location) { + locationApps.push(metadata); + } + } + + return locationApps; +} + +export function getAppFromSlugSync(slug: string | undefined): AppMeta | undefined { + if (!slug) return undefined; + return appSlugToMetadataCache.get(slug); +} + +export function getAppNameSync(slug: string): string | null { + const app = getAppFromSlugSync(slug); + return app?.name ?? null; +} + +export function getAppTypeSync(slug: string): string { + const app = getAppFromSlugSync(slug); + if (!app) return "Unknown"; + + const type = app.type; + if (type.endsWith("_calendar")) { + return "Calendar"; + } + if (type.endsWith("_payment")) { + return "Payment"; + } + return "Unknown"; +} + +export function doesAppSupportTeamInstall({ + appCategories, + concurrentMeetings = undefined, + isPaid, +}: { + appCategories: string[]; + concurrentMeetings: boolean | undefined; + isPaid: boolean; +}) { + if (isPaid) { + return false; + } + return !appCategories.some( + (category) => + category === "calendar" || + (defaultVideoAppCategories.includes(category as AppCategories) && !concurrentMeetings) + ); +} + +export function isConferencing(appCategories: string[]) { + return appCategories.some((category) => category === "conferencing" || category === "video"); +} + +export const defaultVideoAppCategories: AppCategories[] = ["messaging", "conferencing", "video"]; diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 0c48c600d0a70b..e757aa04dc9c86 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -19,6 +19,12 @@ export type LocationOption = { disabled?: boolean; }; +export type CredentialDataWithTeamName = CredentialForCalendarService & { + team?: { + name: string; + } | null; +}; + const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { const metadata = appStoreMetadata[key as keyof typeof appStoreMetadata] as AppMeta; @@ -33,12 +39,6 @@ const ALL_APPS_MAP = Object.keys(appStoreMetadata).reduce((store, key) => { return store; }, {} as Record); -export type CredentialDataWithTeamName = CredentialForCalendarService & { - team?: { - name: string; - } | null; -}; - export const ALL_APPS = Object.values(ALL_APPS_MAP); /** @@ -115,11 +115,25 @@ export function hasIntegrationInstalled(type: App["type"]): boolean { return ALL_APPS.some((app) => app.type === type && !!app.installed); } -export function getAppName(name: string): string | null { +export { + getAppFromSlug, + getAppName, + getAppType, + getAppFromLocationValue, + getCalendarApps, + getPaymentApps, + getAnalyticsApps, + getLocationApps, + getAppFromSlugSync, + getAppNameSync, + getAppTypeSync, +} from "./utils-hybrid"; + +export function getAppNameLegacy(name: string): string | null { return ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP]?.name ?? null; } -export function getAppType(name: string): string { +export function getAppTypeLegacy(name: string): string { const type = ALL_APPS_MAP[name as keyof typeof ALL_APPS_MAP].type; if (type.endsWith("_calendar")) { @@ -131,11 +145,11 @@ export function getAppType(name: string): string { return "Unknown"; } -export function getAppFromSlug(slug: string | undefined): AppMeta | undefined { +export function getAppFromSlugLegacy(slug: string | undefined): AppMeta | undefined { return ALL_APPS.find((app) => app.slug === slug); } -export function getAppFromLocationValue(type: string): AppMeta | undefined { +export function getAppFromLocationValueLegacy(type: string): AppMeta | undefined { return ALL_APPS.find((app) => app?.appData?.location?.type === type); } diff --git a/packages/app-store/video.metadata.generated.ts b/packages/app-store/video.metadata.generated.ts new file mode 100644 index 00000000000000..c8de757ea57b43 --- /dev/null +++ b/packages/app-store/video.metadata.generated.ts @@ -0,0 +1,13 @@ +/** + This file is autogenerated using the command `yarn app-store:build --watch`. + Don't modify this file manually. +**/ +export const VideoMetadataMap = { + dailyvideo: import("./dailyvideo/_metadata"), + googlevideo: import("./googlevideo/_metadata"), + huddle01video: import("./huddle01video/_metadata"), + jitsivideo: import("./jitsivideo/_metadata"), + office365video: import("./office365video/_metadata"), + tandemvideo: import("./tandemvideo/_metadata"), + zoomvideo: import("./zoomvideo/_metadata"), +}; diff --git a/packages/app-store/vimcal/minimal-metadata.ts b/packages/app-store/vimcal/minimal-metadata.ts new file mode 100644 index 00000000000000..3834716fc3a553 --- /dev/null +++ b/packages/app-store/vimcal/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "vimcal", + name: "vimcal", + type: "vimcal_other", +}; diff --git a/packages/app-store/vital/minimal-metadata.ts b/packages/app-store/vital/minimal-metadata.ts new file mode 100644 index 00000000000000..8763fcf2ac65cc --- /dev/null +++ b/packages/app-store/vital/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "vital-automation", + name: "vital", + type: "vital_other", +}; diff --git a/packages/app-store/weather_in_your_calendar/minimal-metadata.ts b/packages/app-store/weather_in_your_calendar/minimal-metadata.ts new file mode 100644 index 00000000000000..b40f01c65af7f4 --- /dev/null +++ b/packages/app-store/weather_in_your_calendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "weather_in_your_calendar", + name: "weather_in_your_calendar", + type: "weather_in_your_calendar_other", +}; diff --git a/packages/app-store/webex/minimal-metadata.ts b/packages/app-store/webex/minimal-metadata.ts new file mode 100644 index 00000000000000..0b0d4a4e1f3cca --- /dev/null +++ b/packages/app-store/webex/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "webex", + name: "webex", + type: "webex_video", + appData: { + location: { + type: "integrations:webex_video", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/app-store/whatsapp/minimal-metadata.ts b/packages/app-store/whatsapp/minimal-metadata.ts new file mode 100644 index 00000000000000..a4f75b5d32e706 --- /dev/null +++ b/packages/app-store/whatsapp/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "whatsapp", + name: "whatsapp", + type: "whatsapp_video", + appData: { + location: { + type: "integrations:whatsapp_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/whereby/minimal-metadata.ts b/packages/app-store/whereby/minimal-metadata.ts new file mode 100644 index 00000000000000..21b0df03f598b4 --- /dev/null +++ b/packages/app-store/whereby/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "whereby", + name: "whereby", + type: "whereby_video", + appData: { + location: { + type: "integrations:whereby_video", + linkType: "static", + }, + }, +}; diff --git a/packages/app-store/wipemycalother/minimal-metadata.ts b/packages/app-store/wipemycalother/minimal-metadata.ts new file mode 100644 index 00000000000000..6171f3006fb6f4 --- /dev/null +++ b/packages/app-store/wipemycalother/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "wipe-my-cal", + name: "wipemycalother", + type: "wipemycal_other", +}; diff --git a/packages/app-store/wordpress/minimal-metadata.ts b/packages/app-store/wordpress/minimal-metadata.ts new file mode 100644 index 00000000000000..5f43ebadc8c362 --- /dev/null +++ b/packages/app-store/wordpress/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "wordpress", + name: "wordpress", + type: "wordpress_other", +}; diff --git a/packages/app-store/zapier/minimal-metadata.ts b/packages/app-store/zapier/minimal-metadata.ts new file mode 100644 index 00000000000000..219f3a62591538 --- /dev/null +++ b/packages/app-store/zapier/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "zapier", + name: "zapier", + type: "zapier_automation", +}; diff --git a/packages/app-store/zoho-bigin/minimal-metadata.ts b/packages/app-store/zoho-bigin/minimal-metadata.ts new file mode 100644 index 00000000000000..06b2231a726e30 --- /dev/null +++ b/packages/app-store/zoho-bigin/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "zoho-bigin", + name: "zoho-bigin", + type: "zoho-bigin_crm", +}; diff --git a/packages/app-store/zohocalendar/minimal-metadata.ts b/packages/app-store/zohocalendar/minimal-metadata.ts new file mode 100644 index 00000000000000..f6fce405be1a05 --- /dev/null +++ b/packages/app-store/zohocalendar/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "zohocalendar", + name: "zohocalendar", + type: "zoho_calendar", +}; diff --git a/packages/app-store/zohocrm/minimal-metadata.ts b/packages/app-store/zohocrm/minimal-metadata.ts new file mode 100644 index 00000000000000..6f366d433dfd68 --- /dev/null +++ b/packages/app-store/zohocrm/minimal-metadata.ts @@ -0,0 +1,5 @@ +export const metadata = { + slug: "zohocrm", + name: "zohocrm", + type: "zohocrm_crm", +}; diff --git a/packages/app-store/zoomvideo/minimal-metadata.ts b/packages/app-store/zoomvideo/minimal-metadata.ts new file mode 100644 index 00000000000000..c1b68a58258041 --- /dev/null +++ b/packages/app-store/zoomvideo/minimal-metadata.ts @@ -0,0 +1,11 @@ +export const metadata = { + slug: "zoom", + name: "zoomvideo", + type: "zoom_video", + appData: { + location: { + type: "integrations:zoom", + linkType: "dynamic", + }, + }, +}; diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index ff1ab0698adf56..cdfed1d3773417 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -126,23 +126,27 @@ const _sendScheduledEmailsAndSMS = async ( } if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) { - emailsToSend.push( - ...formattedCalEvent.attendees.map((attendee) => { - return sendEmail( + for (const attendee of formattedCalEvent.attendees) { + let eventTitle = formattedCalEvent.title; + if (eventNameObject) { + const generatedTitle = await getEventName({ ...eventNameObject, t: attendee.language.translate }); + eventTitle = generatedTitle ?? formattedCalEvent.title; + } + + emailsToSend.push( + sendEmail( () => new AttendeeScheduledEmail( { ...formattedCalEvent, ...(formattedCalEvent.hideCalendarNotes && { additionalNotes: undefined }), - ...(eventNameObject && { - title: getEventName({ ...eventNameObject, t: attendee.language.translate }), - }), + title: eventTitle, }, attendee ) - ); - }) - ); + ) + ); + } } await Promise.all(emailsToSend); @@ -503,29 +507,32 @@ export const sendCancelledEmailsAndSMS = async ( } if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { - emailsToSend.push( - ...calendarEvent.attendees.map((attendee) => { - return sendEmail( + for (const attendee of calendarEvent.attendees) { + const generatedTitle = await getEventName({ + ...eventNameObject, + t: attendee.language.translate, + attendeeName: attendee.name, + host: calendarEvent.organizer.name, + eventType: calendarEvent.title, + eventDuration, + }); + const eventTitle = generatedTitle ?? calendarEvent.title; + + emailsToSend.push( + sendEmail( () => new AttendeeCancelledEmail( { ...calendarEvent, - title: getEventName({ - ...eventNameObject, - t: attendee.language.translate, - attendeeName: attendee.name, - host: calendarEvent.organizer.name, - eventType: calendarEvent.title, - eventDuration, - ...(calendarEvent.responses && { bookingFields: calendarEvent.responses }), - ...(calendarEvent.location && { location: calendarEvent.location }), - }), + title: eventTitle, + ...(calendarEvent.responses && { bookingFields: calendarEvent.responses }), + ...(calendarEvent.location && { location: calendarEvent.location }), }, attendee ) - ); - }) - ); + ) + ); + } } await Promise.all(emailsToSend); diff --git a/packages/emails/src/components/LocationInfo.tsx b/packages/emails/src/components/LocationInfo.tsx index 999d523debf2b7..f257d9761df7f2 100644 --- a/packages/emails/src/components/LocationInfo.tsx +++ b/packages/emails/src/components/LocationInfo.tsx @@ -1,4 +1,5 @@ import type { TFunction } from "i18next"; +import React from "react"; import { guessEventLocationType } from "@calcom/app-store/locations"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; @@ -10,7 +11,13 @@ export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) { const { t } = props; // We would not be able to determine provider name for DefaultEventLocationTypes - const providerName = guessEventLocationType(props.calEvent.location)?.label; + const [providerName, setProviderName] = React.useState(); + + React.useEffect(() => { + guessEventLocationType(props.calEvent.location).then((locationType) => { + setProviderName(locationType?.label); + }); + }, [props.calEvent.location]); const location = props.calEvent.location; let meetingUrl = location?.search(/^https?:/) !== -1 ? location : undefined; diff --git a/packages/features/apps/components/AppList.tsx b/packages/features/apps/components/AppList.tsx index 8b556ce75ff4a0..f0fbf093c402da 100644 --- a/packages/features/apps/components/AppList.tsx +++ b/packages/features/apps/components/AppList.tsx @@ -105,8 +105,8 @@ export const AppList = ({ type="button" color="secondary" StartIcon="video" - onClick={() => { - const locationType = getLocationFromApp(item?.locationOption?.value ?? ""); + onClick={async () => { + const locationType = await getLocationFromApp(item?.locationOption?.value ?? ""); if (locationType?.linkType === "static") { setLocationType({ ...locationType, slug: appSlug }); } else { diff --git a/packages/features/apps/components/AppSetDefaultLinkDialog.tsx b/packages/features/apps/components/AppSetDefaultLinkDialog.tsx index e4f501c7426fa4..8e4510778a06fb 100644 --- a/packages/features/apps/components/AppSetDefaultLinkDialog.tsx +++ b/packages/features/apps/components/AppSetDefaultLinkDialog.tsx @@ -4,7 +4,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import type { EventLocationType } from "@calcom/app-store/locations"; -import { getEventLocationType } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync } from "@calcom/app-store/locations"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Button } from "@calcom/ui/components/button"; @@ -37,7 +37,7 @@ export function AppSetDefaultLinkDialog({ handleUpdateUserDefaultConferencingApp: (params: UpdateUsersDefaultConferencingAppParams) => void; }) { const { t } = useLocale(); - const eventLocationTypeOptions = getEventLocationType(locationType.type); + const eventLocationTypeOptions = getEventLocationTypeSync(locationType.type); const form = useForm({ resolver: zodResolver( diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx index 313bee12877d8b..308b37863e1677 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookingFields.tsx @@ -1,7 +1,7 @@ import { useFormContext } from "react-hook-form"; import type { LocationObject } from "@calcom/app-store/locations"; -import { getOrganizerInputLocationTypes } from "@calcom/app-store/locations"; +import { getOrganizerInputLocationTypesSync } from "@calcom/app-store/locations"; import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; @@ -112,7 +112,7 @@ export const BookingFields = ({ } if (field?.options) { - const organizerInputTypes = getOrganizerInputLocationTypes(); + const organizerInputTypes = getOrganizerInputLocationTypesSync(); const organizerInputObj: Record = {}; field.options.forEach((f) => { diff --git a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx index 1295d0edb1cc6a..747ed751ffdab3 100644 --- a/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx +++ b/packages/features/bookings/components/event-meta/AvailableEventLocations.tsx @@ -3,7 +3,7 @@ import type { EventLocationTypeFromApp, LocationObject, } from "@calcom/app-store/locations"; -import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync, getTranslatedLocationSync } from "@calcom/app-store/locations"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import invertLogoOnDark from "@calcom/lib/invertLogoOnDark"; @@ -43,12 +43,12 @@ function RenderLocationTooltip({ locations }: { locations: LocationObject[] }) { Omit, index: number ) => { - const eventLocationType = getEventLocationType(location.type); + const eventLocationType = getEventLocationTypeSync(location.type); if (!eventLocationType) { return null; } const translatedLocation = - location.customLabel || getTranslatedLocation(location, eventLocationType, t); + location.customLabel || getTranslatedLocationSync(location, eventLocationType, t); return (
@@ -70,13 +70,13 @@ export function AvailableEventLocations({ locations }: { locations: LocationObje location: Pick, "link" | "address"> & Omit, index: number ) => { - const eventLocationType = getEventLocationType(location.type); + const eventLocationType = getEventLocationTypeSync(location.type); if (!eventLocationType) { // It's possible that the location app got uninstalled return null; } - const locationName = location?.customLabel || getTranslatedLocation(location, eventLocationType, t); + const locationName = location?.customLabel || getTranslatedLocationSync(location, eventLocationType, t); return (
diff --git a/packages/features/bookings/lib/getLocationOptionsForSelect.ts b/packages/features/bookings/lib/getLocationOptionsForSelect.ts index 00c6f6cac240a3..dbc8c4118fb9ee 100644 --- a/packages/features/bookings/lib/getLocationOptionsForSelect.ts +++ b/packages/features/bookings/lib/getLocationOptionsForSelect.ts @@ -1,7 +1,7 @@ import type { LocationObject } from "@calcom/app-store/locations"; import { locationKeyToString } from "@calcom/app-store/locations"; -import { getEventLocationType } from "@calcom/app-store/locations"; -import { getTranslatedLocation } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync } from "@calcom/app-store/locations"; +import { getTranslatedLocationSync } from "@calcom/app-store/locations"; import type { useLocale } from "@calcom/lib/hooks/useLocale"; import notEmpty from "@calcom/lib/notEmpty"; @@ -11,7 +11,7 @@ export default function getLocationsOptionsForSelect( ) { return locations .map((location) => { - const eventLocation = getEventLocationType(location.type); + const eventLocation = getEventLocationTypeSync(location.type); const locationString = locationKeyToString(location); if (typeof locationString !== "string" || !eventLocation) { @@ -19,7 +19,8 @@ export default function getLocationsOptionsForSelect( return null; } const type = eventLocation.type; - const translatedLocation = location.customLabel || getTranslatedLocation(location, eventLocation, t); + const translatedLocation = + location.customLabel || getTranslatedLocationSync(location, eventLocation, t); return { // XYZ: is considered a namespace in i18next https://www.i18next.com/principles/namespaces and thus it gets cleaned up. diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index eed351c7b972eb..8c16395e6f0894 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -11,7 +11,7 @@ import { MeetLocationType, OrganizerDefaultConferencingAppType, } from "@calcom/app-store/locations"; -import { getAppFromSlug } from "@calcom/app-store/utils"; +import { getAppFromSlugSync } from "@calcom/app-store/utils"; import dayjs from "@calcom/dayjs"; import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder"; import { @@ -1067,7 +1067,7 @@ async function handler( const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined; if (organizerMetadata?.defaultConferencingApp?.appSlug) { - const app = getAppFromSlug(organizerMetadata?.defaultConferencingApp?.appSlug); + const app = getAppFromSlugSync(organizerMetadata?.defaultConferencingApp?.appSlug); locationBodyString = app?.appData?.location?.type || locationBodyString; if (isManagedEventType || isTeamEventType) { organizerOrFirstDynamicGroupMemberDefaultLocationUrl = @@ -1195,12 +1195,12 @@ async function handler( optionValue: "", }; - const eventName = getEventName(eventNameObject); + const eventName = await getEventName(eventNameObject); const builtEvt = new CalendarEventBuilder() .withBasicDetails({ bookerUrl, - title: eventName, + title: eventName || "", startTime: dayjs(reqBody.start).utc().format(), endTime: dayjs(reqBody.end).utc().format(), additionalNotes, @@ -1610,7 +1610,7 @@ async function handler( const { booking: dryRunBooking, troubleshooterData: _troubleshooterData } = buildDryRunBooking({ eventTypeId, organizerUser, - eventName, + eventName: eventName || "", startTime: reqBody.start, endTime: reqBody.end, contactOwnerFromReq, diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 1ed40527bc692d..90df4a6ed766f0 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -161,7 +161,7 @@ export const roundRobinManualReassignment = async ({ const isManagedEventType = !!eventType.parentId; const isTeamEventType = !!eventType.teamId; - const locationResult = BookingLocationService.getLocationForHost({ + const locationResult = await BookingLocationService.getLocationForHost({ hostMetadata: newUser.metadata, eventTypeLocations: eventType.locations, isManagedEventType, @@ -189,7 +189,7 @@ export const roundRobinManualReassignment = async ({ where: { id: bookingId }, data: { userId: newUserId, - title: newBookingTitle, + title: await newBookingTitle, userPrimaryEmail: newUser.email, reassignReason, reassignById: reassignedById, diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index 476a466fd64675..afcc7ff01a2b46 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -242,7 +242,8 @@ export const roundRobinReassignment = async ({ t: organizerT, }; - newBookingTitle = getEventName(eventNameObject); + const eventNameResult = await getEventName(eventNameObject); + newBookingTitle = eventNameResult || ""; booking = await prisma.booking.update({ where: { diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 9b314684161d8d..6bf2a727314b6b 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -228,34 +228,40 @@ export async function handler(req: NextRequest) { ? !!reminder.booking.eventType?.team?.hideBranding : !!reminder.booking.user?.hideBranding; - const emailSubject = customTemplate( - reminder.workflowStep.emailSubject || "", - variables, - emailLocale, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - brandingDisabled + const emailSubject = ( + await customTemplate( + reminder.workflowStep.emailSubject || "", + variables, + emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), + brandingDisabled + ) ).text; emailContent.emailSubject = emailSubject; - emailContent.emailBody = customTemplate( - reminder.workflowStep.reminderBody || "", - variables, - emailLocale, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - brandingDisabled - ).html; - - emailBodyEmpty = - customTemplate( + emailContent.emailBody = ( + await customTemplate( reminder.workflowStep.reminderBody || "", variables, emailLocale, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), + brandingDisabled + ) + ).html; + + emailBodyEmpty = + ( + await customTemplate( + reminder.workflowStep.reminderBody || "", + variables, + emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) + ) ).text.length === 0; } else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) { const brandingDisabled = reminder.booking.eventType?.team ? !!reminder.booking.eventType?.team?.hideBranding : !!reminder.booking.user?.hideBranding; - emailContent = emailReminderTemplate({ + emailContent = await emailReminderTemplate({ isEditingMode: false, locale: reminder.booking.user?.locale || "en", t: await getTranslation(reminder.booking.user?.locale ?? "en", "common"), @@ -418,7 +424,7 @@ export async function handler(req: NextRequest) { ? !!reminder.booking.eventType?.team?.hideBranding : !!reminder.booking.user?.hideBranding; - emailContent = emailReminderTemplate({ + emailContent = await emailReminderTemplate({ isEditingMode: false, locale: reminder.booking.user?.locale || "en", t: await getTranslation(reminder.booking.user?.locale ?? "en", "common"), diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index d3ed23d6a14db5..09da4d9850e883 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -149,7 +149,7 @@ export async function handler(req: NextRequest) { reminder.booking.attendees[0].timeZone ), }; - const customMessage = customTemplate( + const customMessage = await customTemplate( reminder.workflowStep.reminderBody || "", variables, locale || "en", diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index abec96ad6435c4..83d1f985c0cedd 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -61,7 +61,7 @@ import { import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants"; import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions"; import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate"; -import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; +import { emailReminderTemplateSync } from "../lib/reminders/templates/emailReminderTemplate"; import type { FormValues } from "../pages/workflow"; import { AgentConfigurationSheet } from "./AgentConfigurationSheet"; import { TestAgentDialog } from "./TestAgentDialog"; @@ -248,14 +248,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { // Skip setting reminderBody for CAL_AI actions since they don't need email templates if (!isCalAIAction(action)) { - const template = getTemplateBodyForAction({ + getTemplateBodyForAction({ action, locale: i18n.language, t, template: step.template ?? WorkflowTemplates.REMINDER, timeFormat, + }).then((template) => { + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); }); - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); } } @@ -263,14 +264,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const action = form.getValues(`steps.${step.stepNumber - 1}.action`); // Skip setting emailSubject for CAL_AI actions since they don't need email subjects if (!isCalAIAction(action)) { - const subjectTemplate = emailReminderTemplate({ + const subjectTemplate = emailReminderTemplateSync({ isEditingMode: true, locale: i18n.language, t, action: action, timeFormat, - }).emailSubject; - form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate); + evt: { + title: "Sample Event", + eventType: { title: "Sample Event" }, + startTime: new Date(), + location: "", + }, + }); + form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate.emailSubject); } } @@ -582,16 +589,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (val) { const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`); - const template = getTemplateBodyForAction({ + getTemplateBodyForAction({ action: val.value, locale: i18n.language, t, template: WorkflowTemplates.REMINDER, timeFormat, + }).then((template) => { + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); }); - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); - const setNumberRequiredConfigs = ( phoneNumberIsNeeded: boolean, senderNeeded = true @@ -1046,27 +1053,34 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { if (val) { const action = form.getValues(`steps.${step.stepNumber - 1}.action`); - const template = getTemplateBodyForAction({ + getTemplateBodyForAction({ action, locale: i18n.language, t, template: val.value ?? WorkflowTemplates.REMINDER, timeFormat, + }).then((template) => { + form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); }); - form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template); - if (shouldScheduleEmailReminder(action)) { if (val.value === WorkflowTemplates.REMINDER) { + const subjectTemplate = emailReminderTemplateSync({ + isEditingMode: true, + locale: i18n.language, + t, + action, + timeFormat, + evt: { + title: "Sample Event", + eventType: { title: "Sample Event" }, + startTime: new Date(), + location: "", + }, + }); form.setValue( `steps.${step.stepNumber - 1}.emailSubject`, - emailReminderTemplate({ - isEditingMode: true, - locale: i18n.language, - t, - action, - timeFormat, - }).emailSubject + subjectTemplate.emailSubject ); } else if (val.value === WorkflowTemplates.RATING) { form.setValue( diff --git a/packages/features/ee/workflows/lib/actionHelperFunctions.ts b/packages/features/ee/workflows/lib/actionHelperFunctions.ts index 63d01398f84801..c68a404a5ea4bb 100644 --- a/packages/features/ee/workflows/lib/actionHelperFunctions.ts +++ b/packages/features/ee/workflows/lib/actionHelperFunctions.ts @@ -115,7 +115,7 @@ export function getWhatsappTemplateForAction( return templateFunction(true, locale, action, timeFormat); } -export function getTemplateBodyForAction({ +export async function getTemplateBodyForAction({ action, locale, t, @@ -127,7 +127,7 @@ export function getTemplateBodyForAction({ t: TFunction; template: WorkflowTemplates; timeFormat: TimeFormat; -}): string | null { +}): Promise { if (isSMSAction(action)) { return smsReminderTemplate(true, locale, action, timeFormat); } @@ -139,5 +139,6 @@ export function getTemplateBodyForAction({ // If not a whatsapp action then it's an email action const templateFunction = getEmailTemplateFunction(template); - return templateFunction({ isEditingMode: true, locale, t, action, timeFormat }).emailBody; + const result = await templateFunction({ isEditingMode: true, locale, t, action, timeFormat }); + return result.emailBody; } diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 84ae208d5fc418..63a932951efdd1 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -181,16 +181,19 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => : evt.organizer.language.locale; const emailSubjectTemplate = customTemplate(emailSubject, variables, locale, evt.organizer.timeFormat); - emailContent.emailSubject = emailSubjectTemplate.text; - emailContent.emailBody = customTemplate( + const emailSubjectResult = await emailSubjectTemplate; + emailContent.emailSubject = emailSubjectResult.text; + const emailBodyTemplate = customTemplate( emailBody, variables, locale, evt.organizer.timeFormat, hideBranding - ).html; + ); + const emailBodyResult = await emailBodyTemplate; + emailContent.emailBody = emailBodyResult.html; } else if (template === WorkflowTemplates.REMINDER) { - emailContent = emailReminderTemplate({ + emailContent = await emailReminderTemplate({ isEditingMode: false, locale: evt.organizer.language.locale, t: await getTranslation(evt.organizer.language.locale || "en", "common"), diff --git a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts index 1966c2eb7ce590..106d9708629ab9 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts @@ -35,7 +35,7 @@ function replaceVariablePlaceholders(text: string) { return text.replace(/\{([A-Z0-9_]+)_VARIABLE}/g, (_, base) => `{${base}}`); } -const customTemplate = ( +const customTemplate = async ( text: string, variables: VariablesType, locale: string, @@ -54,7 +54,8 @@ const customTemplate = ( text = replaceVariablePlaceholders(text); if (text.includes("{LOCATION}")) { - locationString = guessEventLocationType(locationString)?.label || locationString; + const locationType = await guessEventLocationType(locationString); + locationString = locationType?.label || locationString; } const cancelLink = variables.cancelLink ?? ""; diff --git a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts index abc41f3e07df7f..e4847e8bb064ac 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/emailReminderTemplate.ts @@ -1,12 +1,12 @@ import type { TFunction } from "i18next"; -import { guessEventLocationType } from "@calcom/app-store/locations"; +import { guessEventLocationType, guessEventLocationTypeSync } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import { APP_NAME } from "@calcom/lib/constants"; import { TimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions } from "@calcom/prisma/enums"; -const emailReminderTemplate = ({ +const emailReminderTemplate = async ({ isEditingMode, locale, t, @@ -41,7 +41,8 @@ const emailReminderTemplate = ({ const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`; let eventDate = ""; - let locationString = `${guessEventLocationType(location)?.label || location} ${meetingUrl}`; + const locationType = await guessEventLocationType(location); + let locationString = `${locationType?.label || location} ${meetingUrl}`; if (isEditingMode) { endTime = "{EVENT_END_TIME}"; @@ -89,6 +90,57 @@ const emailReminderTemplate = ({ return { emailSubject, emailBody }; }; +export type EmailReminderTemplateProps = { + isEditingMode: boolean; + action?: WorkflowActions; + evt: { + title?: string; + eventType?: { title?: string }; + startTime: Date; + location?: string; + }; + locale: string; + t: TFunction; + timeFormat?: TimeFormat; +}; + +const formatTime = (date: Date, timeFormat?: TimeFormat, locale?: string) => { + return dayjs(date) + .locale(locale || "en") + .format(timeFormat === TimeFormat.TWELVE_HOUR ? "h:mm A" : "HH:mm"); +}; + +export const emailReminderTemplateSync = ({ + isEditingMode, + action, + evt, + locale, + t, + timeFormat, +}: EmailReminderTemplateProps) => { + const eventLocationType = guessEventLocationTypeSync(evt.location); + const locationLabel = eventLocationType?.label || evt.location || ""; + + const emailSubject = t("reminder_email_subject", { + eventName: evt.title || evt.eventType?.title, + date: formatTime(evt.startTime, timeFormat, locale), + }); + + const emailBody = isEditingMode + ? t("reminder_email_body_editing", { + eventName: evt.title || evt.eventType?.title, + date: formatTime(evt.startTime, timeFormat, locale), + location: locationLabel, + }) + : t("reminder_email_body", { + eventName: evt.title || evt.eventType?.title, + date: formatTime(evt.startTime, timeFormat, locale), + location: locationLabel, + }); + + return { emailSubject, emailBody }; +}; + export default emailReminderTemplate; export const plainTextTemplate = `Hi {ORGANIZER},This is a reminder about your upcoming event.Event: {EVENT_NAME}Date & Time: {EVENT_DATE_ddd, MMM D, YYYY h:mma} - {EVENT_END_TIME} ({TIMEZONE})Attendees: You & {ATTENDEE}Location: {LOCATION} {MEETING_URL}This reminder was triggered by a Workflow in Cal.`; diff --git a/packages/features/ee/workflows/lib/reminders/utils.ts b/packages/features/ee/workflows/lib/reminders/utils.ts index 2ff24bcf0b9f09..f2b0e9421fdb33 100644 --- a/packages/features/ee/workflows/lib/reminders/utils.ts +++ b/packages/features/ee/workflows/lib/reminders/utils.ts @@ -90,7 +90,8 @@ export const getSMSMessageWithVariables = async ( : evt.organizer.language.locale; const customMessage = customTemplate(smsMessage, variables, locale, evt.organizer.timeFormat); - smsMessage = customMessage.text; + const customMessageResult = await customMessage; + smsMessage = customMessageResult.text; return smsMessage; }; diff --git a/packages/features/eventtypes/components/locations/DefaultLocationSettings.tsx b/packages/features/eventtypes/components/locations/DefaultLocationSettings.tsx index 4cfd3452f29e93..e2b69c9ab4664e 100644 --- a/packages/features/eventtypes/components/locations/DefaultLocationSettings.tsx +++ b/packages/features/eventtypes/components/locations/DefaultLocationSettings.tsx @@ -1,7 +1,7 @@ import { ErrorMessage } from "@hookform/error-message"; import { useFieldArray, useFormContext } from "react-hook-form"; -import { getEventLocationType } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync } from "@calcom/app-store/locations"; import type { LocationFormValues, FormValues } from "@calcom/features/eventtypes/lib/types"; import CheckboxField from "@calcom/features/form/components/CheckboxField"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -30,7 +30,7 @@ const DefaultLocationSettings = ({ name: "locations", }); const defaultLocation = field; - const eventLocationType = getEventLocationType(field.type); + const eventLocationType = getEventLocationTypeSync(field.type); if (!eventLocationType) return null; diff --git a/packages/features/eventtypes/components/locations/Locations.tsx b/packages/features/eventtypes/components/locations/Locations.tsx index c5fc7546e4b92c..b5018755fc985e 100644 --- a/packages/features/eventtypes/components/locations/Locations.tsx +++ b/packages/features/eventtypes/components/locations/Locations.tsx @@ -6,7 +6,7 @@ import { useFieldArray, useFormContext } from "react-hook-form"; import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form"; import type { EventLocationType } from "@calcom/app-store/locations"; -import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync, MeetLocationType } from "@calcom/app-store/locations"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import type { LocationFormValues, @@ -135,7 +135,7 @@ const Locations: React.FC = ({ const validLocations = getValues("locations")?.filter((location) => { - const eventLocation = getEventLocationType(location.type); + const eventLocation = getEventLocationTypeSync(location.type); if (!eventLocation) { // It's possible that the location app in use got uninstalled. return false; @@ -181,7 +181,7 @@ const Locations: React.FC = ({
    {locationFields.map((field, index) => { - const eventLocationType = getEventLocationType(field.type); + const eventLocationType = getEventLocationTypeSync(field.type); const defaultLocation = field; const isCalVideo = field.type === "integrations:daily"; @@ -207,7 +207,7 @@ const Locations: React.FC = ({ setShowEmptyLocationSelect(false); if (e?.value) { const newLocationType = e.value; - const eventLocationType = getEventLocationType(newLocationType); + const eventLocationType = getEventLocationTypeSync(newLocationType); if (!eventLocationType) { return; } @@ -352,7 +352,7 @@ const Locations: React.FC = ({ setShowEmptyLocationSelect(false); if (e?.value) { const newLocationType = e.value; - const eventLocationType = getEventLocationType(newLocationType); + const eventLocationType = getEventLocationTypeSync(newLocationType); if (!eventLocationType) { return; } diff --git a/packages/features/eventtypes/components/tabs/advanced/CustomEventTypeModal.tsx b/packages/features/eventtypes/components/tabs/advanced/CustomEventTypeModal.tsx index bcc382e045d5ce..e538952879cb97 100644 --- a/packages/features/eventtypes/components/tabs/advanced/CustomEventTypeModal.tsx +++ b/packages/features/eventtypes/components/tabs/advanced/CustomEventTypeModal.tsx @@ -6,7 +6,7 @@ import { useForm, useFormContext } from "react-hook-form"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import type { InputClassNames } from "@calcom/features/eventtypes/lib/types"; import type { EventNameObjectType } from "@calcom/lib/event"; -import { getEventName, validateCustomEventName } from "@calcom/lib/event"; +import { getEventNameSync, validateCustomEventName } from "@calcom/lib/event"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; @@ -65,7 +65,7 @@ const CustomEventTypeModalForm: FC = (props) => { close(); }; - const previewText = getEventName({ ...event, eventName: watch("customEventName") }); + const previewText = getEventNameSync({ ...event, eventName: watch("customEventName") }); const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder; return ( diff --git a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx index 85930f7cfbe39a..e6d7ac0044b731 100644 --- a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx +++ b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx @@ -39,7 +39,7 @@ import { MAX_SEATS_PER_TIME_SLOT, } from "@calcom/lib/constants"; import type { EventNameObjectType } from "@calcom/lib/event"; -import { getEventName } from "@calcom/lib/event"; +import { getEventNameSync } from "@calcom/lib/event"; import { generateHashedLink } from "@calcom/lib/generateHashedLink"; import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; import { extractHostTimezone } from "@calcom/lib/hashedLinksUtils"; @@ -484,7 +484,7 @@ export const EventAdvancedTab = ({ translate: t, formMethods, }); - const eventNamePlaceholder = getEventName({ + const eventNamePlaceholder = getEventNameSync({ ...eventNameObject, eventName: formMethods.watch("eventName"), }); diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 74982e7c8eb43d..377a099f36f0ce 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -290,7 +290,7 @@ export const getPublicEvent = async ( const preferedLocationType = firstUsersMetadata?.defaultConferencingApp; if (preferedLocationType?.appSlug) { - const foundApp = getAppFromSlug(preferedLocationType.appSlug); + const foundApp = await getAppFromSlug(preferedLocationType.appSlug); const appType = foundApp?.appData?.location?.type; if (appType) { // Replace the location with the preferred location type @@ -606,7 +606,7 @@ const getPublicEventRefactored = async ( const preferedLocationType = firstUsersMetadata?.defaultConferencingApp; if (preferedLocationType?.appSlug) { - const foundApp = getAppFromSlug(preferedLocationType.appSlug); + const foundApp = await getAppFromSlug(preferedLocationType.appSlug); const appType = foundApp?.appData?.location?.type; if (appType) { // Replace the location with the preferred location type diff --git a/packages/features/tasker/tasks/scanWorkflowBody.ts b/packages/features/tasker/tasks/scanWorkflowBody.ts index 47be78b59fc0d6..8f67fae8baa695 100644 --- a/packages/features/tasker/tasks/scanWorkflowBody.ts +++ b/packages/features/tasker/tasks/scanWorkflowBody.ts @@ -78,7 +78,7 @@ export async function scanWorkflowBody(payload: string) { const timeFormat = getTimeFormatStringFromUserTimeFormat(workflowStep.workflow.user?.timeFormat); // Determine if body is a template - const defaultTemplate = getTemplateBodyForAction({ + const defaultTemplate = await getTemplateBodyForAction({ action: workflowStep.action, locale: workflowStep.workflow.user?.locale ?? "en", t: await getTranslation(workflowStep.workflow.user?.locale ?? "en", "common"), diff --git a/packages/lib/EventManager.ts b/packages/lib/EventManager.ts index a244eee2dcff73..c7f83e8258b9d1 100644 --- a/packages/lib/EventManager.ts +++ b/packages/lib/EventManager.ts @@ -63,8 +63,8 @@ const delegatedCredentialLast = (a: return (a.delegatedToId ? 1 : 0) - (b.delegatedToId ? 1 : 0); }; -export const getLocationRequestFromIntegration = (location: string) => { - const eventLocationType = getLocationFromApp(location); +export const getLocationRequestFromIntegration = async (location: string) => { + const eventLocationType = await getLocationFromApp(location); if (eventLocationType) { const requestId = uuidv5(location, uuidv5.URL); diff --git a/packages/lib/bookings/getCalendarLinks.test.ts b/packages/lib/bookings/getCalendarLinks.test.ts index 7131703adfdfa1..e66af187deb93d 100644 --- a/packages/lib/bookings/getCalendarLinks.test.ts +++ b/packages/lib/bookings/getCalendarLinks.test.ts @@ -65,7 +65,11 @@ describe("getCalendarLinks", () => { }); it("should return all calendar links", async () => { - const result = getCalendarLinks({ booking: baseMockBooking, eventType: baseMockEventType, t: mockT }); + const result = await getCalendarLinks({ + booking: baseMockBooking, + eventType: baseMockEventType, + t: mockT, + }); const googleLink = result.find((link) => link.id === CalendarLinkType.GOOGLE_CALENDAR); const microsoftOfficeLink = result.find((link) => link.id === CalendarLinkType.MICROSOFT_OFFICE); const microsoftOutlookLink = result.find((link) => link.id === CalendarLinkType.MICROSOFT_OUTLOOK); @@ -122,7 +126,7 @@ describe("getCalendarLinks", () => { metadata: { videoCallUrl }, }; - const result = getCalendarLinks({ booking, eventType: baseMockEventType, t: mockT }); + const result = await getCalendarLinks({ booking, eventType: baseMockEventType, t: mockT }); // Check that all links contain the videoCallUrl const googleLink = result.find((link) => link.id === CalendarLinkType.GOOGLE_CALENDAR); @@ -146,7 +150,7 @@ describe("getCalendarLinks", () => { isDynamic: true, // This makes it use the custom title }; - const result = getCalendarLinks({ booking, eventType, t: mockT }); + const result = await getCalendarLinks({ booking, eventType, t: mockT }); const googleLink = result.find((link) => link.id === CalendarLinkType.GOOGLE_CALENDAR); expect(googleLink?.link).toContain(`details=${encodeURIComponent(eventType.description)}`); @@ -181,7 +185,7 @@ describe("getCalendarLinks", () => { const mockRRuleString = "FREQ=WEEKLY;INTERVAL=1;COUNT=5"; vi.spyOn(RRule.prototype, "toString").mockReturnValue(mockRRuleString); - const result = getCalendarLinks({ booking: baseMockBooking, eventType, t: mockT }); + const result = await getCalendarLinks({ booking: baseMockBooking, eventType, t: mockT }); const googleLink = result.find((link) => link.id === "googleCalendar"); expect(googleLink).toBeDefined(); @@ -195,7 +199,11 @@ describe("getCalendarLinks", () => { // Mock console.error to avoid test output noise const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); - const result = getCalendarLinks({ booking: baseMockBooking, eventType: baseMockEventType, t: mockT }); + const result = await getCalendarLinks({ + booking: baseMockBooking, + eventType: baseMockEventType, + t: mockT, + }); const icsLink = result.find((link) => link.id === "ics"); expect(icsLink).toBeDefined(); diff --git a/packages/lib/bookings/getCalendarLinks.ts b/packages/lib/bookings/getCalendarLinks.ts index ef63dd7e941957..23bf5a53fa80b3 100644 --- a/packages/lib/bookings/getCalendarLinks.ts +++ b/packages/lib/bookings/getCalendarLinks.ts @@ -136,7 +136,7 @@ const buildMicrosoftOutlookLink = ({ return microsoftOutlookLink; }; -export const getCalendarLinks = ({ +export const getCalendarLinks = async ({ booking, eventType, t, @@ -183,7 +183,7 @@ export const getCalendarLinks = ({ }; // Create event name and description - const eventName = getEventName(eventNameObject, true); + const eventName = (await getEventName(eventNameObject, true)) || eventType.title; const eventDescription = eventType.description || ""; // Calculate start and end times diff --git a/packages/lib/bulkUpdateEventsToDefaultLocation.ts b/packages/lib/bulkUpdateEventsToDefaultLocation.ts index 7d2fc6a39f3fc3..460d60aa862493 100644 --- a/packages/lib/bulkUpdateEventsToDefaultLocation.ts +++ b/packages/lib/bulkUpdateEventsToDefaultLocation.ts @@ -24,7 +24,7 @@ export const bulkUpdateEventsToDefaultLocation = async ({ }); } - const foundApp = getAppFromSlug(defaultApp.appSlug); + const foundApp = await getAppFromSlug(defaultApp.appSlug); const appType = foundApp?.appData?.location?.type; if (!appType) { throw new TRPCError({ @@ -36,7 +36,7 @@ export const bulkUpdateEventsToDefaultLocation = async ({ const credential = await prisma.credential.findFirst({ where: { userId: user.id, - appId: foundApp.slug, + appId: foundApp?.slug, }, select: { id: true, diff --git a/packages/lib/event-types/getBulkEventTypes.ts b/packages/lib/event-types/getBulkEventTypes.ts index 6117926ee616ac..02d60ed969f192 100644 --- a/packages/lib/event-types/getBulkEventTypes.ts +++ b/packages/lib/event-types/getBulkEventTypes.ts @@ -6,21 +6,23 @@ import { eventTypeLocations as eventTypeLocationsSchema } from "@calcom/prisma/z * Process event types to add logo information * @param eventTypes - The event types to process */ -const processEventTypes = (eventTypes: { id: number; title: string; locations: unknown }[]) => { - const eventTypesWithLogo = eventTypes.map((eventType) => { - const locationParsed = eventTypeLocationsSchema.safeParse(eventType.locations); +const processEventTypes = async (eventTypes: { id: number; title: string; locations: unknown }[]) => { + const eventTypesWithLogo = await Promise.all( + eventTypes.map(async (eventType) => { + const locationParsed = eventTypeLocationsSchema.safeParse(eventType.locations); - // some events has null as location for legacy reasons, so this fallbacks to daily video - const app = getAppFromLocationValue( - locationParsed.success && locationParsed.data?.[0]?.type - ? locationParsed.data[0].type - : "integrations:daily" - ); - return { - ...eventType, - logo: app?.logo, - }; - }); + // some events has null as location for legacy reasons, so this fallbacks to daily video + const app = await getAppFromLocationValue( + locationParsed.success && locationParsed.data?.[0]?.type + ? locationParsed.data[0].type + : "integrations:daily" + ); + return { + ...eventType, + logo: app?.logo, + }; + }) + ); return { eventTypes: eventTypesWithLogo, @@ -40,7 +42,7 @@ export const getBulkUserEventTypes = async (userId: number) => { }, }); - return processEventTypes(eventTypes); + return await processEventTypes(eventTypes); }; export const getBulkTeamEventTypes = async (teamId: number) => { @@ -56,5 +58,5 @@ export const getBulkTeamEventTypes = async (teamId: number) => { }, }); - return processEventTypes(eventTypes); + return await processEventTypes(eventTypes); }; diff --git a/packages/lib/event-types/utils/locationsResolver.ts b/packages/lib/event-types/utils/locationsResolver.ts index 119b00ebdc6bbb..bf4715b4985292 100644 --- a/packages/lib/event-types/utils/locationsResolver.ts +++ b/packages/lib/event-types/utils/locationsResolver.ts @@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js"; // eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router import { z } from "zod"; -import { getEventLocationType } from "@calcom/app-store/locations"; +import { getEventLocationTypeSync } from "@calcom/app-store/locations"; export const locationsResolver = (t: TFunction) => { return z @@ -30,7 +30,7 @@ export const locationsResolver = (t: TFunction) => { .superRefine((val, ctx) => { if (val?.link) { const link = val.link; - const eventLocationType = getEventLocationType(val.type); + const eventLocationType = getEventLocationTypeSync(val.type); if ( eventLocationType && !eventLocationType.default && diff --git a/packages/lib/event.test.ts b/packages/lib/event.test.ts index d44a6eccb1bfad..e19d5d4e00af80 100644 --- a/packages/lib/event.test.ts +++ b/packages/lib/event.test.ts @@ -9,7 +9,7 @@ describe("event tests", () => { it("should return event_between_users message if no name", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -36,7 +36,7 @@ describe("event tests", () => { it("should return event_between_users message if no name with team set", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -64,7 +64,7 @@ describe("event tests", () => { it("should return event name if no vars used", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -79,7 +79,7 @@ describe("event tests", () => { it("should support templating of event type", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -94,7 +94,7 @@ describe("event tests", () => { it("should support templating of scheduler", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -109,7 +109,7 @@ describe("event tests", () => { it("should support templating of organiser", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -124,7 +124,7 @@ describe("event tests", () => { it("should support templating of user", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -139,7 +139,7 @@ describe("event tests", () => { it("should support templating of attendee", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -154,7 +154,7 @@ describe("event tests", () => { it("should support templating of host", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -169,7 +169,7 @@ describe("event tests", () => { it("should support templating of attendee with host/attendee", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -184,7 +184,7 @@ describe("event tests", () => { it("should support templating of host with host/attendee", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName( + const result = event.getEventNameSync( { attendeeName: "example attendee", eventType: "example event type", @@ -202,7 +202,7 @@ describe("event tests", () => { it("should support templating of custom booking fields", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -219,7 +219,7 @@ describe("event tests", () => { it("should support templating of custom booking fields with values", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -239,7 +239,7 @@ describe("event tests", () => { it("should support templating of custom booking fields with non-string values", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -259,7 +259,7 @@ describe("event tests", () => { it("should support templating of custom booking fields with no value", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -279,7 +279,7 @@ describe("event tests", () => { it("should support templating of location via {Location}", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -295,7 +295,7 @@ describe("event tests", () => { it("should support templating of location via {LOCATION}", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -311,7 +311,7 @@ describe("event tests", () => { it("should strip location template if none set", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -326,7 +326,7 @@ describe("event tests", () => { it("should strip location template if empty", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -342,7 +342,7 @@ describe("event tests", () => { it("should template {Location} as passed location if unknown type", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -359,7 +359,7 @@ describe("event tests", () => { it("should support templating of event duration", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", @@ -373,7 +373,7 @@ describe("event tests", () => { it("should support templating of Scheduler first name", () => { const tFunc = vi.fn(() => "foo"); - const result = event.getEventName({ + const result = event.getEventNameSync({ attendeeName: "example attendee", eventType: "example event type", host: "example host", diff --git a/packages/lib/event.ts b/packages/lib/event.ts index 40adea44ca823d..e944b2571b16eb 100644 --- a/packages/lib/event.ts +++ b/packages/lib/event.ts @@ -1,7 +1,7 @@ import type { TFunction } from "i18next"; import z from "zod"; -import { guessEventLocationType } from "@calcom/app-store/locations"; +import { guessEventLocationType, guessEventLocationTypeSync } from "@calcom/app-store/locations"; import type { Prisma } from "@calcom/prisma/client"; export const nameObjectSchema = z.object({ @@ -28,7 +28,7 @@ export type EventNameObjectType = { t: TFunction; }; -export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView = false) { +export async function getEventName(eventNameObj: EventNameObjectType, forAttendeeView = false) { const attendeeName = parseName(eventNameObj.attendeeName); if (!eventNameObj.eventName) @@ -45,7 +45,7 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView let locationString = eventNameObj.location || ""; if (eventNameObj.eventName.includes("{Location}") || eventNameObj.eventName.includes("{LOCATION}")) { - const eventLocationType = guessEventLocationType(eventNameObj.location); + const eventLocationType = await guessEventLocationType(eventNameObj.location); if (eventLocationType) { locationString = eventLocationType.label; } @@ -80,7 +80,7 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView return variable.replace("{", "").replace("}", ""); }); - customInputvariables?.forEach((variable) => { + for (const variable of customInputvariables || []) { if (!eventNameObj.bookingFields) return; const bookingFieldValue = eventNameObj.bookingFields[variable as keyof typeof eventNameObj.bookingFields]; @@ -88,27 +88,126 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView if (bookingFieldValue) { let fieldValue; - if (typeof bookingFieldValue === "object") { + if (typeof bookingFieldValue === "object" && bookingFieldValue !== null) { if ("value" in bookingFieldValue) { const valueAsString = bookingFieldValue.value?.toString(); fieldValue = variable === "location" - ? guessEventLocationType(valueAsString)?.label || valueAsString + ? guessEventLocationTypeSync(valueAsString)?.label || valueAsString : valueAsString; } else if (variable === "name" && "firstName" in bookingFieldValue) { const lastName = "lastName" in bookingFieldValue ? bookingFieldValue.lastName : ""; fieldValue = `${bookingFieldValue.firstName} ${lastName}`.trim(); } } else { - fieldValue = bookingFieldValue.toString(); + fieldValue = bookingFieldValue?.toString() || ""; } dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || ""); } else { dynamicEventName = dynamicEventName.replace(`{${variable}}`, ""); } + } + + return dynamicEventName; +} + +export function getEventNameSync(eventNameObj: EventNameObjectType, forAttendeeView = false) { + const attendeeName = parseName(eventNameObj.attendeeName); + + if (!eventNameObj.eventName) + return eventNameObj.t("event_between_users", { + eventName: eventNameObj.eventType, + host: eventNameObj.teamName || eventNameObj.host, + attendeeName, + interpolation: { + escapeValue: false, + }, + }); + + let eventName = eventNameObj.eventName; + let locationString = eventNameObj.location || ""; + + if (eventNameObj.eventName.includes("{Location}") || eventNameObj.eventName.includes("{LOCATION}")) { + const eventLocationType = guessEventLocationTypeSync(eventNameObj.location); + if (eventLocationType) { + locationString = eventLocationType.label; + } + eventName = eventName.replace("{Location}", locationString); + eventName = eventName.replace("{LOCATION}", locationString); + } + + let dynamicEventName = eventName + // Need this for compatibility with older event names + .replaceAll("{Event type title}", eventNameObj.eventType) + .replaceAll("{Scheduler}", attendeeName) + .replaceAll("{Organiser}", eventNameObj.host) + .replaceAll("{Organiser first name}", eventNameObj.host.split(" ")[0]) + .replaceAll("{USER}", attendeeName) + .replaceAll("{ATTENDEE}", attendeeName) + .replaceAll("{HOST}", eventNameObj.host) + .replaceAll("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : attendeeName) + .replaceAll("{Event duration}", `${String(eventNameObj.eventDuration)} mins`) + .replaceAll( + "{Scheduler first name}", + attendeeName === eventNameObj.t("scheduler") ? "{Scheduler first name}" : attendeeName.split(" ")[0] + ); + + const { bookingFields } = eventNameObj || {}; + const { name } = bookingFields || {}; + + if (name && typeof name === "object" && !Array.isArray(name) && typeof name.lastName === "string") { + dynamicEventName = dynamicEventName.replaceAll("{Scheduler last name}", name.lastName.toString()); + } + + const customInputvariables = dynamicEventName.match(/\{(.+?)}/g)?.map((variable) => { + return variable.replace("{", "").replace("}", ""); }); + for (const variable of customInputvariables || []) { + if (!eventNameObj.bookingFields) return dynamicEventName; + + const bookingFieldValue = eventNameObj.bookingFields[variable as keyof typeof eventNameObj.bookingFields]; + + if (bookingFieldValue) { + let fieldValue; + + if ( + typeof bookingFieldValue === "object" && + bookingFieldValue !== null && + !Array.isArray(bookingFieldValue) + ) { + if ("value" in bookingFieldValue) { + const valueAsString = bookingFieldValue.value?.toString(); + fieldValue = + variable === "location" + ? guessEventLocationTypeSync(valueAsString)?.label || valueAsString + : valueAsString; + } else if ( + variable === "name" && + "firstName" in bookingFieldValue && + typeof bookingFieldValue.firstName === "string" + ) { + const lastName = + "lastName" in bookingFieldValue && typeof bookingFieldValue.lastName === "string" + ? bookingFieldValue.lastName + : ""; + fieldValue = `${bookingFieldValue.firstName} ${lastName}`.trim(); + } + } else if ( + typeof bookingFieldValue === "string" || + typeof bookingFieldValue === "number" || + typeof bookingFieldValue === "boolean" + ) { + fieldValue = bookingFieldValue.toString(); + } + + dynamicEventName = dynamicEventName.replace(`{${variable}}`, fieldValue || ""); + } else { + dynamicEventName = dynamicEventName.replace(`{${variable}}`, ""); + } + } + return dynamicEventName; } diff --git a/packages/lib/getConnectedApps.ts b/packages/lib/getConnectedApps.ts index 09fde58bc01639..67d8231efab67a 100644 --- a/packages/lib/getConnectedApps.ts +++ b/packages/lib/getConnectedApps.ts @@ -196,14 +196,16 @@ export async function getConnectedApps({ let dependencyData: TDependencyData = []; if (app.dependencies?.length) { - dependencyData = app.dependencies.map((dependency) => { - const dependencyInstalled = enabledApps.some( - (dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency - ); - // If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app. - const dependencyName = getAppFromSlug(dependency)?.name; - return { name: dependencyName, installed: dependencyInstalled }; - }); + dependencyData = await Promise.all( + app.dependencies.map(async (dependency) => { + const dependencyInstalled = enabledApps.some( + (dbAppIterator) => dbAppIterator.credentials.length && dbAppIterator.slug === dependency + ); + // If the app marked as dependency is simply deleted from the codebase, we can have the situation where App is marked installed in DB but we couldn't get the app. + const dependencyName = (await getAppFromSlug(dependency))?.name; + return { name: dependencyName, installed: dependencyInstalled }; + }) + ); } return { diff --git a/packages/lib/server/eventTypeSelect.ts b/packages/lib/server/eventTypeSelect.ts index 26c8b1ee946224..bfad34da4f4f1b 100644 --- a/packages/lib/server/eventTypeSelect.ts +++ b/packages/lib/server/eventTypeSelect.ts @@ -48,7 +48,6 @@ export const eventTypeSelect = { onlyShowFirstAvailableSlot: true, allowReschedulingPastBookings: true, hideOrganizerEmail: true, - showOptimizedSlots: true, seatsShowAttendees: true, seatsShowAvailabilityCount: true, scheduleId: true, diff --git a/packages/lib/server/repository/eventTypeRepository.ts b/packages/lib/server/repository/eventTypeRepository.ts index b3151d1ed65163..6d353652e6e8bc 100644 --- a/packages/lib/server/repository/eventTypeRepository.ts +++ b/packages/lib/server/repository/eventTypeRepository.ts @@ -577,7 +577,6 @@ export class EventTypeRepository { eventTypeColor: true, bookingLimits: true, onlyShowFirstAvailableSlot: true, - showOptimizedSlots: true, durationLimits: true, maxActiveBookingsPerBooker: true, maxActiveBookingPerBookerOfferReschedule: true, @@ -874,7 +873,6 @@ export class EventTypeRepository { eventTypeColor: true, bookingLimits: true, onlyShowFirstAvailableSlot: true, - showOptimizedSlots: true, durationLimits: true, maxActiveBookingsPerBooker: true, maxActiveBookingPerBookerOfferReschedule: true, @@ -1200,7 +1198,6 @@ export class EventTypeRepository { onlyShowFirstAvailableSlot: true, allowReschedulingPastBookings: true, hideOrganizerEmail: true, - showOptimizedSlots: true, periodCountCalendarDays: true, rescheduleWithSameRoundRobinHost: true, periodDays: true, diff --git a/packages/lib/server/service/bookingLocationService.test.ts b/packages/lib/server/service/bookingLocationService.test.ts index f25f794d109923..3791142acd4b6f 100644 --- a/packages/lib/server/service/bookingLocationService.test.ts +++ b/packages/lib/server/service/bookingLocationService.test.ts @@ -24,14 +24,14 @@ describe("BookingLocationService", () => { describe("getOrganizerDefaultConferencingAppLocation", () => { describe("Dynamic conferencing apps", () => { - it("should return the app location type for a dynamic app like Google Meet", () => { + it("should return the app location type for a dynamic app like Google Meet", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "google-meet", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:google:meet", @@ -39,7 +39,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -50,14 +50,14 @@ describe("BookingLocationService", () => { expect(getAppFromSlug).toHaveBeenCalledWith("google-meet"); }); - it("should return the app location type for Zoom", () => { + it("should return the app location type for Zoom", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "zoom", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:zoom", @@ -65,7 +65,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -75,16 +75,16 @@ describe("BookingLocationService", () => { }); }); - it("should return null when app is not found", () => { + it("should return null when app is not found", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "unknown-app", }, }; - vi.mocked(getAppFromSlug).mockReturnValue(null); + vi.mocked(getAppFromSlug).mockResolvedValue(null); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -93,7 +93,7 @@ describe("BookingLocationService", () => { }); describe("Static link apps", () => { - it("should return static link for apps with appLink", () => { + it("should return static link for apps with appLink", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "custom-video", @@ -101,7 +101,7 @@ describe("BookingLocationService", () => { }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:custom-video", @@ -109,7 +109,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -122,10 +122,10 @@ describe("BookingLocationService", () => { }); describe("Fallback scenarios", () => { - it("should return null when no conferencing app is set", () => { + it("should return null when no conferencing app is set", async () => { const mockMetadata = {}; - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -133,16 +133,16 @@ describe("BookingLocationService", () => { expect(getAppFromSlug).not.toHaveBeenCalled(); }); - it("should handle null metadata gracefully", () => { - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + it("should handle null metadata gracefully", async () => { + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: null, }); expect(result).toBeNull(); }); - it("should handle undefined metadata gracefully", () => { - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + it("should handle undefined metadata gracefully", async () => { + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: undefined, }); @@ -151,26 +151,26 @@ describe("BookingLocationService", () => { }); describe("Invalid metadata parsing", () => { - it("should handle invalid metadata structure", () => { + it("should handle invalid metadata structure", async () => { const invalidMetadata = { defaultConferencingApp: "not-an-object", }; - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: invalidMetadata, }); expect(result).toBeNull(); }); - it("should handle metadata with missing appSlug", () => { + it("should handle metadata with missing appSlug", async () => { const mockMetadata = { defaultConferencingApp: { appLink: "https://some-link.com", }, }; - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -179,36 +179,36 @@ describe("BookingLocationService", () => { }); describe("Edge cases", () => { - it("should handle app with no appData", () => { + it("should handle app with no appData", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "app-without-data", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({} as any); + vi.mocked(getAppFromSlug).mockResolvedValue({} as any); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); expect(result).toBeNull(); }); - it("should handle app with no location type", () => { + it("should handle app with no location type", async () => { const mockMetadata = { defaultConferencingApp: { appSlug: "app-without-location-type", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: {}, }, } as any); - const result = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + const result = await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ organizerMetadata: mockMetadata, }); @@ -218,14 +218,14 @@ describe("BookingLocationService", () => { }); describe("getLocationForHost", () => { - it("should use organizer default when allowed and available", () => { + it("should use organizer default when allowed and available", async () => { const mockHostMetadata = { defaultConferencingApp: { appSlug: "zoom", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:zoom", @@ -233,7 +233,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getLocationForHost({ + const result = await BookingLocationService.getLocationForHost({ hostMetadata: mockHostMetadata, eventTypeLocations: [{ type: "conferencing" }], isTeamEventType: true, @@ -246,7 +246,7 @@ describe("BookingLocationService", () => { }); }); - it("should use static link when organizer default is a static link app", () => { + it("should use static link when organizer default is a static link app", async () => { const mockHostMetadata = { defaultConferencingApp: { appSlug: "custom-video", @@ -254,7 +254,7 @@ describe("BookingLocationService", () => { }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:custom-video", @@ -262,7 +262,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getLocationForHost({ + const result = await BookingLocationService.getLocationForHost({ hostMetadata: mockHostMetadata, eventTypeLocations: [{ type: "conferencing" }], isTeamEventType: true, @@ -274,10 +274,10 @@ describe("BookingLocationService", () => { }); }); - it("should fallback to first real location when organizer default not available", () => { + it("should fallback to first real location when organizer default not available", async () => { const mockHostMetadata = {}; - const result = BookingLocationService.getLocationForHost({ + const result = await BookingLocationService.getLocationForHost({ hostMetadata: mockHostMetadata, eventTypeLocations: [{ type: "conferencing" }, { type: "integrations:google:meet" }], isTeamEventType: true, @@ -290,10 +290,10 @@ describe("BookingLocationService", () => { }); }); - it("should default to Cal Video when no locations configured", () => { + it("should default to Cal Video when no locations configured", async () => { const mockHostMetadata = {}; - const result = BookingLocationService.getLocationForHost({ + const result = await BookingLocationService.getLocationForHost({ hostMetadata: mockHostMetadata, eventTypeLocations: [{ type: "conferencing" }], isTeamEventType: true, @@ -306,14 +306,14 @@ describe("BookingLocationService", () => { }); }); - it("should not use organizer default for non-team and non-managed events", () => { + it("should not use organizer default for non-team and non-managed events", async () => { const mockHostMetadata = { defaultConferencingApp: { appSlug: "zoom", }, }; - vi.mocked(getAppFromSlug).mockReturnValue({ + vi.mocked(getAppFromSlug).mockResolvedValue({ appData: { location: { type: "integrations:zoom", @@ -321,7 +321,7 @@ describe("BookingLocationService", () => { }, } as any); - const result = BookingLocationService.getLocationForHost({ + const result = await BookingLocationService.getLocationForHost({ hostMetadata: mockHostMetadata, eventTypeLocations: [{ type: "conferencing" }, { type: "integrations:google:meet" }], isTeamEventType: false, diff --git a/packages/lib/server/service/bookingLocationService.ts b/packages/lib/server/service/bookingLocationService.ts index 14b2469d3bab6f..6ce2b9e287136f 100644 --- a/packages/lib/server/service/bookingLocationService.ts +++ b/packages/lib/server/service/bookingLocationService.ts @@ -104,9 +104,9 @@ export class BookingLocationService { * @returns The location configuration or null if no valid conferencing app is found. * Callers should handle the null case with their own fallback logic. */ - static getOrganizerDefaultConferencingAppLocation({ + static async getOrganizerDefaultConferencingAppLocation({ organizerMetadata, - }: GetOrganizerDefaultConferencingAppLocationParams): GetOrganizerDefaultConferencingAppLocationResult | null { + }: GetOrganizerDefaultConferencingAppLocationParams): Promise { // Parse the user metadata to extract conferencing app settings const metadataParseResult = userMetadataSchema.safeParse(organizerMetadata); const parsedMetadata = metadataParseResult.success ? metadataParseResult.data : undefined; @@ -116,7 +116,7 @@ export class BookingLocationService { } // Retrieve the app configuration from the app store - const app = getAppFromSlug(parsedMetadata.defaultConferencingApp.appSlug); + const app = await getAppFromSlug(parsedMetadata.defaultConferencingApp.appSlug); // Extract the location type from the app's metadata (e.g., "integrations:zoom") const conferencingAppLocationType = app?.appData?.location?.type || null; @@ -150,12 +150,12 @@ export class BookingLocationService { * * @returns The location configuration for the new host */ - static getLocationForHost({ + static async getLocationForHost({ hostMetadata, eventTypeLocations, isManagedEventType = false, isTeamEventType = false, - }: GetLocationForHostParams): GetLocationForHostResult { + }: GetLocationForHostParams): Promise { // Check if the event type allows using the organizer's default conferencing app const eventAllowsOrganizerDefault = eventTypeLocations.some( (location) => location.type === OrganizerDefaultConferencingAppType @@ -164,9 +164,10 @@ export class BookingLocationService { const isOrganizerDefaultAllowed = isTeamEventType || isManagedEventType; if (eventAllowsOrganizerDefault && isOrganizerDefaultAllowed) { - const organizerDefaultLocation = BookingLocationService.getOrganizerDefaultConferencingAppLocation({ - organizerMetadata: hostMetadata, - }); + const organizerDefaultLocation = + await BookingLocationService.getOrganizerDefaultConferencingAppLocation({ + organizerMetadata: hostMetadata, + }); if (organizerDefaultLocation) { if (organizerDefaultLocation.isStaticLinkApp) { diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index dee2d8827a78d1..c37d0cec073e1f 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -123,7 +123,6 @@ export const buildEventType = (eventType?: Partial): EventType => { beforeEventBuffer: 0, afterEventBuffer: 0, onlyShowFirstAvailableSlot: false, - showOptimizedSlots: false, seatsPerTimeSlot: null, seatsShowAttendees: null, disableCancelling: false, diff --git a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts index 0da7eadf876419..cff02e21098c13 100644 --- a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts +++ b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts @@ -4,10 +4,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps"; -import { - DEFAULT_PROMPT_VALUE, - DEFAULT_BEGIN_MESSAGE, -} from "@calcom/features/calAIPhone/promptTemplates"; +import { DEFAULT_PROMPT_VALUE, DEFAULT_BEGIN_MESSAGE } from "@calcom/features/calAIPhone/promptTemplates"; import type { TemplateType } from "@calcom/features/calAIPhone/zod-utils"; import { sortHosts } from "@calcom/features/eventtypes/components/HostEditDialogs"; import type { @@ -145,7 +142,6 @@ export const useEventTypeForm = ({ calVideoSettings: eventType.calVideoSettings, maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker || null, maxActiveBookingPerBookerOfferReschedule: eventType.maxActiveBookingPerBookerOfferReschedule, - showOptimizedSlots: eventType.showOptimizedSlots ?? false, }; }, [eventType, periodDates]); diff --git a/packages/trpc/server/routers/viewer/apps/queryForDependencies.handler.ts b/packages/trpc/server/routers/viewer/apps/queryForDependencies.handler.ts index 59b60e40010c77..d60c4beb7d7e19 100644 --- a/packages/trpc/server/routers/viewer/apps/queryForDependencies.handler.ts +++ b/packages/trpc/server/routers/viewer/apps/queryForDependencies.handler.ts @@ -33,7 +33,7 @@ export const queryForDependenciesHandler = async ({ ctx, input }: QueryForDepend }); const appInstalled = !!dbCredential || !!delegationCredentials.length; - const app = getAppFromSlug(dependency); + const app = await getAppFromSlug(dependency); dependencyData.push({ name: app?.name || dependency, slug: dependency, installed: !!appInstalled }); }) diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 217e2435b53829..4c62a45a659abb 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -195,7 +195,7 @@ export class SystemError extends Error { } } -export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ +export async function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ organizer, loggedInUserTranslate: translate, }: { @@ -218,17 +218,17 @@ export function getLocationForOrganizerDefaultConferencingAppInEvtFormat({ ); } const defaultConferencingAppSlug = defaultConferencingApp.appSlug; - const app = getAppFromSlug(defaultConferencingAppSlug); + const app = await getAppFromSlug(defaultConferencingAppSlug); if (!app) { throw new SystemError(`Default conferencing app ${defaultConferencingAppSlug} not found`); } - const defaultConferencingAppLocationType = app.appData?.location?.type; + const defaultConferencingAppLocationType = app?.appData?.location?.type; if (!defaultConferencingAppLocationType) { throw new SystemError("Default conferencing app has no location type"); } const location = defaultConferencingAppLocationType; - const locationType = getEventLocationType(location); + const locationType = await getEventLocationType(location); if (!locationType) { throw new SystemError(`Location type not found: ${location}`); } diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index bfed607ab4a4a6..60a986574e104e 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1155,7 +1155,6 @@ export class AvailableSlotsService { minimumBookingNotice: eventType.minimumBookingNotice, frequency: eventType.slotInterval || input.duration || eventType.length, datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined, - showOptimizedSlots: eventType.showOptimizedSlots, }); let availableTimeSlots: typeof timeSlots = []; diff --git a/packages/trpc/server/routers/viewer/workflows/create.handler.ts b/packages/trpc/server/routers/viewer/workflows/create.handler.ts index 41216f6d5cc000..4bb4d608399a65 100644 --- a/packages/trpc/server/routers/viewer/workflows/create.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/create.handler.ts @@ -75,8 +75,8 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => { stepNumber: 1, action: WorkflowActions.EMAIL_ATTENDEE, template: WorkflowTemplates.REMINDER, - reminderBody: renderedEmailTemplate.emailBody, - emailSubject: renderedEmailTemplate.emailSubject, + reminderBody: (await renderedEmailTemplate).emailBody, + emailSubject: (await renderedEmailTemplate).emailSubject, workflowId: workflow.id, sender: SENDER_NAME, numberVerificationPending: false, diff --git a/packages/trpc/server/routers/viewer/workflows/util.ts b/packages/trpc/server/routers/viewer/workflows/util.ts index a041556c46107d..879beb431aa219 100644 --- a/packages/trpc/server/routers/viewer/workflows/util.ts +++ b/packages/trpc/server/routers/viewer/workflows/util.ts @@ -1027,7 +1027,7 @@ export async function getEmailTemplateText( const timeFormat = getTimeFormatStringFromUserTimeFormat(params.timeFormat); - let { emailBody, emailSubject } = emailReminderTemplate({ + let { emailBody, emailSubject } = await emailReminderTemplate({ isEditingMode: true, locale, t: await getTranslation(locale ?? "en", "common"),