Add new workflow action: Send email to specific email address (#4929)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
Carina Wollendorfer 2022-10-10 15:40:20 +02:00 committed by GitHub
parent 1b15b71a24
commit 2a0a293f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 191 additions and 48 deletions

View File

@ -1296,6 +1296,7 @@
"update_timezone_question": "Update Timezone?",
"update_timezone_description": "It seems like your local timezone has changed to {{formattedCurrentTz}}. It's very important to have the correct timezone to prevent bookings at undesired times. Do you want to update it?",
"dont_update": "Don't update",
"email_address_action": "send email to a specific email address",
"after_event_trigger": "after event ends",
"how_long_after": "How long after event ends?"
}

View File

@ -66,10 +66,18 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
for (const reminder of unscheduledReminders) {
if (dayjs(reminder.scheduledDate).isBefore(dateInSeventyTwoHours)) {
try {
const sendTo =
reminder.workflowStep.action === WorkflowActions.EMAIL_HOST
? reminder.booking?.user?.email
: reminder.booking?.attendees[0].email;
let sendTo;
switch (reminder.workflowStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = reminder.booking?.user?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = reminder.booking?.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = reminder.workflowStep.sendTo;
}
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE

View File

@ -7,7 +7,17 @@ import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Form, Label, Select } from "@calcom/ui/v2";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
EmailField,
Form,
Label,
Select,
} from "@calcom/ui/v2";
import { WORKFLOW_ACTIONS } from "../../lib/constants";
import { getWorkflowActionOptions } from "../../lib/getOptions";
@ -27,13 +37,14 @@ export const AddActionDialog = (props: IAddActionDialog) => {
const { t } = useLocale();
const { isOpenDialog, setIsOpenDialog, addAction } = props;
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
const actionOptions = getWorkflowActionOptions(t);
const formSchema = z.object({
action: z.enum(WORKFLOW_ACTIONS),
sendTo: z
.string()
.refine((val) => isValidPhoneNumber(val))
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
.optional(),
});
@ -58,6 +69,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
form.unregister("action");
setIsOpenDialog(false);
setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false);
}}>
<div className="mt-5 space-y-1">
<Label htmlFor="label">{t("action")}:</Label>
@ -75,11 +87,17 @@ export const AddActionDialog = (props: IAddActionDialog) => {
form.setValue("action", val.value);
if (val.value === WorkflowActions.SMS_NUMBER) {
setIsPhoneNumberNeeded(true);
} else {
setIsEmailAddressNeeded(false);
} else if (val.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true);
setIsPhoneNumberNeeded(false);
} else {
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false);
form.unregister("sendTo");
}
form.unregister("sendTo");
form.clearErrors("action");
form.clearErrors("sendTo");
}
}}
options={actionOptions}
@ -109,6 +127,11 @@ export const AddActionDialog = (props: IAddActionDialog) => {
</div>
</div>
)}
{isEmailAddressNeeded && (
<div className="mt-5">
<EmailField required label={t("email_address")} {...form.register("sendTo")} />
</div>
)}
<DialogFooter>
<DialogClose asChild>
<Button
@ -118,6 +141,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
form.unregister("sendTo");
form.unregister("action");
setIsPhoneNumberNeeded(false);
setIsEmailAddressNeeded(false);
}}>
{t("cancel")}
</Button>

View File

@ -84,6 +84,9 @@ const WorkflowListItem = (props: ItemProps) => {
case WorkflowActions.SMS_NUMBER:
sendTo.add(step.sendTo || "");
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo.add(step.sendTo || "");
break;
}
});

View File

