Files
calendar/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
T
+3
Carina WollendorferGitHubamit@cal.com <samit91848@gmail.com>coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>amit@cal.com <samit91848@gmail.com>Amit SharmaCarinaWollicoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Udit TakkarBenny Joocal.comMorgan
44a3a9eabb feat: form submitted workflow triggers #1 (#23704)
* feat: add 5 new workflow triggers for booking events

- Add BOOKING_REJECTED, BOOKING_REQUESTED, BOOKING_PAYMENT_INITIATED, BOOKING_PAID, BOOKING_NO_SHOW_UPDATED to WorkflowTriggerEvents enum
- Update workflow constants to include new trigger options
- Implement workflow trigger logic for booking rejected and requested events
- Add translations for new workflow triggers following {enum}_trigger format
- Generate updated Prisma types for new schema changes

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: type check, remove as any

* feat: add workflow trigger for BOOKING_REQUESTED in handleNewBooking.ts

- Add WorkflowTriggerEvents import to handleNewBooking.ts
- Implement workflow trigger logic for BOOKING_REQUESTED in else block
- Filter workflows by BOOKING_REQUESTED trigger and call scheduleWorkflowReminders
- Use proper calendar event object construction without type casting
- Add error handling for workflow reminder scheduling

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: resolve type errors in workflow trigger implementations

- Add proper database includes for user information in handleConfirmation.ts
- Fix ExtendedCalendarEvent type structure with correct hosts mapping
- Add missing properties to calendar event objects in handleMarkNoShow.ts
- Ensure all workflow triggers follow proper type patterns

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* feat: add workflow test configurations for new booking triggers

- Add workflow configurations for BOOKING_REQUESTED and BOOKING_PAYMENT_INITIATED in fresh-booking.test.ts
- Add workflow configuration for BOOKING_REJECTED in confirm.handler.test.ts
- Enable previously skipped confirm.handler.test.ts
- Remove workflow test assertions temporarily until triggers are fully functional
- Maintain webhook test coverage while adding workflow test infrastructure

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* fix: add missing mockSuccessfulVideoMeetingCreation import to confirm.handler.test.ts

- Import mockSuccessfulVideoMeetingCreation from bookingScenario utils
- Add mock call to BOOKING_REJECTED workflow test case
- Resolves ReferenceError that was causing unit test CI failure

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* add new triggers

* refactor: improve _scheduleWorkflowReminders readability and add missing booking trigger events

- Extract complex conditional logic into helper functions (isImmediateTrigger, isTimeBased, shouldProcessWorkflow)
- Add missing workflow trigger events with immediate execution logic
- Update test workflows to use different actions (EMAIL_ATTENDEE, SMS_ATTENDEE) for better differentiation
- Fix translation function mock in confirm.handler.test.ts using mockNoTranslations utility
- Maintain existing functionality while improving code maintainability

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* only show customt emplate for form triggers

* filter outside scheduleWorkflowReminder

* fix type check

* chore: add more tests

* test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage

- Create handleMarkNoShow.test.ts following confirm.handler.test.ts pattern
- Add expectBookingNoShowUpdatedWebhookToHaveBeenFired utility function
- Test both webhook and workflow triggers for BOOKING_NO_SHOW_UPDATED
- Cover attendee/host no-show scenarios, multiple attendees, and error cases
- All 6 unit tests pass with proper mocking of external dependencies

Co-Authored-By: amit@cal.com <samit91848@gmail.com>

* Revert "test: add comprehensive unit tests for handleMarkNoShow with webhook and workflow coverage"

This reverts commit 764299220279f0c012392dec24d3150246bfc4ad.

* fix: add new workflow triggers to api/v2

* update swagger docs

* fix: e2e

* fix type check

* fix tests, add test for before after events

* fix unit tests

* revert confirm.handler.test

* fix: unit tests

* dummy form variables

* add routing forms to active on dropdown

* add migration file

* Ui fixes for variables dropdown

* remove other translation keys

* review fixes

* allow routing forms for activeOn

* use repository function to get routing forms

* remove unnecessary code

* adjust logic in update handler

* add triggers to api v2

* remvoe unused file

* rename to getAcitveOnOptions handler

* remove routingFormOptions handler

* clean up getActiveOnOptions

* refactor WorkflowService

* remove logs

* remove unused

* fix: type check

* fix: missed before after events for recurring

* fix: calendarEvent handleMarkNoShow

