fix: formatting

This commit is contained in:
zomars 2023-07-18 13:27:54 -07:00
parent d27ff41682
commit 6b5c8935c5
26 changed files with 286 additions and 217 deletions

View File

@ -22,18 +22,18 @@ Any other relevant information. For example, why do you consider this a bug and
### Actual Results ### Actual Results
- What's happening right now that is different from what is expected - What's happening right now that is different from what is expected
### Expected Results ### Expected Results
- This is an ideal result that the system should get after the tests are performed - This is an ideal result that the system should get after the tests are performed
### Technical details ### Technical details
- Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/). - Browser version, screen recording, console logs, network requests: You can make a recording with [Bird Eats Bug](https://birdeatsbug.com/).
- Node.js version - Node.js version
- Anything else that you think could be an issue. - Anything else that you think could be an issue.
### Evidence ### Evidence
- How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution. - How was this tested? This is quite mandatory in terms of bugs. Providing evidence of your testing with screenshots or/and videos is an amazing way to prove the bug and a troubleshooting chance to find the solution.

View File

@ -11,31 +11,32 @@ Fixes # (issue)
## Requirement/Documentation ## Requirement/Documentation
<!-- Please provide all documents that are important to understand the reason of that PR. --> <!-- Please provide all documents that are important to understand the reason of that PR. -->
- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here. - If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.
## Type of change ## Type of change
<!-- Please delete bullets that are not relevant. --> <!-- Please delete bullets that are not relevant. -->
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements) - [ ] Chore (refactoring code, technical debt, workflow improvements)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update - [ ] This change requires a documentation update
## How should this be tested? ## How should this be tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests --> <!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->
- Are there environment variables that should be set? - Are there environment variables that should be set?
- What are the minimal test data to have? - What are the minimal test data to have?
- What is expected (happy path) to have (input and output)? - What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR - Any other important info that could help to test that PR
## Mandatory Tasks ## Mandatory Tasks
- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected. - [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.
## Checklist ## Checklist

View File

@ -90,7 +90,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
- [Prisma.io](https://prisma.io/?ref=cal.com) - [Prisma.io](https://prisma.io/?ref=cal.com)
- [Daily.co](https://go.cal.com/daily) - [Daily.co](https://go.cal.com/daily)
## Contact us ## Contact us
Meet our sales team for any commercial inquiries. Meet our sales team for any commercial inquiries.

View File

@ -7,7 +7,7 @@
<a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a> <a href="https://cal.com/docs/enterprise-features/api#api-server-specifications">Read the API docs</a>
</div> </div>
# Commercial Cal.com Public API # Commercial Cal.com Public API
Welcome to the Public API ("/apps/api") of the Cal.com. Welcome to the Public API ("/apps/api") of the Cal.com.

View File

@ -17,7 +17,7 @@ import {
allowDisablingHostConfirmationEmails, allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; } from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { EditableSchema } from "@calcom/features/form-builder/FormBuilderFieldsSchema" import { EditableSchema } from "@calcom/features/form-builder/FormBuilderFieldsSchema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants"; import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
@ -87,7 +87,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
return { return {
...field, ...field,
hidden: !enabled, hidden: !enabled,
editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<typeof EditableSchema> editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<
typeof EditableSchema
>,
}; };
} }
return field; return field;

View File

@ -12,6 +12,7 @@ items:
Skiff is a privacy-first, end-to-end encrypted workspace with Mail, Pages, Drive, and Calendar applications. Skiff Calendar integrates with Cal.com, enabling you to schedule appointments, organize events, and use your preferred video conferencing product. Skiff is a privacy-first, end-to-end encrypted workspace with Mail, Pages, Drive, and Calendar applications. Skiff Calendar integrates with Cal.com, enabling you to schedule appointments, organize events, and use your preferred video conferencing product.
## Features ## Features
- **Full workspace** — beyond email and calendar, Skiff enables writing and collaborating on end-to-end encrypted notes or wikis. You can also upload, preview, and share files. - **Full workspace** — beyond email and calendar, Skiff enables writing and collaborating on end-to-end encrypted notes or wikis. You can also upload, preview, and share files.
- **Native applications** — Skiff has native macOS, Windows, iOS, and Android applications, ensuring you can stay on top of your schedule from any device. - **Native applications** — Skiff has native macOS, Windows, iOS, and Android applications, ensuring you can stay on top of your schedule from any device.
- **Multiple accounts** — all Skiff products support multiple accounts, or sharing your entire team to collaborate inside one workspace. - **Multiple accounts** — all Skiff products support multiple accounts, or sharing your entire team to collaborate inside one workspace.

View File

@ -23,7 +23,7 @@ import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerLayout, BookerProps } from "./types"; import type { BookerLayout, BookerProps } from "./types";
import { useEvent } from "./utils/event"; import { useEvent } from "./utils/event";
import { validateLayout } from "./utils/layout"; import { validateLayout } from "./utils/layout";
import { getQueryParam } from './utils/query-param'; import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors"; import { useBrandColors } from "./utils/use-brand-colors";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy")); const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
@ -120,9 +120,9 @@ const BookerComponent = ({
layout !== _layout layout !== _layout
) { ) {
const validLayout = bookerLayouts.enabledLayouts.find((userLayout) => userLayout === layout); const validLayout = bookerLayouts.enabledLayouts.find((userLayout) => userLayout === layout);
validLayout && setLayout(validLayout) validLayout && setLayout(validLayout);
} }
}, [bookerLayouts, validateLayout, setLayout,_layout]); }, [bookerLayouts, validateLayout, setLayout, _layout]);
useEffect(() => { useEffect(() => {
if (event.isLoading) return setBookerState("loading"); if (event.isLoading) return setBookerState("loading");
@ -302,4 +302,4 @@ export const Booker = (props: BookerProps) => {
<BookerComponent {...props} /> <BookerComponent {...props} />
</LazyMotion> </LazyMotion>
); );
}; };

View File

@ -128,7 +128,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
if (["week_view", "column_view"].includes(layout) && !get().selectedDate) { if (["week_view", "column_view"].includes(layout) && !get().selectedDate) {
set({ selectedDate: dayjs().format("YYYY-MM-DD") }); set({ selectedDate: dayjs().format("YYYY-MM-DD") });
} }
updateQueryParam("layout", layout) updateQueryParam("layout", layout);
return set({ layout }); return set({ layout });
}, },
selectedDate: getQueryParam("date") || null, selectedDate: getQueryParam("date") || null,

