Skip to content

Commit 0aca769

Browse files
CarinaWolliCarinaWollihariombalharazomarsalannnc
authored
Allow editing workflow templates (#8028)
* add event end time as variable * add timezone as new variable * add first version of template prefill * set template body when template is updated * set reminder template body and subject when creating workflow * set email subject when changes templates * save emailBody and emailsubject for all templates + fix duplicate template text * add more flexibility for templates * remove console.log * fix {ORAGANIZER} and {ATTENDEE} variable * make sure to always send reminder body and not default template * fix import * remove email body text and match variables in templates * handle translations of formatted variables * fix email reminder template * add cancel and reschedule link as variable * add cancel and reschedule link for scheduled emails/sms * make sure empty empty body and subject are set for reminder template * add info message for testing workflow * fix typo * add sms template * add migration to remove reminderBody and emailSubject * add branding * code clean up * add hide branding everywhere * fix sms reminder template * set sms reminder template if sms body is empty * fix custom inputs variables everywhere * fix variable translations + other small fixes * fix some type errors * fix more type errors * fix everything missing around cron job scheduling * make sure to always use custom template for sms messages * fix type error * code clean up * rename link to url * Add debug logs * Update handleNewBooking.ts * Add debug logs * removed unneded responses * fix booking questions + UI improvements * remove html email body when changing to sms action * code clean up + comments * code clean up * code clean up * remove comment * more clear info message for timezone variable --------- Co-authored-by: CarinaWolli <[email protected]> Co-authored-by: Hariom Balhara <[email protected]> Co-authored-by: zomars <[email protected]> Co-authored-by: alannnc <[email protected]>
1 parent c0bda62 commit 0aca769

File tree

26 files changed

+722
-363
lines changed

26 files changed

+722
-363
lines changed

apps/web/public/static/locales/en/common.json

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,13 +1150,13 @@
11501150
"current_username": "Current username",
11511151
"example_1": "Example 1",
11521152
"example_2": "Example 2",
1153-
"additional_input_label": "Additional Input Label",
1153+
"booking_question_identifier": "Booking Question Identifier",
11541154
"company_size": "Company size",
11551155
"what_help_needed": "What do you need help with?",
11561156
"variable_format": "Variable format",
11571157
"webhook_subscriber_url_reserved": "Webhook subscriber url is already defined",
11581158
"custom_input_as_variable_info": "Ignore all special characters of the additional input label (use only letters and numbers), use uppercase for all letters and replace whitespaces with underscores.",
1159-
"using_additional_inputs_as_variables": "How do I use Additional Inputs as variables?",
1159+
"using_booking_questions_as_variables": "How do I use booking questions as variables?",
11601160
"download_desktop_app": "Download desktop app",
11611161
"set_ping_link": "Set Ping link",
11621162
"rate_limit_exceeded": "Rate limit exceeded",
@@ -1382,7 +1382,7 @@
13821382
"add_limit": "Add Limit",
13831383
"team_name_required": "Team name required",
13841384
"show_attendees": "Share attendee information between guests",
1385-
"how_additional_inputs_as_variables": "How to use Additional Inputs as Variables",
1385+
"how_booking_questions_as_variables": "How to use booking questions as variables?",
13861386
"format": "Format",
13871387
"uppercase_for_letters": "Use uppercase for all letters",
13881388
"replace_whitespaces_underscores": "Replace whitespaces with underscores",
@@ -1397,7 +1397,7 @@
13971397
"billing_help_title": "Need anything else?",
13981398
"billing_help_description": "If you need any further help with billing, our support team are here to help.",
13991399
"billing_help_cta": "Contact support",
1400-
"ignore_special_characters": "Ignore special characters in your Additional Input label. Use only letters and numbers",
1400+
"ignore_special_characters_booking_questions": "Ignore special characters in your booking question identifier. Use only letters and numbers",
14011401
"retry": "Retry",
14021402
"fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again</1> or reach out to customer support.",
14031403
"calendar_connection_fail": "Calendar connection failed",
@@ -1705,9 +1705,17 @@
17051705
"verification_code": "Verification code",
17061706
"can_you_try_again": "Can you try again with a different time?",
17071707
"verify": "Verify",
1708+
"timezone_variable": "Timezone",
1709+
"timezone_info": "The timezone of the person receiving",
1710+
"event_end_time_variable": "Event end time",
1711+
"event_end_time_info": "The event end time",
1712+
"cancel_url_variable": "Cancel URL",
1713+
"cancel_url_info": "The URL to cancel the booking",
1714+
"reschedule_url_variable": "Reschedule URL",
1715+
"reschedule_url_info": "The URL to reschedule the booking",
1716+
"invalid_event_name_variables": "There is an invalid variable in your event name",
17081717
"select_all": "Select All",
17091718
"default_conferencing_bulk_title": "Bulk update existing event types",
1710-
"invalid_event_name_variables": "There is an invalid variable in your event name",
17111719
"members_default_schedule": "Member's default schedule",
17121720
"set_by_admin": "Set by team admin",
17131721
"members_default_location": "Member's default location",
@@ -1769,6 +1777,7 @@
17691777
"collect_no_show_fee": "Collect no-show fee",
17701778
"no_show_fee_charged": "No-show fee charged",
17711779
"insights": "Insights",
1780+
"testing_workflow_info_message": "When testing this workflow, be aware that Emails and SMS can only be scheduled at least 1 hour in advance",
17721781
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
17731782
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
17741783
"card_details": "Card details",

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { WebhookTriggerEvents, WorkflowReminder, Prisma } from "@prisma/client";
1+
import type { Prisma, WebhookTriggerEvents, WorkflowReminder } from "@prisma/client";
22
import { BookingStatus, MembershipRole, WorkflowMethods } from "@prisma/client";
33
import type { NextApiRequest } from "next";
44

@@ -58,6 +58,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
5858
paid: true,
5959
eventType: {
6060
select: {
61+
slug: true,
6162
owner: true,
6263
teamId: true,
6364
recurringEvent: true,
@@ -334,7 +335,11 @@ async function handler(req: CustomRequest) {
334335
await sendCancelledReminders({
335336
workflows: bookingToDelete.eventType?.workflows,
336337
smsReminderNumber: bookingToDelete.smsReminderNumber,
337-
evt,
338+
evt: {
339+
...evt,
340+
...{ eventType: { slug: bookingToDelete.eventType.slug } },
341+
},
342+
hideBranding: !!bookingToDelete.eventType.owner?.hideBranding,
338343
});
339344
}
340345

packages/features/bookings/lib/handleConfirmation.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
1+
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
22
import { BookingStatus, WebhookTriggerEvents } from "@prisma/client";
33

44
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
@@ -10,6 +10,7 @@ import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
1010
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
1111
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
1212
import logger from "@calcom/lib/logger";
13+
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
1314
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
1415

1516
const log = logger.getChildLogger({ prefix: ["[handleConfirmation] book:user"] });
@@ -66,11 +67,24 @@ export async function handleConfirmation(args: {
6667
let updatedBookings: {
6768
scheduledJobs: string[];
6869
id: number;
70+
description: string | null;
71+
location: string | null;
72+
attendees: {
73+
name: string;
74+
email: string;
75+
}[];
6976
startTime: Date;
7077
endTime: Date;
7178
uid: string;
7279
smsReminderNumber: string | null;
80+
metadata: Prisma.JsonValue | null;
81+
customInputs: Prisma.JsonValue;
7382
eventType: {
83+
bookingFields: Prisma.JsonValue | null;
84+
slug: string;
85+
owner: {
86+
hideBranding?: boolean | null;
87+
} | null;
7488
workflows: (WorkflowsOnEventTypes & {
7589
workflow: Workflow & {
7690
steps: WorkflowStep[];
@@ -104,6 +118,13 @@ export async function handleConfirmation(args: {
104118
select: {
105119
eventType: {
106120
select: {
121+
slug: true,
122+
bookingFields: true,
123+
owner: {
124+
select: {
125+
hideBranding: true,
126+
},
127+
},
107128
workflows: {
108129
include: {
109130
workflow: {
@@ -115,15 +136,21 @@ export async function handleConfirmation(args: {
115136
},
116137
},
117138
},
139+
description: true,
140+
attendees: true,
141+
location: true,
118142
uid: true,
119143
startTime: true,
144+
metadata: true,
120145
endTime: true,
121146
smsReminderNumber: true,
147+
customInputs: true,
122148
id: true,
123149
scheduledJobs: true,
124150
},
125151
})
126152
);
153+
127154
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
128155
updatedBookings = updatedBookings.concat(updatedBookingsResult);
129156
} else {
@@ -142,6 +169,13 @@ export async function handleConfirmation(args: {
142169
select: {
143170
eventType: {
144171
select: {
172+
slug: true,
173+
bookingFields: true,
174+
owner: {
175+
select: {
176+
hideBranding: true,
177+
},
178+
},
145179
workflows: {
146180
include: {
147181
workflow: {
@@ -155,8 +189,13 @@ export async function handleConfirmation(args: {
155189
},
156190
uid: true,
157191
startTime: true,
192+
metadata: true,
158193
endTime: true,
159194
smsReminderNumber: true,
195+
description: true,
196+
attendees: true,
197+
location: true,
198+
customInputs: true,
160199
id: true,
161200
scheduledJobs: true,
162201
},
@@ -172,14 +211,21 @@ export async function handleConfirmation(args: {
172211
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
173212
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
174213
evtOfBooking.uid = updatedBookings[index].uid;
214+
const eventTypeSlug = updatedBookings[index].eventType?.slug || "";
175215

176216
const isFirstBooking = index === 0;
217+
const videoCallUrl =
218+
bookingMetadataSchema.parse(updatedBookings[index].metadata || {})?.videoCallUrl || "";
177219

178220
await scheduleWorkflowReminders({
179221
workflows: updatedBookings[index]?.eventType?.workflows || [],
180222
smsReminderNumber: updatedBookings[index].smsReminderNumber,
181-
calendarEvent: evtOfBooking,
223+
calendarEvent: {
224+
...evtOfBooking,
225+
...{ metadata: { videoCallUrl }, eventType: { slug: eventTypeSlug } },
226+
},
182227
isFirstRecurringEvent: isFirstBooking,
228+
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
183229
});
184230
}
185231
}

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
198198
customInputs: true,
199199
disableGuests: true,
200200
users: userSelect,
201+
slug: true,
201202
team: {
202203
select: {
203204
id: true,
@@ -227,6 +228,11 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
227228
seatsShowAttendees: true,
228229
bookingLimits: true,
229230
durationLimits: true,
231+
owner: {
232+
select: {
233+
hideBranding: true,
234+
},
235+
},
230236
workflows: {
231237
include: {
232238
workflow: {
@@ -800,6 +806,7 @@ async function handler(
800806
id: organizerUser.id,
801807
name: organizerUser.name || "Nameless",
802808
email: organizerUser.email || "Email-less",
809+
username: organizerUser.username || undefined,
803810
timeZone: organizerUser.timeZone,
804811
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
805812
timeFormat: organizerUser.timeFormat === 24 ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR,
@@ -1441,7 +1448,7 @@ async function handler(
14411448
await scheduleWorkflowReminders({
14421449
workflows: eventType.workflows,
14431450
smsReminderNumber: smsReminderNumber || null,
1444-
calendarEvent: { ...evt, responses, ...{ metadata } },
1451+
calendarEvent: { ...evt, ...{ metadata, eventType: { slug: eventType.slug } } },
14451452
requiresConfirmation: evt.requiresConfirmation || false,
14461453
isRescheduleEvent: !!rescheduleUid,
14471454
isFirstRecurringEvent: true,
@@ -2058,10 +2065,14 @@ async function handler(
20582065
await scheduleWorkflowReminders({
20592066
workflows: eventType.workflows,
20602067
smsReminderNumber: smsReminderNumber || null,
2061-
calendarEvent: { ...evt, responses, ...{ metadata: metadataFromEvent } },
2068+
calendarEvent: {
2069+
...evt,
2070+
...{ metadata: metadataFromEvent, eventType: { slug: eventType.slug } },
2071+
},
20622072
requiresConfirmation: evt.requiresConfirmation || false,
20632073
isRescheduleEvent: !!rescheduleUid,
20642074
isFirstRecurringEvent: true,
2075+
hideBranding: !!eventType.owner?.hideBranding,
20652076
});
20662077
} catch (error) {
20672078
log.error("Error while scheduling workflow reminders", error);

packages/features/ee/workflows/api/scheduleEmailReminders.ts

Lines changed: 56 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import sgMail from "@sendgrid/mail";
55
import type { NextApiRequest, NextApiResponse } from "next";
66

77
import dayjs from "@calcom/dayjs";
8+
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
89
import { defaultHandler } from "@calcom/lib/server";
910
import prisma from "@calcom/prisma";
1011
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
@@ -164,51 +165,63 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
164165

165166
let emailContent = {
166167
emailSubject: reminder.workflowStep.emailSubject || "",
167-
emailBody: {
168-
text: reminder.workflowStep.reminderBody || "",
169-
html: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
170-
},
168+
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
171169
};
172170

173-
switch (reminder.workflowStep.template) {
174-
case WorkflowTemplates.REMINDER:
175-
emailContent = emailReminderTemplate(
176-
reminder.booking.startTime.toISOString() || "",
177-
reminder.booking.endTime.toISOString() || "",
178-
reminder.booking.eventType?.title || "",
179-
timeZone || "",
180-
attendeeName || "",
181-
name || ""
182-
);
183-
break;
184-
case WorkflowTemplates.CUSTOM:
185-
const variables: VariablesType = {
186-
eventName: reminder.booking?.eventType?.title || "",
187-
organizerName: reminder.booking.user?.name || "",
188-
attendeeName: reminder.booking.attendees[0].name,
189-
attendeeEmail: reminder.booking.attendees[0].email,
190-
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
191-
eventTime: dayjs(reminder.booking.startTime).tz(timeZone),
192-
timeZone: timeZone,
193-
location: reminder.booking.location || "",
194-
additionalNotes: reminder.booking.description,
195-
responses: reminder.booking.responses,
196-
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
197-
};
198-
const emailSubject = await customTemplate(
199-
reminder.workflowStep.emailSubject || "",
200-
variables,
201-
locale || ""
202-
);
203-
emailContent.emailSubject = emailSubject.text;
204-
emailContent.emailBody = await customTemplate(
205-
reminder.workflowStep.reminderBody || "",
206-
variables,
207-
locale || ""
208-
);
209-
break;
171+
let emailBodyEmpty = false;
172+
173+
if (reminder.workflowStep.reminderBody) {
174+
const { responses } = getCalEventResponses({
175+
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
176+
booking: reminder.booking,
177+
});
178+
179+
const variables: VariablesType = {
180+
eventName: reminder.booking.eventType?.title || "",
181+
organizerName: reminder.booking.user?.name || "",
182+
attendeeName: reminder.booking.attendees[0].name,
183+
attendeeEmail: reminder.booking.attendees[0].email,
184+
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
185+
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
186+
timeZone: timeZone,
187+
location: reminder.booking.location || "",
188+
additionalNotes: reminder.booking.description,
189+
responses: responses,
190+
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
191+
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
192+
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
193+
};
194+
const emailSubject = customTemplate(
195+
reminder.workflowStep.emailSubject || "",
196+
variables,
197+
locale || "",
198+
!!reminder.booking.user?.hideBranding
199+
).text;
200+
emailContent.emailSubject = emailSubject;
201+
emailContent.emailBody = customTemplate(
202+
reminder.workflowStep.reminderBody || "",
203+
variables,
204+
locale || "",
205+
!!reminder.booking.user?.hideBranding
206+
).html;
207+
208+
emailBodyEmpty =
209+
customTemplate(reminder.workflowStep.reminderBody || "", variables, locale || "").text.length === 0;
210+
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
211+
emailContent = emailReminderTemplate(
212+
false,
213+
reminder.workflowStep.action,
214+
reminder.booking.startTime.toISOString() || "",
215+
reminder.booking.endTime.toISOString() || "",
216+
reminder.booking.eventType?.title || "",
217+
timeZone || "",
218+
attendeeName || "",
219+
name || "",
220+
!!reminder.booking.user?.hideBranding
221+
);
210222
}
211-
if (emailContent.emailSubject.length > 0 && emailContent.emailBody.text.length > 0 && sendTo) {
223+
224+
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
212225
const batchIdResponse = await client.request({
213226
url: "/v3/mail/batch",
214227
method: "POST",
@@ -224,8 +237,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
224237
name: reminder.workflowStep.sender || "Cal.com",
225238
},
226239
subject: emailContent.emailSubject,
227-
text: emailContent.emailBody.text,
228-
html: emailContent.emailBody.html,
240+
html: emailContent.emailBody,
229241
batchId: batchId,
230242
sendAt: dayjs(reminder.scheduledDate).unix(),
231243
replyTo: reminder.booking.user?.email || senderEmail,

0 commit comments

Comments
 (0)