disable standard confirmation emails if workflow exists (#8747)

* add switches to advanced even type settings

* only show switches when workflow is enabled

* check in event type update handler if user is allowed to disable standard emails

* don't send emails if disabled

* make sure emails can't be disabled if no workflow exist

* send workflow emails to all attendees

* code clean up

* add translations

* always send to all attendees when scheduling reminders

* change text

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Carina Wollendorfer
2023-05-09 17:08:14 +00:00
committed by GitHub
co-authored by CarinaWolli
parent 6fc9e9315d
commit 36d8554fd5
12 changed files with 241 additions and 55 deletions
@@ -11,6 +11,10 @@ import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
@@ -43,6 +47,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const bookingFields: Prisma.JsonObject = {};
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
eventType.bookingFields.forEach(({ name }) => {
bookingFields[name] = name + " input";
});
@@ -338,7 +344,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</>
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<hr className="border-subtle" />
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
onChange(e);
}}
/>
</>
)}
/>
</>
)}
{allowDisablingHostConfirmationEmails(workflows) && (
<>
<hr className="border-subtle" />
<Controller
name="metadata.disableStandardEmails.confirmation.host"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
onChange(e);
}}
/>
</>
)}
/>
</>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
@@ -1039,7 +1039,7 @@
"event_cancelled_trigger": "when event is cancelled",
"new_event_trigger": "when new event is booked",
"email_host_action": "send email to host",
"email_attendee_action": "send email to attendee",
"email_attendee_action": "send email to attendees",
"sms_attendee_action": "send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"workflows": "Workflows",
@@ -1809,5 +1809,9 @@
"pop_up_element_click": "Pop up via element click",
"open_dialog_with_element_click": "Open your Cal dialog when someone clicks an element.",
"need_help_embedding": "Need help? See our guides for embedding Cal on Wix, Squarespace, or WordPress, check our common questions, or explore advanced embed options.",
"book_my_cal": "Book my Cal"
"book_my_cal": "Book my Cal",
"disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees",
"disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.",
"disable_host_confirmation_emails": "Disable default confirmation emails for host",
"disable_host_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the host when the event is booked."
}
+30 -21
View File
@@ -47,33 +47,42 @@ const sendEmail = (prepare: () => BaseEmail) => {
});
};
export const sendScheduledEmails = async (calEvent: CalendarEvent, eventNameObject?: EventNameObjectType) => {
export const sendScheduledEmails = async (
calEvent: CalendarEvent,
eventNameObject?: EventNameObjectType,
hostEmailDisabled?: boolean,
attendeeEmailDisabled?: boolean
) => {
const emailsToSend: Promise<unknown>[] = [];
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent })));
if (!hostEmailDisabled) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent })));
if (calEvent.team) {
for (const teamMember of calEvent.team.members) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
if (calEvent.team) {
for (const teamMember of calEvent.team.members) {
emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember })));
}
}
}
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return sendEmail(
() =>
new AttendeeScheduledEmail(
{
...calEvent,
...(eventNameObject && {
title: getEventName({ ...eventNameObject, t: attendee.language.translate }),
}),
},
attendee
)
);
})
);
if (!attendeeEmailDisabled) {
emailsToSend.push(
...calEvent.attendees.map((attendee) => {
return sendEmail(
() =>
new AttendeeScheduledEmail(
{
...calEvent,
...(eventNameObject && {
title: getEventName({ ...eventNameObject, t: attendee.language.translate }),
}),
},
attendee
)
);
})
);
}
await Promise.all(emailsToSend);
};
+6 -4
View File
@@ -52,9 +52,10 @@ export default class BaseEmail {
...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }),
};
new Promise((resolve, reject) =>
createTransport(this.getMailerOptions().transport)
.sendMail(payloadWithUnEscapedSubject, (_err, info) => {
new Promise((resolve, reject) =>
createTransport(this.getMailerOptions().transport).sendMail(
payloadWithUnEscapedSubject,
(_err, info) => {
if (_err) {
const err = getErrorFromUnknown(_err);
this.printNodeMailerError(err);
@@ -62,7 +63,8 @@ export default class BaseEmail {
} else {
resolve(info);
}
})
}
)
).catch((e) => console.error("sendEmail", e));
return new Promise((resolve) => resolve("send mail async"));
}
@@ -33,6 +33,10 @@ import {
} from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
@@ -1966,6 +1970,26 @@ async function handler(
videoCallUrl = metadata.hangoutLink || defaultLocationUrl || videoCallUrl;
}
if (noEmail !== true) {
let isHostConfirmationEmailsDisabled = false;
let isAttendeeConfirmationEmailDisabled = false;
const workflows = eventType.workflows.map((workflow) => workflow.workflow);
if (eventType.workflows) {
isHostConfirmationEmailsDisabled =
eventType.metadata?.disableStandardEmails?.confirmation?.host || false;
isAttendeeConfirmationEmailDisabled =
eventType.metadata?.disableStandardEmails?.confirmation?.attendee || false;
if (isHostConfirmationEmailsDisabled) {
isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows);
}
if (isAttendeeConfirmationEmailDisabled) {
isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows);
}
}
await sendScheduledEmails(
{
...evt,
@@ -1973,7 +1997,9 @@ async function handler(
additionalNotes,
customInputs,
},
eventNameObject
eventNameObject,
isHostConfirmationEmailsDisabled,
isAttendeeConfirmationEmailDisabled
);
}
}
@@ -54,7 +54,7 @@ const ProfileView = () => {
const utils = trpc.useContext();
const session = useSession();
const [firstRender, setFirstRender] = useState(true);
useLayoutEffect(() => {
document.body.focus();
}, []);
@@ -0,0 +1,27 @@
import type { Workflow, WorkflowStep } from "@calcom/prisma/client";
import { WorkflowTriggerEvents } from "@calcom/prisma/client";
import { WorkflowActions } from "@calcom/prisma/enums";
export function allowDisablingHostConfirmationEmails(
workflows: (Workflow & {
steps: WorkflowStep[];
})[]
) {
return !!workflows.find(
(workflow) =>
workflow.trigger === WorkflowTriggerEvents.NEW_EVENT &&
!!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_HOST)
);
}
export function allowDisablingAttendeeConfirmationEmails(
workflows: (Workflow & {
steps: WorkflowStep[];
})[]
) {
return !!workflows.find(
(workflow) =>
workflow.trigger === WorkflowTriggerEvents.NEW_EVENT &&
!!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_ATTENDEE)
);
}
@@ -134,17 +134,33 @@ export const scheduleEmailReminder = async (
triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT
) {
try {
await sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: sender,
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
batchId: batchIdResponse[1].batch_id,
replyTo: evt.organizer.email,
});
if (Array.isArray(sendTo)) {
for (const email of sendTo) {
await sgMail.send({
to: email,
from: {
email: senderEmail,
name: sender,
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
batchId: batchIdResponse[1].batch_id,
replyTo: evt.organizer.email,
});
}
} else {
await sgMail.send({
to: sendTo,
from: {
email: senderEmail,
name: sender,
},
subject: emailContent.emailSubject,
html: emailContent.emailBody,
batchId: batchIdResponse[1].batch_id,
replyTo: evt.organizer.email,
});
}
} catch (error) {
console.log("Error sending Email");
}
@@ -77,16 +77,19 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST
) {
let sendTo = "";
let sendTo: string[] = [];
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = evt.organizer?.email || "";
sendTo = [evt.organizer?.email || ""];
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = !!emailAttendeeSendToOverride
? emailAttendeeSendToOverride
: evt.attendees?.[0]?.email || "";
const attendees = !!emailAttendeeSendToOverride
? [emailAttendeeSendToOverride]
: evt.attendees?.map((attendee) => attendee.email);
sendTo = attendees;
break;
}
+10
View File
@@ -41,6 +41,16 @@ export const EventTypeMetaDataSchema = z
apps: z.object(appDataSchemas).partial().optional(),
additionalNotesRequired: z.boolean().optional(),
disableSuccessPage: z.boolean().optional(),
disableStandardEmails: z
.object({
confirmation: z
.object({
host: z.boolean().optional(),
attendee: z.boolean().optional(),
})
.optional(),
})
.optional(),
managedEventConfig: z
.object({
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
@@ -5,6 +5,7 @@ import type { NextApiResponse, GetServerSidePropsContext } from "next";
import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import { validateIntervalLimitOrder } from "@calcom/lib";
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
@@ -141,6 +142,44 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
};
}
if (input.metadata?.disableStandardEmails) {
//check if user is allowed to disabled standard emails
const workflows = await ctx.prisma.workflow.findMany({
where: {
activeOn: {
some: {
eventTypeId: input.id,
},
},
trigger: WorkflowTriggerEvents.NEW_EVENT,
},
include: {
steps: true,
},
});
if (input.metadata?.disableStandardEmails.confirmation?.host) {
if (
!workflows.find(
(workflow) => !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_HOST)
)
) {
input.metadata.disableStandardEmails.confirmation.host = false;
}
}
if (input.metadata?.disableStandardEmails.confirmation?.attendee) {
if (
!workflows.find(
(workflow) => !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_ATTENDEE)
)
) {
input.metadata.disableStandardEmails.confirmation.attendee = false;
}
}
}
if (input?.price || input.metadata?.apps?.stripe?.price) {
data.price = input.price || input.metadata?.apps?.stripe?.price;
const paymentCredential = await ctx.prisma.credential.findFirst({
@@ -237,14 +237,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
step.action === WorkflowActions.EMAIL_ATTENDEE /*||
step.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
let sendTo: string[] = [];
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
sendTo = [bookingInfo.organizer?.email];
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
sendTo = bookingInfo.attendees.map((attendee) => attendee.email);
break;
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";*/
@@ -435,14 +435,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
newStep.action === WorkflowActions.EMAIL_ATTENDEE /*||
newStep.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
let sendTo: string[] = [];
switch (newStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
sendTo = [bookingInfo.organizer?.email];
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
sendTo = bookingInfo.attendees.map((attendee) => attendee.email);
break;
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = newStep.sendTo || "";*/
@@ -565,14 +565,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
step.action === WorkflowActions.EMAIL_HOST /*||
step.action === WorkflowActions.EMAIL_ADDRESS*/
) {
let sendTo = "";
let sendTo: string[] = [];
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
sendTo = [bookingInfo.organizer?.email];
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
sendTo = bookingInfo.attendees.map((attendee) => attendee.email);
break;
/*case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";*/