* fix error message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* don't query disabled routing forms

* create tasker function

* add tasker code

* move isFormTrigger function

* small adjustments + todo comments

* remove email to host action for form triggers

* throw trpc error if email to host is added as step

* fix dialog on how to use form responses as variables

* remove add variable dropdown for form triggers

* remove form workfows in event workflows tab

* improvements for workflow logic on form submission

* review fixes

* base setup for seperate schedule functions (evt and form)

* add missing BOOKING_PAID workflow trigger

* fix pathname

* fix: test for BOOKING_REQUESTED

* fix activeOn ids

* pass hideBranding and smsReminderNumber

* adjustments to reminderScheduler

* create empty scheduelForForm functions

* pass locale and timezone with form user

* pass formData instead of responses

* pass timeFormat and locale

* reusable function for email sending and reminder creation

* implement scheduleEmailReminderForForm

* remove added editor field from merge conflict

* don't support cal.ai action with form triggers

* throw bad request if form trigger and cal.ai is combined

* add tests for scheduleFormWorkflows

* add form submission tests

* remove form response varibe info

* clean up workflow actions

* fixes for getting template options

* pass triggerType to getAllWorkflows

* move reusable logic to scheduleSMSReminder

* add formdata to param type

* type fixes for text reminder managers

* implement scheduleSMSReminderForForm

* fix import

* fix isAuthorizedToAddActiveOnIds

* disble whatsapp action

* implement triggerFormSubmittedNoEventWorkflow

* code clean up

* Merge branch 'devin/1755107037-add-workflow-triggers' into feat/routing-form-workflow-triggers

* fix type errors

* remove async from getSubmitterEmail

* fix type errors

* revert cal.ai changes

* fix type error

* add sublogger

* code clean up

* fix type errors

* remove label for attendee whatsapp action

* code clean up

* fixes saving teams on org workflows

* fix type error

* code improvements for activeOn ids

* Revert "code improvements for activeOn ids"

This reverts commit 0a3590a4e2ce541b17d63483ad86ed458795a6a3.

* improve variable name

* fix unit tests

* small fixes

* type fixes

* remove unused translation keys

* fix merge conflict issues

* code clean up

* remove SMS action support

* remove more SMS code

* add missing imports

* set custom template for form action

* type fixes

* fix tasker endpoint

* fix duplicate check

* fix workfows.test.ts

* use repository funciton to getHideBranding

* code clean up

* fix hasDuplicateSubmission

* code clean up

* select only needed properties

* remove repository functions

* Revert "remove repository functions"

This reverts commit 7aa47b1c59c9abd7f964ebf26f746934c53a44f4.

* add scheduleWorkflows function

* Revert "add scheduleWorkflows function"

This reverts commit fe5db4fe3b65e2743c95475d585300cab98beed7.

* move type to /types

* Revert "move type to /types"

This reverts commit 91e0152154594b3772a801a426260068a8ccea54.

* revert changes causing type errors

* remove import

* remove unused import

* Revert "remove unused import"

This reverts commit 1916768c875ea5d0ac5598ccb8f9c796c5622dc9.

* revert changed from attempt to fix type errors

* pass object to gt all workflows

* fix isAuthorized check

* trigger filtering

* remove form submitted no event booked code

* remove form submitted no event from schema

* remove more code

* remove test

* fixes

* add getSubmitterEmail function

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* code clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* fix bug with key

* add missing trigger in update-workflow.input.ts

* ForEvt and ForForm function for aiPhoneCallManager

* chore: add support for form workflows on api v2

* fixup! chore: add support for form workflows on api v2

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* chore: handle workflows api v2

* chore: handle workflows api v2, split in 2 endpoints

* fix workflow step creation

* remove connect agent and fixes types

* add type to workflow

* chore: use workflow type in apiv2 WorkflowsOutputService

* update worklfow type on update

* chore: use workflow type in apiv2 WorkflowsOutputService

* fix template body for torm trigger

* some UI fixes for email subject/body

* resetting email body when changing form triggers

* use type field to query workflows

* clean up all old active on values

* remove responseId from all funciton calls

* remove undefined from updateTemplate

* refactor: split routing form and event-type workflows code

* refactor: split routing form and event-type workflows code

* fix template  text when adding action

* chore: don't rename WorkflowActivationDto to avoid ci blocking

* refine update schedule to use only allowed actions

* fix type error

* don't allow whatsapp action with form trigger

