Files
calendar/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
T

1775 lines
73 KiB
TypeScript

import { type TFunction } from "i18next";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useRef, useState, useCallback } from "react";
import type { UseFormReturn } from "react-hook-form";
import { Controller, useWatch } from "react-hook-form";
import "react-phone-number-input/style.css";
import { useHasActiveTeamPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
import type { RetellAgentWithDetails } from "@calcom/features/calAIPhone/providers/retellAI";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import PhoneInput from "@calcom/features/components/phone-input";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { formatPhoneNumber } from "@calcom/lib/formatPhoneNumber";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { HttpError } from "@calcom/lib/http-error";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { WorkflowStep } from "@calcom/prisma/client";
import {
MembershipRole,
PhoneNumberSubscriptionStatus,
TimeUnit,
WorkflowActions,
WorkflowTemplates,
WorkflowTriggerEvents,
} from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Badge, InfoBadge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/dialog";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { AddVariablesDropdown } from "@calcom/ui/components/editor";
import { Editor } from "@calcom/ui/components/editor";
import { CheckboxField } from "@calcom/ui/components/form";
import { EmailField } from "@calcom/ui/components/form";
import { TextArea } from "@calcom/ui/components/form";
import { Input } from "@calcom/ui/components/form";
import { Label } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Select } from "@calcom/ui/components/form";
import { MultiSelectCheckbox } from "@calcom/ui/components/form";
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";
import {
isSMSAction,
isWhatsappAction,
getTemplateBodyForAction,
shouldScheduleEmailReminder,
isSMSOrWhatsappAction,
isCalAIAction,
isFormTrigger,
hasCalAIAction,
} 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 type { FormValues } from "../pages/workflow";
import "../style/styles.css";
import { TestPhoneCallDialog } from "./TestPhoneCallDialog";
import { TimeTimeUnitInput } from "./TimeTimeUnitInput";
import { WebCallDialog } from "./WebCallDialog";
import { AgentConfigurationSheet } from "./agent-configuration/AgentConfigurationSheet";
type User = RouterOutputs["viewer"]["me"]["get"];
type WorkflowStepProps = {
step?: WorkflowStep;
form: UseFormReturn<FormValues>;
user: User;
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
teamId?: number;
readOnly: boolean;
// Props for trigger section
selectedOptions?: Option[];
setSelectedOptions?: Dispatch<SetStateAction<Option[]>>;
isOrganization?: boolean;
allOptions?: Option[];
eventTypeOptions?: Option[];
onSaveWorkflow?: () => Promise<void>;
setIsDeleteStepDialogOpen?: Dispatch<SetStateAction<boolean>>;
isDeleteStepDialogOpen?: boolean;
agentData?: RetellAgentWithDetails;
isAgentLoading?: boolean;
actionOptions: {
label: string;
value: WorkflowActions;
needsCredits: boolean;
creditsTeamId?: number;
isOrganization: boolean;
isCalAi: boolean;
needsTeamsUpgrade?: boolean;
}[];
updateTemplate: boolean;
setUpdateTemplate: Dispatch<SetStateAction<boolean>>;
inboundAgentData?: RetellAgentWithDetails;
isInboundAgentLoading?: boolean;
};
const getTimeSectionText = (trigger: WorkflowTriggerEvents, t: TFunction) => {
const triggerMap: Partial<Record<WorkflowTriggerEvents, string>> = {
[WorkflowTriggerEvents.AFTER_EVENT]: "how_long_after",
[WorkflowTriggerEvents.BEFORE_EVENT]: "how_long_before",
[WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW]: "how_long_after_hosts_no_show",
[WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW]: "how_long_after_guests_no_show",
[WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT]: "how_long_after_form_submitted_no_event",
};
return triggerMap[trigger] ? t(triggerMap[trigger]) : null;
};
const CalAIAgentDataSkeleton = () => {
return (
<div className="bg-cal-muted mt-4 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<SkeletonText className="h-5 w-28" />
<div className="mt-2 flex items-center gap-2">
<SkeletonText className="h-4 w-4" />
<SkeletonText className="h-4 w-32" />
<SkeletonText className="h-5 w-12" />
</div>
</div>
<div className="flex items-center gap-1">
<SkeletonText className="h-8 w-24" />
<SkeletonText className="h-8 w-8" />
</div>
</div>
</div>
);
};
const getActivePhoneNumbers = (
phoneNumbers?: Array<{ subscriptionStatus?: string; phoneNumber: string }>
) => {
return (
phoneNumbers?.filter(
(phone) =>
phone.subscriptionStatus === PhoneNumberSubscriptionStatus.ACTIVE || !phone.subscriptionStatus
) || []
);
};
export default function WorkflowStepContainer(props: WorkflowStepProps) {
const { t, i18n } = useLocale();
const utils = trpc.useUtils();
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();
const {
step,
form,
reload,
setReload,
teamId,
selectedOptions,
setSelectedOptions,
isOrganization,
allOptions,
agentData,
isAgentLoading,
inboundAgentData,
isInboundAgentLoading: _isInboundAgentLoading,
isDeleteStepDialogOpen,
setIsDeleteStepDialogOpen,
onSaveWorkflow,
actionOptions,
updateTemplate,
setUpdateTemplate,
} = props;
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery(
{ teamId },
{ enabled: !!teamId }
);
const isMobile = useMediaQuery("(max-width: 569px)");
const { data: userTeams } = trpc.viewer.teams.list.useQuery({}, { enabled: !teamId });
const [agentConfigurationSheet, setAgentConfigurationSheet] = useState<{
open: boolean;
activeTab?: "outgoingCalls" | "phoneNumber" | "incomingCalls";
}>({
open: false,
activeTab: "outgoingCalls",
});
const creditsTeamId = userTeams?.find(
(team) => team.accepted && (team.role === MembershipRole.ADMIN || team.role === MembershipRole.OWNER)
)?.id;
const { hasActiveTeamPlan } = useHasActiveTeamPlan();
const { data: _verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({ teamId });
const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat);
const createAgentMutation = trpc.viewer.aiVoiceAgent.create.useMutation({
onSuccess: async (data) => {
showToast(t("agent_created_successfully"), "success");
const url = new URL(window.location.href);
url.searchParams.delete("autoCreateAgent");
url.searchParams.delete("templateWorkflowId");
router.replace(url.pathname + url.search);
if (step) {
const stepIndex = step.stepNumber - 1;
form.setValue(`steps.${stepIndex}.agentId`, data.id);
await utils.viewer.aiVoiceAgent.get.invalidate({ id: data.id });
}
setAgentConfigurationSheet((prev) => ({ ...prev, open: true }));
},
onError: (error: { message: string }) => {
showToast(error.message, "error");
},
});
const stepAgentId = step?.agentId || form.watch(`steps.${step ? step.stepNumber - 1 : 0}.agentId`) || null;
const stepInboundAgentId =
step?.inboundAgentId || form.watch(`steps.${step ? step.stepNumber - 1 : 0}.inboundAgentId`) || null;
const updateAgentMutation = trpc.viewer.aiVoiceAgent.update.useMutation({
onSuccess: async () => {
showToast(t("agent_updated_successfully"), "success");
const currentAgentId = stepAgentId || stepInboundAgentId;
if (currentAgentId) {
utils.viewer.aiVoiceAgent.get.invalidate({ id: currentAgentId });
}
},
onError: (error: { message: string }) => {
showToast(error.message, "error");
},
});
const unsubscribePhoneNumberMutation = trpc.viewer.phoneNumber.update.useMutation({
onSuccess: async () => {
showToast(t("phone_number_unsubscribed_successfully"), "success");
setIsUnsubscribeDialogOpen(false);
const currentAgentId = stepAgentId || stepInboundAgentId;
if (currentAgentId) {
utils.viewer.aiVoiceAgent.get.invalidate({ id: currentAgentId });
}
},
onError: (error: { message: string }) => {
showToast(error.message, "error");
},
});
const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber) || [];
const verifiedEmails = _verifiedEmails || [];
const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false);
const [isTestAgentDialogOpen, setIsTestAgentDialogOpen] = useState(false);
const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false);
const [isUnsubscribeDialogOpen, setIsUnsubscribeDialogOpen] = useState(false);
const [verificationCode, setVerificationCode] = useState("");
const action = step?.action;
const requirePhoneNumber =
WorkflowActions.SMS_NUMBER === action || WorkflowActions.WHATSAPP_NUMBER === action;
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(requirePhoneNumber);
const [firstRender, setFirstRender] = useState(true);
const senderNeeded =
step?.action === WorkflowActions.SMS_NUMBER || step?.action === WorkflowActions.SMS_ATTENDEE;
const [_isSenderIsNeeded, setIsSenderIsNeeded] = useState(senderNeeded);
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ADDRESS ? true : false
);
const [isEmailSubjectNeeded, setIsEmailSubjectNeeded] = useState(
step?.action === WorkflowActions.EMAIL_ATTENDEE ||
step?.action === WorkflowActions.EMAIL_HOST ||
step?.action === WorkflowActions.EMAIL_ADDRESS
? true
: false
);
const trigger = useWatch({
control: form.control,
name: "trigger",
});
const [timeSectionText, setTimeSectionText] = useState(getTimeSectionText(trigger, t));
const isCreatingAgent = useRef(false);
const hasAutoCreated = useRef(false);
const handleCreateAgent = useCallback(
async (templateWorkflowId?: string) => {
if (isCreatingAgent.current || createAgentMutation.isPending) {
return;
}
isCreatingAgent.current = true;
try {
// Save workflow first to ensure we have step ID
if (onSaveWorkflow) {
await onSaveWorkflow();
}
const updatedSteps = form.getValues("steps");
const currentStepIndex = step ? step.stepNumber - 1 : 0;
const updatedStep = updatedSteps[currentStepIndex];
if (updatedStep?.action !== WorkflowActions.CAL_AI_PHONE_CALL) {
form.setValue(`steps.${currentStepIndex}.action`, WorkflowActions.CAL_AI_PHONE_CALL);
}
if (updatedStep?.id) {
createAgentMutation.mutate({
teamId,
workflowStepId: updatedStep.id,
...(templateWorkflowId && { templateWorkflowId }),
});
} else {
showToast(t("failed_to_get_workflow_step_id"), "error");
}
} catch (error) {
console.error("Failed to create agent:", error);
showToast(t("failed_to_create_agent"), "error");
} finally {
isCreatingAgent.current = false;
}
},
[createAgentMutation, onSaveWorkflow, form, step, teamId, t]
);
const autoCreateAgent = searchParams?.get("autoCreateAgent");
const templateWorkflowId = searchParams?.get("templateWorkflowId");
useEffect(() => {
const shouldAutoCreate =
autoCreateAgent === "true" &&
templateWorkflowId &&
step?.id &&
step.action === WorkflowActions.CAL_AI_PHONE_CALL &&
!stepAgentId &&
!hasAutoCreated.current &&
!createAgentMutation.isPending &&
!createAgentMutation.isSuccess;
if (shouldAutoCreate && onSaveWorkflow) {
hasAutoCreated.current = true;
handleCreateAgent(templateWorkflowId);
}
}, [
autoCreateAgent,
templateWorkflowId,
step?.id,
step?.action,
stepAgentId,
createAgentMutation.isPending,
createAgentMutation.isSuccess,
onSaveWorkflow,
handleCreateAgent,
]);
const triggerOptions = getWorkflowTriggerOptions(t, hasActiveTeamPlan);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan, trigger);
const steps = useWatch({
control: form.control,
name: "steps",
});
const hasEmailToHostAction = steps.some((s) => s.action === WorkflowActions.EMAIL_HOST);
const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action));
const disallowFormTriggers = hasEmailToHostAction || hasWhatsappAction;
const filteredTriggerOptions = triggerOptions.filter(
(option) => !(isFormTrigger(option.value) && disallowFormTriggers)
);
const { ref: emailSubjectFormRef, ...restEmailSubjectForm } = step
? form.register(`steps.${step.stepNumber - 1}.emailSubject`)
: { ref: null, name: "" };
const refEmailSubject = useRef<HTMLTextAreaElement | null>(null);
const getNumberVerificationStatus = () =>
!!step &&
!!verifiedNumbers.find(
(number: string) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
);
const getEmailVerificationStatus = () =>
!!step &&
!!verifiedEmails.find((email: string) => email === form.getValues(`steps.${step.stepNumber - 1}.sendTo`));
const [numberVerified, setNumberVerified] = useState(getNumberVerificationStatus());
const [emailVerified, setEmailVerified] = useState(getEmailVerificationStatus());
useEffect(() => setNumberVerified(getNumberVerificationStatus()), [verifiedNumbers.length]);
useEffect(() => setEmailVerified(getEmailVerificationStatus()), [verifiedEmails.length]);
const addVariableEmailSubject = (variable: string) => {
if (step) {
const currentEmailSubject = refEmailSubject?.current?.value || "";
const cursorPosition = refEmailSubject?.current?.selectionStart || currentEmailSubject.length;
const subjectWithAddedVariable = `${currentEmailSubject.substring(0, cursorPosition)}{${variable
.toUpperCase()
.replace(/ /g, "_")}}${currentEmailSubject.substring(cursorPosition)}`;
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, subjectWithAddedVariable);
}
};
const sendVerificationCodeMutation = trpc.viewer.workflows.sendVerificationCode.useMutation({
onSuccess: async () => {
showToast(t("verification_code_sent"), "success");
},
onError: async (error) => {
showToast(error.message, "error");
},
});
const verifyPhoneNumberMutation = trpc.viewer.workflows.verifyPhoneNumber.useMutation({
onSuccess: async (isVerified) => {
showToast(isVerified ? t("verified_successfully") : t("wrong_code"), "success");
setNumberVerified(isVerified);
if (
step &&
form?.formState?.errors?.steps &&
form.formState.errors.steps[step.stepNumber - 1]?.sendTo &&
isVerified
) {
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
}
utils.viewer.workflows.getVerifiedNumbers.invalidate();
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setNumberVerified(false);
}
},
});
const sendEmailVerificationCodeMutation = trpc.viewer.auth.sendVerifyEmailCode.useMutation({
onSuccess() {
showToast(t("email_sent"), "success");
},
onError: () => {
showToast(t("email_not_sent"), "error");
},
});
const verifyEmailCodeMutation = trpc.viewer.workflows.verifyEmailCode.useMutation({
onSuccess: (isVerified) => {
showToast(isVerified ? t("verified_successfully") : t("wrong_code"), "success");
setEmailVerified(true);
if (
step &&
form?.formState?.errors?.steps &&
form.formState.errors.steps[step.stepNumber - 1]?.sendTo &&
isVerified
) {
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
}
utils.viewer.workflows.getVerifiedEmails.invalidate();
},
onError: (err) => {
if (err.message === "invalid_code") {
showToast(t("code_provided_invalid"), "error");
setEmailVerified(false);
}
},
});
/* const testActionMutation = trpc.viewer.workflows.testAction.useMutation({
onSuccess: async () => {
showToast(t("notification_sent"), "success");
},
onError: (err) => {
let message = t("unexpected_error_try_again");
if (err instanceof TRPCClientError) {
if (err.message === "rate-limit-exceeded") {
message = t("rate_limit_exceeded");
} else {
message = err.message;
}
}
if (err instanceof HttpError) {
message = `${err.statusCode}: ${err.message}`;
}
showToast(message, "error");
},
}); */
//trigger
if (!step) {
const triggerString = t(`${trigger.toLowerCase()}_trigger`);
const selectedTrigger = {
label: triggerString.charAt(0).toUpperCase() + triggerString.slice(1),
value: trigger,
};
return (
<>
<div className="mb-3">
<Label className="text-default text-sm leading-none">{t("when")}</Label>
<Controller
name="trigger"
control={form.control}
render={() => {
return (
<Select
isSearchable={false}
innerClassNames={{ valueContainer: "font-medium" }}
className="text-sm font-medium"
id="trigger-select"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const triggerValue = val.value as WorkflowTriggerEvents;
const currentTrigger = form.getValues("trigger") as WorkflowTriggerEvents;
const isCurrentFormTrigger = isFormTrigger(currentTrigger);
const isNewFormTrigger = isFormTrigger(triggerValue);
form.setValue("trigger", triggerValue);
// Reset activeOn when switching between form and non-form triggers
if (isCurrentFormTrigger !== isNewFormTrigger) {
form.setValue("activeOn", []);
if (setSelectedOptions) {
setSelectedOptions([]);
}
form.setValue("selectAll", false);
}
const newTimeSectionText = getTimeSectionText(triggerValue, t);
if (newTimeSectionText) {
setTimeSectionText(newTimeSectionText);
if (
triggerValue === WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW ||
triggerValue === WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
) {
form.setValue("time", 5);
form.setValue("timeUnit", TimeUnit.MINUTE);
} else {
form.setValue("time", 24);
form.setValue("timeUnit", TimeUnit.HOUR);
}
} else {
setTimeSectionText(null);
form.unregister("time");
form.unregister("timeUnit");
}
if (isFormTrigger(triggerValue)) {
const steps = form.getValues("steps");
if (steps?.length) {
const updatedSteps = steps.map((step) =>
step.template === WorkflowTemplates.CUSTOM
? step
: {
...step,
reminderBody: " ",
emailSubject: " ",
template: WorkflowTemplates.CUSTOM,
}
);
form.setValue("steps", updatedSteps);
setUpdateTemplate(!updateTemplate);
}
}
}
}}
defaultValue={selectedTrigger}
options={filteredTriggerOptions.map((option) => ({
label: option.label,
value: option.value,
needsTeamsUpgrade: option.needsTeamsUpgrade,
}))}
isOptionDisabled={(option: { label: string; value: string; needsTeamsUpgrade?: boolean }) =>
!!option.needsTeamsUpgrade
}
/>
);
}}
/>
</div>
<div>
{!!timeSectionText && (
<div className="mb-3 mt-3">
<Label className="text-default mb-0">{timeSectionText}</Label>
<TimeTimeUnitInput disabled={props.readOnly} />
</div>
)}
{/* Event Type/Team Selection */}
{selectedOptions && setSelectedOptions && allOptions && (
<div className={classNames(!!timeSectionText && "mb-3")}>
{isOrganization ? (
<div className="text-default flex items-center gap-2">
<Label>{t("which_team_apply")}</Label>
<div className="mb-2">
<InfoBadge content={t("team_select_info")} />
</div>
</div>
) : (
<Label>
{isFormTrigger(trigger) ? t("which_routing_form_apply") : t("which_event_type_apply")}
</Label>
)}
<Controller
name="activeOn"
control={form.control}
render={() => {
return (
<MultiSelectCheckbox
options={allOptions}
isDisabled={props.readOnly || form.getValues("selectAll")}
className="w-full"
setSelected={setSelectedOptions}
selected={form.getValues("selectAll") ? allOptions : selectedOptions}
setValue={(s: Option[]) => {
form.setValue("activeOn", s);
}}
countText={
isOrganization
? "count_team"
: isFormTrigger(form.getValues("trigger"))
? "nr_routing_form"
: "nr_event_type"
}
/>
);
}}
/>
{!hasCalAIAction(steps) && (
<div className="mt-1">
<Controller
name="selectAll"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={
isOrganization
? t("apply_to_all_teams")
: isFormTrigger(form.getValues("trigger"))
? t("apply_to_all_routing_forms")
: t("apply_to_all_event_types")
}
disabled={props.readOnly}
descriptionClassName="ml-0"
onChange={(e) => {
onChange(e);
if (e.target.value) {
setSelectedOptions(allOptions);
form.setValue("activeOn", allOptions);
}
}}
checked={value}
/>
)}
/>
</div>
)}
</div>
)}
{!!timeSectionText && (
<div className="mt-1 flex text-gray-500">
<Icon name="info" className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_sms_workflow_info_message")}</p>
</div>
)}
</div>
</>
);
}
if (step && step.action) {
const actionString = t(`${step.action.toLowerCase()}_action`);
const selectedAction = {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: step.action,
needsCredits: isSMSOrWhatsappAction(step.action),
isCalAi: isCalAIAction(step.action),
creditsTeamId: teamId ?? creditsTeamId,
isOrganization: props.isOrganization,
};
const selectedTemplate = {
label: t(`${step.template.toLowerCase()}`),
value: step.template,
needsTeamsUpgrade: false,
};
const canRequirePhoneNumber = (workflowStep: string) => {
return (
WorkflowActions.SMS_ATTENDEE === workflowStep || WorkflowActions.WHATSAPP_ATTENDEE === workflowStep
);
};
const arePhoneNumbersActive = getActivePhoneNumbers(
agentData?.outboundPhoneNumbers?.map((phone) => ({
...phone,
subscriptionStatus: phone.subscriptionStatus ?? undefined,
}))
);
return (
<>
<div>
<div>
<Label>{t("do_this")}</Label>
<Controller
name={`steps.${step.stepNumber - 1}.action`}
control={form.control}
render={() => {
return (
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
const template = getTemplateBodyForAction({
action: val.value,
locale: i18n.language,
t,
template: WorkflowTemplates.REMINDER,
timeFormat,
});
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template);
const setNumberRequiredConfigs = (
phoneNumberIsNeeded: boolean,
senderNeeded = true
) => {
setIsSenderIsNeeded(senderNeeded);
setIsEmailAddressNeeded(false);
setIsPhoneNumberNeeded(phoneNumberIsNeeded);
setNumberVerified(getNumberVerificationStatus());
};
if (isSMSAction(val.value)) {
setNumberRequiredConfigs(val.value === WorkflowActions.SMS_NUMBER);
// email action changes to sms action
if (!isSMSAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.sender`, SENDER_ID);
}
setIsEmailSubjectNeeded(false);
form.setValue(`steps.${step.stepNumber - 1}.agentId`, null);
} else if (isWhatsappAction(val.value)) {
setNumberRequiredConfigs(val.value === WorkflowActions.WHATSAPP_NUMBER, false);
if (!isWhatsappAction(oldValue)) {
form.setValue(`steps.${step.stepNumber - 1}.sender`, "");
}
setIsEmailSubjectNeeded(false);
form.setValue(`steps.${step.stepNumber - 1}.agentId`, null);
} else if (isCalAIAction(val.value)) {
setIsPhoneNumberNeeded(false);
setIsSenderIsNeeded(false);
setIsEmailAddressNeeded(false);
setIsEmailSubjectNeeded(false);
form.setValue(`steps.${step.stepNumber - 1}.emailSubject`, null);
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, null);
form.setValue(`steps.${step.stepNumber - 1}.sender`, null);
} else {
setIsPhoneNumberNeeded(false);
setIsSenderIsNeeded(false);
setIsEmailAddressNeeded(val.value === WorkflowActions.EMAIL_ADDRESS);
setIsEmailSubjectNeeded(true);
form.setValue(`steps.${step.stepNumber - 1}.agentId`, null);
}
form.setValue(`steps.${step.stepNumber - 1}.sendTo`, null);
form.clearErrors(`steps.${step.stepNumber - 1}.sendTo`);
form.setValue(`steps.${step.stepNumber - 1}.action`, val.value);
setUpdateTemplate(!updateTemplate);
}
}}
defaultValue={selectedAction}
options={actionOptions.map((option) => ({
...option,
creditsTeamId: teamId ?? creditsTeamId,
}))}
/>
);
}}
/>
</div>
{!isWhatsappAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) &&
!isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div>
{_isSenderIsNeeded ? (
<>
<div className="pt-4">
<div className="flex items-center">
<Label>{t("sender_id")}</Label>
</div>
<Input
type="text"
placeholder={SENDER_ID}
disabled={props.readOnly}
maxLength={11}
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
/>
<div className="mt-1.5 flex items-center gap-1">
<Icon name="info" size="10" className="text-gray-500" />
<div className="text-subtle text-xs">{t("sender_id_info")}</div>
</div>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sender && (
<p className="text-error mt-1 text-xs">{t("sender_id_error_message")}</p>
)}
</>
) : (
<>
<div className="pt-4">
<Label>{t("sender_name")}</Label>
<Input
type="text"
disabled={props.readOnly}
placeholder={SENDER_NAME}
{...form.register(`steps.${step.stepNumber - 1}.senderName`)}
/>
</div>
</>
)}
</div>
)}
{isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && !stepAgentId && (
<div className="bg-cal-muted border-muted mt-2 rounded-2xl border p-3">
<div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-center sm:gap-0">
<div>
<h2 className="text-emphasis text-sm font-medium leading-none">
{t("cal_ai_agent")}
<Badge startIcon="info" className="ms-2 rounded-md" variant="warning">
{t("set_up_required")}
</Badge>
</h2>
<p className="text-muted mt-1 text-sm font-medium leading-none">
{t("no_phone_number_connected")}.
</p>
</div>
<Button
color="primary"
disabled={props.readOnly || isCreatingAgent.current || hasAutoCreated.current}
className="flex items-center justify-center"
onClick={() => handleCreateAgent()}
loading={createAgentMutation.isPending || isCreatingAgent.current}>
{t("set_up_agent")}
</Button>
</div>
</div>
)}
{stepAgentId && isAgentLoading && <CalAIAgentDataSkeleton />}
{stepAgentId && agentData && (
<div className="bg-cal-muted mt-4 rounded-lg p-4">
<div
className="flex cursor-pointer items-center justify-between"
onClick={(e) => {
const target = e.target as HTMLElement;
if (
target.closest("button") ||
target.closest('[role="menu"]') ||
target.closest('[role="menuitem"]')
) {
return;
}
if (!props.readOnly) {
setAgentConfigurationSheet({ open: true, activeTab: "outgoingCalls" });
}
}}>
<div>
<h3 className="text-emphasis text-base font-medium">{t("cal_ai_agent")}</h3>
{arePhoneNumbersActive.length > 0 ? (
<div className="flex items-center gap-2">
<Icon name="phone" className="text-emphasis h-4 w-4" />
<span className="text-emphasis text-sm">
{formatPhoneNumber(arePhoneNumbersActive[0].phoneNumber)}
</span>
<Badge variant="green" size="sm" withDot>
{t("active")}
</Badge>
</div>
) : (
<div className="flex items-center gap-1">
<span className="text-subtle text-sm">{t("no_phone_number_connected")}</span>
</div>
)}
</div>
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
{arePhoneNumbersActive.length > 0 ? (
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
color="secondary"
className="rounded-[10px]"
disabled={props.readOnly}
EndIcon="chevron-down">
{t("test_agent")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="phone"
onClick={() => setIsTestAgentDialogOpen(true)}>
{t("phone_call")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="monitor"
onClick={() => setIsWebCallDialogOpen(true)}>
{t("web_call")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
) : !isMobile ? (
<>
<Button
color="secondary"
onClick={() => {
setAgentConfigurationSheet((prev) => ({
...prev,
open: true,
activeTab: "phoneNumber",
}));
}}
disabled={props.readOnly}>
{t("connect_phone_number")}
</Button>
<Button
color="secondary"
onClick={() => setIsWebCallDialogOpen(true)}
disabled={props.readOnly}
StartIcon="monitor">
{t("test_web_call")}
</Button>
</>
) : null}
<Dropdown>
<DropdownMenuTrigger asChild>
<Button
type="button"
color="secondary"
variant="icon"
StartIcon="ellipsis"
className="rounded-[10px]"
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{isMobile && arePhoneNumbersActive.length <= 0 && (
<>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="plus"
disabled={props.readOnly}
onClick={() => {
setAgentConfigurationSheet((prev) => ({
...prev,
open: true,
activeTab: "phoneNumber",
}));
}}>
{t("connect_phone_number")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
onClick={() => setIsWebCallDialogOpen(true)}
disabled={props.readOnly}
StartIcon="monitor">
{t("test_web_call")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="pencil"
onClick={() =>
setAgentConfigurationSheet({ open: true, activeTab: "outgoingCalls" })
}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</div>
</div>
)}
{isPhoneNumberNeeded && (
<div className="bg-cal-muted mt-2 rounded-md p-4 pt-0">
<Label className="pt-4">{t("custom_phone_number")}</Label>
<div className="block sm:flex">
<Controller
name={`steps.${step.stepNumber - 1}.sendTo`}
render={({ field: { value, onChange } }) => (
<PhoneInput
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="min-w-fit sm:rounded-r-none sm:rounded-bl-md sm:rounded-tl-md"
required
disabled={props.readOnly}
value={value}
onChange={(val) => {
const isAlreadyVerified = !!verifiedNumbers
?.concat([])
.find((number) => number.replace(/\s/g, "") === val?.replace(/\s/g, ""));
setNumberVerified(isAlreadyVerified);
onChange(val);
}}
/>
)}
/>
<Button
color="secondary"
disabled={numberVerified || props.readOnly || false}
className={classNames(
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-bl-none sm:rounded-tl-none",
numberVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() =>
sendVerificationCodeMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
})
}>
{t("send_code")}
</Button>
</div>
{form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="text-error mt-1 text-xs">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
{numberVerified ? (
<div className="mt-1">
<Badge variant="green">{t("number_verified")}</Badge>
</div>
) : (
!props.readOnly && (
<>
<div className="mt-6 flex w-full flex-col">
<Label className="">{t("verification_code")}</Label>
<div className="flex w-full items-center">
<TextField
containerClassName="w-full"
className="h-8 rounded-xl"
placeholder={t("code")}
disabled={props.readOnly}
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
size="sm"
className="ml-2 min-w-fit rounded-[10px]"
disabled={verifyPhoneNumberMutation.isPending || props.readOnly}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
teamId,
});
}}>
{t("verify")}
</Button>
</div>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="text-error mt-1 text-xs">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
)
)}
</div>
)}
{canRequirePhoneNumber(form.getValues(`steps.${step.stepNumber - 1}.action`)) &&
!isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="mt-2">
<Controller
name={`steps.${step.stepNumber - 1}.numberRequired`}
control={form.control}
render={() => (
<CheckboxField
disabled={props.readOnly}
defaultChecked={form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false}
description={t("make_phone_number_required")}
descriptionClassName="ml-0"
onChange={(e) =>
form.setValue(`steps.${step.stepNumber - 1}.numberRequired`, e.target.checked)
}
/>
)}
/>
</div>
)}
{isEmailAddressNeeded && !isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="bg-cal-muted border-muted mt-5 rounded-2xl border p-4">
<Label>{t("email_address")}</Label>
<div className="block items-center gap-2 sm:flex">
<Controller
name={`steps.${step.stepNumber - 1}.sendTo`}
render={({ field: { value, onChange } }) => (
<EmailField
required
containerClassName="w-full"
className="h-8 min-w-fit"
placeholder={t("email_address")}
value={value}
disabled={props.readOnly}
onChange={(val) => {
const isAlreadyVerified = !!verifiedEmails
?.concat([])
.find((email) => email === val.target.value);
setEmailVerified(isAlreadyVerified);
onChange(val);
}}
/>
)}
/>
<Button
color="secondary"
size="sm"
disabled={emailVerified || props.readOnly || false}
className={classNames(
"min-w-fit text-sm font-medium",
emailVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() => {
const email = form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "";
sendEmailVerificationCodeMutation.mutate({
email,
isVerifyingEmail: true,
language: i18n.language || "en",
});
}}>
{t("send_code")}
</Button>
</div>
{form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="text-error mt-1 text-xs">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
{emailVerified ? (
<div className="mt-1">
<Badge variant="green">{t("email_verified")}</Badge>
</div>
) : (
!props.readOnly && (
<>
<div className="mt-3 flex w-full flex-col">
<Label className="">{t("verification_code")}</Label>
<div className="flex w-full items-center">
<TextField
containerClassName="w-full"
className="h-8 rounded-xl"
placeholder={t("code")}
disabled={props.readOnly}
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
size="sm"
className="ml-2 min-w-fit rounded-[10px]"
disabled={verifyEmailCodeMutation.isPending || props.readOnly}
onClick={() => {
verifyEmailCodeMutation.mutate({
code: verificationCode,
email: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
teamId,
});
}}>
{t("verify")}
</Button>
</div>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="text-error mt-1 text-xs">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
)
)}
</div>
)}
{!isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="mt-3">
<Label>{t("message_template")}</Label>
<Controller
name={`steps.${step.stepNumber - 1}.template`}
control={form.control}
render={({ field }) => {
return (
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const action = form.getValues(`steps.${step.stepNumber - 1}.action`);
const value = val.value as WorkflowTemplates;
const template = getTemplateBodyForAction({
action,
locale: i18n.language,
t,
template: value ?? WorkflowTemplates.REMINDER,
timeFormat,
});
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, template);
if (shouldScheduleEmailReminder(action)) {
if (value === WorkflowTemplates.REMINDER) {
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailReminderTemplate({
isEditingMode: true,
locale: i18n.language,
t,
action,
timeFormat,
}).emailSubject
);
} else if (value === WorkflowTemplates.RATING) {
form.setValue(
`steps.${step.stepNumber - 1}.emailSubject`,
emailRatingTemplate({
isEditingMode: true,
locale: i18n.language,
action,
t,
timeFormat,
}).emailSubject
);
}
}
field.onChange(value);
form.setValue(`steps.${step.stepNumber - 1}.template`, value);
setUpdateTemplate(!updateTemplate);
}
}}
defaultValue={selectedTemplate}
value={selectedTemplate}
options={templateOptions.map((option) => ({
label: option.label,
value: option.value,
needsTeamsUpgrade:
option.needsTeamsUpgrade &&
!isSMSAction(form.getValues(`steps.${step.stepNumber - 1}.action`)),
}))}
isOptionDisabled={(option: {
label: string;
value: string;
needsTeamsUpgrade: boolean;
}) => option.needsTeamsUpgrade}
/>
);
}}
/>
</div>
)}
{!isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && (
<div className="bg-cal-muted border-muted mt-3 rounded-2xl border p-3">
{isEmailSubjectNeeded && (
<div className="mb-6">
<div className="flex items-center">
<Label
className={classNames(
"flex-none",
props.readOnly || isFormTrigger(trigger) ? "mb-2" : "mb-0"
)}>
{t("email_subject")}
</Label>
{!props.readOnly && !isFormTrigger(trigger) && (
<div className="grow text-right">
<AddVariablesDropdown
addVariable={addVariableEmailSubject}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
)}
</div>
<TextArea
ref={(e) => {
emailSubjectFormRef?.(e);
refEmailSubject.current = e;
}}
rows={2}
disabled={props.readOnly || !hasActiveTeamPlan}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
/>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && (
<p className="text-error mt-1 text-xs">
{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}
</p>
)}
</div>
)}
<div className="mb-2 flex items-center pb-1">
<Label className="mb-0 flex-none">
{isEmailSubjectNeeded ? t("email_body") : t("text_message")}
</Label>
</div>
<Editor
getText={() => props.form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || ""}
setText={(text: string) => {
props.form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, text);
props.form.clearErrors();
}}
variables={!isFormTrigger(trigger) ? DYNAMIC_TEXT_VARIABLES : undefined}
addVariableButtonTop={isSMSAction(step.action)}
height="200px"
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
editable={
!props.readOnly &&
!isWhatsappAction(step.action) &&
(hasActiveTeamPlan || isSMSAction(step.action))
}
excludedToolbarItems={
!isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"]
}
plainText={isSMSAction(step.action)}
/>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody && (
<p className="text-error mt-1 text-sm">
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
</p>
)}
{isEmailSubjectNeeded && (
<div className="mt-2">
<Controller
name={`steps.${step.stepNumber - 1}.includeCalendarEvent`}
control={form.control}
render={() => (
<CheckboxField
disabled={props.readOnly}
defaultChecked={
form.getValues(`steps.${step.stepNumber - 1}.includeCalendarEvent`) || false
}
description={t("include_calendar_event")}
descriptionClassName="ml-0"
onChange={(e) =>
form.setValue(`steps.${step.stepNumber - 1}.includeCalendarEvent`, e.target.checked)
}
/>
)}
/>
</div>
)}
{!props.readOnly && (
<div className="ml-1 mt-2">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-subtle ml-1 flex items-center gap-2">
<Icon name="circle-help" className="h-3 w-3" />
<p className="text-left text-xs">
{isFormTrigger(trigger)
? t("using_form_responses_as_variables")
: t("using_booking_questions_as_variables")}
</p>
</div>
</button>
</div>
)}
</div>
)}
{/* {form.getValues(`steps.${step.stepNumber - 1}.action`) !== WorkflowActions.SMS_ATTENDEE && (
<Button
type="button"
className="mt-7 w-full"
onClick={() => {
let isEmpty = false;
if (!form.getValues(`steps.${step.stepNumber - 1}.sendTo`) && isPhoneNumberNeeded) {
form.setError(`steps.${step.stepNumber - 1}.sendTo`, {
type: "custom",
message: t("no_input"),
});
isEmpty = true;
}
if (!numberVerified && isPhoneNumberNeeded) {
form.setError(`steps.${step.stepNumber - 1}.sendTo`, {
type: "custom",
message: t("not_verified"),
});
}
if (
form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.CUSTOM
) {
if (!form.getValues(`steps.${step.stepNumber - 1}.reminderBody`)) {
form.setError(`steps.${step.stepNumber - 1}.reminderBody`, {
type: "custom",
message: t("no_input"),
});
isEmpty = true;
} else if (
isEmailSubjectNeeded &&
!form.getValues(`steps.${step.stepNumber - 1}.emailSubject`)
) {
form.setError(`steps.${step.stepNumber - 1}.emailSubject`, {
type: "custom",
message: t("no_input"),
});
isEmpty = true;
}
}
if (!isPhoneNumberNeeded && !isEmpty) {
//translate body and reminder to english
const emailSubject = translateVariablesToEnglish(
form.getValues(`steps.${step.stepNumber - 1}.emailSubject`) || "",
{ locale: i18n.language, t }
);
const reminderBody = translateVariablesToEnglish(
form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || "",
{ locale: i18n.language, t }
);
testActionMutation.mutate({
step,
emailSubject,
reminderBody,
});
} else {
const isNumberValid =
form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo
? false
: true;
if (isPhoneNumberNeeded && isNumberValid && !isEmpty && numberVerified) {
setConfirmationDialogOpen(true);
}
}
}}
color="secondary">
<div className="w-full">{t("test_action")}</div>
</Button>
)} */}
</div>
{/* <Dialog open={confirmationDialogOpen} onOpenChange={setConfirmationDialogOpen}>
<ConfirmationDialogContent
variety="warning"
title={t("test_workflow_action")}
confirmBtnText={t("send_sms")}
onConfirm={(e) => {
e.preventDefault();
const reminderBody = translateVariablesToEnglish(
form.getValues(`steps.${step.stepNumber - 1}.reminderBody`) || "",
{ locale: i18n.language, t }
);
testActionMutation.mutate({
step,
emailSubject: "",
reminderBody: reminderBody || "",
});
setConfirmationDialogOpen(false);
}}>
{t("send_sms_to_number", { number: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) })}
</ConfirmationDialogContent>
</Dialog> */}
<Dialog open={isAdditionalInputsDialogOpen} onOpenChange={setIsAdditionalInputsDialogOpen}>
<DialogContent enableOverflow type="creation" className="sm:max-w-[610px]">
<div>
<h1 className="w-full text-xl font-semibold">
{isFormTrigger(trigger)
? t("how_form_responses_as_variables")
: t("how_booking_questions_as_variables")}
</h1>
<div className="mb-6 rounded-md sm:p-4">
<p className="test-sm font-medium">{t("format")}</p>
<ul className="text-emphasis ml-5 mt-2 list-disc">
<li>{t("uppercase_for_letters")}</li>
<li>{t("replace_whitespaces_underscores")}</li>
<li>
{isFormTrigger(trigger)
? t("ignore_special_characters_form_responses")
: t("ignore_special_characters_booking_questions")}
</li>
</ul>
<div className="mt-4">
<p className="test-sm w-full font-medium">{t("example_1")}</p>
<div className="mt-2 grid grid-cols-12">
<div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
{isFormTrigger(trigger) ? t("form_field_identifier") : t("booking_question_identifier")}
</div>
<div className="test-sm text-emphasis col-span-7">{t("company_size")}</div>
<div className="test-sm text-default col-span-5 w-full">{t("variable")}</div>
<div className="test-sm text-emphasis col-span-7 wrap-break-word">
{" "}
{`{${t("company_size")
.replace(/[^a-zA-Z0-9 ]/g, "")
.trim()
.replace(/ /g, "_")
.toUpperCase()}}`}
</div>
</div>
</div>
<div className="mt-4">
<p className="test-sm w-full font-medium">{t("example_2")}</p>
<div className="mt-2 grid grid-cols-12">
<div className="test-sm text-default col-span-5 ltr:mr-2 rtl:ml-2">
{isFormTrigger(trigger) ? t("form_field_identifier") : t("booking_question_identifier")}
</div>
<div className="test-sm text-emphasis col-span-7">{t("what_help_needed")}</div>
<div className="test-sm text-default col-span-5">{t("variable")}</div>
<div className="test-sm text-emphasis col-span-7 wrap-break-word">
{" "}
{`{${t("what_help_needed")
.replace(/[^a-zA-Z0-9 ]/g, "")
.trim()
.replace(/ /g, "_")
.toUpperCase()}}`}
</div>
</div>
</div>
</div>
</div>
<DialogFooter showDivider>
<DialogClose color="primary" />
</DialogFooter>
</DialogContent>
</Dialog>
{agentConfigurationSheet.open && (
<AgentConfigurationSheet
open={agentConfigurationSheet.open}
activeTab={agentConfigurationSheet.activeTab}
onOpenChange={(val) => setAgentConfigurationSheet((prev) => ({ ...prev, open: val }))}
agentId={stepAgentId}
inboundAgentId={stepInboundAgentId}
agentData={agentData}
inboundAgentData={inboundAgentData}
onUpdate={(data) => {
updateAgentMutation.mutate({
id: data.id,
teamId: teamId,
generalPrompt: data.generalPrompt,
beginMessage: data.beginMessage,
generalTools: data.generalTools,
});
}}
readOnly={props.readOnly}
teamId={teamId}
isOrganization={props.isOrganization}
workflowId={params?.workflow as string}
workflowStepId={step?.id}
form={form}
eventTypeOptions={props.eventTypeOptions}
/>
)}
{stepAgentId && (
<TestPhoneCallDialog
open={isTestAgentDialogOpen}
onOpenChange={setIsTestAgentDialogOpen}
agentId={stepAgentId || ""}
teamId={teamId}
form={form}
eventTypeIds={props.eventTypeOptions?.map((opt) => parseInt(opt.value, 10))}
outboundEventTypeId={agentData?.outboundEventTypeId}
/>
)}
{stepAgentId && (
<WebCallDialog
open={isWebCallDialogOpen}
onOpenChange={setIsWebCallDialogOpen}
agentId={stepAgentId || ""}
teamId={teamId}
isOrganization={props.isOrganization}
form={form}
eventTypeIds={props.eventTypeOptions?.map((opt) => parseInt(opt.value, 10)) || []}
outboundEventTypeId={agentData?.outboundEventTypeId}
/>
)}
{/* Unsubscribe Confirmation Dialog */}
<Dialog open={isUnsubscribeDialogOpen} onOpenChange={setIsUnsubscribeDialogOpen}>
<DialogContent type="creation" title={t("unsubscribe_phone_number")}>
<div className="stack-y-4">
<p className="text-default text-sm">{t("do_you_still_want_to_unsubscribe")}</p>
{getActivePhoneNumbers(
agentData?.outboundPhoneNumbers?.map((phone) => ({
...phone,
subscriptionStatus: phone.subscriptionStatus ?? undefined,
}))
).length > 0 && (
<div className="bg-cal-muted rounded-lg p-3">
<div className="flex items-center gap-2">
<Icon name="phone" className="text-emphasis h-4 w-4" />
<span className="text-emphasis text-sm font-medium">
{formatPhoneNumber(
getActivePhoneNumbers(
agentData?.outboundPhoneNumbers?.map((phone) => ({
...phone,
subscriptionStatus: phone.subscriptionStatus ?? undefined,
}))
)?.[0]?.phoneNumber
)}
</span>
</div>
</div>
)}
<p className="text-subtle text-sm">{t("the_action_will_disconnect_phone_number")}</p>
</div>
<DialogFooter showDivider>
<Button type="button" color="secondary" onClick={() => setIsUnsubscribeDialogOpen(false)}>
{t("cancel")}
</Button>
<Button
type="button"
StartIcon="trash"
color="destructive"
onClick={() => {
const activePhoneNumbers = getActivePhoneNumbers(
agentData?.outboundPhoneNumbers?.map((phone) => ({
...phone,
subscriptionStatus: phone.subscriptionStatus ?? undefined,
}))
);
if (activePhoneNumbers?.[0]) {
unsubscribePhoneNumberMutation.mutate({
phoneNumber: activePhoneNumbers[0].phoneNumber,
outboundAgentId: null,
});
}
}}
loading={unsubscribePhoneNumberMutation.isPending}>
{t("unsubscribe")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Step Confirmation Dialog */}
<Dialog open={isDeleteStepDialogOpen} onOpenChange={setIsDeleteStepDialogOpen}>
<DialogContent type="confirmation" title={t("delete_workflow_step")}>
<div className="stack-y-4">
<p className="text-default text-sm">{t("are_you_sure_you_want_to_delete_workflow_step")}</p>
{(() => {
const relevantPhoneNumbers =
agentData?.outboundPhoneNumbers?.filter(
(phone) => phone.subscriptionStatus !== PhoneNumberSubscriptionStatus.CANCELLED
) || [];
return (
relevantPhoneNumbers.length > 0 && (
<>
<div className="bg-attention rounded-lg p-3">
<div className="flex items-start gap-2">
<Icon name="info" className="text-attention mt-0.5 h-4 w-4" />
<div className="stack-y-2">
<p className="text-attention text-sm font-medium">{t("this_action_will_also")}</p>
<ul className="text-attention list-inside list-disc stack-y-1 text-sm">
{relevantPhoneNumbers.some(
(phone) => phone.subscriptionStatus === PhoneNumberSubscriptionStatus.ACTIVE
) && <li>{t("cancel_your_phone_number_subscription")}</li>}
<li>{t("delete_associated_phone_number")}</li>
</ul>
</div>
</div>
</div>
{relevantPhoneNumbers.map((phone) => (
<div key={phone.phoneNumber} className="p-3 rounded-lg bg-cal-muted">
<div className="flex gap-2 items-center">
<Icon name="phone" className="w-4 h-4 text-emphasis" />
<span className="text-sm font-medium text-emphasis">
{formatPhoneNumber(phone.phoneNumber)}
</span>
{phone.subscriptionStatus === PhoneNumberSubscriptionStatus.ACTIVE && (
<Badge variant="green" size="sm" withDot>
{t("active_subscription")}
</Badge>
)}
</div>
</div>
))}
</>
)
);
})()}
</div>
<DialogFooter showDivider>
<Button type="button" color="secondary" onClick={() => setIsDeleteStepDialogOpen?.(false)}>
{t("cancel")}
</Button>
<Button
type="button"
StartIcon="trash"
color="destructive"
onClick={() => {
// Proceed with deletion
const steps = form.getValues("steps");
const updatedSteps = steps
?.filter((currStep) => currStep.id !== step.id)
.map((s) => {
const updatedStep = s;
if (step.stepNumber < updatedStep.stepNumber) {
updatedStep.stepNumber = updatedStep.stepNumber - 1;
}
return updatedStep;
});
form.setValue("steps", updatedSteps);
if (setReload) {
setReload(!reload);
}
setIsDeleteStepDialogOpen?.(false);
}}>
{t("delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
return <></>;
}