diff --git a/apps/web/app/future/event-types/[type]/page.tsx b/apps/web/app/future/event-types/[type]/page.tsx
index 6cc40aa59ce0e5..6bd794a2b1aac0 100644
--- a/apps/web/app/future/event-types/[type]/page.tsx
+++ b/apps/web/app/future/event-types/[type]/page.tsx
@@ -4,12 +4,12 @@ import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { cookies, headers } from "next/headers";
+import { EventType } from "@calcom/atoms/monorepo";
+
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { getServerSideProps } from "@lib/event-types/[type]/getServerSideProps";
import type { PageProps as EventTypePageProps } from "@lib/event-types/[type]/getServerSideProps";
-import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
-
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
const { eventType } = await getData(legacyCtx);
@@ -21,5 +21,5 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
};
const getData = withAppDirSsr(getServerSideProps);
-const Page = (props: EventTypePageProps) => ;
+const Page = ({ type, ...rest }: EventTypePageProps) => ;
export default WithLayout({ getLayout: null, getData, Page })<"P">;
diff --git a/apps/web/modules/event-types/views/event-types-single-view.tsx b/apps/web/modules/event-types/views/event-types-single-view.tsx
index cd0dfee920b298..d860f83e4aab47 100644
--- a/apps/web/modules/event-types/views/event-types-single-view.tsx
+++ b/apps/web/modules/event-types/views/event-types-single-view.tsx
@@ -1,38 +1,11 @@
"use client";
-import { EventType } from "@calcom/features/eventtypes/components/EventType";
-import { EventTypeAppDir } from "@calcom/features/eventtypes/components/EventTypeAppDir";
-import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
-
-/* eslint-disable @typescript-eslint/no-empty-function */
-// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
-import { trpc } from "@calcom/trpc/react";
+import { EventType } from "@calcom/atoms/monorepo";
import type { PageProps } from "@lib/event-types/[type]/getServerSideProps";
-const EventTypePageWrapper = (props: PageProps & { isAppDir?: boolean }) => {
- const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
-
- if (!data) return null;
-
- const eventType = data.eventType;
-
- const { data: workflows } = trpc.viewer.workflows.getAllActiveWorkflows.useQuery({
- eventType: {
- id: props.type,
- teamId: eventType.teamId,
- userId: eventType.userId,
- parent: eventType.parent,
- metadata: eventType.metadata,
- },
- });
-
- const propsData = {
- ...(data as EventTypeSetupProps),
- allActiveWorkflows: workflows,
- };
-
- return props.isAppDir ? : ;
+const EventTypePageWrapper = ({ type, ...rest }: PageProps) => {
+ return ;
};
export default EventTypePageWrapper;
diff --git a/packages/features/eventtypes/components/EventType.tsx b/packages/features/eventtypes/components/EventType.tsx
index 6880badb49609b..67ca18191226e8 100644
--- a/packages/features/eventtypes/components/EventType.tsx
+++ b/packages/features/eventtypes/components/EventType.tsx
@@ -2,84 +2,25 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import dynamic from "next/dynamic";
+import type { UseFormReturn } from "react-hook-form";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
-import { useRouter } from "next/router";
-import { useEffect, useMemo, useState, useRef } from "react";
-import { useForm } from "react-hook-form";
import { z } from "zod";
-import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
-import { validateCustomEventName } from "@calcom/core/event";
-import {
- DEFAULT_PROMPT_VALUE,
- DEFAULT_BEGIN_MESSAGE,
-} from "@calcom/features/ee/cal-ai-phone/promptTemplates";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
-import { sortHosts } from "@calcom/features/eventtypes/components/HostEditDialogs";
-import type { FormValues } from "@calcom/features/eventtypes/lib/types";
-import { validateIntervalLimitOrder } from "@calcom/lib";
-import { WEBSITE_URL } from "@calcom/lib/constants";
-import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment";
-import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
-import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type {
+ TabMap,
+ EventTypeSetupProps,
+ FormValues,
+ EventTypeApps,
+} from "@calcom/features/eventtypes/lib/types";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
-import { HttpError } from "@calcom/lib/http-error";
-import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
-import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
-import type { Prisma } from "@calcom/prisma/client";
-import { SchedulingType } from "@calcom/prisma/enums";
-import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
-import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
+import type { customInputSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
-import { trpc } from "@calcom/trpc/react";
-import { Form, showToast } from "@calcom/ui";
+import { Form } from "@calcom/ui";
import { EventTypeSingleLayout } from "./EventTypeLayout";
-// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
-const EventSetupTab = dynamic(() => import("./tabs/setup/EventSetupTab").then((mod) => mod.EventSetupTab));
-
-const EventAvailabilityTab = dynamic(() =>
- import("./tabs/availability/EventAvailabilityTab").then((mod) => mod.EventAvailabilityTab)
-);
-
-const EventTeamAssignmentTab = dynamic(() =>
- import("./tabs/assignment/EventTeamAssignmentTab").then((mod) => mod.EventTeamAssignmentTab)
-);
-
-const EventLimitsTab = dynamic(() =>
- import("./tabs/limits/EventLimitsTab").then((mod) => mod.EventLimitsTab)
-);
-
-const EventAdvancedTab = dynamic(() =>
- import("./tabs/advanced/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
-);
-
-const EventInstantTab = dynamic(() =>
- import("./tabs/instant/EventInstantTab").then((mod) => mod.EventInstantTab)
-);
-
-const EventRecurringTab = dynamic(() =>
- import("./tabs/recurring/EventRecurringTab").then((mod) => mod.EventRecurringTab)
-);
-
-const EventAppsTab = dynamic(() => import("./tabs/apps/EventAppsTab").then((mod) => mod.EventAppsTab));
-
-const EventWorkflowsTab = dynamic(() => import("./tabs/workflows/EventWorkfowsTab"));
-
-const EventWebhooksTab = dynamic(() =>
- import("./tabs/webhooks/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
-);
-
-const EventAITab = dynamic(() => import("./tabs/ai/EventAITab").then((mod) => mod.EventAITab));
-
-const ManagedEventTypeDialog = dynamic(() => import("./dialogs/ManagedEventDialog"));
-
-const AssignmentWarningDialog = dynamic(() => import("./dialogs/AssignmentWarningDialog"));
-
export type Host = {
isFixed: boolean;
userId: number;
@@ -109,236 +50,31 @@ const querySchema = z.object({
.default("setup"),
});
-export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"];
export type EventTypeHosts = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["hosts"];
-export const EventType = (props: EventTypeSetupProps & { allActiveWorkflows?: Workflow[] }) => {
- const { t } = useLocale();
- const utils = trpc.useUtils();
- const telemetry = useTelemetry();
+export const EventType = (
+ props: EventTypeSetupProps & {
+ allActiveWorkflows?: Workflow[];
+ tabMap: TabMap;
+ onDelete: () => void;
+ onConflict: (eventTypes: ChildrenEventType[]) => void;
+ children?: React.ReactNode;
+ handleSubmit: (values: FormValues) => void;
+ formMethods: UseFormReturn;
+ eventTypeApps?: EventTypeApps;
+ isUpdating: boolean;
+ }
+) => {
const {
data: { tabName },
} = useTypedQuery(querySchema);
- const { data: eventTypeApps } = trpc.viewer.integrations.useQuery({
- extendsFeature: "EventType",
- teamId: props.eventType.team?.id || props.eventType.parent?.teamId,
- onlyInstalled: true,
- });
+ const { formMethods, eventTypeApps } = props;
+ const { eventType, team, currentUserMembership, tabMap, isUpdating } = props;
- const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
- const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState(false);
- const [pendingRoute, setPendingRoute] = useState("");
- const leaveWithoutAssigningHosts = useRef(false);
- const isTeamEventTypeDeleted = useRef(false);
const [animationParentRef] = useAutoAnimate();
- const updateMutation = trpc.viewer.eventTypes.update.useMutation({
- onSuccess: async () => {
- const currentValues = formMethods.getValues();
-
- currentValues.children = currentValues.children.map((child) => ({
- ...child,
- created: true,
- }));
- currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
-
- // Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
- formMethods.reset(currentValues);
-
- showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
- },
- async onSettled() {
- await utils.viewer.eventTypes.get.invalidate();
- await utils.viewer.eventTypes.getByViewer.invalidate();
- },
- onError: (err) => {
- let message = "";
- if (err instanceof HttpError) {
- const message = `${err.statusCode}: ${err.message}`;
- showToast(message, "error");
- }
-
- if (err.data?.code === "UNAUTHORIZED") {
- message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`;
- }
-
- if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
- message = `${err.data.code}: ${t(err.message)}`;
- }
-
- if (err.data?.code === "INTERNAL_SERVER_ERROR") {
- message = t("unexpected_error_try_again");
- }
-
- showToast(message ? t(message) : t(err.message), "error");
- },
- });
-
- const router = useRouter();
-
- const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
- startDate: new Date(eventType.periodStartDate || Date.now()),
- endDate: new Date(eventType.periodEndDate || Date.now()),
- });
-
- const bookingFields: Prisma.JsonObject = {};
-
- eventType.bookingFields.forEach(({ name }) => {
- bookingFields[name] = name;
- });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const defaultValues: any = useMemo(() => {
- return {
- title: eventType.title,
- id: eventType.id,
- slug: eventType.slug,
- afterEventBuffer: eventType.afterEventBuffer,
- beforeEventBuffer: eventType.beforeEventBuffer,
- eventName: eventType.eventName || "",
- scheduleName: eventType.scheduleName,
- periodDays: eventType.periodDays,
- requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
- seatsPerTimeSlot: eventType.seatsPerTimeSlot,
- seatsShowAttendees: eventType.seatsShowAttendees,
- seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
- lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
- locations: eventType.locations || [],
- destinationCalendar: eventType.destinationCalendar,
- recurringEvent: eventType.recurringEvent || null,
- isInstantEvent: eventType.isInstantEvent,
- instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
- description: eventType.description ?? undefined,
- schedule: eventType.schedule || undefined,
- instantMeetingSchedule: eventType.instantMeetingSchedule || undefined,
- bookingLimits: eventType.bookingLimits || undefined,
- onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
- durationLimits: eventType.durationLimits || undefined,
- length: eventType.length,
- hidden: eventType.hidden,
- hashedLink: eventType.hashedLink?.link || undefined,
- eventTypeColor: eventType.eventTypeColor || null,
- periodDates: {
- startDate: periodDates.startDate,
- endDate: periodDates.endDate,
- },
- hideCalendarNotes: eventType.hideCalendarNotes,
- offsetStart: eventType.offsetStart,
- bookingFields: eventType.bookingFields,
- periodType: eventType.periodType,
- periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
- schedulingType: eventType.schedulingType,
- requiresConfirmation: eventType.requiresConfirmation,
- requiresConfirmationWillBlockSlot: eventType.requiresConfirmationWillBlockSlot,
- slotInterval: eventType.slotInterval,
- minimumBookingNotice: eventType.minimumBookingNotice,
- metadata: eventType.metadata,
- hosts: eventType.hosts.sort((a, b) => sortHosts(a, b, eventType.isRRWeightsEnabled)),
- successRedirectUrl: eventType.successRedirectUrl || "",
- forwardParamsSuccessRedirect: eventType.forwardParamsSuccessRedirect,
- users: eventType.users,
- useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
- secondaryEmailId: eventType?.secondaryEmailId || -1,
- children: eventType.children.map((ch) => ({
- ...ch,
- created: true,
- owner: {
- ...ch.owner,
- eventTypeSlugs:
- eventType.team?.members
- .find((mem) => mem.user.id === ch.owner.id)
- ?.user.eventTypes.map((evTy) => evTy.slug)
- .filter((slug) => slug !== eventType.slug) ?? [],
- },
- })),
- seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
- rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
- assignAllTeamMembers: eventType.assignAllTeamMembers,
- aiPhoneCallConfig: {
- generalPrompt: eventType.aiPhoneCallConfig?.generalPrompt ?? DEFAULT_PROMPT_VALUE,
- enabled: eventType.aiPhoneCallConfig?.enabled,
- beginMessage: eventType.aiPhoneCallConfig?.beginMessage ?? DEFAULT_BEGIN_MESSAGE,
- guestName: eventType.aiPhoneCallConfig?.guestName,
- guestEmail: eventType.aiPhoneCallConfig?.guestEmail,
- guestCompany: eventType.aiPhoneCallConfig?.guestCompany,
- yourPhoneNumber: eventType.aiPhoneCallConfig?.yourPhoneNumber,
- numberToCall: eventType.aiPhoneCallConfig?.numberToCall,
- templateType: eventType.aiPhoneCallConfig?.templateType ?? "CUSTOM_TEMPLATE",
- schedulerName: eventType.aiPhoneCallConfig?.schedulerName,
- },
- isRRWeightsEnabled: eventType.isRRWeightsEnabled,
- };
- }, [eventType, periodDates]);
- const formMethods = useForm({
- defaultValues,
- resolver: zodResolver(
- z
- .object({
- // Length if string, is converted to a number or it can be a number
- // Make it optional because it's not submitted from all tabs of the page
- eventName: z
- .string()
- .superRefine((val, ctx) => {
- const validationResult = validateCustomEventName(val, bookingFields);
- if (validationResult !== true) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("invalid_event_name_variables", { item: validationResult }),
- });
- }
- })
- .optional(),
- length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
- offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
- bookingFields: eventTypeBookingFields,
- locations: locationsResolver(t),
- })
- // TODO: Add schema for other fields later.
- .passthrough()
- ),
- });
- const {
- formState: { isDirty: isFormDirty, dirtyFields },
- } = formMethods;
-
- const onDelete = () => {
- isTeamEventTypeDeleted.current = true;
- };
-
- useEffect(() => {
- const handleRouteChange = (url: string) => {
- const paths = url.split("/");
-
- // If the event-type is deleted, we can't show the empty assignment warning
- if (isTeamEventTypeDeleted.current) return;
-
- if (
- !!team &&
- !leaveWithoutAssigningHosts.current &&
- (url === "/event-types" || paths[1] !== "event-types") &&
- checkForEmptyAssignment({
- assignedUsers: eventType.children,
- hosts: eventType.hosts,
- assignAllTeamMembers: eventType.assignAllTeamMembers,
- isManagedEventType: eventType.schedulingType === SchedulingType.MANAGED,
- })
- ) {
- setIsOpenAssignmentWarnDialog(true);
- setPendingRoute(url);
- router.events.emit(
- "routeChangeError",
- new Error(`Aborted route change to ${url} because none was assigned to team event`)
- );
- throw "Aborted";
- }
- };
- router.events.on("routeChangeStart", handleRouteChange);
- return () => {
- router.events.off("routeChangeStart", handleRouteChange);
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [router, eventType.hosts, eventType.children, eventType.assignAllTeamMembers]);
const appsMetadata = formMethods.getValues("metadata")?.apps;
const availability = formMethods.watch("availability");
@@ -351,265 +87,8 @@ export const EventType = (props: EventTypeSetupProps & { allActiveWorkflows?: Wo
).length;
}
- const permalink = `${WEBSITE_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
- eventType.slug
- }`;
- const tabMap = {
- setup: (
-
- ),
- availability: ,
- team: ,
- limits: ,
- advanced: ,
- instant: ,
- recurring: ,
- apps: ,
- workflows: props.allActiveWorkflows ? (
-
- ) : (
- <>>
- ),
- webhooks: ,
- ai: ,
- } as const;
- const isObject = (value: T): boolean => {
- return value !== null && typeof value === "object" && !Array.isArray(value);
- };
-
- const isArray = (value: T): boolean => {
- return Array.isArray(value);
- };
-
- const isFieldDirty = (fieldName: keyof FormValues) => {
- // If the field itself is directly marked as dirty
- if (dirtyFields[fieldName] === true) {
- return true;
- }
-
- // Check if the field is an object or an array
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const fieldValue: any = getNestedField(dirtyFields, fieldName);
- if (isObject(fieldValue)) {
- for (const key in fieldValue) {
- if (fieldValue[key] === true) {
- return true;
- }
-
- if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
- const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
- // Recursive call for nested objects or arrays
- if (isFieldDirty(nestedFieldName)) {
- return true;
- }
- }
- }
- }
- if (isArray(fieldValue)) {
- for (const element of fieldValue) {
- // If element is an object, check each property of the object
- if (isObject(element)) {
- for (const key in element) {
- if (element[key] === true) {
- return true;
- }
-
- if (isObject(element[key]) || isArray(element[key])) {
- const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
- // Recursive call for nested objects or arrays within each element
- if (isFieldDirty(nestedFieldName)) {
- return true;
- }
- }
- }
- } else if (element === true) {
- return true;
- }
- }
- }
-
- return false;
- };
-
- const getNestedField = (obj: typeof dirtyFields, path: string) => {
- const keys = path.split(".");
- let current = obj;
-
- for (let i = 0; i < keys.length; i++) {
- // @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
- const currentKey = current[keys[i]];
- if (currentKey === undefined) return undefined;
- current = currentKey;
- }
-
- return current;
- };
-
- const getDirtyFields = (values: FormValues): Partial => {
- if (!isFormDirty) {
- return {};
- }
- const updatedFields: Partial = {};
- Object.keys(dirtyFields).forEach((key) => {
- const typedKey = key as keyof typeof dirtyFields;
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- updatedFields[typedKey] = undefined;
- const isDirty = isFieldDirty(typedKey);
- if (isDirty) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- updatedFields[typedKey] = values[typedKey];
- }
- });
- return updatedFields;
- };
-
- const handleSubmit = async (values: FormValues) => {
- const { children } = values;
- const dirtyValues = getDirtyFields(values);
- const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
- const {
- periodDates,
- periodCountCalendarDays,
- beforeEventBuffer,
- afterEventBuffer,
- seatsPerTimeSlot,
- seatsShowAttendees,
- seatsShowAvailabilityCount,
- bookingLimits,
- onlyShowFirstAvailableSlot,
- durationLimits,
- recurringEvent,
- eventTypeColor,
- locations,
- metadata,
- customInputs,
- assignAllTeamMembers,
- // We don't need to send send these values to the backend
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- seatsPerTimeSlotEnabled,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- minimumBookingNoticeInDurationType,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- bookerLayouts,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- multipleDurationEnabled,
- length,
- ...input
- } = dirtyValues;
- if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
-
- if (bookingLimits) {
- const isValid = validateIntervalLimitOrder(bookingLimits);
- if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
- }
-
- if (durationLimits) {
- const isValid = validateIntervalLimitOrder(durationLimits);
- if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
- }
-
- const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
- if (layoutError) throw new Error(t(layoutError));
-
- if (metadata?.multipleDuration !== undefined) {
- if (metadata?.multipleDuration.length < 1) {
- throw new Error(t("event_setup_multiple_duration_error"));
- } else {
- // if length is unchanged, we skip this check
- if (length !== undefined) {
- if (!length && !metadata?.multipleDuration?.includes(length)) {
- //This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
- throw new Error(t("event_setup_multiple_duration_default_error"));
- }
- }
- }
- }
-
- // Prevent two payment apps to be enabled
- // Ok to cast type here because this metadata will be updated as the event type metadata
- if (checkForMultiplePaymentApps(metadata as z.infer))
- throw new Error(t("event_setup_multiple_payment_apps_error"));
-
- if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
- throw new Error(t("seats_and_no_show_fee_error"));
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { availability, users, scheduleName, ...rest } = input;
- const payload = {
- ...rest,
- length,
- locations,
- recurringEvent,
- periodStartDate: periodDates?.startDate,
- periodEndDate: periodDates?.endDate,
- periodCountCalendarDays,
- id: eventType.id,
- beforeEventBuffer,
- afterEventBuffer,
- bookingLimits,
- onlyShowFirstAvailableSlot,
- durationLimits,
- eventTypeColor,
- seatsPerTimeSlot,
- seatsShowAttendees,
- seatsShowAvailabilityCount,
- metadata,
- customInputs,
- children,
- assignAllTeamMembers,
- };
- // Filter out undefined values
- const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
- if (value !== undefined) {
- // @ts-expect-error Element implicitly has any type
- acc[key] = value;
- }
- return acc;
- }, {});
-
- if (dirtyFieldExists) {
- updateMutation.mutate({ ...filteredPayload, id: eventType.id });
- }
- };
-
- const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState([]);
- const slug = formMethods.watch("slug") ?? eventType.slug;
-
// Optional prerender all tabs after 300 ms on mount
- useEffect(() => {
- const timeout = setTimeout(() => {
- const Components = [
- EventSetupTab,
- EventAvailabilityTab,
- EventTeamAssignmentTab,
- EventLimitsTab,
- EventAdvancedTab,
- EventInstantTab,
- EventRecurringTab,
- EventAppsTab,
- EventWorkflowsTab,
- EventWebhooksTab,
- ];
- Components.forEach((C) => {
- // @ts-expect-error Property 'render' does not exist on type 'ComponentClass
- C.render.preload();
- });
- }, 300);
-
- return () => {
- clearTimeout(timeout);
- };
- }, []);
return (
<>
webhook.active).length}
team={team}
availability={availability}
- isUpdateMutationLoading={updateMutation.isPending}
+ isUpdateMutationLoading={isUpdating}
formMethods={formMethods}
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
disableBorder={true}
currentUserMembership={currentUserMembership}
bookerUrl={eventType.bookerUrl}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}
- onDelete={onDelete}>
-
- {slugExistsChildrenDialogOpen.length ? (
- {
- setSlugExistsChildrenDialogOpen([]);
- }}
- slug={slug}
- onConfirm={(e: { preventDefault: () => void }) => {
- e.preventDefault();
- handleSubmit(formMethods.getValues());
- telemetry.event(telemetryEventTypes.slugReplacementAction);
- setSlugExistsChildrenDialogOpen([]);
- }}
- />
- ) : null}
-
+ {props.children}
>
);
};
diff --git a/packages/features/eventtypes/components/EventTypeAppDir.tsx b/packages/features/eventtypes/components/EventTypeAppDir.tsx
deleted file mode 100644
index 666f5219830d42..00000000000000
--- a/packages/features/eventtypes/components/EventTypeAppDir.tsx
+++ /dev/null
@@ -1,755 +0,0 @@
-"use client";
-
-/* eslint-disable @typescript-eslint/no-empty-function */
-import { useAutoAnimate } from "@formkit/auto-animate/react";
-import { zodResolver } from "@hookform/resolvers/zod";
-import dynamic from "next/dynamic";
-import { usePathname } from "next/navigation";
-import { useEffect, useMemo, useState, useRef } from "react";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
-import { validateCustomEventName } from "@calcom/core/event";
-import {
- DEFAULT_PROMPT_VALUE,
- DEFAULT_BEGIN_MESSAGE,
-} from "@calcom/features/ee/cal-ai-phone/promptTemplates";
-import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
-import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
-import { sortHosts } from "@calcom/features/eventtypes/components/HostEditDialogs";
-import type { FormValues } from "@calcom/features/eventtypes/lib/types";
-import { validateIntervalLimitOrder } from "@calcom/lib";
-import { WEBSITE_URL } from "@calcom/lib/constants";
-import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment";
-import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
-import { useLocale } from "@calcom/lib/hooks/useLocale";
-import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
-import { HttpError } from "@calcom/lib/http-error";
-import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
-import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
-import type { Prisma } from "@calcom/prisma/client";
-import { SchedulingType } from "@calcom/prisma/enums";
-import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
-import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
-import type { RouterOutputs } from "@calcom/trpc/react";
-import { trpc } from "@calcom/trpc/react";
-import { Form, showToast } from "@calcom/ui";
-
-import { EventTypeSingleLayout } from "./EventTypeLayout";
-
-// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
-const EventSetupTab = dynamic(() => import("./tabs/setup/EventSetupTab").then((mod) => mod.EventSetupTab));
-
-const EventAvailabilityTab = dynamic(() =>
- import("./tabs/availability/EventAvailabilityTab").then((mod) => mod.EventAvailabilityTab)
-);
-
-const EventTeamAssignmentTab = dynamic(() =>
- import("./tabs/assignment/EventTeamAssignmentTab").then((mod) => mod.EventTeamAssignmentTab)
-);
-
-const EventLimitsTab = dynamic(() =>
- import("./tabs/limits/EventLimitsTab").then((mod) => mod.EventLimitsTab)
-);
-
-const EventAdvancedTab = dynamic(() =>
- import("./tabs/advanced/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
-);
-
-const EventInstantTab = dynamic(() =>
- import("./tabs/instant/EventInstantTab").then((mod) => mod.EventInstantTab)
-);
-
-const EventRecurringTab = dynamic(() =>
- import("./tabs/recurring/EventRecurringTab").then((mod) => mod.EventRecurringTab)
-);
-
-const EventAppsTab = dynamic(() => import("./tabs/apps/EventAppsTab").then((mod) => mod.EventAppsTab));
-
-const EventWorkflowsTab = dynamic(() => import("./tabs/workflows/EventWorkfowsTab"));
-
-const EventWebhooksTab = dynamic(() =>
- import("./tabs/webhooks/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
-);
-
-const EventAITab = dynamic(() => import("./tabs/ai/EventAITab").then((mod) => mod.EventAITab));
-
-const ManagedEventTypeDialog = dynamic(() => import("./dialogs/ManagedEventDialog"));
-
-const AssignmentWarningDialog = dynamic(() => import("./dialogs/AssignmentWarningDialog"));
-
-export type Host = {
- isFixed: boolean;
- userId: number;
- priority: number;
- weight: number;
- weightAdjustment: number;
-};
-
-export type CustomInputParsed = typeof customInputSchema._output;
-
-const querySchema = z.object({
- tabName: z
- .enum([
- "setup",
- "availability",
- "apps",
- "limits",
- "instant",
- "recurring",
- "team",
- "advanced",
- "workflows",
- "webhooks",
- "ai",
- ])
- .optional()
- .default("setup"),
-});
-
-export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
-export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
-export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"];
-export type EventTypeHosts = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["hosts"];
-
-export const EventTypeAppDir = (props: EventTypeSetupProps & { allActiveWorkflows?: Workflow[] }) => {
- const { t } = useLocale();
- const utils = trpc.useUtils();
- const telemetry = useTelemetry();
- const {
- data: { tabName },
- } = useTypedQuery(querySchema);
-
- const { data: eventTypeApps } = trpc.viewer.integrations.useQuery({
- extendsFeature: "EventType",
- teamId: props.eventType.team?.id || props.eventType.parent?.teamId,
- onlyInstalled: true,
- });
-
- const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
- const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState(false);
- const [pendingRoute, setPendingRoute] = useState("");
- const leaveWithoutAssigningHosts = useRef(false);
- const isTeamEventTypeDeleted = useRef(false);
- const [animationParentRef] = useAutoAnimate();
- const updateMutation = trpc.viewer.eventTypes.update.useMutation({
- onSuccess: async () => {
- const currentValues = formMethods.getValues();
-
- currentValues.children = currentValues.children.map((child) => ({
- ...child,
- created: true,
- }));
- currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
-
- // Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
- formMethods.reset(currentValues);
-
- showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
- },
- async onSettled() {
- await utils.viewer.eventTypes.get.invalidate();
- },
- onError: (err) => {
- let message = "";
- if (err instanceof HttpError) {
- const message = `${err.statusCode}: ${err.message}`;
- showToast(message, "error");
- }
-
- if (err.data?.code === "UNAUTHORIZED") {
- message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`;
- }
-
- if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
- message = `${err.data.code}: ${t(err.message)}`;
- }
-
- if (err.data?.code === "INTERNAL_SERVER_ERROR") {
- message = t("unexpected_error_try_again");
- }
-
- showToast(message ? t(message) : t(err.message), "error");
- },
- });
-
- const pathname = usePathname();
-
- const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
- startDate: new Date(eventType.periodStartDate || Date.now()),
- endDate: new Date(eventType.periodEndDate || Date.now()),
- });
-
- const bookingFields: Prisma.JsonObject = {};
-
- eventType.bookingFields.forEach(({ name }) => {
- bookingFields[name] = name;
- });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const defaultValues: any = useMemo(() => {
- return {
- title: eventType.title,
- id: eventType.id,
- slug: eventType.slug,
- afterEventBuffer: eventType.afterEventBuffer,
- beforeEventBuffer: eventType.beforeEventBuffer,
- eventName: eventType.eventName || "",
- scheduleName: eventType.scheduleName,
- periodDays: eventType.periodDays,
- requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
- seatsPerTimeSlot: eventType.seatsPerTimeSlot,
- seatsShowAttendees: eventType.seatsShowAttendees,
- seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
- lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
- locations: eventType.locations || [],
- destinationCalendar: eventType.destinationCalendar,
- recurringEvent: eventType.recurringEvent || null,
- isInstantEvent: eventType.isInstantEvent,
- instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
- description: eventType.description ?? undefined,
- schedule: eventType.schedule || undefined,
- instantMeetingSchedule: eventType.instantMeetingSchedule || undefined,
- bookingLimits: eventType.bookingLimits || undefined,
- onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
- durationLimits: eventType.durationLimits || undefined,
- length: eventType.length,
- hidden: eventType.hidden,
- hashedLink: eventType.hashedLink?.link || undefined,
- eventTypeColor: eventType.eventTypeColor || null,
- periodDates: {
- startDate: periodDates.startDate,
- endDate: periodDates.endDate,
- },
- hideCalendarNotes: eventType.hideCalendarNotes,
- offsetStart: eventType.offsetStart,
- bookingFields: eventType.bookingFields,
- periodType: eventType.periodType,
- periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
- schedulingType: eventType.schedulingType,
- requiresConfirmation: eventType.requiresConfirmation,
- requiresConfirmationWillBlockSlot: eventType.requiresConfirmationWillBlockSlot,
- slotInterval: eventType.slotInterval,
- minimumBookingNotice: eventType.minimumBookingNotice,
- metadata: eventType.metadata,
- hosts: eventType.hosts.sort((a, b) => sortHosts(a, b, eventType.isRRWeightsEnabled)),
- successRedirectUrl: eventType.successRedirectUrl || "",
- forwardParamsSuccessRedirect: eventType.forwardParamsSuccessRedirect,
- users: eventType.users,
- useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
- secondaryEmailId: eventType?.secondaryEmailId || -1,
- children: eventType.children.map((ch) => ({
- ...ch,
- created: true,
- owner: {
- ...ch.owner,
- eventTypeSlugs:
- eventType.team?.members
- .find((mem) => mem.user.id === ch.owner.id)
- ?.user.eventTypes.map((evTy) => evTy.slug)
- .filter((slug) => slug !== eventType.slug) ?? [],
- },
- })),
- seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
- rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
- assignAllTeamMembers: eventType.assignAllTeamMembers,
- aiPhoneCallConfig: {
- generalPrompt: eventType.aiPhoneCallConfig?.generalPrompt ?? DEFAULT_PROMPT_VALUE,
- enabled: eventType.aiPhoneCallConfig?.enabled,
- beginMessage: eventType.aiPhoneCallConfig?.beginMessage ?? DEFAULT_BEGIN_MESSAGE,
- guestName: eventType.aiPhoneCallConfig?.guestName,
- guestEmail: eventType.aiPhoneCallConfig?.guestEmail,
- guestCompany: eventType.aiPhoneCallConfig?.guestCompany,
- yourPhoneNumber: eventType.aiPhoneCallConfig?.yourPhoneNumber,
- numberToCall: eventType.aiPhoneCallConfig?.numberToCall,
- templateType: eventType.aiPhoneCallConfig?.templateType ?? "CUSTOM_TEMPLATE",
- schedulerName: eventType.aiPhoneCallConfig?.schedulerName,
- },
- isRRWeightsEnabled: eventType.isRRWeightsEnabled,
- };
- }, [eventType, periodDates]);
- const formMethods = useForm({
- defaultValues,
- resolver: zodResolver(
- z
- .object({
- // Length if string, is converted to a number or it can be a number
- // Make it optional because it's not submitted from all tabs of the page
- eventName: z
- .string()
- .superRefine((val, ctx) => {
- const validationResult = validateCustomEventName(val, bookingFields);
- if (validationResult !== true) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: t("invalid_event_name_variables", { item: validationResult }),
- });
- }
- })
- .optional(),
- length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
- offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
- bookingFields: eventTypeBookingFields,
- locations: locationsResolver(t),
- })
- // TODO: Add schema for other fields later.
- .passthrough()
- ),
- });
- const {
- formState: { isDirty: isFormDirty, dirtyFields },
- } = formMethods;
-
- const onDelete = () => {
- isTeamEventTypeDeleted.current = true;
- };
-
- useEffect(() => {
- const handleRouteChange = (url: string) => {
- const paths = url.split("/");
-
- // If the event-type is deleted, we can't show the empty assignment warning
- if (isTeamEventTypeDeleted.current) return;
-
- if (
- !!team &&
- !leaveWithoutAssigningHosts.current &&
- (url === "/event-types" || paths[1] !== "event-types") &&
- checkForEmptyAssignment({
- assignedUsers: eventType.children,
- hosts: eventType.hosts,
- assignAllTeamMembers: eventType.assignAllTeamMembers,
- isManagedEventType: eventType.schedulingType === SchedulingType.MANAGED,
- })
- ) {
- setIsOpenAssignmentWarnDialog(true);
- setPendingRoute(url);
- // The part where it is different from the original code in `EventType` component STARTS HERE
- throw new Error(`Aborted route change to ${url} because none was assigned to team event`);
- }
- };
- handleRouteChange(pathname || "");
-
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [pathname, eventType.hosts, eventType.children, eventType.assignAllTeamMembers]);
- // The part where it is different from the original code in `EventType` component ENDS HERE
-
- const appsMetadata = formMethods.getValues("metadata")?.apps;
- const availability = formMethods.watch("availability");
- let numberOfActiveApps = 0;
-
- if (appsMetadata) {
- numberOfActiveApps = Object.entries(appsMetadata).filter(
- ([appId, appData]) =>
- eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
- ).length;
- }
-
- const permalink = `${WEBSITE_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
- eventType.slug
- }`;
- const tabMap = {
- setup: (
-
- ),
- availability: ,
- team: ,
- limits: ,
- advanced: ,
- instant: ,
- recurring: ,
- apps: ,
- workflows: props.allActiveWorkflows ? (
-
- ) : (
- <>>
- ),
- webhooks: ,
- ai: ,
- } as const;
- const isObject = (value: T): boolean => {
- return value !== null && typeof value === "object" && !Array.isArray(value);
- };
-
- const isArray = (value: T): boolean => {
- return Array.isArray(value);
- };
-
- const isFieldDirty = (fieldName: keyof FormValues) => {
- // If the field itself is directly marked as dirty
- if (dirtyFields[fieldName] === true) {
- return true;
- }
-
- // Check if the field is an object or an array
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const fieldValue: any = getNestedField(dirtyFields, fieldName);
- if (isObject(fieldValue)) {
- for (const key in fieldValue) {
- if (fieldValue[key] === true) {
- return true;
- }
-
- if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
- const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
- // Recursive call for nested objects or arrays
- if (isFieldDirty(nestedFieldName)) {
- return true;
- }
- }
- }
- }
- if (isArray(fieldValue)) {
- for (const element of fieldValue) {
- // If element is an object, check each property of the object
- if (isObject(element)) {
- for (const key in element) {
- if (element[key] === true) {
- return true;
- }
-
- if (isObject(element[key]) || isArray(element[key])) {
- const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
- // Recursive call for nested objects or arrays within each element
- if (isFieldDirty(nestedFieldName)) {
- return true;
- }
- }
- }
- } else if (element === true) {
- return true;
- }
- }
- }
-
- return false;
- };
-
- const getNestedField = (obj: typeof dirtyFields, path: string) => {
- const keys = path.split(".");
- let current = obj;
-
- for (let i = 0; i < keys.length; i++) {
- // @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
- const currentKey = current[keys[i]];
- if (currentKey === undefined) return undefined;
- current = currentKey;
- }
-
- return current;
- };
-
- const getDirtyFields = (values: FormValues): Partial => {
- if (!isFormDirty) {
- return {};
- }
- const updatedFields: Partial = {};
- Object.keys(dirtyFields).forEach((key) => {
- const typedKey = key as keyof typeof dirtyFields;
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- updatedFields[typedKey] = undefined;
- const isDirty = isFieldDirty(typedKey);
- if (isDirty) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- updatedFields[typedKey] = values[typedKey];
- }
- });
- return updatedFields;
- };
-
- const handleSubmit = async (values: FormValues) => {
- const { children } = values;
- const dirtyValues = getDirtyFields(values);
- const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
- const {
- periodDates,
- periodCountCalendarDays,
- beforeEventBuffer,
- afterEventBuffer,
- seatsPerTimeSlot,
- seatsShowAttendees,
- seatsShowAvailabilityCount,
- bookingLimits,
- onlyShowFirstAvailableSlot,
- durationLimits,
- recurringEvent,
- eventTypeColor,
- locations,
- metadata,
- customInputs,
- assignAllTeamMembers,
- // We don't need to send send these values to the backend
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- seatsPerTimeSlotEnabled,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- minimumBookingNoticeInDurationType,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- bookerLayouts,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- multipleDurationEnabled,
- length,
- ...input
- } = dirtyValues;
- if (!Number(length)) throw new Error(t("event_setup_length_error"));
-
- if (bookingLimits) {
- const isValid = validateIntervalLimitOrder(bookingLimits);
- if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
- }
-
- if (durationLimits) {
- const isValid = validateIntervalLimitOrder(durationLimits);
- if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
- }
-
- const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
- if (layoutError) throw new Error(t(layoutError));
-
- if (metadata?.multipleDuration !== undefined) {
- if (metadata?.multipleDuration.length < 1) {
- throw new Error(t("event_setup_multiple_duration_error"));
- } else {
- // if length is unchanged, we skip this check
- if (length !== undefined) {
- if (!length && !metadata?.multipleDuration?.includes(length)) {
- //This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
- throw new Error(t("event_setup_multiple_duration_default_error"));
- }
- }
- }
- }
-
- // Prevent two payment apps to be enabled
- // Ok to cast type here because this metadata will be updated as the event type metadata
- if (checkForMultiplePaymentApps(metadata as z.infer))
- throw new Error(t("event_setup_multiple_payment_apps_error"));
-
- if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
- throw new Error(t("seats_and_no_show_fee_error"));
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { availability, users, scheduleName, ...rest } = input;
- const payload = {
- ...rest,
- length,
- locations,
- recurringEvent,
- periodStartDate: periodDates?.startDate,
- periodEndDate: periodDates?.endDate,
- periodCountCalendarDays,
- id: eventType.id,
- beforeEventBuffer,
- afterEventBuffer,
- bookingLimits,
- onlyShowFirstAvailableSlot,
- durationLimits,
- eventTypeColor,
- seatsPerTimeSlot,
- seatsShowAttendees,
- seatsShowAvailabilityCount,
- metadata,
- customInputs,
- children,
- assignAllTeamMembers,
- };
- // Filter out undefined values
- const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
- if (value !== undefined) {
- // @ts-expect-error Element implicitly has any type
- acc[key] = value;
- }
- return acc;
- }, {});
-
- if (dirtyFieldExists) {
- updateMutation.mutate({ ...filteredPayload, id: eventType.id });
- }
- };
-
- const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState([]);
- const slug = formMethods.watch("slug") ?? eventType.slug;
-
- // Optional prerender all tabs after 300 ms on mount
- useEffect(() => {
- const timeout = setTimeout(() => {
- const Components = [
- EventSetupTab,
- EventAvailabilityTab,
- EventTeamAssignmentTab,
- EventLimitsTab,
- EventAdvancedTab,
- EventInstantTab,
- EventRecurringTab,
- EventAppsTab,
- EventWorkflowsTab,
- EventWebhooksTab,
- ];
-
- Components.forEach((C) => {
- // @ts-expect-error Property 'render' does not exist on type 'ComponentClass
- C.render.preload();
- });
- }, 300);
-
- return () => {
- clearTimeout(timeout);
- };
- }, []);
- return (
- <>
- webhook.active).length}
- team={team}
- availability={availability}
- isUpdateMutationLoading={updateMutation.isPending}
- formMethods={formMethods}
- // disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
- disableBorder={true}
- currentUserMembership={currentUserMembership}
- bookerUrl={eventType.bookerUrl}
- isUserOrganizationAdmin={props.isUserOrganizationAdmin}
- onDelete={onDelete}>
-
-
- {slugExistsChildrenDialogOpen.length ? (
- {
- setSlugExistsChildrenDialogOpen([]);
- }}
- slug={slug}
- onConfirm={(e: { preventDefault: () => void }) => {
- e.preventDefault();
- handleSubmit(formMethods.getValues());
- telemetry.event(telemetryEventTypes.slugReplacementAction);
- setSlugExistsChildrenDialogOpen([]);
- }}
- />
- ) : null}
-
- >
- );
-};
diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts
index 8c4031ef0e8c12..4dbe945348483b 100644
--- a/packages/features/eventtypes/lib/types.ts
+++ b/packages/features/eventtypes/lib/types.ts
@@ -7,7 +7,7 @@ import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/pris
import type { customInputSchema } from "@calcom/prisma/zod-utils";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { eventTypeColor } from "@calcom/prisma/zod-utils";
-import type { RouterOutputs } from "@calcom/trpc/react";
+import type { RouterOutputs, RouterInputs } from "@calcom/trpc/react";
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
export type CustomInputParsed = typeof customInputSchema._output;
@@ -20,6 +20,7 @@ export type AvailabilityOption = {
};
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
+export type EventTypeApps = RouterOutputs["viewer"]["integrations"];
export type Host = {
isFixed: boolean;
userId: number;
@@ -137,3 +138,17 @@ export type LocationFormValues = Pick;
+
+export const useEventTypeForm = ({
+ eventType,
+ onSubmit,
+}: {
+ eventType: EventTypeSetupProps["eventType"];
+ onSubmit: (data: EventTypeUpdateInput) => void;
+}) => {
+ const { t } = useLocale();
+ const bookingFields: Record = {};
+ const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
+ startDate: new Date(eventType.periodStartDate || Date.now()),
+ endDate: new Date(eventType.periodEndDate || Date.now()),
+ });
+ eventType.bookingFields.forEach(({ name }: { name: string }) => {
+ bookingFields[name] = name;
+ });
+
+ // this is a nightmare to type, will do in follow up PR
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const defaultValues: any = useMemo(() => {
+ return {
+ title: eventType.title,
+ id: eventType.id,
+ slug: eventType.slug,
+ afterEventBuffer: eventType.afterEventBuffer,
+ beforeEventBuffer: eventType.beforeEventBuffer,
+ eventName: eventType.eventName || "",
+ scheduleName: eventType.scheduleName,
+ periodDays: eventType.periodDays,
+ requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
+ seatsPerTimeSlot: eventType.seatsPerTimeSlot,
+ seatsShowAttendees: eventType.seatsShowAttendees,
+ seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
+ lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
+ locations: eventType.locations || [],
+ destinationCalendar: eventType.destinationCalendar,
+ recurringEvent: eventType.recurringEvent || null,
+ isInstantEvent: eventType.isInstantEvent,
+ instantMeetingExpiryTimeOffsetInSeconds: eventType.instantMeetingExpiryTimeOffsetInSeconds,
+ description: eventType.description ?? undefined,
+ schedule: eventType.schedule || undefined,
+ instantMeetingSchedule: eventType.instantMeetingSchedule || undefined,
+ bookingLimits: eventType.bookingLimits || undefined,
+ onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
+ durationLimits: eventType.durationLimits || undefined,
+ length: eventType.length,
+ hidden: eventType.hidden,
+ hashedLink: eventType.hashedLink?.link || undefined,
+ eventTypeColor: eventType.eventTypeColor || null,
+ periodDates: {
+ startDate: periodDates.startDate,
+ endDate: periodDates.endDate,
+ },
+ hideCalendarNotes: eventType.hideCalendarNotes,
+ offsetStart: eventType.offsetStart,
+ bookingFields: eventType.bookingFields,
+ periodType: eventType.periodType,
+ periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
+ schedulingType: eventType.schedulingType,
+ requiresConfirmation: eventType.requiresConfirmation,
+ requiresConfirmationWillBlockSlot: eventType.requiresConfirmationWillBlockSlot,
+ slotInterval: eventType.slotInterval,
+ minimumBookingNotice: eventType.minimumBookingNotice,
+ metadata: eventType.metadata,
+ hosts: eventType.hosts.sort((a, b) => sortHosts(a, b, eventType.isRRWeightsEnabled)),
+ successRedirectUrl: eventType.successRedirectUrl || "",
+ forwardParamsSuccessRedirect: eventType.forwardParamsSuccessRedirect,
+ users: eventType.users,
+ useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
+ secondaryEmailId: eventType?.secondaryEmailId || -1,
+ children: eventType.children.map((ch) => ({
+ ...ch,
+ created: true,
+ owner: {
+ ...ch.owner,
+ eventTypeSlugs:
+ eventType.team?.members
+ .find((mem) => mem.user.id === ch.owner.id)
+ ?.user.eventTypes.map((evTy) => evTy.slug)
+ .filter((slug) => slug !== eventType.slug) ?? [],
+ },
+ })),
+ seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
+ rescheduleWithSameRoundRobinHost: eventType.rescheduleWithSameRoundRobinHost,
+ assignAllTeamMembers: eventType.assignAllTeamMembers,
+ aiPhoneCallConfig: {
+ generalPrompt: eventType.aiPhoneCallConfig?.generalPrompt ?? DEFAULT_PROMPT_VALUE,
+ enabled: eventType.aiPhoneCallConfig?.enabled,
+ beginMessage: eventType.aiPhoneCallConfig?.beginMessage ?? DEFAULT_BEGIN_MESSAGE,
+ guestName: eventType.aiPhoneCallConfig?.guestName,
+ guestEmail: eventType.aiPhoneCallConfig?.guestEmail,
+ guestCompany: eventType.aiPhoneCallConfig?.guestCompany,
+ yourPhoneNumber: eventType.aiPhoneCallConfig?.yourPhoneNumber,
+ numberToCall: eventType.aiPhoneCallConfig?.numberToCall,
+ templateType: eventType.aiPhoneCallConfig?.templateType ?? "CUSTOM_TEMPLATE",
+ schedulerName: eventType.aiPhoneCallConfig?.schedulerName,
+ },
+ isRRWeightsEnabled: eventType.isRRWeightsEnabled,
+ };
+ }, [eventType, periodDates]);
+
+ const form = useForm({
+ defaultValues,
+ resolver: zodResolver(
+ z
+ .object({
+ // Length if string, is converted to a number or it can be a number
+ // Make it optional because it's not submitted from all tabs of the page
+ eventName: z
+ .string()
+ .superRefine((val, ctx) => {
+ const validationResult = validateCustomEventName(val, bookingFields);
+ if (validationResult !== true) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: t("invalid_event_name_variables", { item: validationResult }),
+ });
+ }
+ })
+ .optional(),
+ length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
+ offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
+ bookingFields: eventTypeBookingFieldsSchema,
+ locations: locationsResolver(t),
+ })
+ // TODO: Add schema for other fields later.
+ .passthrough()
+ ),
+ });
+
+ const {
+ formState: { isDirty: isFormDirty, dirtyFields },
+ } = form;
+
+ const isObject = (value: T): boolean => {
+ return value !== null && typeof value === "object" && !Array.isArray(value);
+ };
+
+ const isArray = (value: T): boolean => {
+ return Array.isArray(value);
+ };
+
+ const getNestedField = (obj: typeof dirtyFields, path: string) => {
+ const keys = path.split(".");
+ let current = obj;
+
+ for (let i = 0; i < keys.length; i++) {
+ // @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
+ const currentKey = current[keys[i]];
+ if (currentKey === undefined) return undefined;
+ current = currentKey;
+ }
+
+ return current;
+ };
+
+ const getDirtyFields = (values: FormValues): Partial => {
+ if (!isFormDirty) {
+ return {};
+ }
+
+ const isFieldDirty = (fieldName: keyof FormValues) => {
+ // If the field itself is directly marked as dirty
+ if (dirtyFields[fieldName] === true) {
+ return true;
+ }
+
+ // Check if the field is an object or an array
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const fieldValue: any = getNestedField(dirtyFields, fieldName);
+ if (isObject(fieldValue)) {
+ for (const key in fieldValue) {
+ if (fieldValue[key] === true) {
+ return true;
+ }
+
+ if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
+ const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
+ // Recursive call for nested objects or arrays
+ if (isFieldDirty(nestedFieldName)) {
+ return true;
+ }
+ }
+ }
+ }
+ if (isArray(fieldValue)) {
+ for (const element of fieldValue) {
+ // If element is an object, check each property of the object
+ if (isObject(element)) {
+ for (const key in element) {
+ if (element[key] === true) {
+ return true;
+ }
+
+ if (isObject(element[key]) || isArray(element[key])) {
+ const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
+ // Recursive call for nested objects or arrays within each element
+ if (isFieldDirty(nestedFieldName)) {
+ return true;
+ }
+ }
+ }
+ } else if (element === true) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ const updatedFields: Partial = {};
+ Object.keys(dirtyFields).forEach((key) => {
+ const typedKey = key as keyof typeof dirtyFields;
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ updatedFields[typedKey] = undefined;
+ const isDirty = isFieldDirty(typedKey);
+ if (isDirty) {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ updatedFields[typedKey] = values[typedKey];
+ }
+ });
+ return updatedFields;
+ };
+
+ const handleSubmit = async (values: FormValues) => {
+ const { children } = values;
+ const dirtyValues = getDirtyFields(values);
+ const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
+ const {
+ periodDates,
+ periodCountCalendarDays,
+ beforeEventBuffer,
+ afterEventBuffer,
+ seatsPerTimeSlot,
+ seatsShowAttendees,
+ seatsShowAvailabilityCount,
+ bookingLimits,
+ onlyShowFirstAvailableSlot,
+ durationLimits,
+ recurringEvent,
+ eventTypeColor,
+ locations,
+ metadata,
+ customInputs,
+ assignAllTeamMembers,
+ // We don't need to send send these values to the backend
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ seatsPerTimeSlotEnabled,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ minimumBookingNoticeInDurationType,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ bookerLayouts,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ multipleDurationEnabled,
+ length,
+ ...input
+ } = dirtyValues;
+ if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
+
+ if (bookingLimits) {
+ const isValid = validateIntervalLimitOrder(bookingLimits);
+ if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
+ }
+
+ if (durationLimits) {
+ const isValid = validateIntervalLimitOrder(durationLimits);
+ if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
+ }
+
+ const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
+ if (layoutError) throw new Error(t(layoutError));
+
+ if (metadata?.multipleDuration !== undefined) {
+ if (metadata?.multipleDuration.length < 1) {
+ throw new Error(t("event_setup_multiple_duration_error"));
+ } else {
+ // if length is unchanged, we skip this check
+ if (length !== undefined) {
+ if (!length && !metadata?.multipleDuration?.includes(length)) {
+ //This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
+ throw new Error(t("event_setup_multiple_duration_default_error"));
+ }
+ }
+ }
+ }
+
+ // Prevent two payment apps to be enabled
+ // Ok to cast type here because this metadata will be updated as the event type metadata
+ if (checkForMultiplePaymentApps(metadata as z.infer))
+ throw new Error(t("event_setup_multiple_payment_apps_error"));
+
+ if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
+ throw new Error(t("seats_and_no_show_fee_error"));
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { availability, users, scheduleName, ...rest } = input;
+ const payload = {
+ ...rest,
+ length,
+ locations,
+ recurringEvent,
+ periodStartDate: periodDates?.startDate,
+ periodEndDate: periodDates?.endDate,
+ periodCountCalendarDays,
+ id: eventType.id,
+ beforeEventBuffer,
+ afterEventBuffer,
+ bookingLimits,
+ onlyShowFirstAvailableSlot,
+ durationLimits,
+ eventTypeColor,
+ seatsPerTimeSlot,
+ seatsShowAttendees,
+ seatsShowAvailabilityCount,
+ metadata,
+ customInputs,
+ children,
+ assignAllTeamMembers,
+ aiPhoneCallConfig: rest.aiPhoneCallConfig
+ ? { ...rest.aiPhoneCallConfig, templateType: rest.aiPhoneCallConfig.templateType as TemplateType }
+ : undefined,
+ } satisfies EventTypeUpdateInput;
+ // Filter out undefined values
+ const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
+ if (value !== undefined) {
+ // @ts-expect-error Element implicitly has any type
+ acc[key] = value;
+ }
+ return acc;
+ }, {}) as EventTypeUpdateInput;
+
+ if (dirtyFieldExists) {
+ onSubmit({ ...filteredPayload, id: eventType.id });
+ }
+ };
+
+ return { form, handleSubmit };
+};
diff --git a/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts b/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts
new file mode 100644
index 00000000000000..b1256b48fca66d
--- /dev/null
+++ b/packages/platform/atoms/event-types/hooks/useHandleRouteChange.ts
@@ -0,0 +1,62 @@
+// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
+import { useEffect } from "react";
+
+import type {
+ EventTypeAssignedUsers,
+ EventTypeHosts,
+} from "@calcom/features/eventtypes/components/EventType";
+import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment";
+
+export const useHandleRouteChange = ({
+ isTeamEventTypeDeleted,
+ isleavingWithoutAssigningHosts,
+ isTeamEventType,
+ assignedUsers,
+ hosts,
+ assignAllTeamMembers,
+ isManagedEventType,
+ onError,
+ onStart,
+ onEnd,
+ watchTrigger,
+}: {
+ isTeamEventTypeDeleted: boolean;
+ isleavingWithoutAssigningHosts: boolean;
+ isTeamEventType: boolean;
+ assignedUsers: EventTypeAssignedUsers;
+ hosts: EventTypeHosts;
+ assignAllTeamMembers: boolean;
+ isManagedEventType: boolean;
+ watchTrigger: unknown;
+ onError?: (url: string) => void;
+ onStart?: (handleRouteChange: (url: string) => void) => void;
+ onEnd?: (handleRouteChange: (url: string) => void) => void;
+}) => {
+ useEffect(() => {
+ const handleRouteChange = (url: string) => {
+ const paths = url.split("/");
+
+ // If the event-type is deleted, we can't show the empty assignment warning
+ if (isTeamEventTypeDeleted) return;
+
+ if (
+ !!isTeamEventType &&
+ !isleavingWithoutAssigningHosts &&
+ (url === "/event-types" || paths[1] !== "event-types") &&
+ checkForEmptyAssignment({
+ assignedUsers,
+ hosts,
+ assignAllTeamMembers,
+ isManagedEventType,
+ })
+ ) {
+ onError?.(url);
+ }
+ };
+ onStart?.(handleRouteChange);
+ return () => {
+ onEnd?.(handleRouteChange);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [watchTrigger, hosts, assignedUsers, assignAllTeamMembers, onError, onStart, onEnd]);
+};
diff --git a/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx
new file mode 100644
index 00000000000000..8c5f6ca961de7b
--- /dev/null
+++ b/packages/platform/atoms/event-types/wrappers/EventTypeWebWrapper.tsx
@@ -0,0 +1,349 @@
+"use client";
+
+import dynamic from "next/dynamic";
+import { usePathname } from "next/navigation";
+// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
+import { useRouter as usePageRouter } from "next/router";
+// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
+import type { NextRouter as NextPageRouter } from "next/router";
+import React, { useEffect, useRef, useState } from "react";
+
+import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
+import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType";
+import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
+import { WEBSITE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HttpError } from "@calcom/lib/http-error";
+import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { SchedulingType } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc/react";
+import { showToast } from "@calcom/ui";
+
+import { useEventTypeForm } from "../hooks/useEventTypeForm";
+import { useHandleRouteChange } from "../hooks/useHandleRouteChange";
+
+const ManagedEventTypeDialog = dynamic(
+ () => import("@calcom/features/eventtypes/components/dialogs/ManagedEventDialog")
+);
+
+const AssignmentWarningDialog = dynamic(
+ () => import("@calcom/features/eventtypes/components/dialogs/AssignmentWarningDialog")
+);
+
+const EventSetupTab = dynamic(() =>
+ // import web wrapper when it's ready
+ import("@calcom/features/eventtypes/components/tabs/setup/EventSetupTab").then((mod) => mod.EventSetupTab)
+);
+
+const EventAvailabilityTab = dynamic(() =>
+ // import web wrapper when it's ready
+ import("@calcom/features/eventtypes/components/tabs/availability/EventAvailabilityTab").then(
+ (mod) => mod.EventAvailabilityTab
+ )
+);
+
+const EventTeamAssignmentTab = dynamic(() =>
+ import("@calcom/features/eventtypes/components/tabs/assignment/EventTeamAssignmentTab").then(
+ (mod) => mod.EventTeamAssignmentTab
+ )
+);
+
+const EventLimitsTab = dynamic(() =>
+ // import web wrapper when it's ready
+ import("@calcom/features/eventtypes/components/tabs/limits/EventLimitsTab").then(
+ (mod) => mod.EventLimitsTab
+ )
+);
+
+const EventAdvancedTab = dynamic(() =>
+ // import web wrapper when it's ready
+ import("@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab").then(
+ (mod) => mod.EventAdvancedTab
+ )
+);
+
+const EventInstantTab = dynamic(() =>
+ import("@calcom/features/eventtypes/components/tabs/instant/EventInstantTab").then(
+ (mod) => mod.EventInstantTab
+ )
+);
+
+const EventRecurringTab = dynamic(() =>
+ // import web wrapper when it's ready
+ import("@calcom/features/eventtypes/components/tabs/recurring/EventRecurringTab").then(
+ (mod) => mod.EventRecurringTab
+ )
+);
+
+const EventAppsTab = dynamic(() =>
+ import("@calcom/features/eventtypes/components/tabs/apps/EventAppsTab").then((mod) => mod.EventAppsTab)
+);
+
+const EventWorkflowsTab = dynamic(
+ () => import("@calcom/features/eventtypes/components/tabs/workflows/EventWorkfowsTab")
+);
+
+const EventWebhooksTab = dynamic(() =>
+ import("@calcom/features/eventtypes/components/tabs/webhooks/EventWebhooksTab").then(
+ (mod) => mod.EventWebhooksTab
+ )
+);
+
+const EventAITab = dynamic(() =>
+ import("@calcom/features/eventtypes/components/tabs/ai/EventAITab").then((mod) => mod.EventAITab)
+);
+
+export type EventTypeWebWrapperProps = {
+ id: number;
+ isAppDir?: boolean;
+};
+
+// discriminative factor: isAppDir
+type EventTypeAppComponentProp = {
+ id: number;
+ isAppDir: true;
+ pathname: string;
+ pageRouter: null;
+};
+
+// discriminative factor: isAppDir
+type EventTypePageComponentProp = {
+ id: number;
+ isAppDir: false;
+ pageRouter: NextPageRouter;
+ pathname: null;
+};
+
+type EventTypeAppPageComponentProp = EventTypeAppComponentProp | EventTypePageComponentProp;
+
+export const EventTypeWebWrapper = ({ id, isAppDir }: EventTypeWebWrapperProps & { isAppDir?: boolean }) => {
+ const { data: eventTypeQueryData } = trpc.viewer.eventTypes.get.useQuery({ id });
+
+ if (!eventTypeQueryData) return null;
+
+ return isAppDir ? (
+
+ ) : (
+
+ );
+};
+
+const EventTypePageWrapper = ({ id, ...rest }: EventTypeSetupProps & { id: number }) => {
+ const router = usePageRouter();
+ return ;
+};
+
+const EventTypeAppWrapper = ({ id, ...rest }: EventTypeSetupProps & { id: number }) => {
+ const pathname = usePathname();
+ return ;
+};
+
+const EventTypeWeb = ({
+ id,
+ isAppDir,
+ pageRouter,
+ pathname,
+ ...rest
+}: EventTypeSetupProps & EventTypeAppPageComponentProp) => {
+ const { t } = useLocale();
+ const utils = trpc.useUtils();
+
+ const isTeamEventTypeDeleted = useRef(false);
+ const leaveWithoutAssigningHosts = useRef(false);
+ const telemetry = useTelemetry();
+ const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState(false);
+ const [pendingRoute, setPendingRoute] = useState("");
+ const { eventType, locationOptions, team, teamMembers, destinationCalendar } = rest;
+ const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState([]);
+ const { data: eventTypeApps } = trpc.viewer.integrations.useQuery({
+ extendsFeature: "EventType",
+ teamId: eventType.team?.id || eventType.parent?.teamId,
+ onlyInstalled: true,
+ });
+ const updateMutation = trpc.viewer.eventTypes.update.useMutation({
+ onSuccess: async () => {
+ const currentValues = form.getValues();
+
+ currentValues.children = currentValues.children.map((child) => ({
+ ...child,
+ created: true,
+ }));
+ currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
+
+ // Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
+ form.reset(currentValues);
+
+ showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
+ },
+ async onSettled() {
+ await utils.viewer.eventTypes.get.invalidate();
+ await utils.viewer.eventTypes.getByViewer.invalidate();
+ },
+ onError: (err) => {
+ let message = "";
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+
+ if (err.data?.code === "UNAUTHORIZED") {
+ message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`;
+ }
+
+ if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
+ message = `${err.data.code}: ${t(err.message)}`;
+ }
+
+ if (err.data?.code === "INTERNAL_SERVER_ERROR") {
+ message = t("unexpected_error_try_again");
+ }
+
+ showToast(message ? t(message) : t(err.message), "error");
+ },
+ });
+
+ const { form, handleSubmit } = useEventTypeForm({ eventType, onSubmit: updateMutation.mutate });
+ const slug = form.watch("slug") ?? eventType.slug;
+
+ const { data: allActiveWorkflows } = trpc.viewer.workflows.getAllActiveWorkflows.useQuery({
+ eventType: {
+ id,
+ teamId: eventType.teamId,
+ userId: eventType.userId,
+ parent: eventType.parent,
+ metadata: eventType.metadata,
+ },
+ });
+
+ const permalink = `${WEBSITE_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
+ eventType.slug
+ }`;
+ const tabMap = {
+ setup: (
+
+ ),
+ availability: ,
+ team: ,
+ limits: ,
+ advanced: ,
+ instant: ,
+ recurring: ,
+ apps: ,
+ workflows: allActiveWorkflows ? (
+
+ ) : (
+ <>>
+ ),
+ webhooks: ,
+ ai: ,
+ } as const;
+
+ useHandleRouteChange({
+ watchTrigger: isAppDir ? pageRouter : pathname,
+ isTeamEventTypeDeleted: isTeamEventTypeDeleted.current,
+ isleavingWithoutAssigningHosts: leaveWithoutAssigningHosts.current,
+ isTeamEventType: !!team,
+ assignedUsers: eventType.children,
+ hosts: eventType.hosts,
+ assignAllTeamMembers: eventType.assignAllTeamMembers,
+ isManagedEventType: eventType.schedulingType === SchedulingType.MANAGED,
+ onError: (url) => {
+ setIsOpenAssignmentWarnDialog(true);
+ setPendingRoute(url);
+ if (!isAppDir) {
+ pageRouter.events.emit(
+ "routeChangeError",
+ new Error(`Aborted route change to ${url} because none was assigned to team event`)
+ );
+ throw "Aborted";
+ }
+
+ if (isAppDir) throw new Error(`Aborted route change to ${url} because none was assigned to team event`);
+ },
+ onStart: (handleRouteChange) => {
+ !isAppDir && pageRouter.events.on("routeChangeStart", handleRouteChange);
+ isAppDir && handleRouteChange(pathname || "");
+ },
+ onEnd: (handleRouteChange) => {
+ !isAppDir && pageRouter.events.off("routeChangeStart", handleRouteChange);
+ },
+ });
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ const Components = [
+ EventSetupTab,
+ EventAvailabilityTab,
+ EventTeamAssignmentTab,
+ EventLimitsTab,
+ EventAdvancedTab,
+ EventInstantTab,
+ EventRecurringTab,
+ EventAppsTab,
+ EventWorkflowsTab,
+ EventWebhooksTab,
+ ];
+
+ Components.forEach((C) => {
+ // how to preload with app dir?
+ // @ts-expect-error Property 'render' does not exist on type 'ComponentClass
+ C.render?.preload();
+ });
+ }, 300);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, []);
+
+ const onDelete = () => {
+ isTeamEventTypeDeleted.current = true;
+ };
+ const onConflict = (conflicts: ChildrenEventType[]) => {
+ setSlugExistsChildrenDialogOpen(conflicts);
+ };
+ return (
+
+ <>
+ {slugExistsChildrenDialogOpen.length ? (
+ {
+ setSlugExistsChildrenDialogOpen([]);
+ }}
+ slug={slug}
+ onConfirm={(e: { preventDefault: () => void }) => {
+ e.preventDefault();
+ handleSubmit(form.getValues());
+ telemetry.event(telemetryEventTypes.slugReplacementAction);
+ setSlugExistsChildrenDialogOpen([]);
+ }}
+ />
+ ) : null}
+
+ >
+
+ );
+};
diff --git a/packages/platform/atoms/monorepo.ts b/packages/platform/atoms/monorepo.ts
index 98c961139113c0..6b75d91aa324b6 100644
--- a/packages/platform/atoms/monorepo.ts
+++ b/packages/platform/atoms/monorepo.ts
@@ -9,3 +9,4 @@ export { Timezone } from "./timezone";
export { SelectedCalendarsSettingsWebWrapper } from "./selected-calendars/wrappers/SelectedCalendarsSettingsWebWrapper";
export { DestinationCalendarSettingsWebWrapper } from "./destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper";
export * from "./availability";
+export { EventTypeWebWrapper as EventType } from "./event-types/wrappers/EventTypeWebWrapper";