* fix type error

* return early if activeOn array is empty

* fix: from step type in BaseFormWorkflowStepDto

* fixup! fix: from step type in BaseFormWorkflowStepDto

* move all prisma calls to repository (service/workflows.ts)

* use FORM_TRIGGER_WORKFLOW_EVENTS for form queries

* use userRepository

* use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger

* code clean up

* code clean up

* add back trpc  import

* fix agent repository functions

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: amit@cal.com <samit91848@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-13 12:20:47 +02:00

308 lines
12 KiB
TypeScript

import { useSearchParams } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useState, useEffect } from "react";
import type { UseFormReturn } from "react-hook-form";
import { SENDER_ID, SENDER_NAME, SCANNING_WORKFLOW_STEPS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { WorkflowPermissions } from "@calcom/lib/server/repository/workflow-permissions";
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 { 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 { ALLOWED_FORM_WORKFLOW_ACTIONS } from "../lib/constants";
import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate";
import type { FormValues } from "../pages/workflow";
import { AddActionDialog } from "./AddActionDialog";
import WorkflowStepContainer from "./WorkflowStepContainer";
type User = RouterOutputs["viewer"]["me"]["get"];
interface Props {
form: UseFormReturn<FormValues>;
workflowId: number;
selectedOptions: Option[];
setSelectedOptions: Dispatch<SetStateAction<Option[]>>;
teamId?: number;
user: User;
isOrg: boolean;
allOptions: Option[];
onSaveWorkflow?: () => Promise<void>;
permissions: WorkflowPermissions;
}
export default function WorkflowDetailsPage(props: Props) {
const { form, workflowId, selectedOptions, setSelectedOptions, teamId, isOrg, allOptions, permissions } =
props;
const { t, i18n } = useLocale();
const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
const [isDeleteStepDialogOpen, setIsDeleteStepDialogOpen] = useState(false);
const [reload, setReload] = useState(false);
const [updateTemplate, setUpdateTemplate] = useState(false);
const searchParams = useSearchParams();
const eventTypeId = searchParams?.get("eventTypeId");
// Get base action options and transform them for form triggers
const { data: baseActionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const transformedActionOptions = baseActionOptions
? baseActionOptions
.filter((option) => {
const isFormWorkflowWithInvalidSteps =
isFormTrigger(form.getValues("trigger")) &&
!ALLOWED_FORM_WORKFLOW_ACTIONS.some((action) => action === option.value);
const isSelectAllCalAiAction = isCalAIAction(option.value) && form.watch("selectAll");
const isOrgCalAiAction = isCalAIAction(option.value) && isOrg;
if (isFormWorkflowWithInvalidSteps || isSelectAllCalAiAction || isOrgCalAiAction) {
return false;
}
return true;
})
.map((option) => {
let label = option.label;
// Transform labels for form triggers
if (isFormTrigger(form.getValues("trigger"))) {
if (option.value === WorkflowActions.EMAIL_ATTENDEE) {
label = t("email_attendee_action_form");
}
}
return {
...option,
label,
creditsTeamId: teamId,
isOrganization: isOrg,
isCalAi: isCalAIAction(option.value),
};
})
: [];
useEffect(() => {
const matchingOption = allOptions.find((option) => option.value === eventTypeId);
if (matchingOption && !selectedOptions.find((option) => option.value === eventTypeId)) {
const newOptions = [...selectedOptions, matchingOption];
setSelectedOptions(newOptions);
form.setValue("activeOn", newOptions);
}
}, [eventTypeId]);
const addAction = (
action: WorkflowActions,
sendTo?: string,
numberRequired?: boolean,
sender?: string,
senderName?: string
) => {
const steps = form.getValues("steps");
const id =
steps?.length > 0
? steps.sort((a, b) => {
return a.id - b.id;
})[0].id - 1
: 0;
const timeFormat = getTimeFormatStringFromUserTimeFormat(props.user.timeFormat);
const template = isFormTrigger(form.getValues("trigger"))
? WorkflowTemplates.CUSTOM
: WorkflowTemplates.REMINDER;
const { emailBody: reminderBody, emailSubject } =
template !== WorkflowTemplates.CUSTOM
? emailReminderTemplate({
isEditingMode: true,
locale: i18n.language,
t,
action,
timeFormat,
})
: { emailBody: null, emailSubject: null };
const step = {
id: id > 0 ? 0 : id, //id of new steps always <= 0
action,
stepNumber:
steps && steps.length > 0
? steps.sort((a, b) => {
return a.stepNumber - b.stepNumber;
})[steps.length - 1].stepNumber + 1
: 1,
sendTo: sendTo || null,
workflowId: workflowId,
reminderBody,
emailSubject,
template,
numberRequired: numberRequired || false,
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
numberVerificationPending: false,
includeCalendarEvent: false,
verifiedAt: SCANNING_WORKFLOW_STEPS ? null : new Date(),
agentId: null,
inboundAgentId: null,
};
steps?.push(step);
form.setValue("steps", steps);
};
const { outboundAgentQueries: agentQueriesTrpc, inboundAgentQueries: inboundAgentQueriesTrpc } =
useAgentsData(form);
return (
<>
<div>
<FormCard
className="border-muted mb-0"
collapsible={false}
label={
<div className="flex items-center gap-2 pb-2 pt-1">
<div className="border-subtle text-subtle ml-1 rounded-lg border p-1">
<Icon name="zap" size="16" />
</div>
<div className="text-sm font-medium leading-none">{t("trigger")}</div>
</div>
}>
<FormCardBody className="border-muted">
<WorkflowStepContainer
form={form}
user={props.user}
teamId={teamId}
readOnly={permissions.readOnly}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
isOrganization={isOrg}
allOptions={allOptions}
onSaveWorkflow={props.onSaveWorkflow}
actionOptions={transformedActionOptions}
updateTemplate={updateTemplate}
setUpdateTemplate={setUpdateTemplate}
/>
</FormCardBody>
</FormCard>
<div className="!mt-0 ml-7 h-3 w-2 border-l" />
{form.getValues("steps") && (
<div className="">
{form.getValues("steps")?.map((step, index) => {
const agentData = agentQueriesTrpc[index]?.data;
const isAgentLoading = agentQueriesTrpc[index]?.isPending;
const inboundAgentData = inboundAgentQueriesTrpc[index]?.data;
const isInboundAgentLoading = inboundAgentQueriesTrpc[index]?.isPending;
return (
<div key={index}>
<FormCard
key={step.id}
className="bg-muted border-muted mb-0"
collapsible={false}
label={
<div className="flex items-center gap-2 pb-2 pt-1">
<div className="border-subtle text-subtle rounded-lg border p-1">
<Icon name="arrow-right" size="16" />
</div>
<div className="text-sm font-medium leading-none">{t("action")}</div>
</div>
}
deleteField={
!permissions.readOnly
? {
color: "destructive",
check: () => true,
disabled: !permissions.canUpdate,
fn: () => {
if (
isCalAIAction(step.action) &&
agentData?.outboundPhoneNumbers &&
agentData.outboundPhoneNumbers.length > 0
) {
setIsDeleteStepDialogOpen(true);
} else {
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);
}
}
},
}
: null
}>
<FormCardBody className="border-muted">
<WorkflowStepContainer
form={form}
user={props.user}
step={step}
reload={reload}
setReload={setReload}
teamId={teamId}
readOnly={permissions.readOnly}
onSaveWorkflow={props.onSaveWorkflow}
setIsDeleteStepDialogOpen={setIsDeleteStepDialogOpen}
isDeleteStepDialogOpen={isDeleteStepDialogOpen}
isAgentLoading={isAgentLoading}
agentData={agentData}
inboundAgentData={inboundAgentData}
isInboundAgentLoading={isInboundAgentLoading}
allOptions={allOptions}
actionOptions={transformedActionOptions}
updateTemplate={updateTemplate}
setUpdateTemplate={setUpdateTemplate}
/>
</FormCardBody>
</FormCard>
{index !== form.getValues("steps").length - 1 && (
<div className="border-default !mt-0 ml-7 h-3 w-2 border-l" />
)}
</div>
);
})}
</div>
)}
{!permissions.readOnly && (
<>
<div className="border-default !mt-0 ml-7 h-3 w-2 border-l" />
<Button
type="button"
onClick={() => setIsAddActionDialogOpen(true)}
color="secondary"
className="bg-default">
{t("add_action")}
</Button>
</>
)}
</div>
<AddActionDialog
isOpenDialog={isAddActionDialogOpen}
setIsOpenDialog={setIsAddActionDialogOpen}
addAction={addAction}
actionOptions={transformedActionOptions}
/>
</>
);
}