@ -20,7 +20,7 @@ import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { Button, DialogClose, DialogContent } from "@calcom/ui/v2";
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
import Select from "@calcom/ui/v2/core/form/Select";
import { Label, TextArea } from "@calcom/ui/v2/core/form/fields";
import { EmailField, Label, TextArea } from "@calcom/ui/v2/core/form/fields";
import { AddVariablesDropdown } from "../../components/v2/AddVariablesDropdown";
import {
@ -49,12 +49,18 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
step?.action === WorkflowActions.SMS_NUMBER ? true : false
);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
);
const [isCustomReminderBodyNeeded, setIsCustomReminderBodyNeeded] = useState(
step?.template === WorkflowTemplates.CUSTOM ? true : false
);
const [isEmailSubjectNeeded, setIsEmailSubjectNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ATTENDEE || step?.action === WorkflowActions.EMAIL_HOST
step?.action === WorkflowActions.EMAIL_ATTENDEE ||
step?.action === WorkflowActions.EMAIL_HOST ||
step?.action === WorkflowActions.EMAIL_ADDRESS
? true
: false
);
@ -270,12 +276,20 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (val) {
if (val.value === WorkflowActions.SMS_NUMBER) {
setIsPhoneNumberNeeded(true);
setIsEmailAddressNeeded(false);
} else if (val.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true);
setIsPhoneNumberNeeded(false);
} else {
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false);
}
form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
if (
val.value === WorkflowActions.EMAIL_ATTENDEE ||
val.value === WorkflowActions.EMAIL_HOST
val.value === WorkflowActions.EMAIL_HOST ||
val.value === WorkflowActions.EMAIL_ADDRESS
) {
setIsEmailSubjectNeeded(true);
} else {
@ -316,6 +330,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)}
</div>
)}
{isEmailAddressNeeded && (
<div className="mt-5 rounded-md bg-gray-50 p-4">
<EmailField
required
label={t("email_address")}
{...form.register(`steps.${step.stepNumber - 1}.sendTo`)}
/>
</div>
)}
<div className="mt-5">
<Label>{t("message_template")}</Label>
<Controller
@ -451,16 +474,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
reminderBody,
template: step.template,
});
}
} else {
const isNumberValid =
form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo
? false
: true;
const isNumberValid =
form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo
? false
: true;
if (isPhoneNumberNeeded && isNumberValid && !isEmpty) {
setConfirmationDialogOpen(true);
if (isPhoneNumberNeeded && isNumberValid && !isEmpty) {
setConfirmationDialogOpen(true);
}
}
}}
color="secondary">

View File

