import type { App_RoutingForms_Form, Team } from "@prisma/client"; import Link from "next/link"; import { useEffect, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import AddMembersWithSwitch from "@calcom/features/eventtypes/components/AddMembersWithSwitch"; import { ShellMain } from "@calcom/features/shell/Shell"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Alert, Badge, Button, ButtonGroup, Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DropdownMenuSeparator, Form, Meta, SettingsToggle, showToast, TextAreaField, TextField, Tooltip, VerticalDivider, } from "@calcom/ui"; import { getAbsoluteEventTypeRedirectUrl } from "../getEventTypeRedirectUrl"; import { RoutingPages } from "../lib/RoutingPages"; import { isFallbackRoute } from "../lib/isFallbackRoute"; import { processRoute } from "../lib/processRoute"; import type { Response, Route, SerializableForm } from "../types/types"; import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions"; import FormInputFields from "./FormInputFields"; import RoutingNavBar from "./RoutingNavBar"; import { getServerSidePropsForSingleFormView } from "./getServerSidePropsSingleForm"; type RoutingForm = SerializableForm; export type RoutingFormWithResponseCount = RoutingForm & { team: { slug: Team["slug"]; name: Team["name"]; } | null; _count: { responses: number; }; }; const Actions = ({ form, mutation, }: { form: RoutingFormWithResponseCount; mutation: { isPending: boolean; }; }) => { const { t } = useLocale(); const { data: typeformApp } = useApp("typeform"); return (
{typeformApp?.isInstalled ? ( {t("Copy Typeform Redirect Url")} ) : null}
{t("preview")} {t("copy_link_to_form")} {t("download_responses")} {t("embed")} {typeformApp ? ( {t("Copy Typeform Redirect Url")} ) : null} {t("delete")}
); }; type SingleFormComponentProps = { form: RoutingFormWithResponseCount; appUrl: string; Page: React.FC<{ form: RoutingFormWithResponseCount; appUrl: string; hookForm: UseFormReturn; }>; enrichedWithUserProfileForm?: inferSSRProps< typeof getServerSidePropsForSingleFormView >["enrichedWithUserProfileForm"]; }; function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleFormComponentProps) { const utils = trpc.useUtils(); const { t } = useLocale(); const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); const [response, setResponse] = useState({}); const [decidedAction, setDecidedAction] = useState(null); const [skipFirstUpdate, setSkipFirstUpdate] = useState(true); const [eventTypeUrl, setEventTypeUrl] = useState(""); function testRouting() { const action = processRoute({ form, response }); if (action.type === "eventTypeRedirectUrl") { setEventTypeUrl( enrichedWithUserProfileForm ? getAbsoluteEventTypeRedirectUrl({ eventTypeRedirectUrl: action.value, form: enrichedWithUserProfileForm, allURLSearchParams: new URLSearchParams(), }) : "" ); } setDecidedAction(action); } const hookForm = useFormContext(); useEffect(() => { // The first time a tab is opened, the hookForm copies the form data (saved version, from the backend), // and then it is considered the source of truth. // There are two events we need to overwrite the hookForm data with the form data coming from the server. // 1 - When we change the edited form. // 2 - When the form is saved elsewhere (such as in another browser tab) // In the second case. We skipped the first execution of useEffect to differentiate a tab change from a form change, // because each time a tab changes, a new component is created and another useEffect is executed. // An update from the form always occurs after the first useEffect execution. if (Object.keys(hookForm.getValues()).length === 0 || hookForm.getValues().id !== form.id) { hookForm.reset(form); } if (skipFirstUpdate) { setSkipFirstUpdate(false); } else { hookForm.reset(form); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [form]); const sendUpdatesTo = hookForm.watch("settings.sendUpdatesTo", []) as number[]; const sendToAll = hookForm.watch("settings.sendToAll", false) as boolean; const mutation = trpc.viewer.appRoutingForms.formMutation.useMutation({ onSuccess() { showToast(t("form_updated_successfully"), "success"); }, onError(e) { if (e.message) { showToast(e.message, "error"); return; } showToast(`Something went wrong`, "error"); }, onSettled() { utils.viewer.appRoutingForms.formQuery.invalidate({ id: form.id }); }, }); const connectedForms = form.connectedForms; return ( <>
{ // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore mutation.mutate({ ...data, }); }}>
{form.name}
{form.team && ( {form.team.name} )} } subtitle={form.description || ""} backPath={`/${appUrl}/forms`} CTA={}>
{form.teamId ? (
{t("routing_forms_send_email_to")} ({ value: member.id.toString(), label: member.name || "", avatar: member.avatarUrl || "", email: member.email, isFixed: true, }))} value={sendUpdatesTo.map((userId) => ({ isFixed: true, userId: userId, priority: 1, }))} onChange={(value) => { hookForm.setValue( "settings.sendUpdatesTo", value.map((teamMember) => teamMember.userId), { shouldDirty: true } ); hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true, }); }} assignAllTeamMembers={sendToAll} setAssignAllTeamMembers={(value) => { hookForm.setValue("settings.sendToAll", !!value, { shouldDirty: true }); }} automaticAddAllEnabled={true} isFixed={true} onActive={() => { hookForm.setValue( "settings.sendUpdatesTo", form.teamMembers.map((teamMember) => teamMember.id), { shouldDirty: true } ); hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true, }); }} placeholder={t("select_members")} containerClassName="!px-0 !pb-0 !pt-0" />
) : ( { return ( { onChange(val); hookForm.unregister("settings.sendUpdatesTo"); }} /> ); }} /> )}
{form.routers.length ? (
{t("routers")}

{t("modifications_in_fields_warning")}

{form.routers.map((router) => { return (
{router.name}
); })}
) : null} {connectedForms?.length ? (
{t("connected_forms")}

{t("form_modifications_warning")}

{connectedForms.map((router) => { return (
{router.name}
); })}
) : null}
{form.routes?.every(isFallbackRoute) && ( )} {!form._count?.responses && ( <> )}
{ e.preventDefault(); testRouting(); }}>
{form && }
{decidedAction && (
{t("route_to")}:
{RoutingPages.map((page) => { if (page.value !== decidedAction.type) return null; return ( {page.label} ); })} :{" "} {decidedAction.type === "customPageMessage" ? ( {decidedAction.value} ) : decidedAction.type === "externalRedirectUrl" ? ( {decidedAction.value} ) : ( {decidedAction.value} )}
)}
{ setIsTestPreviewOpen(false); setDecidedAction(null); setResponse({}); }}> {t("close")}
); } export default function SingleFormWrapper({ form: _form, ...props }: SingleFormComponentProps) { const { data: form, isPending } = trpc.viewer.appRoutingForms.formQuery.useQuery( { id: _form.id }, { initialData: _form, trpc: {}, } ); const { t } = useLocale(); if (isPending) { // It shouldn't be possible because we are passing the data from SSR to it as initialData. So, no need for skeleton here return null; } if (!form) { throw new Error(t("something_went_wrong")); } return ( ); } export { getServerSidePropsForSingleFormView };