diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 9b13c3ebf2..7c68cfd51c 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2853,7 +2853,9 @@ "call_to_confirm_booking": "Call to confirm booking", "cal_ai_phone_call_action_description": "2 hrs before event starts", "cal_ai_agent_configuration": "Cal.ai Agent Configuration", + "select_event_type_to_schedule_calls": "Please select an event type to schedule calls", "choose_at_least_one_event_type_test_call": "Please choose at least one event type to make a test call.", + "choose_event_type_in_agent_setup": "Please choose an event type in the agent setup.", "mark_as_no_show": "Mark as no-show", "unmark_as_no_show": "Unmark no-show", "account_unlinked_success": "Account unlinked successfully", @@ -3865,7 +3867,10 @@ "please_select_event_type_first": "Please select an event type first", "edit_configuration": "Edit Configuration", "select_event_type_for_inbound_calls": "Inbound calls can book only one event type. Select the event type where meetings will be scheduled when callers reach your agent.", + "select_event_type_for_outbound_calls": "Select the event type that should be used to schedule meetings.", "setup_inbound_agent_for_incoming_calls": "Set up inbound agent for incoming calls", + "no_event_types_available_for_test_call": "No event types available for a test call", + "create_block_entry": "Create Block Entry", "blocklist_entry_details": "Blocklist Entry Details", "blocklist_entry_not_found": "Blocklist entry not found", "blocklist_entry_created": "Blocklist entry created successfully", diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts index b2b77bb137..16a00c943d 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts @@ -4,6 +4,8 @@ import { prisma } from "@calcom/prisma/__mocks__/prisma"; import { describe, it, vi, expect, beforeEach, afterEach } from "vitest"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; +import type { Workflow } from "@calcom/features/ee/workflows/lib/types"; +import type { GetWebhooksReturnType } from "@calcom/features/webhooks/lib/getWebhooks"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload"; import { @@ -11,8 +13,10 @@ import { WorkflowTriggerEvents, WorkflowActions, WorkflowTemplates, + TimeUnit, } from "@calcom/prisma/enums"; +import type { FormResponse, Field } from "../types/types"; import { _onFormSubmission } from "./formSubmissionUtils"; vi.mock("@calcom/prisma", () => ({ @@ -59,16 +63,27 @@ describe("_onFormSubmission", () => { const mockForm = { id: "form-1", name: "Test Form", + disabled: false, + userId: 1, + position: 0, + description: null, + updatedById: null, fields: [ - { id: "field-1", identifier: "email", label: "Email", type: "email" }, - { id: "field-2", identifier: "name", label: "Name", type: "text" }, - ], + { id: "field-1", identifier: "email", label: "Email", type: "email", required: false }, + { id: "field-2", identifier: "name", label: "Name", type: "text", required: false }, + ] as Field[], user: { id: 1, email: "test@example.com", timeFormat: 12, locale: "en" }, teamId: null, settings: { emailOwnerOnSubmission: true }, + routes: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + connectedForms: [], + routers: [], + teamMembers: [], }; - const mockResponse = { + const mockResponse: FormResponse = { "field-1": { label: "Email", value: "test@response.com" }, "field-2": { label: "Name", value: "Test Name" }, }; @@ -85,9 +100,19 @@ describe("_onFormSubmission", () => { describe("Webhooks", () => { it("should call FORM_SUBMITTED webhooks", async () => { - vi.mocked(getWebhooks).mockResolvedValueOnce([{ id: "wh-1", secret: "secret" } as any]); + const mockWebhook: GetWebhooksReturnType[number] = { + id: "wh-1", + secret: "secret", + subscriberUrl: "https://example.com/webhook", + payloadTemplate: null, + appId: null, + eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED], + time: null, + timeUnit: null, + }; + vi.mocked(getWebhooks).mockResolvedValueOnce([mockWebhook]); - await _onFormSubmission(mockForm as any, mockResponse, responseId); + await _onFormSubmission(mockForm, mockResponse, responseId); expect(getWebhooks).toHaveBeenCalledWith({ userId: 1, @@ -101,7 +126,7 @@ describe("_onFormSubmission", () => { describe("Workflows", () => { it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED workflows", async () => { - const mockWorkflows = [ + const mockWorkflows: Workflow[] = [ { id: 1, name: "Form Submitted Workflow", @@ -115,6 +140,7 @@ describe("_onFormSubmission", () => { id: 1, action: WorkflowActions.EMAIL_ATTENDEE, sendTo: null, + sender: null, reminderBody: "Thank you for your submission!", emailSubject: "Form Received", template: WorkflowTemplates.CUSTOM, @@ -127,9 +153,9 @@ describe("_onFormSubmission", () => { }, ]; - vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows as any); + vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows); - await _onFormSubmission(mockForm as any, mockResponse, responseId); + await _onFormSubmission(mockForm, mockResponse, responseId); expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm); expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ @@ -142,6 +168,7 @@ describe("_onFormSubmission", () => { name: { value: "Test Name", response: "Test Name" }, }, responseId, + routedEventTypeId: null, form: { ...mockForm, fields: mockForm.fields.map((field) => ({ @@ -153,7 +180,7 @@ describe("_onFormSubmission", () => { }); it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED_NO_EVENT workflows", async () => { - const mockWorkflows = [ + const mockWorkflows: Workflow[] = [ { id: 2, name: "Form Follow-up Workflow", @@ -161,12 +188,13 @@ describe("_onFormSubmission", () => { teamId: null, trigger: WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT, time: 30, - timeUnit: "MINUTE", + timeUnit: TimeUnit.MINUTE, steps: [ { id: 2, action: WorkflowActions.EMAIL_ATTENDEE, sendTo: null, + sender: null, reminderBody: "Follow up on your form submission", emailSubject: "Follow Up", template: WorkflowTemplates.CUSTOM, @@ -179,9 +207,9 @@ describe("_onFormSubmission", () => { }, ]; - vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows as any); + vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows); - await _onFormSubmission(mockForm as any, mockResponse, responseId); + await _onFormSubmission(mockForm, mockResponse, responseId); expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm); expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ @@ -193,6 +221,66 @@ describe("_onFormSubmission", () => { }, name: { value: "Test Name", response: "Test Name" }, }, + routedEventTypeId: null, + responseId, + form: { + ...mockForm, + fields: mockForm.fields.map((field) => ({ + type: field.type, + identifier: field.identifier, + })), + }, + }); + }); + + it("should pass routedEventTypeId when chosenAction is eventTypeRedirectUrl", async () => { + const mockWorkflows: Workflow[] = [ + { + id: 3, + name: "Event Type Workflow", + userId: 1, + teamId: null, + trigger: WorkflowTriggerEvents.FORM_SUBMITTED, + time: null, + timeUnit: null, + steps: [ + { + id: 3, + action: WorkflowActions.CAL_AI_PHONE_CALL, + sendTo: null, + sender: null, + reminderBody: null, + emailSubject: null, + template: WorkflowTemplates.CUSTOM, + verifiedAt: new Date(), + includeCalendarEvent: false, + numberVerificationPending: false, + numberRequired: false, + }, + ], + }, + ]; + + const chosenAction = { + type: "eventTypeRedirectUrl" as const, + value: "/team/test-team/test-event", + eventTypeId: 42, + }; + + vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows); + + await _onFormSubmission(mockForm, mockResponse, responseId, chosenAction); + + expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith({ + workflows: mockWorkflows, + responses: { + email: { + value: "test@response.com", + response: "test@response.com", + }, + name: { value: "Test Name", response: "Test Name" }, + }, + routedEventTypeId: 42, responseId, form: { ...mockForm, @@ -214,7 +302,7 @@ describe("_onFormSubmission", () => { user: { id: 1, email: "test@example.com", timeFormat: 12, locale: "en" }, }; - await _onFormSubmission(teamForm as any, mockResponse, responseId); + await _onFormSubmission(teamForm, mockResponse, responseId); expect(mockResponseEmailConstructor).toHaveBeenCalledWith({ form: teamForm, @@ -230,7 +318,7 @@ describe("_onFormSubmission", () => { settings: { emailOwnerOnSubmission: true }, }; - await _onFormSubmission(ownerForm as any, mockResponse, responseId); + await _onFormSubmission(ownerForm, mockResponse, responseId); expect(mockResponseEmailConstructor).toHaveBeenCalledWith({ form: ownerForm, @@ -246,7 +334,7 @@ describe("_onFormSubmission", () => { settings: { emailOwnerOnSubmission: false }, }; - await _onFormSubmission(ownerForm as any, mockResponse, responseId); + await _onFormSubmission(ownerForm, mockResponse, responseId); expect(mockResponseEmailConstructor).not.toHaveBeenCalled(); expect(mockSendEmail).not.toHaveBeenCalled(); diff --git a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts index 0c2e8172b0..965eb0ba79 100644 --- a/packages/app-store/routing-forms/lib/formSubmissionUtils.ts +++ b/packages/app-store/routing-forms/lib/formSubmissionUtils.ts @@ -125,6 +125,7 @@ export async function _onFormSubmission( chosenAction?: { type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl"; value: string; + eventTypeId?: number; } ) { const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {}; @@ -219,11 +220,15 @@ export async function _onFormSubmission( await Promise.all(promises); const workflows = await WorkflowService.getAllWorkflowsFromRoutingForm(form); - + const routedEventTypeId: number | null = + chosenAction && chosenAction.type === "eventTypeRedirectUrl" && chosenAction.eventTypeId + ? chosenAction.eventTypeId + : null; await WorkflowService.scheduleFormWorkflows({ workflows, responseId, responses: fieldResponsesByIdentifier, + routedEventTypeId, form: { ...form, fields: form.fields.map((field) => ({ @@ -287,6 +292,7 @@ export const onSubmissionOfFormResponse = async ({ chosenRouteAction: { type: "customPageMessage" | "externalRedirectUrl" | "eventTypeRedirectUrl"; value: string; + eventTypeId?: number; } | null; }) => { if (!form.fields) { diff --git a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts index 5be00ba24a..67c1010a32 100644 --- a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts +++ b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts @@ -331,6 +331,8 @@ export interface AIPhoneServiceProvider; voiceId?: string; language?: string; + outboundEventTypeId?: number; + timeZone?: string; }): Promise<{ message: string }>; /** diff --git a/packages/features/calAIPhone/providers/adapters/PrismaAgentRepositoryAdapter.ts b/packages/features/calAIPhone/providers/adapters/PrismaAgentRepositoryAdapter.ts index 101cdf397e..4285e4e2be 100644 --- a/packages/features/calAIPhone/providers/adapters/PrismaAgentRepositoryAdapter.ts +++ b/packages/features/calAIPhone/providers/adapters/PrismaAgentRepositoryAdapter.ts @@ -90,4 +90,9 @@ export class PrismaAgentRepositoryAdapter implements AgentRepositoryInterface { const agentRepo = new PrismaAgentRepository(prisma); await agentRepo.linkInboundAgentToWorkflow(params); } + + async updateOutboundEventTypeId(params: { agentId: string; eventTypeId: number }): Promise { + const agentRepo = new PrismaAgentRepository(prisma); + await agentRepo.updateOutboundEventTypeId(params); + } } diff --git a/packages/features/calAIPhone/providers/interfaces/AgentRepositoryInterface.ts b/packages/features/calAIPhone/providers/interfaces/AgentRepositoryInterface.ts index 5452c314ad..9cede3aa38 100644 --- a/packages/features/calAIPhone/providers/interfaces/AgentRepositoryInterface.ts +++ b/packages/features/calAIPhone/providers/interfaces/AgentRepositoryInterface.ts @@ -74,6 +74,11 @@ export interface AgentRepositoryInterface { * Link inbound agent to workflow */ linkInboundAgentToWorkflow(params: { workflowStepId: number; agentId: string }): Promise; + + /** + * Update outbound event type ID for an agent + */ + updateOutboundEventTypeId(params: { agentId: string; eventTypeId: number }): Promise; } /** @@ -87,6 +92,7 @@ export interface AgentData { userId: number | null; teamId: number | null; inboundEventTypeId?: number | null; + outboundEventTypeId?: number | null; createdAt: Date; updatedAt: Date; } diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts index 533ba682a6..44294ebf16 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts @@ -264,6 +264,8 @@ export class RetellAIPhoneServiceProvider generalTools?: AIPhoneServiceTools; voiceId?: string; language?: Language; + outboundEventTypeId?: number; + timeZone?: string; }): Promise<{ message: string }> { return await this.service.updateAgentConfiguration(params); } diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts index e75dd7e1aa..7df281dcb6 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts @@ -199,6 +199,8 @@ export class RetellAIService { generalTools?: RetellLLMGeneralTools; voiceId?: string; language?: Language; + outboundEventTypeId?: number; + timeZone?: string; }) { return this.agentService.updateAgentConfiguration({ ...params, diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIServiceMapper.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIServiceMapper.ts index 4800dca139..4585dd3179 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIServiceMapper.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIServiceMapper.ts @@ -174,6 +174,7 @@ export class RetellAIServiceMapper { userId: agent.userId, teamId: agent.teamId, inboundEventTypeId: agent.inboundEventTypeId, + outboundEventTypeId: agent.outboundEventTypeId, outboundPhoneNumbers: agent.outboundPhoneNumbers, retellData: { agentId: retellAgent.agent_id, diff --git a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts index 871d64342e..f05e5af6b2 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts @@ -2,11 +2,13 @@ import { isValidPhoneNumber } from "libphonenumber-js/max"; import { v4 as uuidv4 } from "uuid"; import { replaceEventTypePlaceholders } from "@calcom/features/ee/workflows/components/agent-configuration/utils/promptUtils"; +import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { RETELL_AI_TEST_MODE, RETELL_AI_TEST_EVENT_TYPE_MAP } from "@calcom/lib/constants"; import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema"; import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import { PrismaApiKeyRepository } from "@calcom/lib/server/repository/PrismaApiKeyRepository"; +import prisma from "@calcom/prisma"; import type { AIPhoneServiceUpdateModelParams, @@ -623,6 +625,8 @@ export class AgentService { generalTools, voiceId, language, + outboundEventTypeId, + timeZone, updateLLMConfiguration, }: { id: string; @@ -634,6 +638,8 @@ export class AgentService { generalTools?: AIPhoneServiceTools; voiceId?: string; language?: Language; + outboundEventTypeId?: number; + timeZone?: string; updateLLMConfiguration: ( llmId: string, data: AIPhoneServiceUpdateModelParams @@ -712,6 +718,41 @@ export class AgentService { } } + if (outboundEventTypeId && agent.outboundEventTypeId !== outboundEventTypeId) { + const eventTypeRepository = new EventTypeRepository(prisma); + + const outBoundEventType = await eventTypeRepository.findByIdMinimal({ + id: outboundEventTypeId, + }); + + if (!outBoundEventType) { + throw new HttpError({ + statusCode: 404, + message: "Event type not found.", + }); + } + + if (userId !== outBoundEventType.userId && teamId !== outBoundEventType.teamId) { + throw new HttpError({ + statusCode: 403, + message: "You don't have permission to use this event type.", + }); + } + + const userTimeZone = timeZone || "UTC"; + await this.updateToolsFromAgentId(agent.providerAgentId, { + eventTypeId: outboundEventTypeId, + timeZone: userTimeZone, + userId, + teamId, + }); + + await this.deps.agentRepository.updateOutboundEventTypeId({ + agentId: id, + eventTypeId: outboundEventTypeId, + }); + } + return { message: "Agent updated successfully" }; } diff --git a/packages/features/calAIPhone/providers/retellAI/types.ts b/packages/features/calAIPhone/providers/retellAI/types.ts index fe14777da8..be0a57b658 100644 --- a/packages/features/calAIPhone/providers/retellAI/types.ts +++ b/packages/features/calAIPhone/providers/retellAI/types.ts @@ -114,6 +114,7 @@ export type RetellAgentWithDetails = { userId: number | null; teamId: number | null; inboundEventTypeId?: number | null; + outboundEventTypeId?: number | null; outboundPhoneNumbers: Array<{ id: number; phoneNumber: string; diff --git a/packages/features/ee/workflows/components/TestPhoneCallDialog.tsx b/packages/features/ee/workflows/components/TestPhoneCallDialog.tsx index fa4fb921a9..83ca2892cf 100644 --- a/packages/features/ee/workflows/components/TestPhoneCallDialog.tsx +++ b/packages/features/ee/workflows/components/TestPhoneCallDialog.tsx @@ -11,6 +11,7 @@ import { Label } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; +import { getEventTypeIdForCalAiTest } from "../lib/actionHelperFunctions"; import type { FormValues } from "../pages/workflow"; interface TestPhoneCallDialogProps { @@ -19,9 +20,19 @@ interface TestPhoneCallDialogProps { agentId: string; teamId?: number; form: UseFormReturn; + eventTypeIds?: number[]; + outboundEventTypeId?: number | null; } -export function TestPhoneCallDialog({ open, onOpenChange, agentId, teamId, form }: TestPhoneCallDialogProps) { +export function TestPhoneCallDialog({ + open, + onOpenChange, + agentId, + teamId, + form, + eventTypeIds = [], + outboundEventTypeId, +}: TestPhoneCallDialogProps) { const { t } = useLocale(); const [testPhoneNumber, setTestPhoneNumber] = useState(""); @@ -41,9 +52,17 @@ export function TestPhoneCallDialog({ open, onOpenChange, agentId, teamId, form showToast(t("please_enter_phone_number"), "error"); return; } - const firstEventTypeId = form.getValues("activeOn")?.[0]?.value; - if (!firstEventTypeId) { - showToast(t("choose_at_least_one_event_type_test_call"), "error"); + + const eventTypeValidation = getEventTypeIdForCalAiTest({ + trigger: form.getValues("trigger"), + outboundEventTypeId, + eventTypeIds, + activeOnEventTypeId: form.getValues("activeOn")?.[0]?.value, + t, + }); + + if (eventTypeValidation.error || !eventTypeValidation.eventTypeId) { + showToast(eventTypeValidation.error || t("no_event_type_selected"), "error"); return; } @@ -52,7 +71,7 @@ export function TestPhoneCallDialog({ open, onOpenChange, agentId, teamId, form agentId: agentId, phoneNumber: testPhoneNumber, teamId: teamId, - eventTypeId: parseInt(firstEventTypeId, 10), + eventTypeId: eventTypeValidation.eventTypeId, }); } }; diff --git a/packages/features/ee/workflows/components/WebCallDialog.tsx b/packages/features/ee/workflows/components/WebCallDialog.tsx index 00614fe2f4..8e8acc4f0c 100644 --- a/packages/features/ee/workflows/components/WebCallDialog.tsx +++ b/packages/features/ee/workflows/components/WebCallDialog.tsx @@ -13,6 +13,7 @@ import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; +import { getEventTypeIdForCalAiTest } from "../lib/actionHelperFunctions"; import type { FormValues } from "../pages/workflow"; interface WebCallDialogProps { @@ -22,6 +23,8 @@ interface WebCallDialogProps { teamId?: number; isOrganization?: boolean; form: UseFormReturn; + eventTypeIds?: number[]; + outboundEventTypeId?: number | null; } interface TranscriptEntry { @@ -39,6 +42,8 @@ export function WebCallDialog({ teamId, isOrganization = false, form, + eventTypeIds = [], + outboundEventTypeId, }: WebCallDialogProps) { const { t } = useLocale(); const [callStatus, setCallStatus] = useState("idle"); @@ -195,9 +200,16 @@ export function WebCallDialog({ }; const handleStartCall = () => { - const firstEventTypeId = form.getValues("activeOn")?.[0]?.value; - if (!firstEventTypeId) { - showToast(t("choose_at_least_one_event_type_test_call"), "error"); + const eventTypeValidation = getEventTypeIdForCalAiTest({ + trigger: form.getValues("trigger"), + outboundEventTypeId, + eventTypeIds, + activeOnEventTypeId: form.getValues("activeOn")?.[0]?.value, + t, + }); + + if (eventTypeValidation.error || !eventTypeValidation.eventTypeId) { + showToast(eventTypeValidation.error || t("no_event_type_selected"), "error"); return; } @@ -208,7 +220,7 @@ export function WebCallDialog({ createWebCallMutation.mutate({ agentId: agentId, teamId: teamId, - eventTypeId: parseInt(firstEventTypeId, 10), + eventTypeId: eventTypeValidation.eventTypeId, }); } }; @@ -278,7 +290,6 @@ export function WebCallDialog({ useEffect(() => { if (transcriptEndRef.current) { - // eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed transcriptEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [transcript]); diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index edd0f863ab..9c9976d7d5 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -10,15 +10,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions } from "@calcom/prisma/enums"; import { WorkflowTemplates } from "@calcom/prisma/enums"; -import type { RouterOutputs } from "@calcom/trpc/react"; -import { trpc } from "@calcom/trpc/react"; +import { trpc, type RouterOutputs } from "@calcom/trpc/react"; import { Button } from "@calcom/ui/components/button"; import { FormCard, FormCardBody } from "@calcom/ui/components/card"; import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { useAgentsData } from "../hooks/useAgentsData"; -import { isCalAIAction, isFormTrigger, isSMSAction } from "../lib/actionHelperFunctions"; +import { isCalAIAction, isSMSAction, isFormTrigger, isWhatsappAction } from "../lib/actionHelperFunctions"; import { ALLOWED_FORM_WORKFLOW_ACTIONS } from "../lib/constants"; import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; import type { FormValues } from "../pages/workflow"; @@ -36,13 +35,23 @@ interface Props { user: User; isOrg: boolean; allOptions: Option[]; + eventTypeOptions: Option[]; onSaveWorkflow?: () => Promise; permissions: WorkflowPermissions; } export default function WorkflowDetailsPage(props: Props) { - const { form, workflowId, selectedOptions, setSelectedOptions, teamId, isOrg, allOptions, permissions } = - props; + const { + form, + workflowId, + selectedOptions, + setSelectedOptions, + teamId, + isOrg, + allOptions, + eventTypeOptions, + permissions, + } = props; const { t, i18n } = useLocale(); const { hasActiveTeamPlan } = useHasActiveTeamPlan(); @@ -194,6 +203,7 @@ export default function WorkflowDetailsPage(props: Props) { setSelectedOptions={setSelectedOptions} isOrganization={isOrg} allOptions={allOptions} + eventTypeOptions={eventTypeOptions} onSaveWorkflow={props.onSaveWorkflow} actionOptions={transformedActionOptions} updateTemplate={updateTemplate} @@ -267,6 +277,7 @@ export default function WorkflowDetailsPage(props: Props) { setReload={setReload} teamId={teamId} readOnly={permissions.readOnly} + eventTypeOptions={eventTypeOptions} onSaveWorkflow={props.onSaveWorkflow} setIsDeleteStepDialogOpen={setIsDeleteStepDialogOpen} isDeleteStepDialogOpen={isDeleteStepDialogOpen} diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 8c6e32ba58..8938afec89 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -89,6 +89,7 @@ type WorkflowStepProps = { setSelectedOptions?: Dispatch>; isOrganization?: boolean; allOptions?: Option[]; + eventTypeOptions?: Option[]; onSaveWorkflow?: () => Promise; setIsDeleteStepDialogOpen?: Dispatch>; isDeleteStepDialogOpen?: boolean; @@ -122,19 +123,19 @@ const getTimeSectionText = (trigger: WorkflowTriggerEvents, t: TFunction) => { const CalAIAgentDataSkeleton = () => { return ( -
-
+
+
- -
- - - + +
+ + +
-
- - +
+ +
@@ -380,11 +381,10 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { name: "steps", }); - const hasAiAction = hasCalAIAction(steps); const hasEmailToHostAction = steps.some((s) => s.action === WorkflowActions.EMAIL_HOST); const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action)); - const disallowFormTriggers = hasAiAction || hasEmailToHostAction || hasWhatsappAction; + const disallowFormTriggers = hasEmailToHostAction || hasWhatsappAction; const filteredTriggerOptions = triggerOptions.filter( (option) => !(isFormTrigger(option.value) && disallowFormTriggers) @@ -518,7 +518,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { return ( <>
- +
{!!timeSectionText && ( -
- +
+
)} @@ -610,7 +610,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {selectedOptions && setSelectedOptions && allOptions && (
{isOrganization ? ( -
+
@@ -677,7 +677,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)} {!!timeSectionText && ( -
+

{t("testing_sms_workflow_info_message")}

@@ -824,12 +824,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { />
-
{t("sender_id_info")}
+
{t("sender_id_info")}
{form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sender && ( -

{t("sender_id_error_message")}

+

{t("sender_id_error_message")}

)} ) : ( @@ -848,23 +848,23 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)} {isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && !stepAgentId && ( -
-
+
+
-

+

{t("cal_ai_agent")} - + {t("set_up_required")}

-

+

{t("no_phone_number_connected")}.

{form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( -

+

{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}

)} @@ -1185,9 +1185,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { ) : ( !props.readOnly && ( <> -
+
-
+
{form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && ( -

+

{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}

)} @@ -1304,7 +1304,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
)} {!isCalAIAction(form.getValues(`steps.${step.stepNumber - 1}.action`)) && ( -
+
{isEmailSubjectNeeded && (
@@ -1337,14 +1337,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { /> {form.formState.errors.steps && form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject && ( -

+

{form.formState?.errors?.steps[step.stepNumber - 1]?.emailSubject?.message || ""}

)}
)} -
-
)} {!props.readOnly && !isFormTrigger(trigger) && ( -
+
@@ -1512,23 +1512,23 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {

{t("how_booking_questions_as_variables")}

-
-

{t("format")}

-
    +
    +

    {t("format")}

    +
    • {t("uppercase_for_letters")}
    • {t("replace_whitespaces_underscores")}
    • {t("ignore_special_characters_booking_questions")}
    -

    {t("example_1")}

    -
    -
    +

    {t("example_1")}

    +
    +
    {t("booking_question_identifier")}
    -
    {t("company_size")}
    -
    {t("variable")}
    +
    {t("company_size")}
    +
    {t("variable")}
    -
    +
    {" "} {`{${t("company_size") .replace(/[^a-zA-Z0-9 ]/g, "") @@ -1539,14 +1539,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    -

    {t("example_2")}

    -
    -
    +

    {t("example_2")}

    +
    +
    {t("booking_question_identifier")}
    -
    {t("what_help_needed")}
    -
    {t("variable")}
    -
    +
    {t("what_help_needed")}
    +
    {t("variable")}
    +
    {" "} {`{${t("what_help_needed") .replace(/[^a-zA-Z0-9 ]/g, "") @@ -1587,7 +1587,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { workflowId={params?.workflow as string} workflowStepId={step?.id} form={form} - eventTypeOptions={props.allOptions} + eventTypeOptions={props.eventTypeOptions} /> )} @@ -1598,6 +1598,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { agentId={stepAgentId || ""} teamId={teamId} form={form} + eventTypeIds={props.eventTypeOptions?.map((opt) => parseInt(opt.value, 10))} + outboundEventTypeId={agentData?.outboundEventTypeId} /> )} @@ -1609,6 +1611,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { teamId={teamId} isOrganization={props.isOrganization} form={form} + eventTypeIds={props.eventTypeOptions?.map((opt) => parseInt(opt.value, 10)) || []} + outboundEventTypeId={agentData?.outboundEventTypeId} /> )} @@ -1616,17 +1620,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    -

    {t("do_you_still_want_to_unsubscribe")}

    +

    {t("do_you_still_want_to_unsubscribe")}

    {getActivePhoneNumbers( agentData?.outboundPhoneNumbers?.map((phone) => ({ ...phone, subscriptionStatus: phone.subscriptionStatus ?? undefined, })) ).length > 0 && ( -
    -
    - - +
    +
    + + {formatPhoneNumber( getActivePhoneNumbers( agentData?.outboundPhoneNumbers?.map((phone) => ({ @@ -1639,7 +1643,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
    )} -

    {t("the_action_will_disconnect_phone_number")}

    +

    {t("the_action_will_disconnect_phone_number")}