@ -11,6 +11,7 @@ export const WORKFLOW_TRIGGER_EVENTS = [
export const WORKFLOW_ACTIONS = [
WorkflowActions.EMAIL_HOST,
WorkflowActions.EMAIL_ATTENDEE,
WorkflowActions.EMAIL_ADDRESS,
WorkflowActions.SMS_ATTENDEE,
WorkflowActions.SMS_NUMBER,
] as const;

View File

@ -61,9 +61,26 @@ export const scheduleEmailReminder = async (
method: "POST",
});
const name = action === WorkflowActions.EMAIL_HOST ? evt.organizer.name : evt.attendees[0].name;
const attendeeName = action === WorkflowActions.EMAIL_HOST ? evt.attendees[0].name : evt.organizer.name;
const timeZone = action === WorkflowActions.EMAIL_HOST ? evt.organizer.timeZone : evt.attendees[0].timeZone;
let name = "";
let attendeeName = "";
let timeZone = "";
switch (action) {
case WorkflowActions.EMAIL_HOST:
name = evt.organizer.name;
attendeeName = evt.attendees[0].name;
timeZone = evt.organizer.timeZone;
break;
case WorkflowActions.EMAIL_ATTENDEE:
name = evt.attendees[0].name;
attendeeName = evt.organizer.name;
timeZone = evt.attendees[0].timeZone;
break;
case WorkflowActions.EMAIL_ADDRESS:
name = "";
attendeeName = evt.attendees[0].name;
timeZone = evt.organizer.timeZone;
}
let emailContent = {
emailSubject,

View File

@ -51,10 +51,21 @@ export const scheduleWorkflowReminders = async (
);
} else if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
) {
const sendTo =
step.action === WorkflowActions.EMAIL_HOST ? evt.organizer.email : evt.attendees[0].email;
let sendTo = "";
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = evt.organizer.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = evt.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
scheduleEmailReminder(
evt,
workflow.trigger,
@ -108,10 +119,21 @@ export const sendCancelledReminders = async (
);
} else if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
) {
const sendTo =
step.action === WorkflowActions.EMAIL_HOST ? evt.organizer.email : evt.attendees[0].email;
let sendTo = "";
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = evt.organizer.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = evt.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
scheduleEmailReminder(
evt,
workflow.trigger,

View File

@ -18,7 +18,9 @@ const emailReminderTemplate = (
.tz(timeZone)
.format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`;
const introHtml = `<body>Hi ${name},<br><br>This is a reminder about your upcoming event.<br><br>`;
const introHtml = `<body>Hi${
name ? " " + name : ""
},<br><br>This is a reminder about your upcoming event.<br><br>`;
const eventHtml = `<div style="font-weight: bold;">Event:</div>${eventName}<br><br>`;

View File

@ -55,7 +55,7 @@ const formSchema = z.object({
template: z.nativeEnum(WorkflowTemplates),
sendTo: z
.string()
.refine((val) => isValidPhoneNumber(val))
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
.nullable(),
})
.array(),
@ -77,6 +77,7 @@ function WorkflowPage() {
mode: "onBlur",
resolver: zodResolver(formSchema),
});
const { workflow: workflowId } = router.isReady ? querySchema.parse(router.query) : { workflow: -1 };
const utils = trpc.useContext();

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "WorkflowActions" ADD VALUE 'EMAIL_ADDRESS';

View File

@ -540,6 +540,7 @@ enum WorkflowActions {
EMAIL_ATTENDEE
SMS_ATTENDEE
SMS_NUMBER
EMAIL_ADDRESS
}
model WorkflowStep {

View File

@ -416,12 +416,22 @@ export const workflowsRouter = createProtectedRouter()
};
if (
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ATTENDEE
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_ADDRESS
) {
const sendTo =
step.action === WorkflowActions.EMAIL_HOST
? bookingInfo.organizer?.email
: bookingInfo.attendees[0].email;
let sendTo = "";
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
await scheduleEmailReminder(
bookingInfo,
trigger,
@ -503,7 +513,11 @@ export const workflowsRouter = createProtectedRouter()
},
data: {
action: newStep.action,
sendTo: newStep.action === WorkflowActions.SMS_NUMBER ? newStep.sendTo : null,
sendTo:
newStep.action === WorkflowActions.SMS_NUMBER ||
newStep.action === WorkflowActions.EMAIL_ADDRESS
? newStep.sendTo
: null,
stepNumber: newStep.stepNumber,
workflowId: newStep.workflowId,
reminderBody: newStep.template === WorkflowTemplates.CUSTOM ? newStep.reminderBody : null,
@ -577,12 +591,22 @@ export const workflowsRouter = createProtectedRouter()
};
if (
newStep.action === WorkflowActions.EMAIL_HOST ||
newStep.action === WorkflowActions.EMAIL_ATTENDEE
newStep.action === WorkflowActions.EMAIL_ATTENDEE ||
newStep.action === WorkflowActions.EMAIL_ADDRESS
) {
const sendTo =
newStep.action === WorkflowActions.EMAIL_HOST
? bookingInfo.organizer?.email
: bookingInfo.attendees[0].email;
let sendTo = "";
switch (newStep.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = newStep.sendTo || "";
}
await scheduleEmailReminder(
bookingInfo,
trigger,
@ -681,12 +705,22 @@ export const workflowsRouter = createProtectedRouter()
if (
step.action === WorkflowActions.EMAIL_ATTENDEE ||
step.action === WorkflowActions.EMAIL_HOST
step.action === WorkflowActions.EMAIL_HOST ||
step.action === WorkflowActions.EMAIL_ADDRESS
) {
const sendTo =
step.action === WorkflowActions.EMAIL_HOST
? bookingInfo.organizer?.email
: bookingInfo.attendees[0].email;
let sendTo = "";
switch (step.action) {
case WorkflowActions.EMAIL_HOST:
sendTo = bookingInfo.organizer?.email;
break;
case WorkflowActions.EMAIL_ATTENDEE:
sendTo = bookingInfo.attendees[0].email;
break;
case WorkflowActions.EMAIL_ADDRESS:
sendTo = step.sendTo || "";
}
await scheduleEmailReminder(
bookingInfo,
trigger,
@ -821,7 +855,11 @@ export const workflowsRouter = createProtectedRouter()
};
}
if (action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST) {
if (
action === WorkflowActions.EMAIL_ATTENDEE ||
action === WorkflowActions.EMAIL_HOST ||
action === WorkflowActions.EMAIL_ADDRESS
) {
scheduleEmailReminder(
evt,
WorkflowTriggerEvents.NEW_EVENT,