* feat: Cal.ai Self Serve #2 * chore: fix import and remove logs * fix: update checkout session * fix: type errors and test * fix: imports * fix: type err * fix: type error * fix: tests * chore: save progress * fix: workflow flow * fix: workflow update bug * tests: add unit tests for retell ai webhoo * fix: status code * fix: test and delete bug * fix: add dynamic variables * fix: type err * chore: update unit test * fix: type error * chore: update default prompt * fix: type errors * fix: workflow permissions * fix: workflow page * fix: translations * feat: add call duration * chore: add booking uid * fix: button positioning * chore: update tests * chore: improvements * chore: some more improvements * refactor: improvements * refactor: code feedback * refactor: improvements * feat: enable credits for orgs (#23077) * Show credits UI for orgs * fix stripe callback url when buying credits * give orgs 20% credits * add test for calulating credits --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * fix: types * fix: types * chore: error * fix: type error * fix: type error * chore: mock env * feat: add idempotency key to prevent double charging * chore: add userId and teamId * fix: skip inbound calls * chore: update tests * feat: add feature flag for voice agent * feat: finish test call and other improvements * chore: add alert * chore: update .env.example * chore: improvements * fix: update tests * refactor: remove un necessary * feat: add setup badge * chore: improvements * fix: use referene id * chore: improvements * fix: type error * fix: type * refactor: change pricing logic * refactor: update tests * fix: conflicts * fix: billing link for orgs * fix: types * refactor: move feature flag up * fix: alert and test call credit check * fix: update unit tests * fix: feedback * refactor: improvements * refactor: move handlers to separate files * fix: types * fix: missing import * fix: type * refactor: change general tools functions handling * refactor: use repository * refactor: improvements * fix: types * fix: type errorr * fix: auth check * feat: add creditFor * fix: update defualt prompt * fix: throw error on frontend * fix: update unit tests * fix: use deleteAllWorkflowReminders * refactor: add connect phone number * refactor: improvements * chore: translation * chore: update message * chore: translation * design improvements buy number dialog * add translation for error message * use translation key in error message * refactor: improve connect phone number tab * feat: support un saved workflow to tests * chore: remove un used * fix: remove un used * fix: remove un used * refactor: similify billing --------- Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
275 lines
9.1 KiB
TypeScript
275 lines
9.1 KiB
TypeScript
import { useRouter, useSearchParams } from "next/navigation";
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import { useState, useEffect } from "react";
|
|
import type { UseFormReturn } from "react-hook-form";
|
|
import { Controller } 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 { WorkflowActions } from "@calcom/prisma/enums";
|
|
import { WorkflowTemplates } from "@calcom/prisma/enums";
|
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
|
import { InfoBadge } from "@calcom/ui/components/badge";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui/components/form";
|
|
import { Label, MultiSelectCheckbox, TextField, CheckboxField } from "@calcom/ui/components/form";
|
|
import { Icon } from "@calcom/ui/components/icon";
|
|
|
|
import { isSMSAction, isCalAIAction } from "../lib/actionHelperFunctions";
|
|
import type { FormValues } from "../pages/workflow";
|
|
import { AddActionDialog } from "./AddActionDialog";
|
|
import { DeleteDialog } from "./DeleteDialog";
|
|
import WorkflowStepContainer from "./WorkflowStepContainer";
|
|
|
|
type User = RouterOutputs["viewer"]["me"]["get"];
|
|
|
|
interface WorkflowPermissions {
|
|
canView: boolean;
|
|
canUpdate: boolean;
|
|
canDelete: boolean;
|
|
readOnly: boolean; // Keep for backward compatibility
|
|
}
|
|
|
|
interface Props {
|
|
form: UseFormReturn<FormValues>;
|
|
workflowId: number;
|
|
selectedOptions: Option[];
|
|
setSelectedOptions: Dispatch<SetStateAction<Option[]>>;
|
|
teamId?: number;
|
|
user: User;
|
|
readOnly: boolean;
|
|
isOrg: boolean;
|
|
allOptions: Option[];
|
|
permissions?: WorkflowPermissions;
|
|
onSaveWorkflow?: () => Promise<void>;
|
|
}
|
|
|
|
export default function WorkflowDetailsPage(props: Props) {
|
|
const {
|
|
form,
|
|
workflowId,
|
|
selectedOptions,
|
|
setSelectedOptions,
|
|
teamId,
|
|
isOrg,
|
|
allOptions,
|
|
permissions: _permissions,
|
|
} = props;
|
|
const { t } = useLocale();
|
|
const router = useRouter();
|
|
|
|
const hasCalAIAction = () => {
|
|
const steps = form.getValues("steps") || [];
|
|
return steps.some((step) => isCalAIAction(step.action));
|
|
};
|
|
|
|
const permissions = _permissions || {
|
|
canView: !teamId ? true : !props.readOnly,
|
|
canUpdate: !teamId ? true : !props.readOnly,
|
|
canDelete: !teamId ? true : !props.readOnly,
|
|
readOnly: !teamId ? false : props.readOnly,
|
|
};
|
|
|
|
const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
|
|
|
|
const [reload, setReload] = useState(false);
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
|
const searchParams = useSearchParams();
|
|
const eventTypeId = searchParams?.get("eventTypeId");
|
|
|
|
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);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [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 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: null,
|
|
emailSubject: null,
|
|
template: WorkflowTemplates.REMINDER,
|
|
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,
|
|
};
|
|
steps?.push(step);
|
|
form.setValue("steps", steps);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="z-1 my-8 sm:my-0 md:flex">
|
|
<div className="pl-2 pr-3 md:sticky md:top-6 md:h-0 md:pl-0">
|
|
<div className="mb-5">
|
|
<TextField
|
|
data-testid="workflow-name"
|
|
disabled={props.readOnly}
|
|
label={`${t("workflow_name")}:`}
|
|
type="text"
|
|
{...form.register("name")}
|
|
/>
|
|
</div>
|
|
{isOrg ? (
|
|
<div className="flex">
|
|
<Label>{t("which_team_apply")}</Label>
|
|
<div className="-mt-0.5">
|
|
<InfoBadge content={t("team_select_info")} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Label>{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 md:w-64"
|
|
setSelected={setSelectedOptions}
|
|
selected={form.getValues("selectAll") ? allOptions : selectedOptions}
|
|
setValue={(s: Option[]) => {
|
|
form.setValue("activeOn", s);
|
|
}}
|
|
countText={isOrg ? "count_team" : "nr_event_type"}
|
|
/>
|
|
);
|
|
}}
|
|
/>
|
|
{!hasCalAIAction() && (
|
|
<div className="mt-3">
|
|
<Controller
|
|
name="selectAll"
|
|
render={({ field: { value, onChange } }) => (
|
|
<CheckboxField
|
|
description={isOrg ? t("apply_to_all_teams") : t("apply_to_all_event_types")}
|
|
disabled={props.readOnly}
|
|
onChange={(e) => {
|
|
onChange(e);
|
|
if (e.target.value) {
|
|
setSelectedOptions(allOptions);
|
|
form.setValue("activeOn", allOptions);
|
|
}
|
|
}}
|
|
checked={value}
|
|
/>
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className="md:border-subtle my-7 border-transparent md:border-t" />
|
|
{permissions.canDelete && (
|
|
<Button
|
|
type="button"
|
|
StartIcon="trash-2"
|
|
color="destructive"
|
|
className="border"
|
|
onClick={() => setDeleteDialogOpen(true)}>
|
|
{t("delete_workflow")}
|
|
</Button>
|
|
)}
|
|
<div className="border-subtle my-7 border-t md:border-none" />
|
|
</div>
|
|
|
|
{/* Workflow Trigger Event & Steps */}
|
|
<div className="bg-muted border-subtle w-full rounded-md border p-3 py-5 md:ml-3 md:p-8">
|
|
{form.getValues("trigger") && (
|
|
<div>
|
|
<WorkflowStepContainer
|
|
form={form}
|
|
user={props.user}
|
|
teamId={teamId}
|
|
readOnly={props.readOnly}
|
|
isOrganization={isOrg}
|
|
onSaveWorkflow={props.onSaveWorkflow}
|
|
/>
|
|
</div>
|
|
)}
|
|
{form.getValues("steps") && (
|
|
<>
|
|
{form.getValues("steps")?.map((step) => {
|
|
return (
|
|
<WorkflowStepContainer
|
|
key={step.id}
|
|
form={form}
|
|
user={props.user}
|
|
step={step}
|
|
reload={reload}
|
|
setReload={setReload}
|
|
teamId={teamId}
|
|
readOnly={props.readOnly}
|
|
isOrganization={isOrg}
|
|
onSaveWorkflow={props.onSaveWorkflow}
|
|
/>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
{!props.readOnly && (
|
|
<>
|
|
<div className="my-3 flex justify-center">
|
|
<Icon name="arrow-down" className="text-subtle stroke-[1.5px] text-3xl" />
|
|
</div>
|
|
<div className="flex justify-center">
|
|
<Button
|
|
type="button"
|
|
onClick={() => setIsAddActionDialogOpen(true)}
|
|
color="secondary"
|
|
className="bg-default">
|
|
{t("add_action")}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<AddActionDialog
|
|
isOpenDialog={isAddActionDialogOpen}
|
|
setIsOpenDialog={setIsAddActionDialogOpen}
|
|
addAction={addAction}
|
|
/>
|
|
<DeleteDialog
|
|
isOpenDialog={deleteDialogOpen}
|
|
setIsOpenDialog={setDeleteDialogOpen}
|
|
workflowId={workflowId}
|
|
additionalFunction={async () => router.push("/workflows")}
|
|
/>
|
|
</>
|
|
);
|
|
}
|