Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Compare workflow reminder bodies to default template #19060

Draft
wants to merge 11 commits into
base: tasker-scan-workflow-templates
Choose a base branch
from
195 changes: 55 additions & 140 deletions packages/features/ee/workflows/components/WorkflowStepContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,15 @@ import {
} from "@calcom/ui";

import {
getWhatsappTemplateForAction,
isAttendeeAction,
isSMSAction,
isSMSOrWhatsappAction,
isWhatsappAction,
getTemplateBodyForAction,
shouldScheduleEmailReminder,
} from "../lib/actionHelperFunctions";
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 smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate";
import { whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp";
import type { FormValues } from "../pages/workflow";
import { TimeTimeUnitInput } from "./TimeTimeUnitInput";

Expand Down Expand Up @@ -133,37 +130,25 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const triggerOptions = getWorkflowTriggerOptions(t);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan);
if (step && !form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
const template = getTemplateBodyForAction({
action,
locale: i18n.language,
template: step.template ?? WorkflowTemplates.REMINDER,
timeFormat,
});
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template);
}

if (step && form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.REMINDER) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type of logic was repeated through out the component. I decided to abstract this to getTemplateBodyForAction so it can be be used in other parts of the codebase

if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (isSMSAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true, i18n.language, action, timeFormat)
);
} else if (isWhatsappAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
whatsappReminderTemplate(true, i18n.language, action, timeFormat)
);
} else {
const reminderBodyTemplate = emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody;
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate);
}
}
if (!form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) {
const subjectTemplate = emailReminderTemplate(
true,
i18n.language,
form.getValues(`steps.${step.stepNumber - 1}.action`),
timeFormat
).emailSubject;
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate);
}
} else if (step && isWhatsappAction(step.action)) {
const templateBody = getWhatsappTemplateForAction(step.action, i18n.language, step.template, timeFormat);
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody);
if (step && !form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)) {
const subjectTemplate = emailReminderTemplate({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emails are the only one with subjects so keeping it as is

isEditingMode: true,
locale: i18n.language,
action: form.getValues(`steps.${step.stepNumber - 1}.action`),
timeFormat,
}).emailSubject;
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate);
}

const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step
Expand Down Expand Up @@ -457,6 +442,15 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);

const template = getTemplateBodyForAction({
action: val.value,
locale: i18n.language,
template: WorkflowTemplates.REMINDER,
timeFormat,
});

form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template);

const setNumberRequiredConfigs = (
phoneNumberIsNeeded: boolean,
senderNeeded = true
Expand All @@ -471,7 +465,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER);
// email action changes to sms action
if (!isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.sender`, SENDER_ID);
}

Expand All @@ -480,7 +473,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
setNumberRequiredConfigs(val.value === WorkflowActions.WHATSAPP_NUMBER, false);

if (!isWhatsappAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.sender`, "");
}

Expand All @@ -492,65 +484,6 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
setIsEmailSubjectNeeded(true);
}

if (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled with getTemplateBodyForAction

form.getValues(`steps.${step.stepNumber - 1}.template`) ===
WorkflowTemplates.REMINDER
) {
if (isSMSOrWhatsappAction(val.value) === isSMSOrWhatsappAction(oldValue)) {
if (isAttendeeAction(oldValue) !== isAttendeeAction(val.value)) {
const currentReminderBody =
form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || "";
const newReminderBody = currentReminderBody
.replaceAll("{ORGANIZER}", "{PLACEHOLDER}")
.replaceAll("{ATTENDEE}", "{ORGANIZER}")
.replaceAll("{PLACEHOLDER}", "{ATTENDEE}");
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, newReminderBody);

if (!isSMSOrWhatsappAction(val.value)) {
const currentEmailSubject =
form.getValues(`steps.${step.stepNumber - 1}.emailSubject`) || "";
const newEmailSubject = isAttendeeAction(val.value)
? currentEmailSubject.replace("{ORGANIZER}", "{ATTENDEE}")
: currentEmailSubject.replace("{ATTENDEE}", "{ORGANIZER}");

form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
newEmailSubject || ""
);
}
}
} else {
if (isSMSAction(val.value)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true, i18n.language, val.value, timeFormat)
);
} else if (isWhatsappAction(val.value)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
whatsappReminderTemplate(true, i18n.language, val.value, timeFormat)
);
} else {
const emailReminderBody = emailReminderTemplate(
true,
i18n.language,
val.value,
timeFormat
);
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
emailReminderBody.emailBody
);
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailReminderBody.emailSubject
);
}
}
} else {
const template = isWhatsappAction(val.value) ? "REMINDER" : "CUSTOM";
template && form.setValue(`steps.${step.stepNumber - 1}.template`, template);
}
form.unregister(`steps.${step.stepNumber - 1}.sendTo`);
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
form.setValue(`steps.${step.stepNumber - 1}.action`, val.value);
Expand Down Expand Up @@ -827,55 +760,37 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
onChange={(val) => {
if (val) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (val.value === WorkflowTemplates.REMINDER) {
if (isWhatsappAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
whatsappReminderTemplate(true, i18n.language, action, timeFormat)
);
} else if (isSMSAction(action)) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true, i18n.language, action, timeFormat)
);
} else {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
emailReminderTemplate(true, i18n.language, action, timeFormat).emailBody
);

const template = getTemplateBodyForAction({
action,
locale: i18n.language,
template: val.value ?? WorkflowTemplates.REMINDER,
timeFormat,
});

form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template);