View File

@ -1,8 +1,8 @@
import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations"; import { getEventLocationType, getTranslatedLocation } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "@calcom/ui"; import { Tooltip } from "@calcom/ui";
import { MapPin } from "@calcom/ui/components/icon"; import { MapPin } from "@calcom/ui/components/icon";
import { classNames } from "@calcom/lib";
import type { PublicEvent } from "../../types"; import type { PublicEvent } from "../../types";
import { EventMetaBlock } from "./Details"; import { EventMetaBlock } from "./Details";
@ -20,7 +20,7 @@ export const EventLocations = ({ event }: { event: PublicEvent }) => {
return translatedLocation; return translatedLocation;
}; };
const eventLocationType = getEventLocationType(locations[0].type); const eventLocationType = getEventLocationType(locations[0].type);
const icon = (locations.length > 1 || !eventLocationType?.iconUrl) ? MapPin : eventLocationType.iconUrl; const icon = locations.length > 1 || !eventLocationType?.iconUrl ? MapPin : eventLocationType.iconUrl;
return ( return (
<EventMetaBlock icon={icon} isDark={eventLocationType?.iconUrl?.includes("-dark")}> <EventMetaBlock icon={icon} isDark={eventLocationType?.iconUrl?.includes("-dark")}>
@ -44,13 +44,15 @@ export const EventLocations = ({ event }: { event: PublicEvent }) => {
<li key={`${location.type}-${index}`} className="mt-1"> <li key={`${location.type}-${index}`} className="mt-1">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<img <img
src={getEventLocationType(location.type)?.iconUrl} src={getEventLocationType(location.type)?.iconUrl}
className={classNames( className={classNames(
"h-3 w-3 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ", "h-3 w-3 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!getEventLocationType(location.type)?.iconUrl?.startsWith("/app-store") ? "dark:invert-[.65]" : "" !getEventLocationType(location.type)?.iconUrl?.startsWith("/app-store")
)} ? "dark:invert-[.65]"
alt={`${getEventLocationType(location.type)?.label} icon`} : ""
/> )}
alt={`${getEventLocationType(location.type)?.label} icon`}
/>
<span>{getLocationToDisplay(location)}</span> <span>{getLocationToDisplay(location)}</span>
</div> </div>
</li> </li>

View File

@ -116,8 +116,7 @@ const PaymentForm = (props: Props) => {
color="minimal" color="minimal"
disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)} disabled={!holdAcknowledged || ["processing", "error"].includes(state.status)}
id="cancel" id="cancel"
onClick={() => router.back()} onClick={() => router.back()}>
>
<span id="button-text">{t("cancel")}</span> <span id="button-text">{t("cancel")}</span>
</Button> </Button>
<Button <Button

View File

@ -207,8 +207,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
).html; ).html;
emailBodyEmpty = emailBodyEmpty =
customTemplate(reminder.workflowStep.reminderBody || "", variables, emailLocale).text.length === customTemplate(reminder.workflowStep.reminderBody || "", variables, emailLocale).text.length === 0;
0;
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) { } else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
emailContent = emailReminderTemplate( emailContent = emailReminderTemplate(
false, false,

View File

@ -93,7 +93,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsPhoneNumberNeeded(true); setIsPhoneNumberNeeded(true);
setIsSenderIdNeeded(true); setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID }) form.resetField("senderId", { defaultValue: SENDER_ID });
} else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) { } else if (newValue.value === WorkflowActions.EMAIL_ADDRESS) {
setIsEmailAddressNeeded(true); setIsEmailAddressNeeded(true);
setIsSenderIdNeeded(false); setIsSenderIdNeeded(false);
@ -102,7 +102,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
setIsSenderIdNeeded(true); setIsSenderIdNeeded(true);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(false); setIsPhoneNumberNeeded(false);
form.resetField("senderId", { defaultValue: SENDER_ID }) form.resetField("senderId", { defaultValue: SENDER_ID });
} else if (newValue.value === WorkflowActions.WHATSAPP_NUMBER) { } else if (newValue.value === WorkflowActions.WHATSAPP_NUMBER) {
setIsSenderIdNeeded(false); setIsSenderIdNeeded(false);
setIsPhoneNumberNeeded(true); setIsPhoneNumberNeeded(true);
@ -123,17 +123,16 @@ export const AddActionDialog = (props: IAddActionDialog) => {
const canRequirePhoneNumber = (workflowStep: string) => { const canRequirePhoneNumber = (workflowStep: string) => {
return ( return (
WorkflowActions.SMS_ATTENDEE === workflowStep || WorkflowActions.SMS_ATTENDEE === workflowStep || WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
WorkflowActions.WHATSAPP_ATTENDEE === workflowStep );
) };
}
const showSender = (action: string) => { const showSender = (action: string) => {
return !isSenderIdNeeded && !( return (
WorkflowActions.WHATSAPP_NUMBER === action || !isSenderIdNeeded &&
WorkflowActions.WHATSAPP_ATTENDEE === action !(WorkflowActions.WHATSAPP_NUMBER === action || WorkflowActions.WHATSAPP_ATTENDEE === action)
) );
} };
return ( return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}> <Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
@ -186,7 +185,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
{isPhoneNumberNeeded && ( {isPhoneNumberNeeded && (
<div className="mt-5 space-y-1"> <div className="mt-5 space-y-1">
<Label htmlFor="sendTo">{t("phone_number")}</Label> <Label htmlFor="sendTo">{t("phone_number")}</Label>
<div className="mt-1 mb-5"> <div className="mb-5 mt-1">
<Controller <Controller
control={form.control} control={form.control}
name="sendTo" name="sendTo"
@ -228,7 +227,7 @@ export const AddActionDialog = (props: IAddActionDialog) => {
)} )}
</> </>
)} )}
{showSender(form.getValues('action')) && ( {showSender(form.getValues("action")) && (
<div className="mt-5"> <div className="mt-5">
<Label>{t("sender_name")}</Label> <Label>{t("sender_name")}</Label>
<Input type="text" placeholder={SENDER_NAME} {...form.register(`senderName`)} /> <Input type="text" placeholder={SENDER_NAME} {...form.register(`senderName`)} />

View File

@ -39,8 +39,14 @@ import {
} from "@calcom/ui"; } from "@calcom/ui";
import { ArrowDown, MoreHorizontal, Trash2, HelpCircle, Info } from "@calcom/ui/components/icon"; import { ArrowDown, MoreHorizontal, Trash2, HelpCircle, Info } from "@calcom/ui/components/icon";
import {
isAttendeeAction,
isSMSAction,
isSMSOrWhatsappAction,
isWhatsappAction,
getWhatsappTemplateForAction,
} from "../lib/actionHelperFunctions";
import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants"; import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants";
import { isAttendeeAction, isSMSAction, isSMSOrWhatsappAction, isWhatsappAction, getWhatsappTemplateForAction } from "../lib/actionHelperFunctions";
import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions"; import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate"; import smsReminderTemplate from "../lib/reminders/templates/smsReminderTemplate";
@ -71,14 +77,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const [verificationCode, setVerificationCode] = useState(""); const [verificationCode, setVerificationCode] = useState("");
const action = step?.action const action = step?.action;
const requirePhoneNumber = WorkflowActions.SMS_NUMBER === action || WorkflowActions.WHATSAPP_NUMBER === action; const requirePhoneNumber =
WorkflowActions.SMS_NUMBER === action || WorkflowActions.WHATSAPP_NUMBER === action;
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(requirePhoneNumber); const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(requirePhoneNumber);
const [updateTemplate, setUpdateTemplate] = useState(false); const [updateTemplate, setUpdateTemplate] = useState(false);
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const senderNeeded = step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE; const senderNeeded =
step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE;
const [isSenderIsNeeded, setIsSenderIsNeeded] = useState(senderNeeded); const [isSenderIsNeeded, setIsSenderIsNeeded] = useState(senderNeeded);
@ -110,20 +118,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) { if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`); const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (isSMSAction(action)) { if (isSMSAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, smsReminderTemplate( form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, smsReminderTemplate(true, action));
true,
action
));
} else if (isWhatsappAction(action)) { } else if (isWhatsappAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, whatsappReminderTemplate( form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, whatsappReminderTemplate(true, action));
true,
action
))
} else { } else {
const reminderBodyTemplate = emailReminderTemplate( const reminderBodyTemplate = emailReminderTemplate(true, action).emailBody;
true,
action
).emailBody;
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate); form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, reminderBodyTemplate);
} }
} }
@ -135,8 +134,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate); form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectTemplate);
} }
} else if (step && isWhatsappAction(step.action)) { } else if (step && isWhatsappAction(step.action)) {
const templateBody = getWhatsappTemplateForAction(step.action, step.template) const templateBody = getWhatsappTemplateForAction(step.action, step.template);
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody) form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, templateBody);
} }
const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step
@ -151,7 +150,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const refReminderBody = useRef<HTMLTextAreaElement | null>(null); const refReminderBody = useRef<HTMLTextAreaElement | null>(null);
const getNumberVerificationStatus = () => !!step && !!verifiedNumbers.find((number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)) const getNumberVerificationStatus = () =>
!!step &&
!!verifiedNumbers.find(
(number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
);
const [numberVerified, setNumberVerified] = useState(getNumberVerificationStatus()); const [numberVerified, setNumberVerified] = useState(getNumberVerificationStatus());
@ -236,17 +239,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
return ( return (
<> <>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="w-full border rounded-md min-w-80 bg-default border-subtle p-7"> <div className="min-w-80 bg-default border-subtle w-full rounded-md border p-7">
<div className="flex"> <div className="flex">
<div className="bg-subtle text-default mt-[3px] flex h-5 w-5 items-center justify-center rounded-full p-1 text-xs font-medium ltr:mr-5 rtl:ml-5"> <div className="bg-subtle text-default mt-[3px] flex h-5 w-5 items-center justify-center rounded-full p-1 text-xs font-medium ltr:mr-5 rtl:ml-5">
1 1
</div> </div>
<div> <div>
<div className="text-base font-bold text-emphasis">{t("trigger")}</div> <div className="text-emphasis text-base font-bold">{t("trigger")}</div>
<div className="text-sm text-default">{t("when_something_happens")}</div> <div className="text-default text-sm">{t("when_something_happens")}</div>
</div> </div>
</div> </div>
<div className="border-t border-subtle my-7" /> <div className="border-subtle my-7 border-t" />
<Label>{t("when")}</Label> <Label>{t("when")}</Label>
<Controller <Controller
name="trigger" name="trigger"
@ -291,7 +294,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Label>{showTimeSectionAfter ? t("how_long_after") : t("how_long_before")}</Label> <Label>{showTimeSectionAfter ? t("how_long_after") : t("how_long_before")}</Label>
<TimeTimeUnitInput form={form} disabled={props.readOnly} /> <TimeTimeUnitInput form={form} disabled={props.readOnly} />
{!props.readOnly && ( {!props.readOnly && (
<div className="flex mt-1 text-gray-500"> <div className="mt-1 flex text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" /> <Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p> <p className="text-sm">{t("testing_workflow_info_message")}</p>
</div> </div>
@ -318,18 +321,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const canRequirePhoneNumber = (workflowStep: string) => { const canRequirePhoneNumber = (workflowStep: string) => {
return ( return (
WorkflowActions.SMS_ATTENDEE === workflowStep || WorkflowActions.SMS_ATTENDEE === workflowStep || WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
WorkflowActions.WHATSAPP_ATTENDEE === workflowStep );
) };
}
return ( return (
<> <>
<div className="flex justify-center my-3"> <div className="my-3 flex justify-center">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" /> <ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<div className="flex w-full border rounded-md min-w-80 bg-default border-subtle p-7"> <div className="min-w-80 bg-default border-subtle flex w-full rounded-md border p-7">
<div className="w-full"> <div className="w-full">
<div className="flex"> <div className="flex">
<div className="w-full"> <div className="w-full">
@ -338,8 +340,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{step.stepNumber + 1} {step.stepNumber + 1}
</div> </div>
<div> <div>
<div className="text-base font-bold text-emphasis">{t("action")}</div> <div className="text-emphasis text-base font-bold">{t("action")}</div>
<div className="text-sm text-default">{t("action_is_performed")}</div> <div className="text-default text-sm">{t("action_is_performed")}</div>
</div> </div>
</div> </div>
</div> </div>
@ -379,7 +381,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div> </div>
)} )}
</div> </div>
<div className="border-t border-subtle my-7" /> <div className="border-subtle my-7 border-t" />
<div> <div>
<Label>{t("do_this")}</Label> <Label>{t("do_this")}</Label>
<Controller <Controller
@ -395,16 +397,18 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
if (val) { if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`); const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
const setNumberRequiredConfigs = (phoneNumberIsNeeded: boolean, senderNeeded = true) => { const setNumberRequiredConfigs = (
phoneNumberIsNeeded: boolean,
senderNeeded = true
) => {
setIsSenderIsNeeded(senderNeeded); setIsSenderIsNeeded(senderNeeded);
setIsEmailAddressNeeded(false); setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(phoneNumberIsNeeded); setIsPhoneNumberNeeded(phoneNumberIsNeeded);
setNumberVerified(getNumberVerificationStatus()); setNumberVerified(getNumberVerificationStatus());
} };
if (isSMSAction(val.value)) { if (isSMSAction(val.value)) {
setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER);
setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER)
// email action changes to sms action // email action changes to sms action
if (!isSMSAction(oldValue)) { if (!isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
@ -501,7 +505,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/> />
</div> </div>
{isPhoneNumberNeeded && ( {isPhoneNumberNeeded && (
<div className="p-4 pt-0 mt-2 rounded-md bg-muted"> <div className="bg-muted mt-2 rounded-md p-4 pt-0">
<Label className="pt-4">{t("custom_phone_number")}</Label> <Label className="pt-4">{t("custom_phone_number")}</Label>
<div className="block sm:flex"> <div className="block sm:flex">
<Controller <Controller
@ -553,7 +557,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
) : ( ) : (
!props.readOnly && ( !props.readOnly && (
<> <>
<div className="flex mt-3"> <div className="mt-3 flex">
<TextField <TextField
className="rounded-r-none border-r-transparent" className="rounded-r-none border-r-transparent"
placeholder="Verification code" placeholder="Verification code"
@ -589,43 +593,45 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)} )}
</div> </div>
)} )}
{!isWhatsappAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (<div className="p-4 pt-0 mt-2 rounded-md bg-muted"> {!isWhatsappAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
{isSenderIsNeeded ? ( <div className="bg-muted mt-2 rounded-md p-4 pt-0">
<> {isSenderIsNeeded ? (
<div className="pt-4"> <>
<div className="flex"> <div className="pt-4">
<Label>{t("sender_id")}</Label> <div className="flex">
<Tooltip content={t("sender_id_info")}> <Label>{t("sender_id")}</Label>
<Info className="ml-2 mr-1 mt-0.5 h-4 w-4 text-gray-500" /> <Tooltip content={t("sender_id_info")}>
</Tooltip> <Info className="ml-2 mr-1 mt-0.5 h-4 w-4 text-gray-500" />
</Tooltip>
</div>
<Input
type="text"
placeholder={SENDER_ID}
disabled={props.readOnly}
maxLength={11}
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
/>
</div> </div>
<Input {form.formState.errors.steps &&
type="text" form.formState?.errors?.steps[step.stepNumber - 1]?.sender && (
placeholder={SENDER_ID} <p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p>
disabled={props.readOnly} )}
maxLength={11} </>
{...form.register(`steps.${step.stepNumber - 1}.sender`)} ) : (
/> <>
</div> <div className="pt-4">
{form.formState.errors.steps && <Label>{t("sender_name")}</Label>
form.formState?.errors?.steps[step.stepNumber - 1]?.sender && ( <Input
<p className="mt-1 text-xs text-red-500">{t("sender_id_error_message")}</p> type="text"
)} disabled={props.readOnly}
</> placeholder={SENDER_NAME}
) : ( {...form.register(`steps.${step.stepNumber - 1}.senderName`)}
<> />
<div className="pt-4"> </div>
<Label>{t("sender_name")}</Label> </>
<Input )}
type="text" </div>
disabled={props.readOnly} )}
placeholder={SENDER_NAME}
{...form.register(`steps.${step.stepNumber - 1}.senderName`)}
/>
</div>
</>
)}
</div>)}
{canRequirePhoneNumber(form.getValues(`steps.${step.stepNumber - 1}.action`)) && ( {canRequirePhoneNumber(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="mt-2"> <div className="mt-2">
<Controller <Controller
@ -647,7 +653,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div> </div>
)} )}
{isEmailAddressNeeded && ( {isEmailAddressNeeded && (
<div className="p-4 mt-5 rounded-md bg-muted"> <div className="bg-muted mt-5 rounded-md p-4">
<EmailField <EmailField
required required
disabled={props.readOnly} disabled={props.readOnly}
@ -669,20 +675,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
isDisabled={props.readOnly} isDisabled={props.readOnly}
onChange={(val) => { onChange={(val) => {
if (val) { if (val) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`) const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
if (val.value === WorkflowTemplates.REMINDER) { if (val.value === WorkflowTemplates.REMINDER) {
if (isWhatsappAction(action)) { if (isWhatsappAction(action)) {
form.setValue( form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`, `steps.${step.stepNumber - 1}.reminderBody`,
whatsappReminderTemplate( whatsappReminderTemplate(true, action)
true,
action
)
); );
} else if (isSMSAction(action)) { } else if (isSMSAction(action)) {
form.setValue( form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`, `steps.${step.stepNumber - 1}.reminderBody`,
smsReminderTemplate(true,action) smsReminderTemplate(true, action)
); );
} else { } else {
form.setValue( form.setValue(
@ -696,13 +699,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
} }
} else { } else {
if (isWhatsappAction(action)) { if (isWhatsappAction(action)) {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, getWhatsappTemplateForAction(action, val.value)) form.setValue(
`steps.${step.stepNumber - 1}.reminderBody`,
getWhatsappTemplateForAction(action, val.value)
);
} else { } else {
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, ""); form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, ""); form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, "");
} }
} }
field.onChange(val.value) field.onChange(val.value);
form.setValue(`steps.${step.stepNumber - 1}.template`, val.value); form.setValue(`steps.${step.stepNumber - 1}.template`, val.value);
setUpdateTemplate(!updateTemplate); setUpdateTemplate(!updateTemplate);
} }
@ -715,7 +721,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}} }}
/> />
</div> </div>
<div className="pt-2 mt-2 rounded-md bg-muted md:p-6 md:pt-4"> <div className="bg-muted mt-2 rounded-md pt-2 md:p-6 md:pt-4">
{isEmailSubjectNeeded && ( {isEmailSubjectNeeded && (
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center"> <div className="flex items-center">
@ -755,7 +761,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
step.action !== WorkflowActions.SMS_NUMBER ? ( step.action !== WorkflowActions.SMS_NUMBER ? (
<> <>
<div className="mb-2 flex items-center pb-[1.5px]"> <div className="mb-2 flex items-center pb-[1.5px]">
<Label className="flex-none mb-0 "> <Label className="mb-0 flex-none ">
{isEmailSubjectNeeded ? t("email_body") : t("text_message")} {isEmailSubjectNeeded ? t("email_body") : t("text_message")}
</Label> </Label>
</div> </div>
@ -795,7 +801,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
reminderBodyFormRef?.(e); reminderBodyFormRef?.(e);
refReminderBody.current = e; refReminderBody.current = e;
}} }}
className="h-24 my-0" className="my-0 h-24"
disabled={props.readOnly} disabled={props.readOnly}
required required
{...restReminderBodyForm} {...restReminderBodyForm}
@ -811,7 +817,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{!props.readOnly && ( {!props.readOnly && (
<div className="mt-3 "> <div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}> <button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="flex mt-2 text-sm text-default"> <div className="text-default mt-2 flex text-sm">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" /> <HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p> <p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div> </div>
@ -923,23 +929,23 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<DialogContent type="creation" className="sm:max-w-[610px]"> <DialogContent type="creation" className="sm:max-w-[610px]">
<div className="-m-3 h-[430px] overflow-x-hidden overflow-y-scroll sm:m-0"> <div className="-m-3 h-[430px] overflow-x-hidden overflow-y-scroll sm:m-0">
<h1 className="w-full text-xl font-semibold ">{t("how_booking_questions_as_variables")}</h1> <h1 className="w-full text-xl font-semibold ">{t("how_booking_questions_as_variables")}</h1>
<div className="mb-6 rounded-md bg-muted-3 sm:p-4"> <div className="bg-muted-3 mb-6 rounded-md sm:p-4">
<p className="font-medium test-sm">{t("format")}</p> <p className="test-sm font-medium">{t("format")}</p>
<ul className="mt-2 ml-5 list-disc text-emphasis"> <ul className="text-emphasis ml-5 mt-2 list-disc">
<li>{t("uppercase_for_letters")}</li> <li>{t("uppercase_for_letters")}</li>
<li>{t("replace_whitespaces_underscores")}</li> <li>{t("replace_whitespaces_underscores")}</li>
<li>{t("ignore_special_characters_booking_questions")}</li> <li>{t("ignore_special_characters_booking_questions")}</li>
</ul> </ul>
<div className="mt-4"> <div className="mt-4">
<p className="w-full font-medium test-sm">{t("example_1")}</p> <p className="test-sm w-full font-medium">{t("example_1")}</p>
<div className="grid grid-cols-12 mt-2"> <div className="mt-2 grid grid-cols-12">
<div className="col-span-5 test-sm text-default ltr:mr-2 rtl:ml-2"> <div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
{t("booking_question_identifier")} {t("booking_question_identifier")}
</div> </div>
<div className="col-span-7 test-sm text-emphasis">{t("company_size")}</div> <div className="test-sm text-emphasis col-span-7">{t("company_size")}</div>
<div className="w-full col-span-5 test-sm text-default">{t("variable")}</div> <div className="test-sm text-default col-span-5 w-full">{t("variable")}</div>
<div className="col-span-7 break-words test-sm text-emphasis"> <div className="test-sm text-emphasis col-span-7 break-words">
{" "} {" "}
{`{${t("company_size") {`{${t("company_size")
.replace(/[^a-zA-Z0-9 ]/g, "") .replace(/[^a-zA-Z0-9 ]/g, "")
@ -950,14 +956,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<p className="w-full font-medium test-sm">{t("example_2")}</p> <p className="test-sm w-full font-medium">{t("example_2")}</p>
<div className="grid grid-cols-12 mt-2"> <div className="mt-2 grid grid-cols-12">
<div className="col-span-5 test-sm text-default ltr:mr-2 rtl:ml-2"> <div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
{t("booking_question_identifier")} {t("booking_question_identifier")}
</div> </div>
<div className="col-span-7 test-sm text-emphasis">{t("what_help_needed")}</div> <div className="test-sm text-emphasis col-span-7">{t("what_help_needed")}</div>
<div className="col-span-5 test-sm text-default">{t("variable")}</div> <div className="test-sm text-default col-span-5">{t("variable")}</div>
<div className="col-span-7 break-words test-sm text-emphasis"> <div className="test-sm text-emphasis col-span-7 break-words">
{" "} {" "}
{`{${t("what_help_needed") {`{${t("what_help_needed")
.replace(/[^a-zA-Z0-9 ]/g, "") .replace(/[^a-zA-Z0-9 ]/g, "")

View File

@ -1,5 +1,11 @@
import { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@prisma/client"; import { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@prisma/client";
import { whatsappEventCancelledTemplate, whatsappEventCompletedTemplate, whatsappEventRescheduledTemplate, whatsappReminderTemplate } from "../lib/reminders/templates/whatsapp";
import {
whatsappEventCancelledTemplate,
whatsappEventCompletedTemplate,
whatsappEventRescheduledTemplate,
whatsappReminderTemplate,
} from "../lib/reminders/templates/whatsapp";
export function isSMSAction(action: WorkflowActions) { export function isSMSAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER; return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
@ -10,46 +16,53 @@ export function isWhatsappAction(action: WorkflowActions) {
} }
export function isSMSOrWhatsappAction(action: WorkflowActions) { export function isSMSOrWhatsappAction(action: WorkflowActions) {
return isSMSAction(action) || isWhatsappAction(action) return isSMSAction(action) || isWhatsappAction(action);
} }
export function isAttendeeAction(action: WorkflowActions) { export function isAttendeeAction(action: WorkflowActions) {
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE; return (
action === WorkflowActions.SMS_ATTENDEE ||
action === WorkflowActions.EMAIL_ATTENDEE ||
action === WorkflowActions.WHATSAPP_ATTENDEE
);
} }
export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates { export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates {
switch(trigger) { switch (trigger) {
case "NEW_EVENT": case "NEW_EVENT":
case "BEFORE_EVENT": case "BEFORE_EVENT":
return WorkflowTemplates.REMINDER return WorkflowTemplates.REMINDER;
case "AFTER_EVENT": case "AFTER_EVENT":
return WorkflowTemplates.COMPLETED return WorkflowTemplates.COMPLETED;
case "EVENT_CANCELLED": case "EVENT_CANCELLED":
return WorkflowTemplates.CANCELLED return WorkflowTemplates.CANCELLED;
case "RESCHEDULE_EVENT": case "RESCHEDULE_EVENT":
return WorkflowTemplates.RESCHEDULED return WorkflowTemplates.RESCHEDULED;
default: default:
return WorkflowTemplates.REMINDER return WorkflowTemplates.REMINDER;
} }
} }
export function getWhatsappTemplateFunction(template: WorkflowTemplates): typeof whatsappReminderTemplate { export function getWhatsappTemplateFunction(template: WorkflowTemplates): typeof whatsappReminderTemplate {
switch(template) { switch (template) {
case "CANCELLED": case "CANCELLED":
return whatsappEventCancelledTemplate return whatsappEventCancelledTemplate;
case "COMPLETED": case "COMPLETED":
return whatsappEventCompletedTemplate return whatsappEventCompletedTemplate;
case "RESCHEDULED": case "RESCHEDULED":
return whatsappEventRescheduledTemplate return whatsappEventRescheduledTemplate;
case "CUSTOM": case "CUSTOM":
case "REMINDER": case "REMINDER":
return whatsappReminderTemplate return whatsappReminderTemplate;
default: default:
return whatsappReminderTemplate return whatsappReminderTemplate;
} }
} }
export function getWhatsappTemplateForAction(action: WorkflowActions, template: WorkflowTemplates): string | null { export function getWhatsappTemplateForAction(
const templateFunction = getWhatsappTemplateFunction(template) action: WorkflowActions,
return templateFunction(true, action) template: WorkflowTemplates
): string | null {
const templateFunction = getWhatsappTemplateFunction(template);
return templateFunction(true, action);
} }

View File

@ -25,19 +25,16 @@ export const WORKFLOW_TEMPLATES = [
WorkflowTemplates.REMINDER, WorkflowTemplates.REMINDER,
WorkflowTemplates.CANCELLED, WorkflowTemplates.CANCELLED,
WorkflowTemplates.COMPLETED, WorkflowTemplates.COMPLETED,
WorkflowTemplates.RESCHEDULED WorkflowTemplates.RESCHEDULED,
] as const; ] as const;
export const BASIC_WORKFLOW_TEMPLATES = [ export const BASIC_WORKFLOW_TEMPLATES = [WorkflowTemplates.CUSTOM, WorkflowTemplates.REMINDER] as const;
WorkflowTemplates.CUSTOM,
WorkflowTemplates.REMINDER,
] as const;
export const WHATSAPP_WORKFLOW_TEMPLATES = [ export const WHATSAPP_WORKFLOW_TEMPLATES = [
WorkflowTemplates.REMINDER, WorkflowTemplates.REMINDER,
WorkflowTemplates.COMPLETED, WorkflowTemplates.COMPLETED,
WorkflowTemplates.CANCELLED, WorkflowTemplates.CANCELLED,
WorkflowTemplates.RESCHEDULED WorkflowTemplates.RESCHEDULED,
] as const; ] as const;
export const DYNAMIC_TEXT_VARIABLES = [ export const DYNAMIC_TEXT_VARIABLES = [

View File

@ -1,9 +1,9 @@
import type { WorkflowStep } from "@prisma/client"; import type { WorkflowStep } from "@prisma/client";
import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import { WorkflowActions } from "@calcom/prisma/enums"; import { WorkflowActions } from "@calcom/prisma/enums";
import { Zap, Smartphone, Mail, Bell } from "@calcom/ui/components/icon"; import { Zap, Smartphone, Mail, Bell } from "@calcom/ui/components/icon";
import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
export function getActionIcon(steps: WorkflowStep[], className?: string): JSX.Element { export function getActionIcon(steps: WorkflowStep[], className?: string): JSX.Element {
if (steps.length === 0) { if (steps.length === 0) {

View File

@ -3,7 +3,13 @@ import type { TFunction } from "next-i18next";
import { WorkflowActions } from "@calcom/prisma/enums"; import { WorkflowActions } from "@calcom/prisma/enums";
import { isSMSOrWhatsappAction, isWhatsappAction } from "./actionHelperFunctions"; import { isSMSOrWhatsappAction, isWhatsappAction } from "./actionHelperFunctions";
import { TIME_UNIT, WHATSAPP_WORKFLOW_TEMPLATES, WORKFLOW_ACTIONS, BASIC_WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS } from "./constants"; import {
TIME_UNIT,
WHATSAPP_WORKFLOW_TEMPLATES,
WORKFLOW_ACTIONS,
BASIC_WORKFLOW_TEMPLATES,
WORKFLOW_TRIGGER_EVENTS,
} from "./constants";
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) { export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
@ -33,7 +39,8 @@ export function getWorkflowTimeUnitOptions(t: TFunction) {
} }
export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions | undefined) { export function getWorkflowTemplateOptions(t: TFunction, action: WorkflowActions | undefined) {
const TEMPLATES = (action && isWhatsappAction(action)) ? WHATSAPP_WORKFLOW_TEMPLATES : BASIC_WORKFLOW_TEMPLATES; const TEMPLATES =
action && isWhatsappAction(action) ? WHATSAPP_WORKFLOW_TEMPLATES : BASIC_WORKFLOW_TEMPLATES;
return TEMPLATES.map((template) => { return TEMPLATES.map((template) => {
return { label: t(`${template.toLowerCase()}`), value: template }; return { label: t(`${template.toLowerCase()}`), value: template };
}) as { label: string; value: any }[]; }) as { label: string; value: any }[];

View File

@ -1,6 +1,7 @@
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client"; import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import type { MailData } from "@sendgrid/helpers/classes/mail"; import type { MailData } from "@sendgrid/helpers/classes/mail";
import { isWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants"; import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { WorkflowTriggerEvents } from "@calcom/prisma/enums"; import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { WorkflowActions } from "@calcom/prisma/enums"; import { WorkflowActions } from "@calcom/prisma/enums";
@ -10,8 +11,6 @@ import { scheduleEmailReminder } from "./emailReminderManager";
import { scheduleSMSReminder } from "./smsReminderManager"; import { scheduleSMSReminder } from "./smsReminderManager";
import { scheduleWhatsappReminder } from "./whatsappReminderManager"; import { scheduleWhatsappReminder } from "./whatsappReminderManager";
import { isWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
type ExtendedCalendarEvent = CalendarEvent & { type ExtendedCalendarEvent = CalendarEvent & {
metadata?: { videoCallUrl: string | undefined }; metadata?: { videoCallUrl: string | undefined };
eventType: { slug?: string }; eventType: { slug?: string };
@ -113,7 +112,8 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
hideBranding hideBranding
); );
} else if (isWhatsappAction(step.action)) { } else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo; const sendTo =
step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder( await scheduleWhatsappReminder(
evt, evt,
sendTo, sendTo,
@ -208,7 +208,8 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
hideBranding hideBranding
); );
} else if (isWhatsappAction(step.action)) { } else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo; const sendTo =
step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleWhatsappReminder( await scheduleWhatsappReminder(
evt, evt,
sendTo, sendTo,

View File

@ -20,11 +20,11 @@ function assertTwilio(twilio: TwilioClient.Twilio | undefined): asserts twilio i
} }
function getDefaultSender(whatsapp = false) { function getDefaultSender(whatsapp = false) {
let defaultSender = process.env.TWILIO_PHONE_NUMBER let defaultSender = process.env.TWILIO_PHONE_NUMBER;
if (whatsapp) { if (whatsapp) {
defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}` defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}`;
} }
return defaultSender return defaultSender;
} }
function getSMSNumber(phone: string, whatsapp = false) { function getSMSNumber(phone: string, whatsapp = false) {
@ -43,7 +43,13 @@ export const sendSMS = async (phoneNumber: string, body: string, sender: string,
return response; return response;
}; };
export const scheduleSMS = async (phoneNumber: string, body: string, scheduledDate: Date, sender: string, whatsapp = false) => { export const scheduleSMS = async (
phoneNumber: string,
body: string,
scheduledDate: Date,
sender: string,
whatsapp = false
) => {
assertTwilio(twilio); assertTwilio(twilio);
const response = await twilio.messages.create({ const response = await twilio.messages.create({
body: body, body: body,

View File

@ -1,4 +1,4 @@
export * from "./whatsappEventCancelledTemplate" export * from "./whatsappEventCancelledTemplate";
export * from "./whatsappEventCompletedTemplate" export * from "./whatsappEventCompletedTemplate";
export * from "./whatsappEventReminderTemplate" export * from "./whatsappEventReminderTemplate";
export * from "./whatsappEventRescheduledTemplate" export * from "./whatsappEventRescheduledTemplate";

View File

@ -1,6 +1,7 @@
import dayjs from "@calcom/dayjs";
import { WorkflowActions } from "@prisma/client"; import { WorkflowActions } from "@prisma/client";
import dayjs from "@calcom/dayjs";
export const whatsappEventCancelledTemplate = ( export const whatsappEventCancelledTemplate = (
isEditingMode: boolean, isEditingMode: boolean,
action?: WorkflowActions, action?: WorkflowActions,
@ -26,8 +27,8 @@ export const whatsappEventCancelledTemplate = (
const templateOne = `Hi${ const templateOne = `Hi${
name ? ` ${name}` : `` name ? ` ${name}` : ``
}, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been canceled.` }, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been canceled.`;
//Twilio supports up to 1024 characters for whatsapp template messages //Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne; if (templateOne.length <= 1024) return templateOne;

View File

@ -1,6 +1,7 @@
import dayjs from "@calcom/dayjs";
import { WorkflowActions } from "@prisma/client"; import { WorkflowActions } from "@prisma/client";
import dayjs from "@calcom/dayjs";
export const whatsappEventCompletedTemplate = ( export const whatsappEventCompletedTemplate = (
isEditingMode: boolean, isEditingMode: boolean,
action?: WorkflowActions, action?: WorkflowActions,

View File

@ -1,4 +1,5 @@
import { WorkflowActions } from "@prisma/client"; import { WorkflowActions } from "@prisma/client";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
export const whatsappEventRescheduledTemplate = ( export const whatsappEventRescheduledTemplate = (
@ -27,8 +28,7 @@ export const whatsappEventRescheduledTemplate = (
const templateOne = `Hi${ const templateOne = `Hi${
name ? ` ${name}` : `` name ? ` ${name}` : ``
}, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been rescheduled.`; }, your meeting (*${eventName}*) with ${attendee} on ${eventDate} at ${startTime} ${timeZone} has been rescheduled.`;
//Twilio supports up to 1024 characters for whatsapp template messages //Twilio supports up to 1024 characters for whatsapp template messages
if (templateOne.length <= 1024) return templateOne; if (templateOne.length <= 1024) return templateOne;

View File

@ -2,12 +2,17 @@ import type { TimeUnit } from "@prisma/client";
import { WorkflowTriggerEvents, WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@prisma/client"; import { WorkflowTriggerEvents, WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@prisma/client";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import { BookingInfo, deleteScheduledSMSReminder, timeUnitLowerCase } from "./smsReminderManager"; import prisma from "@calcom/prisma";
import * as twilio from "./smsProviders/twilioProvider"; import * as twilio from "./smsProviders/twilioProvider";
import { whatsappEventCancelledTemplate, whatsappEventCompletedTemplate, whatsappEventRescheduledTemplate, whatsappReminderTemplate } from "./templates/whatsapp"; import { BookingInfo, deleteScheduledSMSReminder, timeUnitLowerCase } from "./smsReminderManager";
import {
whatsappEventCancelledTemplate,
whatsappEventCompletedTemplate,
whatsappEventRescheduledTemplate,
whatsappReminderTemplate,
} from "./templates/whatsapp";
const log = logger.getChildLogger({ prefix: ["[whatsappReminderManager]"] }); const log = logger.getChildLogger({ prefix: ["[whatsappReminderManager]"] });
@ -55,25 +60,57 @@ export const scheduleWhatsappReminder = async (
} }
const name = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].name : evt.organizer.name; const name = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].name : evt.organizer.name;
const attendeeName = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name; const attendeeName =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name;
const timeZone = const timeZone =
action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone; action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].timeZone : evt.organizer.timeZone;
switch(template) { switch (template) {
case WorkflowTemplates.REMINDER: case WorkflowTemplates.REMINDER:
message = whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; message =
whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) ||
message;
break; break;
case WorkflowTemplates.CANCELLED: case WorkflowTemplates.CANCELLED:
message = whatsappEventCancelledTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; message =
break whatsappEventCancelledTemplate(
false,
action,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
case WorkflowTemplates.RESCHEDULED: case WorkflowTemplates.RESCHEDULED:
message = whatsappEventRescheduledTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; message =
whatsappEventRescheduledTemplate(
false,
action,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break; break;
case WorkflowTemplates.COMPLETED: case WorkflowTemplates.COMPLETED:
message = whatsappEventCompletedTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; message =
break whatsappEventCompletedTemplate(
false,
action,
evt.startTime,
evt.title,
timeZone,
attendeeName,
name
) || message;
break;
default: default:
message = whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) || message; message =
whatsappReminderTemplate(false, action, evt.startTime, evt.title, timeZone, attendeeName, name) ||
message;
} }
// Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages // Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages

View File

@ -221,7 +221,7 @@ function WorkflowPage() {
//check if phone number is verified //check if phone number is verified
if ( if (
(step.action === WorkflowActions.SMS_NUMBER || step.action === WorkflowActions.WHATSAPP_NUMBER) && (step.action === WorkflowActions.SMS_NUMBER || step.action === WorkflowActions.WHATSAPP_NUMBER) &&
!verifiedNumbers?.find((verifiedNumber) => verifiedNumber.phoneNumber === step.sendTo) !verifiedNumbers?.find((verifiedNumber) => verifiedNumber.phoneNumber === step.sendTo)
) { ) {
isVerified = false; isVerified = false;

View File

@ -71,9 +71,7 @@ const fieldSchema = z.object({
required: z.boolean().default(false).optional(), required: z.boolean().default(false).optional(),
hidden: z.boolean().optional(), hidden: z.boolean().optional(),
editable: EditableSchema editable: EditableSchema.default("user").optional(),
.default("user")
.optional(),
sources: z sources: z
.array( .array(
z.object({ z.object({