chore: reorganize event type files (#16393)
* chore: reorganize event type files * fixup! chore: reorganize event type files * fixup! Merge branch 'main' into refactor-event-types-components-folder
This commit is contained in:
1 parent
f3462776e8
commit
40fee7af51
43 files changed
+1048
-963
No files matched your search
@@ -9,6 +9,7 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetaDataSchema, eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
@@ -17,8 +18,6 @@ import { Button, Form, Icon } from "@calcom/ui";
|
||||
import EventTypeAppSettingsWrapper from "@components/apps/installation/EventTypeAppSettingsWrapper";
|
||||
import EventTypeConferencingAppSettings from "@components/apps/installation/EventTypeConferencingAppSettings";
|
||||
|
||||
import { locationsResolver } from "~/event-types/views/event-types-single-view";
|
||||
|
||||
export type TFormType = {
|
||||
id: number;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
|
||||
@@ -3,8 +3,7 @@ import { useEffect, type FC } from "react";
|
||||
|
||||
import { EventTypeAppSettings } from "@calcom/app-store/_components/EventTypeAppSettingsInterface";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
|
||||
import useAppsData from "@lib/hooks/useAppsData";
|
||||
import useAppsData from "@calcom/lib/hooks/useAppsData";
|
||||
|
||||
import type { ConfigureStepCardProps } from "@components/apps/installation/ConfigureStepCard";
|
||||
|
||||
|
||||
@@ -3,7 +3,11 @@ import { useMemo } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
|
||||
import type { TLocationOptions } from "@calcom/features/eventtypes/components/Locations";
|
||||
import type { TEventTypeLocation } from "@calcom/features/eventtypes/components/Locations";
|
||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -13,10 +17,6 @@ import { Skeleton, Label } from "@calcom/ui";
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import type { TFormType } from "@components/apps/installation/ConfigureStepCard";
|
||||
import type { TLocationOptions } from "@components/eventtype/Locations";
|
||||
import type { TEventTypeLocation } from "@components/eventtype/Locations";
|
||||
import Locations from "@components/eventtype/Locations";
|
||||
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
|
||||
|
||||
const LocationsWrapper = ({
|
||||
eventType,
|
||||
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
LocationType,
|
||||
OrganizerDefaultConferencingAppType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import CheckboxField from "@calcom/features/form/components/CheckboxField";
|
||||
import type { LocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||
import LocationSelect from "@calcom/features/form/components/LocationSelect";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -22,10 +25,6 @@ import { Button, Icon, Input, Dialog, DialogContent, DialogFooter, Form, PhoneIn
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import type { LocationOption } from "@components/ui/form/LocationSelect";
|
||||
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
interface ISetLocationDialog {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@calcom/features/ee/workflows/components/EventWorkflowsTab";
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from "react";
|
||||
import type { Props } from "react-select";
|
||||
|
||||
import Select from "@calcom/features/form/components/Select";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type CheckedSelectOption = {
|
||||
avatar: string;
|
||||
label: string;
|
||||
|
||||
@@ -14,6 +14,9 @@ import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/emb
|
||||
import { EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import SkeletonLoader, {
|
||||
InfiniteSkeletonLoader,
|
||||
} from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -66,8 +69,6 @@ import type { AppProps } from "@lib/app-providers";
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import SkeletonLoader, { InfiniteSkeletonLoader } from "@components/eventtype/SkeletonLoader";
|
||||
|
||||
type GetUserEventGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"];
|
||||
type GetEventTypesFromGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getEventTypesFromGroup"];
|
||||
|
||||
|
||||
@@ -1,835 +1,16 @@
|
||||
"use client";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import dynamic from "next/dynamic";
|
||||
// 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 { EventType } from "@calcom/features/eventtypes/components/EventType";
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
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 { 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";
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { checkForEmptyAssignment } from "@lib/checkForEmptyAssignment";
|
||||
|
||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||
|
||||
import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
|
||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
|
||||
const EventSetupTab = dynamic(() =>
|
||||
import("@components/eventtype/EventSetupTab").then((mod) => mod.EventSetupTab)
|
||||
);
|
||||
|
||||
const EventAvailabilityTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAvailabilityTab").then((mod) => mod.EventAvailabilityTab)
|
||||
);
|
||||
|
||||
const EventTeamTab = dynamic(() =>
|
||||
import("@components/eventtype/EventTeamTab").then((mod) => mod.EventTeamTab)
|
||||
);
|
||||
|
||||
const EventLimitsTab = dynamic(() =>
|
||||
import("@components/eventtype/EventLimitsTab").then((mod) => mod.EventLimitsTab)
|
||||
);
|
||||
|
||||
const EventAdvancedTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
|
||||
);
|
||||
|
||||
const EventInstantTab = dynamic(() =>
|
||||
import("@components/eventtype/EventInstantTab").then((mod) => mod.EventInstantTab)
|
||||
);
|
||||
|
||||
const EventRecurringTab = dynamic(() =>
|
||||
import("@components/eventtype/EventRecurringTab").then((mod) => mod.EventRecurringTab)
|
||||
);
|
||||
|
||||
const EventAppsTab = dynamic(() =>
|
||||
import("@components/eventtype/EventAppsTab").then((mod) => mod.EventAppsTab)
|
||||
);
|
||||
|
||||
const EventWorkflowsTab = dynamic(() => import("@components/eventtype/EventWorkfowsTab"));
|
||||
|
||||
const EventWebhooksTab = dynamic(() =>
|
||||
import("@components/eventtype/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
|
||||
);
|
||||
|
||||
const EventAITab = dynamic(() => import("@components/eventtype/EventAITab").then((mod) => mod.EventAITab));
|
||||
|
||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||
|
||||
const AssignmentWarningDialog = dynamic(() => import("@components/eventtype/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 locationsResolver = (t: TFunction) => {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
type: z.string(),
|
||||
address: z.string().optional(),
|
||||
link: z.string().url().optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
hostPhoneNumber: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
displayLocationPublicly: z.boolean().optional(),
|
||||
credentialId: z.number().optional(),
|
||||
teamName: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.superRefine((val, ctx) => {
|
||||
if (val?.link) {
|
||||
const link = val.link;
|
||||
const eventLocationType = getEventLocationType(val.type);
|
||||
if (
|
||||
eventLocationType &&
|
||||
!eventLocationType.default &&
|
||||
eventLocationType.linkType === "static" &&
|
||||
eventLocationType.urlRegExp
|
||||
) {
|
||||
const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(link).success;
|
||||
|
||||
if (!valid) {
|
||||
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||
message: t("invalid_url_error_message", {
|
||||
label: eventLocationType.label,
|
||||
sampleUrl: sampleUrl ?? "https://cal.com",
|
||||
}),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = z.string().url().optional().safeParse(link).success;
|
||||
|
||||
if (!valid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||
message: `Invalid URL`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
})
|
||||
)
|
||||
.optional();
|
||||
};
|
||||
|
||||
const EventTypePage = (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<boolean>(false);
|
||||
const [pendingRoute, setPendingRoute] = useState("");
|
||||
const leaveWithoutAssigningHosts = useRef(false);
|
||||
const isTeamEventTypeDeleted = useRef(false);
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
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 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,
|
||||
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<FormValues>({
|
||||
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");
|
||||
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: (
|
||||
<EventSetupTab
|
||||
eventType={eventType}
|
||||
locationOptions={locationOptions}
|
||||
team={team}
|
||||
teamMembers={teamMembers}
|
||||
destinationCalendar={destinationCalendar}
|
||||
/>
|
||||
),
|
||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
apps: <EventAppsTab eventType={{ ...eventType, URL: permalink }} />,
|
||||
workflows: props.allActiveWorkflows ? (
|
||||
<EventWorkflowsTab eventType={eventType} workflows={props.allActiveWorkflows} />
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
ai: <EventAITab eventType={eventType} isTeamEvent={!!team} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(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<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
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<typeof EventTypeMetaDataSchema>))
|
||||
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<ChildrenEventType[]>([]);
|
||||
const slug = formMethods.watch("slug") ?? eventType.slug;
|
||||
|
||||
// Optional prerender all tabs after 300 ms on mount
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
const Components = [
|
||||
EventSetupTab,
|
||||
EventAvailabilityTab,
|
||||
EventTeamTab,
|
||||
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 (
|
||||
<>
|
||||
<EventTypeSingleLayout
|
||||
enabledAppsNumber={numberOfActiveApps}
|
||||
installedAppsNumber={eventTypeApps?.items.length || 0}
|
||||
enabledWorkflowsNumber={props.allActiveWorkflows ? props.allActiveWorkflows.length : 0}
|
||||
eventType={eventType}
|
||||
activeWebhooksNumber={eventType.webhooks.filter((webhook) => 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}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
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,
|
||||
// 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
|
||||
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 !== 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<typeof EventTypeMetaDataSchema>))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
eventTypeColor,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
};
|
||||
// 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, hashedLink: values.hashedLink });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
</EventTypeSingleLayout>
|
||||
{slugExistsChildrenDialogOpen.length ? (
|
||||
<ManagedEventTypeDialog
|
||||
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
||||
isPending={formMethods.formState.isSubmitting}
|
||||
onOpenChange={() => {
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}
|
||||
slug={slug}
|
||||
onConfirm={(e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(formMethods.getValues());
|
||||
telemetry.event(telemetryEventTypes.slugReplacementAction);
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<AssignmentWarningDialog
|
||||
isOpenAssignmentWarnDialog={isOpenAssignmentWarnDialog}
|
||||
setIsOpenAssignmentWarnDialog={setIsOpenAssignmentWarnDialog}
|
||||
pendingRoute={pendingRoute}
|
||||
leaveWithoutAssigningHosts={leaveWithoutAssigningHosts}
|
||||
id={eventType.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const EventTypePageWrapper: React.FC<PageProps> & {
|
||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
getLayout?: AppProps["Component"]["getLayout"];
|
||||
@@ -855,7 +36,7 @@ const EventTypePageWrapper: React.FC<PageProps> & {
|
||||
allActiveWorkflows: workflows,
|
||||
};
|
||||
|
||||
return <EventTypePage {...propsData} />;
|
||||
return <EventType {...propsData} />;
|
||||
};
|
||||
|
||||
export default EventTypePageWrapper;
|
||||
@@ -3,6 +3,7 @@ import Link from "next/link";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import SkeletonLoader from "@calcom/features/availability/components/SkeletonLoader";
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
@@ -19,7 +20,6 @@ import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
|
||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -4,12 +4,12 @@ import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
|
||||
import { getServerSideProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
|
||||
export type {
|
||||
FormValues,
|
||||
CustomInputParsed,
|
||||
EventTypeSetup,
|
||||
EventTypeSetupProps,
|
||||
Host,
|
||||
} from "~/event-types/views/event-types-single-view";
|
||||
export type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
EventTypePageWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { checkForEmptyAssignment } from "../../lib/checkForEmptyAssignment";
|
||||
import { checkForEmptyAssignment } from "@calcom/lib/event-types/utils/checkForEmptyAssignment";
|
||||
|
||||
describe("Tests to Check if Event Types have empty Assignment", () => {
|
||||
it("should return true if managed event type has no assigned users", () => {
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { Button, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
<ul className="divide-subtle border-subtle bg-default animate-pulse divide-y rounded-md border sm:mx-0 sm:overflow-hidden">
|
||||
@@ -0,0 +1,759 @@
|
||||
"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";
|
||||
// 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 { 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 EventType = (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<boolean>(false);
|
||||
const [pendingRoute, setPendingRoute] = useState("");
|
||||
const leaveWithoutAssigningHosts = useRef(false);
|
||||
const isTeamEventTypeDeleted = useRef(false);
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
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 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,
|
||||
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<FormValues>({
|
||||
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");
|
||||
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: (
|
||||
<EventSetupTab
|
||||
eventType={eventType}
|
||||
locationOptions={locationOptions}
|
||||
team={team}
|
||||
teamMembers={teamMembers}
|
||||
destinationCalendar={destinationCalendar}
|
||||
/>
|
||||
),
|
||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
team: <EventTeamAssignmentTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
apps: <EventAppsTab eventType={{ ...eventType, URL: permalink }} />,
|
||||
workflows: props.allActiveWorkflows ? (
|
||||
<EventWorkflowsTab eventType={eventType} workflows={props.allActiveWorkflows} />
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
ai: <EventAITab eventType={eventType} isTeamEvent={!!team} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(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<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
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<typeof EventTypeMetaDataSchema>))
|
||||
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<ChildrenEventType[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<EventTypeSingleLayout
|
||||
enabledAppsNumber={numberOfActiveApps}
|
||||
installedAppsNumber={eventTypeApps?.items.length || 0}
|
||||
enabledWorkflowsNumber={props.allActiveWorkflows ? props.allActiveWorkflows.length : 0}
|
||||
eventType={eventType}
|
||||
activeWebhooksNumber={eventType.webhooks.filter((webhook) => 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}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
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,
|
||||
// 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
|
||||
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 !== 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<typeof EventTypeMetaDataSchema>))
|
||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
bookingLimits,
|
||||
onlyShowFirstAvailableSlot,
|
||||
durationLimits,
|
||||
eventTypeColor,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
};
|
||||
// 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, hashedLink: values.hashedLink });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
</EventTypeSingleLayout>
|
||||
{slugExistsChildrenDialogOpen.length ? (
|
||||
<ManagedEventTypeDialog
|
||||
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
||||
isPending={formMethods.formState.isSubmitting}
|
||||
onOpenChange={() => {
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}
|
||||
slug={slug}
|
||||
onConfirm={(e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(formMethods.getValues());
|
||||
telemetry.event(telemetryEventTypes.slugReplacementAction);
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<AssignmentWarningDialog
|
||||
isOpenAssignmentWarnDialog={isOpenAssignmentWarnDialog}
|
||||
setIsOpenAssignmentWarnDialog={setIsOpenAssignmentWarnDialog}
|
||||
pendingRoute={pendingRoute}
|
||||
leaveWithoutAssigningHosts={leaveWithoutAssigningHosts}
|
||||
id={eventType.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+4
-68
@@ -1,26 +1,20 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||
import type { FormValues, AvailabilityOption } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import type { VerticalTabItemProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ConfirmationDialogContent,
|
||||
Dialog,
|
||||
DropdownMenuSeparator,
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
@@ -38,6 +32,8 @@ import {
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { DeleteDialog } from "./dialogs/DeleteDialog";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
eventType: EventTypeSetupProps["eventType"];
|
||||
@@ -113,66 +109,6 @@ function getNavigation({
|
||||
] satisfies VerticalTabItemProps[];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onDelete,
|
||||
}: {
|
||||
isManagedEvent: string;
|
||||
eventTypeId: number;
|
||||
onDelete: () => void;
|
||||
} & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
onDelete();
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
children,
|
||||
eventType,
|
||||
+4
-6
@@ -2,22 +2,20 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import type { LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { LocationFormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import CheckboxField from "@calcom/features/form/components/CheckboxField";
|
||||
import type { SingleValueLocationOption } from "@calcom/features/form/components/LocationSelect";
|
||||
import LocationSelect from "@calcom/features/form/components/LocationSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, Input, PhoneInput, Button, showToast } from "@calcom/ui";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
|
||||
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||
|
||||
export type TEventTypeLocation = Pick<EventTypeSetupProps["eventType"], "locations">;
|
||||
export type TLocationOptions = Pick<EventTypeSetupProps, "locationOptions">["locationOptions"];
|
||||
export type TDestinationCalendar = { integration: string } | null;
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,68 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import { ConfirmationDialogContent, Dialog, showToast } from "@calcom/ui";
|
||||
|
||||
export function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onDelete,
|
||||
}: {
|
||||
isManagedEvent: string;
|
||||
eventTypeId: number;
|
||||
onDelete: () => void;
|
||||
} & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
onDelete();
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,82 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
SettingsToggle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
InputField,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface DisableEmailsSettingProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (e: boolean) => void;
|
||||
recipient: "attendees" | "hosts";
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export const DisableAllEmailsSetting = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
recipient,
|
||||
t,
|
||||
}: DisableEmailsSettingProps) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState("");
|
||||
|
||||
const title =
|
||||
recipient === "attendees" ? t("disable_all_emails_to_attendees") : t("disable_all_emails_to_hosts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={dialogOpen} onOpenChange={(e) => setDialogOpen(e)}>
|
||||
<DialogContent title={title} Icon="circle-alert">
|
||||
<p className="text-default text-sm">
|
||||
<Trans i18nKey="disable_attendees_emails_description">
|
||||
This will disable all emails to {{ recipient }}. This includes booking confirmations, requests,
|
||||
reschedules and reschedule requests, cancellation emails, and any other emails related to
|
||||
booking updates.
|
||||
<br />
|
||||
<br />
|
||||
It is your responsibility to ensure that your {{ recipient }} are aware of any bookings and
|
||||
changes to their bookings.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-default mb-1 mt-2 text-sm">{t("type_confirm_to_continue")}</p>
|
||||
<InputField
|
||||
value={confirmText}
|
||||
onChange={(e) => {
|
||||
setConfirmText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
disabled={confirmText !== "confirm"}
|
||||
onClick={(e) => {
|
||||
onCheckedChange(true);
|
||||
setDialogOpen(false);
|
||||
}}>
|
||||
{t("disable_email")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={title}
|
||||
description={t("disable_all_emails_description")}
|
||||
checked={!!checked}
|
||||
onCheckedChange={() => {
|
||||
checked ? onCheckedChange(!checked) : setDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+3
-4
@@ -1,5 +1,4 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import type { fieldSchema } from "@calcom/features/form-builder/schema";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
@@ -44,12 +43,12 @@ import {
|
||||
ColorPicker,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { DisableAllEmailsSetting } from "./DisableAllEmailsSetting";
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
import { DisableAllEmailsSetting } from "./settings/DisableAllEmailsSetting";
|
||||
|
||||
type BookingField = z.infer<typeof fieldSchema>;
|
||||
|
||||
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||
const CustomEventTypeModal = dynamic(() => import("./CustomEventTypeModal"));
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
+1
-1
@@ -1,13 +1,13 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { UnitTypeLongPlural } from "dayjs";
|
||||
import { Trans } from "next-i18next";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { EventTypeSetup } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import React, { useState } from "react";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -9,7 +8,7 @@ import { getTemplateFieldsSchema } from "@calcom/features/ee/cal-ai-phone/getTem
|
||||
import { TEMPLATES_FIELDS } from "@calcom/features/ee/cal-ai-phone/template-fields-map";
|
||||
import type { TemplateType } from "@calcom/features/ee/cal-ai-phone/zod-utils";
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { EventTypeSetup, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { ComponentForField } from "@calcom/features/form-builder/FormBuilderField";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
import AIEventController from "./AIEventController";
|
||||
|
||||
+2
-4
@@ -1,19 +1,17 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import useAppsData from "@calcom/lib/hooks/useAppsData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
||||
|
||||
import useAppsData from "@lib/hooks/useAppsData";
|
||||
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
|
||||
+7
-3
@@ -1,6 +1,5 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, Host } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ComponentProps, Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
@@ -12,7 +11,12 @@ import AddMembersWithSwitch, {
|
||||
import AssignAllTeamMembers from "@calcom/features/eventtypes/components/AssignAllTeamMembers";
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { sortHosts, weightDescription } from "@calcom/features/eventtypes/components/HostEditDialogs";
|
||||
import type { FormValues, TeamMember } from "@calcom/features/eventtypes/lib/types";
|
||||
import type {
|
||||
FormValues,
|
||||
TeamMember,
|
||||
EventTypeSetupProps,
|
||||
Host,
|
||||
} from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
@@ -388,7 +392,7 @@ const Hosts = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const EventTeamTab = ({
|
||||
export const EventTeamAssignmentTab = ({
|
||||
team,
|
||||
teamMembers,
|
||||
eventType,
|
||||
+2
-4
@@ -1,12 +1,12 @@
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState, memo, useEffect } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { SelectSkeletonLoader } from "@calcom/features/availability/components/SkeletonLoader";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { AvailabilityOption, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { AvailabilityOption, FormValues, EventTypeSetup } from "@calcom/features/eventtypes/lib/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
@@ -16,8 +16,6 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||
const { label, isDefault, isManaged = false } = props.data;
|
||||
const { t } = useLocale();
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
|
||||
import InstantEventController from "./InstantEventController";
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext, Controller } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { EventTypeSetup, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn, UseFormReturn } from "react-hook-form";
|
||||
@@ -9,7 +8,8 @@ import type { SingleValue } from "react-select";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { getDefinedBufferTimes } from "@calcom/features/eventtypes/lib/getDefinedBufferTimes";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import CheckboxField from "@calcom/features/form/components/CheckboxField";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { ROLLING_WINDOW_PERIOD_MAX_DAYS_TO_CHECK } from "@calcom/lib/constants";
|
||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -21,8 +21,6 @@ import { PeriodType } from "@calcom/prisma/enums";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
|
||||
type IPeriodType = (typeof PeriodType)[keyof typeof PeriodType];
|
||||
|
||||
/**
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
|
||||
import RecurringEventController from "./RecurringEventController";
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { EventTypeSetup } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
|
||||
@@ -6,6 +5,8 @@ import type { MultiValue } from "react-select";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import Locations from "@calcom/features/eventtypes/components/Locations";
|
||||
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, LocationFormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -14,8 +15,6 @@ import { slugify } from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { Label, Select, SettingsToggle, Skeleton, TextField, Editor } from "@calcom/ui";
|
||||
|
||||
import Locations from "@components/eventtype/Locations";
|
||||
|
||||
export const EventSetupTab = (
|
||||
props: Pick<
|
||||
EventTypeSetupProps,
|
||||
+1
-2
@@ -1,12 +1,11 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
+4
-5
@@ -4,7 +4,11 @@ import { useEffect, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import SkeletonLoader from "@calcom/features/ee/workflows/components/SkeletonLoaderEventWorkflowsTab";
|
||||
import type { WorkflowType } from "@calcom/features/ee/workflows/components/WorkflowListPage";
|
||||
import { getActionIcon } from "@calcom/features/ee/workflows/lib/getActionIcon";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -14,11 +18,6 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen, Icon, showToast, Switch, Tooltip } from "@calcom/ui";
|
||||
|
||||
import LicenseRequired from "../../common/components/LicenseRequired";
|
||||
import { getActionIcon } from "../lib/getActionIcon";
|
||||
import SkeletonLoader from "./SkeletonLoaderEventWorkflowsTab";
|
||||
import type { WorkflowType } from "./WorkflowListPage";
|
||||
|
||||
type PartialWorkflowType = Pick<WorkflowType, "name" | "activeOn" | "isOrg" | "steps" | "id" | "readOnly">;
|
||||
|
||||
type ItemProps = {
|
||||
@@ -133,3 +133,6 @@ export type FormValues = {
|
||||
};
|
||||
|
||||
export type LocationFormValues = Pick<FormValues, "id" | "locations" | "bookingFields" | "seatsPerTimeSlot">;
|
||||
|
||||
export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"];
|
||||
export type EventTypeHosts = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["hosts"];
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+4
-1
@@ -1,4 +1,7 @@
|
||||
import type { EventTypeAssignedUsers, EventTypeHosts } from "~/event-types/views/event-types-single-view";
|
||||
import type {
|
||||
EventTypeAssignedUsers,
|
||||
EventTypeHosts,
|
||||
} from "@calcom/features/eventtypes/components/EventType";
|
||||
|
||||
// This function checks if EventType requires assignment.
|
||||
// returns true: if EventType requires assignment but there is no assignment yet done by the user.
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import type { TFunction } from "next-i18next";
|
||||
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
|
||||
import { z } from "zod";
|
||||
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
|
||||
export const locationsResolver = (t: TFunction) => {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
type: z.string(),
|
||||
address: z.string().optional(),
|
||||
link: z.string().url().optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
hostPhoneNumber: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
displayLocationPublicly: z.boolean().optional(),
|
||||
credentialId: z.number().optional(),
|
||||
teamName: z.string().optional(),
|
||||
})
|
||||
.passthrough()
|
||||
.superRefine((val, ctx) => {
|
||||
if (val?.link) {
|
||||
const link = val.link;
|
||||
const eventLocationType = getEventLocationType(val.type);
|
||||
if (
|
||||
eventLocationType &&
|
||||
!eventLocationType.default &&
|
||||
eventLocationType.linkType === "static" &&
|
||||
eventLocationType.urlRegExp
|
||||
) {
|
||||
const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(link).success;
|
||||
|
||||
if (!valid) {
|
||||
const sampleUrl = eventLocationType.organizerInputPlaceholder;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||
message: t("invalid_url_error_message", {
|
||||
label: eventLocationType.label,
|
||||
sampleUrl: sampleUrl ?? "https://cal.com",
|
||||
}),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const valid = z.string().url().optional().safeParse(link).success;
|
||||
|
||||
if (!valid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [eventLocationType?.defaultValueVariable ?? "link"],
|
||||
message: `Invalid URL`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
})
|
||||
)
|
||||
.optional();
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FormValues } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
const useAppsData = () => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
Reference in new issue
Block a user