if (shouldScheduleEmailReminder(action)) {
if (val.value === WorkflowTemplates.REMINDER) {
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailReminderTemplate(true, i18n.language, action, timeFormat).emailSubject
emailReminderTemplate({
isEditingMode: true,
locale: i18n.language,
action,
timeFormat,
}).emailSubject
);
}
} else if (val.value === WorkflowTemplates.RATING) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
emailRatingTemplate({
isEditingMode: true,
locale: i18n.language,
action,
timeFormat,
}).emailBody
);
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailRatingTemplate({
isEditingMode: true,
locale: i18n.language,
action,
timeFormat,
}).emailSubject
);
} else {
if (isWhatsappAction(action)) {
} else if (val.value === WorkflowTemplates.RATING) {
form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
getWhatsappTemplateForAction(action, i18n.language, val.value, timeFormat)
`steps.${step.stepNumber - 1}.emailSubject`,
emailRatingTemplate({
isEditingMode: true,
locale: i18n.language,
action,
timeFormat,
}).emailSubject
);
} else {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, "");
}
}
field.onChange(val.value);
Expand Down
39 changes: 39 additions & 0 deletions packages/features/ee/workflows/lib/actionHelperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
whatsappEventRescheduledTemplate,
whatsappReminderTemplate,
} from "../lib/reminders/templates/whatsapp";
import emailRatingTemplate from "./reminders/templates/emailRatingTemplate";
import emailReminderTemplate from "./reminders/templates/emailReminderTemplate";
import smsReminderTemplate from "./reminders/templates/smsReminderTemplate";

export function shouldScheduleEmailReminder(action: WorkflowActions) {
return action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST;
Expand Down Expand Up @@ -86,6 +89,17 @@ export function getWhatsappTemplateFunction(template?: WorkflowTemplates): typeo
}
}

function getEmailTemplateFunction(template?: WorkflowTemplates) {
switch (template) {
case WorkflowTemplates.REMINDER:
return emailReminderTemplate;
case WorkflowTemplates.RATING:
return emailRatingTemplate;
default:
return emailReminderTemplate;
}
}

export function getWhatsappTemplateForAction(
action: WorkflowActions,
locale: string,
Expand All @@ -95,3 +109,28 @@ export function getWhatsappTemplateForAction(
const templateFunction = getWhatsappTemplateFunction(template);
return templateFunction(true, locale, action, timeFormat);
}

export function getTemplateBodyForAction({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is used in WorkflowStepContainer when changing the template or action. Also used in tasker to grab the default template

action,
locale,
template,
timeFormat,
}: {
action: WorkflowActions;
locale: string;
template: WorkflowTemplates;
timeFormat: TimeFormat;
}): string | null {
if (isSMSAction(action)) {
return smsReminderTemplate(true, locale, action, timeFormat);
}

if (isWhatsappAction(action)) {
const templateFunction = getWhatsappTemplateFunction(template);
return templateFunction(true, locale, action, timeFormat);
}

// If not a whatsapp action then it's an email action
const templateFunction = getEmailTemplateFunction(template);
return templateFunction({ isEditingMode: true, locale, action, timeFormat }).emailBody;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that the workflow editor was adding HTML tags that weren't in the standard template. I decided that to compare if the workflow body was the same as the template is to stripe all of the HTML tags out of the reminder body and template and compare the content.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const compareReminderBodyToTemplate = ({
reminderBody,
template,
}: {
reminderBody: string;
template: string;
}) => {
const stripHTML = (html: string) => html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

& is encoded / not encoded between the editor and the template. Decided to keep both templates the same by adding &


const stripedReminderBody = stripHTML(reminderBody);
const stripedTemplate = stripHTML(template);

return stripedReminderBody === stripedTemplate;
};

export default compareReminderBodyToTemplate;
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ const emailRatingTemplate = ({
};

export default emailRatingTemplate;

export const plainTextTemplate =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests, it wouldn't make sense to strip the templates twice with the same function since it would always pass. By adding the plain text templates, we can detect whether the template was changed or something changed with the comparison function.

"Hi {ORGANIZER},We're always looking to improve our customer's experience. How satisfied were you with your recent meeting?😠 🙁 😐 😄 😍{ORGANIZER} didn't join the meeting? Reschedule hereEvent: {EVENT_NAME}Date & Time: {EVENT_DATE_ddd, MMM D, YYYY h:mma} - {EVENT_END_TIME} ({TIMEZONE})Attendees: You & {ORGANIZER}This survey was triggered by a Workflow in Cal.";
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@ import { APP_NAME } from "@calcom/lib/constants";
import { TimeFormat } from "@calcom/lib/timeFormat";
import { WorkflowActions } from "@calcom/prisma/enums";

const emailReminderTemplate = (
isEditingMode: boolean,
locale: string,
action?: WorkflowActions,
timeFormat?: TimeFormat,
startTime?: string,
endTime?: string,
eventName?: string,
timeZone?: string,
location?: string,
meetingUrl?: string,
otherPerson?: string,
name?: string,
isBrandingDisabled?: boolean
) => {
const emailReminderTemplate = ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored this function to take in an object as a param for readability and to align with other template functions.

isEditingMode,
locale,
action,
timeFormat,
startTime,
endTime,
eventName,
timeZone,
location,
meetingUrl,
otherPerson,
name,
isBrandingDisabled,
}: {
isEditingMode: boolean;
locale: string;
action?: WorkflowActions;
timeFormat?: TimeFormat;
startTime?: string;
endTime?: string;
eventName?: string;
timeZone?: string;
location?: string;
meetingUrl?: string;
otherPerson?: string;
name?: string;
isBrandingDisabled?: boolean;
}) => {
const currentTimeFormat = timeFormat || TimeFormat.TWELVE_HOUR;
const dateTimeFormat = `ddd, MMM D, YYYY ${currentTimeFormat}`;

Expand Down Expand Up @@ -63,3 +77,6 @@ const emailReminderTemplate = (
};

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.";
Loading