diff --git a/apps/web/components/eventtype/CustomEventTypeModal.tsx b/apps/web/components/eventtype/CustomEventTypeModal.tsx index fc40298427..94cbc33836 100644 --- a/apps/web/components/eventtype/CustomEventTypeModal.tsx +++ b/apps/web/components/eventtype/CustomEventTypeModal.tsx @@ -70,6 +70,10 @@ const CustomEventTypeModalForm: FC = (props) => {

{`{Event type title}`}

{t("event_name_info")}

+
+

{`{Event duration}`}

+

{t("event_duration_info")}

+

{`{Organiser}`}

{t("your_full_name")}

diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index c0cedaedc8..17b4855be7 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -74,6 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick { attendeeName: "example attendee", eventType: "example event type", host: "example host", + eventDuration: 15, t: tFunc as TFunction, }); @@ -39,6 +40,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", teamName: "example team name", + eventDuration: 15, t: tFunc as TFunction, }); @@ -66,6 +68,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "example event name", + eventDuration: 15, t: tFunc as TFunction, }); @@ -81,6 +84,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "event type: {Event type title}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -96,6 +100,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "scheduler: {Scheduler}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -111,6 +116,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "organiser: {Organiser}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -126,6 +132,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "user: {USER}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -141,6 +148,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "attendee: {ATTENDEE}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -156,6 +164,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "host: {HOST}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -171,6 +180,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "host or attendee: {HOST/ATTENDEE}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -187,6 +197,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "host or attendee: {HOST/ATTENDEE}", + eventDuration: 15, t: tFunc as TFunction, }, true @@ -222,6 +233,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "custom field: {customField}", + eventDuration: 15, bookingFields: { customField: { value: "example custom field", @@ -242,6 +254,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "custom field: {customField}", + eventDuration: 15, bookingFields: { customField: { value: 808, @@ -262,6 +275,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "custom field: {customField}", + eventDuration: 15, bookingFields: { customField: { value: undefined, @@ -283,6 +297,7 @@ describe("event tests", () => { host: "example host", location: "attendeeInPerson", eventName: "location: {Location}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -299,6 +314,7 @@ describe("event tests", () => { host: "example host", location: "attendeeInPerson", eventName: "location: {LOCATION}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -314,6 +330,7 @@ describe("event tests", () => { eventType: "example event type", host: "example host", eventName: "location: {Location}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -330,6 +347,7 @@ describe("event tests", () => { host: "example host", location: "", eventName: "location: {Location}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -346,6 +364,7 @@ describe("event tests", () => { host: "example host", location: "unknownNonsense", eventName: "location: {Location}", + eventDuration: 15, t: tFunc as TFunction, }); @@ -354,6 +373,20 @@ describe("event tests", () => { }); }); + it("should support templating of event duration", () => { + const tFunc = vi.fn(() => "foo"); + + const result = event.getEventName({ + attendeeName: "example attendee", + eventType: "example event type", + host: "example host", + eventName: "event duration: {Event duration}", + eventDuration: 15, + t: tFunc as TFunction, + }); + expect(result).toBe("event duration: 15 mins"); + }); + describe("fn: validateCustomEventName", () => { it("should be valid when no variables used", () => { expect(event.validateCustomEventName("foo")).toBe(true); @@ -369,6 +402,7 @@ describe("event tests", () => { "HOST", "ATTENDEE", "USER", + "Event duration", ].forEach((value) => { it(`should support {${value}} variable`, () => { expect(event.validateCustomEventName(`foo {${value}} bar`)).toBe(true); diff --git a/packages/core/event.ts b/packages/core/event.ts index 0383b7ec83..6dc9653c5a 100644 --- a/packages/core/event.ts +++ b/packages/core/event.ts @@ -23,6 +23,7 @@ export type EventNameObjectType = { teamName?: string | null; host: string; location?: string; + eventDuration: number; bookingFields?: Prisma.JsonObject; t: TFunction; }; @@ -61,7 +62,8 @@ export function getEventName(eventNameObj: EventNameObjectType, forAttendeeView .replaceAll("{USER}", attendeeName) .replaceAll("{ATTENDEE}", attendeeName) .replaceAll("{HOST}", eventNameObj.host) - .replaceAll("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : attendeeName); + .replaceAll("{HOST/ATTENDEE}", forAttendeeView ? eventNameObj.host : attendeeName) + .replaceAll("{Event duration}", `${String(eventNameObj.eventDuration)} mins`); const { bookingFields } = eventNameObj || {}; const { name } = bookingFields || {}; @@ -116,6 +118,7 @@ export const validateCustomEventName = (value: string, bookingFields?: Prisma.Js "{Organiser first name}", "{Scheduler first name}", "{Scheduler last name}", + "{Event duration}", //allowed for fallback reasons "{LOCATION}", "{HOST/ATTENDEE}", diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 43575daaa7..484464efb4 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -6,6 +6,8 @@ import type { EventNameObjectType } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event"; import type BaseEmail from "@calcom/emails/templates/_base-email"; import { formatCalEvent } from "@calcom/lib/formatCalendarEvent"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { MonthlyDigestEmailData } from "./src/templates/MonthlyDigestEmail"; @@ -280,6 +282,14 @@ export const sendCancelledEmails = async ( const emailsToSend: Promise[] = []; emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent }))); + const calEventLength = calendarEvent.length; + if (typeof calEventLength !== "number") { + logger.error( + "`calEventLength` is not a number", + safeStringify({ calEventLength, calEventTitle: calEvent.title, bookingId: calEvent.bookingId }) + ); + } + const eventDuration = calEventLength as number; if (calendarEvent.team?.members) { for (const teamMember of calendarEvent.team.members) { @@ -302,6 +312,7 @@ export const sendCancelledEmails = async ( attendeeName: attendee.name, host: calendarEvent.organizer.name, eventType: calendarEvent.type, + eventDuration, ...(calendarEvent.responses && { bookingFields: calendarEvent.responses }), ...(calendarEvent.location && { location: calendarEvent.location }), }), diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index e0bab0549d..48d84cf2f7 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -270,6 +270,7 @@ async function handler(req: CustomRequest) { const evt: CalendarEvent = { title: bookingToDelete?.title, + length: bookingToDelete?.eventType?.length, type: bookingToDelete?.eventType?.slug as string, description: bookingToDelete?.description || "", customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs), diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index af2b300e82..905094b4e4 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -802,6 +802,7 @@ async function handler( // TODO: Can we have an unnamed organizer? If not, I would really like to throw an error here. host: organizerUser.name || "Nameless", location: bookingLocation, + eventDuration: eventType.length, bookingFields: { ...responses }, t: tOrganizer, }; diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index dd10ed1748..494442484f 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -164,6 +164,7 @@ export interface CalendarEvent { endTime: string; organizer: Person; attendees: Person[]; + length?: number | null; additionalNotes?: string | null; customInputs?: Prisma.JsonObject | null; description?: string | null;