feat: Event Type Atom base pr (#16831)

* feat: Event Type Atom base pr

* fixup! Merge branch 'main' into chore-event-type-atom-platform-wrapper-base-pr

* Update EventTypeWebWrapper.tsx

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
This commit is contained in:
Morgan
2024-09-30 13:44:11 +03:00
committed by GitHub
co-authored by Somay Chauhan
parent ac300ff670
commit ad347cb59f
29 changed files with 1148 additions and 357 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.43",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.44",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -1,12 +1,9 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
export class EventTypesRepository_2024_06_14 {
@@ -69,21 +66,6 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getUserEventTypeForAtom(
user: UserWithProfile,
isUserOrganizationAdmin: boolean,
eventTypeId: number
) {
return await getEventTypeById({
currentOrganizationId: this.usersService.getUserMainOrgId(user),
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
isUserOrganizationAdmin,
isTrpcCall: true,
});
}
async getEventTypeById(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -142,27 +142,6 @@ export class EventTypesService_2024_06_14 {
});
}
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
const organizationId = this.usersService.getUserMainOrgId(user);
const isUserOrganizationAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const eventType = await this.eventTypesRepository.getUserEventTypeForAtom(
user,
isUserOrganizationAdmin,
eventTypeId
);
if (!eventType) {
return null;
}
this.checkUserOwnsEventType(user.id, eventType.eventType);
return eventType;
}
async getEventTypesPublicByUsername(username: string): Promise<EventTypesPublic> {
const user = await this.usersRepository.findByUsername(username);
if (!user) {
@@ -0,0 +1,23 @@
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesRepository_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.repository";
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersService } from "@/modules/users/services/users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, EventTypesModule_2024_06_14],
providers: [
EventTypesAtomService,
MembershipsRepository,
UsersRepository,
UsersService,
SchedulesRepository_2024_06_11,
],
exports: [EventTypesAtomService],
controllers: [AtomsController],
})
export class AtomsModule {}
@@ -0,0 +1,67 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
Get,
Param,
ParseIntPipe,
UseGuards,
Version,
VERSION_NEUTRAL,
Patch,
Body,
} from "@nestjs/common";
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
import { SUCCESS_STATUS } from "@calcom/platform-constants";
import type { UpdateEventTypeReturn } from "@calcom/platform-libraries";
import { ApiResponse } from "@calcom/platform-types";
/*
Endpoints used only by platform atoms, reusing code from other modules, data is already formatted and ready to be used by frontend atoms
these endpoints should not be recommended for use by third party and are excluded from docs
*/
@Controller({
path: "/v2/atoms",
version: API_VERSIONS_VALUES,
})
@DocsTags("Atoms - endpoints for atoms")
@DocsExcludeController(true)
export class AtomsController {
constructor(private readonly eventTypesService: EventTypesAtomService) {}
@Get("event-types/:eventTypeId")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
async getAtomEventType(
@GetUser() user: UserWithProfile,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number
): Promise<ApiResponse<unknown>> {
const eventType = await this.eventTypesService.getUserEventType(user, eventTypeId);
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
@Patch("event-types/:eventTypeId")
@Version(VERSION_NEUTRAL)
@UseGuards(ApiAuthGuard)
async updateAtomEventType(
@GetUser() user: UserWithProfile,
@Param("eventTypeId", ParseIntPipe) eventTypeId: number,
@Body() body: UpdateEventTypeReturn
): Promise<ApiResponse<UpdateEventTypeReturn>> {
const eventType = await this.eventTypesService.updateEventType(eventTypeId, body, user);
return {
status: SUCCESS_STATUS,
data: eventType,
};
}
}
@@ -0,0 +1,80 @@
import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/event-types.service";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable, NotFoundException } from "@nestjs/common";
import {
updateEventType,
TUpdateEventTypeInputSchema,
systemBeforeFieldEmail,
getEventTypeById,
} from "@calcom/platform-libraries";
import { PrismaClient } from "@calcom/prisma/client";
@Injectable()
export class EventTypesAtomService {
constructor(
private readonly membershipsRepository: MembershipsRepository,
private readonly usersService: UsersService,
private readonly dbWrite: PrismaWriteService,
private readonly dbRead: PrismaReadService,
private readonly eventTypeService: EventTypesService_2024_06_14
) {}
async getUserEventType(user: UserWithProfile, eventTypeId: number) {
this.eventTypeService.checkUserOwnsEventType(user.id, { id: eventTypeId, userId: user.id });
const organizationId = this.usersService.getUserMainOrgId(user);
const isUserOrganizationAdmin = organizationId
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
: false;
const eventType = await getEventTypeById({
currentOrganizationId: this.usersService.getUserMainOrgId(user),
eventTypeId,
userId: user.id,
prisma: this.dbRead.prisma as unknown as PrismaClient,
isUserOrganizationAdmin,
isTrpcCall: true,
});
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
return eventType;
}
async updateEventType(eventTypeId: number, body: TUpdateEventTypeInputSchema, user: UserWithProfile) {
this.eventTypeService.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
const bookingFields = [...(body.bookingFields || [])];
if (
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
}
const eventType = await updateEventType({
input: { id: eventTypeId, ...body, bookingFields },
ctx: {
user: eventTypeUser,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
prisma: this.dbWrite.prisma,
},
});
if (!eventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
}
return eventType.eventType;
}
}
@@ -1,4 +1,5 @@
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
import { AtomsModule } from "@/modules/atoms/atoms.module";
import { BillingModule } from "@/modules/billing/billing.module";
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
@@ -18,6 +19,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
UsersModule,
WebhooksModule,
DestinationCalendarsModule,
AtomsModule,
],
})
export class EndpointsModule implements NestModule {
+1 -1
View File
@@ -136,7 +136,7 @@
"stripe": "^9.16.0",
"superjson": "1.9.1",
"tailwindcss-radix": "^2.6.0",
"turndown": "^7.1.1",
"turndown": "^7.1.3",
"uuid": "^8.3.2",
"zod": "^3.22.4"
},
@@ -3,8 +3,6 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { UseFormReturn } from "react-hook-form";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import { z } from "zod";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
@@ -14,9 +12,9 @@ import type {
FormValues,
EventTypeApps,
} from "@calcom/features/eventtypes/lib/types";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import type { customInputSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { VerticalTabItemProps } from "@calcom/ui";
import { Form } from "@calcom/ui";
import { EventTypeSingleLayout } from "./EventTypeLayout";
@@ -31,24 +29,19 @@ export type Host = {
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"),
});
const tabs = [
"setup",
"availability",
"team",
"limits",
"advanced",
"instant",
"recurring",
"apps",
"workflows",
"webhooks",
"ai",
] as const;
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
export type EventTypeAssignedUsers = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]["children"];
@@ -58,47 +51,29 @@ export const EventType = (
props: EventTypeSetupProps & {
allActiveWorkflows?: Workflow[];
tabMap: TabMap;
onDelete: () => void;
onDelete: (id: number) => void;
isDeleting?: boolean;
onConflict: (eventTypes: ChildrenEventType[]) => void;
children?: React.ReactNode;
handleSubmit: (values: FormValues) => void;
formMethods: UseFormReturn<FormValues>;
eventTypeApps?: EventTypeApps;
isUpdating: boolean;
isPlatform?: boolean;
tabName: (typeof tabs)[number];
tabsNavigation: VerticalTabItemProps[];
}
) => {
const {
data: { tabName },
} = useTypedQuery(querySchema);
const { formMethods, eventTypeApps } = props;
const { formMethods, isPlatform, tabName } = props;
const { eventType, team, currentUserMembership, tabMap, isUpdating } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
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;
}
// Optional prerender all tabs after 300 ms on mount
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={isUpdating}
formMethods={formMethods}
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
@@ -106,7 +81,10 @@ export const EventType = (
currentUserMembership={currentUserMembership}
bookerUrl={eventType.bookerUrl}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}
onDelete={props.onDelete}>
onDelete={props.onDelete}
isDeleting={props.isDeleting}
isPlatform={isPlatform}
tabsNavigation={props.tabsNavigation}>
<Form form={formMethods} id="event-type-form" handleSubmit={props.handleSubmit}>
<div ref={animationParentRef}>{tabMap[tabName]}</div>
</Form>
@@ -1,14 +1,13 @@
import type { TFunction } from "next-i18next";
import { useMemo, useState, Suspense } from "react";
import type { UseFormReturn } from "react-hook-form";
import { Shell as PlatformShell } from "@calcom/atoms/monorepo";
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 { FormValues } from "@calcom/features/eventtypes/lib/types";
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import Shell from "@calcom/features/shell/Shell";
import WebShell from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import type { VerticalTabItemProps } from "@calcom/ui";
@@ -40,91 +39,30 @@ type Props = {
currentUserMembership: EventTypeSetupProps["currentUserMembership"];
team: EventTypeSetupProps["team"];
disableBorder?: boolean;
enabledAppsNumber: number;
installedAppsNumber: number;
enabledWorkflowsNumber: number;
formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
activeWebhooksNumber: number;
onDelete: () => void;
onDelete: (id: number) => void;
isDeleting?: boolean;
isPlatform?: boolean;
tabsNavigation: VerticalTabItemProps[];
};
type getNavigationProps = {
t: TFunction;
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${id}?tabName=setup`,
icon: "link",
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${id}?tabName=limits`,
icon: "clock",
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${id}?tabName=advanced`,
icon: "sliders-vertical",
info: `event_advanced_tab_description`,
},
{
name: "apps",
href: `/event-types/${id}?tabName=apps`,
icon: "grid-3x3",
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${id}?tabName=workflows`,
icon: "zap",
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
] satisfies VerticalTabItemProps[];
}
function EventTypeSingleLayout({
children,
eventType,
currentUserMembership,
team,
disableBorder,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
isUpdateMutationLoading,
formMethods,
availability,
isUserOrganizationAdmin,
bookerUrl,
activeWebhooksNumber,
onDelete,
isDeleting,
isPlatform,
tabsNavigation,
}: Props) {
const { t } = useLocale();
const eventTypesLockedByOrg = eventType.team?.parent?.organizationSettings?.lockEventTypeCreationForUsers;
@@ -142,110 +80,7 @@ function EventTypeSingleLayout({
translate: t,
formMethods,
});
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
const paymentAppData = getPaymentAppData(eventType);
const requirePayment = paymentAppData.price > 0;
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
const navigation: VerticalTabItemProps[] = getNavigation({
t,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
availability,
});
if (!requirePayment) {
navigation.splice(3, 0, {
name: "recurring",
href: `/event-types/${formMethods.getValues("id")}?tabName=recurring`,
icon: "repeat",
info: `recurring_event_tab_description`,
});
}
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: "calendar",
info:
isManagedEventType || isChildrenManagedEventType
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: "users",
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
if (showWebhooks) {
if (team) {
navigation.push({
name: "instant_tab_title",
href: `/event-types/${eventType.id}?tabName=instant`,
icon: "phone-call",
info: `instant_event_tab_description`,
});
}
navigation.push({
name: "webhooks",
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: "webhook",
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
const hidden = true; // hidden while in alpha trial. you can access it with tabName=ai
if (team && hidden) {
navigation.push({
name: "Cal.ai",
href: `/event-types/${eventType.id}?tabName=ai`,
icon: "sparkles",
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
});
}
return navigation;
}, [
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
availability,
isManagedEventType,
isChildrenManagedEventType,
team,
length,
requirePayment,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
const EventTypeTabs = tabsNavigation;
const permalink = `${bookerUrl}/${
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
}/${eventType.slug}`;
@@ -254,7 +89,11 @@ function EventTypeSingleLayout({
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
}/${formMethods.getValues("slug")}`;
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
// const title = formMethods.watch("title");
const [Shell] = useMemo(() => {
return isPlatform ? [PlatformShell] : [WebShell];
}, [isPlatform]);
return (
<Shell
backPath="/event-types"
@@ -305,41 +144,47 @@ function EventTypeSingleLayout({
{!isManagedEventType && (
<>
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
<Tooltip content={t("preview")} side="bottom" sideOffset={4}>
{!isPlatform && (
<Tooltip content={t("preview")} side="bottom" sideOffset={4}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon="external-link"
/>
</Tooltip>
)}
{!isPlatform && (
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}
rel="noreferrer"
StartIcon="external-link"
StartIcon="link"
tooltip={t("copy_link")}
tooltipSide="bottom"
tooltipOffset={4}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
</Tooltip>
<Button
color="secondary"
variant="icon"
StartIcon="link"
tooltip={t("copy_link")}
tooltipSide="bottom"
tooltipOffset={4}
onClick={() => {
navigator.clipboard.writeText(permalink);
showToast("Link copied!", "success");
}}
/>
<EventTypeEmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon="code"
color="secondary"
variant="icon"
namespace={eventType.slug}
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={formMethods.getValues("id")}
/>
)}
{!isPlatform && (
<EventTypeEmbedButton
embedUrl={encodeURIComponent(embedLink)}
StartIcon="code"
color="secondary"
variant="icon"
namespace={eventType.slug}
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={formMethods.getValues("id")}
/>
)}
</>
)}
{!isChildrenManagedEventType && (
@@ -455,9 +300,10 @@ function EventTypeSingleLayout({
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
onDelete={onDelete}
isDeleting={isDeleting}
/>
<EventTypeEmbedDialog />
{!isPlatform && <EventTypeEmbedDialog />}
</Shell>
);
}
@@ -8,6 +8,7 @@ import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "reac
import type { EventLocationType } from "@calcom/app-store/locations";
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
import { useIsPlatform } from "@calcom/atoms/monorepo";
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";
@@ -209,6 +210,8 @@ const Locations: React.FC<LocationsProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [prefillLocation, seatsEnabled]);
const isPlatorm = useIsPlatform();
return (
<div className="w-full">
<ul ref={animationRef} className="space-y-2">
@@ -417,7 +420,7 @@ const Locations: React.FC<LocationsProps> = ({
</li>
)}
</ul>
{props.showAppStoreLink && (
{props.showAppStoreLink && !isPlatorm && (
<p className="text-default mt-2 text-sm">
<Trans i18nKey="cant_find_the_right_conferencing_app_visit_our_app_store">
Can&apos;t find the right conferencing app? Visit our
@@ -1,11 +1,8 @@
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";
import { ConfirmationDialogContent, Dialog } from "@calcom/ui";
export function DeleteDialog({
isManagedEvent,
@@ -13,44 +10,26 @@ export function DeleteDialog({
open,
onOpenChange,
onDelete,
isDeleting,
}: {
isManagedEvent: string;
eventTypeId: number;
onDelete: () => void;
onDelete: (id: number) => void;
isDeleting?: boolean;
} & 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}
isPending={isDeleting}
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 });
onDelete(eventTypeId);
}}>
<p className="mt-5">
<Trans
@@ -4,11 +4,9 @@ import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "reac
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";
import { md } from "@calcom/lib/markdownIt";
import { slugify } from "@calcom/lib/slugify";
@@ -19,15 +17,15 @@ export type EventSetupTabProps = Pick<
EventTypeSetupProps,
"eventType" | "locationOptions" | "team" | "teamMembers" | "destinationCalendar"
>;
export const EventSetupTab = (props: EventSetupTabProps) => {
export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; hasOrgBranding: boolean }) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, team } = props;
const { eventType, team, urlPrefix, hasOrgBranding } = props;
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const [firstRender, setFirstRender] = useState(true);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
const multipleDurationOptions = [
@@ -54,9 +52,6 @@ export const EventSetupTab = (props: EventSetupTabProps) => {
const descriptionLockedProps = shouldLockDisableProps("description");
const urlLockedProps = shouldLockDisableProps("slug");
const titleLockedProps = shouldLockDisableProps("title");
const urlPrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
return (
<div>
@@ -98,7 +93,7 @@ export const EventSetupTab = (props: EventSetupTabProps) => {
{urlPrefix}/
{!isManagedEventType
? team
? (orgBranding ? "" : "team/") + team.slug
? (hasOrgBranding ? "" : "team/") + team.slug
: formMethods.getValues("users")[0].username
: t("username_placeholder")}
/
@@ -2,9 +2,10 @@ import type { GroupBase, Props, SingleValue } from "react-select";
import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { classNames } from "@calcom/lib";
import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
import { Select } from "@calcom/ui";
import { Select, Icon } from "@calcom/ui";
export type LocationOption = {
label: string;
@@ -20,7 +21,37 @@ export type SingleValueLocationOption = SingleValue<LocationOption>;
export type GroupOptionType = GroupBase<LocationOption>;
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
const OptionWithIcon = ({ icon, label, value }: { icon?: string; label: string; value: string }) => {
const isPlatform = useIsPlatform();
const getIconFromValue = (value: string) => {
switch (value) {
case "phone":
return <Icon name="phone" className="h-3.5 w-3.5" />;
case "userPhone":
return <Icon name="phone" className="h-3.5 w-3.5" />;
case "inPerson":
return <Icon name="map-pin" className="h-3.5 w-3.5" />;
case "attendeeInPerson":
return <Icon name="map-pin" className="h-3.5 w-3.5" />;
case "link":
return <Icon name="link" className="h-3.5 w-3.5" />;
case "somewhereElse":
return <Icon name="map" className="h-3.5 w-3.5" />;
default:
return <Icon name="video" className="h-3.5 w-3.5" />;
}
};
if (isPlatform) {
return (
<div className="flex items-center gap-3">
{getIconFromValue(value)}
<span className={classNames("text-sm font-medium")}>{label}</span>
</div>
);
}
return (
<div className="flex items-center gap-3">
{icon && <img src={icon} alt="cover" className={classNames("h-3.5 w-3.5", invertLogoOnDark(icon))} />}
@@ -30,6 +61,7 @@ const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
};
export default function LocationSelect(props: Props<LocationOption, false, GroupOptionType>) {
const isPlatform = useIsPlatform();
return (
<Select<LocationOption>
name="location"
@@ -40,22 +72,24 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
return (
<components.Option {...props}>
<div data-testid={`location-select-item-${props.data.value}`}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
<OptionWithIcon icon={props.data.icon} label={props.data.label} value={props.data.value} />
</div>
</components.Option>
);
},
SingleValue: (props) => (
<components.SingleValue {...props}>
<div data-testid={`location-select-item-${props.data.value}`}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
</div>
</components.SingleValue>
),
SingleValue: (props) => {
return (
<components.SingleValue {...props}>
<div data-testid={`location-select-item-${props.data.value}`}>
<OptionWithIcon icon={props.data.icon} label={props.data.label} value={props.data.value} />
</div>
</components.SingleValue>
);
},
}}
formatOptionLabel={(e) => (
formatOptionLabel={(e, d) => (
<div className="flex items-center gap-3">
{e.icon && (
{e.icon && !isPlatform && (
<img
src={e.icon}
alt="app-icon"
@@ -0,0 +1,27 @@
import { useQuery } from "@tanstack/react-query";
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
import { useAtomsContext } from "../../hooks/useAtomsContext";
import http from "../../lib/http";
export const QUERY_KEY = "use-event-by-id";
export const useAtomsEventTypeById = (id: number | null) => {
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${id}`;
const { isInit, accessToken } = useAtomsContext();
return useQuery({
queryKey: [QUERY_KEY, id],
queryFn: () => {
return http?.get<ApiResponse<EventTypeSetupProps>>(pathname).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (res.data as ApiSuccessResponse<EventTypeSetupProps>).data;
}
throw new Error(res.data.error.message);
});
},
enabled: !!id && isInit && !!accessToken,
});
};
@@ -0,0 +1,42 @@
import { useMutation } from "@tanstack/react-query";
import type { EventTypeUpdateInput } from "@calcom/features/eventtypes/lib/types";
import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
import http from "../../lib/http";
export const QUERY_KEY = "use-event-by-id";
export type UseAtomUpdateEventTypeProps = {
onSuccess?: () => void;
onError?: (err: Error) => void;
onSettled?: () => void;
};
export const useAtomUpdateEventType = ({
onSuccess = () => {
return;
},
onError = () => {
return;
},
onSettled = () => {
return;
},
}: UseAtomUpdateEventTypeProps) => {
return useMutation({
onSuccess,
onError,
onSettled,
mutationFn: (data: EventTypeUpdateInput) => {
if (!data.id) throw new Error("Event type id is required");
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}/${data.id}`;
return http?.patch<ApiResponse<EventType>>(pathname, data).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (res.data as ApiSuccessResponse<EventType>).data;
}
throw new Error(res.data.error.message);
});
},
});
};
@@ -0,0 +1,168 @@
"use client";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import type { TFunction } from "next-i18next";
import { useMemo, useState, useEffect } from "react";
import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { EventTypeSetupProps, FormValues } from "@calcom/features/eventtypes/lib/types";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { VerticalTabItemProps } from "@calcom/ui";
import type { PlatformTabs } from "../wrappers/EventTypePlatformWrapper";
type Props = {
formMethods: UseFormReturn<FormValues>;
eventType: EventTypeSetupProps["eventType"];
team: EventTypeSetupProps["team"];
tabs: PlatformTabs[];
};
export const usePlatformTabsNavigations = ({ formMethods, eventType, team, tabs }: Props) => {
const [url, setUrl] = useState("");
const [currentTab, setCurrentTab] = useState<PlatformTabs>(tabs[0]);
useEffect(() => {
// Get the current URL from window.location
const currentUrl = window.location.href;
setUrl(currentUrl);
}, []);
const { t } = useLocale();
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
const availability = formMethods.watch("availability");
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
eventType,
translate: t,
formMethods,
});
const paymentAppData = getPaymentAppData(eventType);
const requirePayment = paymentAppData.price > 0;
const EventTypeTabs = useMemo(() => {
const navigation: VerticalTabItemProps[] = getNavigation({
t,
length,
multipleDuration,
id: formMethods.getValues("id"),
tabs,
url,
onClick: (tab) => {
setCurrentTab(tab);
},
});
if (!requirePayment) {
navigation.splice(3, 0, {
name: "recurring",
onClick: () => setCurrentTab("recurring"),
href: `${url}?tabName=recurring`,
icon: "repeat",
info: `recurring_event_tab_description`,
});
}
navigation.splice(1, 0, {
name: "availability",
onClick: () => setCurrentTab("availability"),
href: `${url}?tabName=availability`,
icon: "calendar",
info:
isManagedEventType || isChildrenManagedEventType
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
onClick: () => setCurrentTab("team"),
href: `${url}?tabName=team`,
icon: "users",
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
return navigation;
}, [
t,
availability,
isManagedEventType,
isChildrenManagedEventType,
team,
length,
requirePayment,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
url,
tabs,
currentTab,
]);
return { tabsNavigation: EventTypeTabs, currentTab };
};
type getNavigationProps = {
t: TFunction;
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
tabs: PlatformTabs[];
url: string;
onClick: (tab: PlatformTabs) => void;
};
function getNavigation({ length, multipleDuration, t, tabs, url, onClick }: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
const tabsNavigation: VerticalTabItemProps[] = [];
tabs.includes("setup") &&
tabsNavigation.push({
name: "event_setup_tab_title",
onClick: () => onClick("setup"),
href: `${url}?tabName=setup`,
icon: "link",
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
});
tabs.includes("limits") &&
tabsNavigation.push({
name: "event_limit_tab_title",
onClick: () => onClick("limits"),
href: `${url}?tabName=limits`,
icon: "clock",
info: `event_limit_tab_description`,
});
tabs.includes("advanced") &&
tabsNavigation.push({
name: "event_advanced_tab_title",
onClick: () => onClick("advanced"),
href: `${url}?tabName=advanced`,
icon: "sliders-vertical",
info: `event_advanced_tab_description`,
});
return tabsNavigation;
}
@@ -0,0 +1,220 @@
"use client";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import type { TFunction } from "next-i18next";
import { useMemo } from "react";
import type { UseFormReturn } from "react-hook-form";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type {
EventTypeSetupProps,
AvailabilityOption,
FormValues,
EventTypeApps,
} from "@calcom/features/eventtypes/lib/types";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { VerticalTabItemProps } from "@calcom/ui";
type Props = {
formMethods: UseFormReturn<FormValues>;
eventType: EventTypeSetupProps["eventType"];
team: EventTypeSetupProps["team"];
eventTypeApps?: EventTypeApps;
allActiveWorkflows?: Workflow[];
};
export const useTabsNavigations = ({
formMethods,
eventType,
team,
eventTypeApps,
allActiveWorkflows,
}: Props) => {
const { t } = useLocale();
const length = formMethods.watch("length");
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
const watchSchedulingType = formMethods.watch("schedulingType");
const watchChildrenCount = formMethods.watch("children").length;
const availability = formMethods.watch("availability");
const appsMetadata = formMethods.getValues("metadata")?.apps;
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
eventType,
translate: t,
formMethods,
});
let enabledAppsNumber = 0;
if (appsMetadata) {
enabledAppsNumber = Object.entries(appsMetadata).filter(
([appId, appData]) =>
eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
).length;
}
const paymentAppData = getPaymentAppData(eventType);
const requirePayment = paymentAppData.price > 0;
const activeWebhooksNumber = eventType.webhooks.filter((webhook) => webhook.active).length;
const installedAppsNumber = eventTypeApps?.items.length || 0;
const enabledWorkflowsNumber = allActiveWorkflows ? allActiveWorkflows.length : 0;
const EventTypeTabs = useMemo(() => {
const navigation: VerticalTabItemProps[] = getNavigation({
t,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
availability,
});
if (!requirePayment) {
navigation.splice(3, 0, {
name: "recurring",
href: `/event-types/${formMethods.getValues("id")}?tabName=recurring`,
icon: "repeat",
info: `recurring_event_tab_description`,
});
}
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: "calendar",
info:
isManagedEventType || isChildrenManagedEventType
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: "users",
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
if (showWebhooks) {
if (team) {
navigation.push({
name: "instant_tab_title",
href: `/event-types/${eventType.id}?tabName=instant`,
icon: "phone-call",
info: `instant_event_tab_description`,
});
}
navigation.push({
name: "webhooks",
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: "webhook",
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
const hidden = true; // hidden while in alpha trial. you can access it with tabName=ai
if (team && hidden) {
navigation.push({
name: "Cal.ai",
href: `/event-types/${eventType.id}?tabName=ai`,
icon: "sparkles",
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
});
}
return navigation;
}, [
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
availability,
isManagedEventType,
isChildrenManagedEventType,
team,
length,
requirePayment,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
return { tabsNavigation: EventTypeTabs };
};
type getNavigationProps = {
t: TFunction;
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${id}?tabName=setup`,
icon: "link",
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${id}?tabName=limits`,
icon: "clock",
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${id}?tabName=advanced`,
icon: "sliders-vertical",
info: `event_advanced_tab_description`,
},
{
name: "apps",
href: `/event-types/${id}?tabName=apps`,
icon: "grid-3x3",
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${id}?tabName=workflows`,
icon: "zap",
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
] satisfies VerticalTabItemProps[];
}
@@ -0,0 +1,10 @@
import React from "react";
import type { EventSetupTabProps } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
import { EventSetupTab } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
const EventSetupTabPlatformWrapper = (props: EventSetupTabProps) => {
return <EventSetupTab {...props} urlPrefix="" hasOrgBranding={false} />;
};
export default EventSetupTabPlatformWrapper;
@@ -1,8 +1,14 @@
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import type { EventSetupTabProps } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
import { EventSetupTab } from "@calcom/features/eventtypes/components/tabs/setup/EventSetupTab";
import { WEBSITE_URL } from "@calcom/lib/constants";
const EventSetupTabWebWrapper = (props: EventSetupTabProps) => {
return <EventSetupTab {...props} />;
const orgBranding = useOrgBranding();
const urlPrefix = orgBranding
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
return <EventSetupTab urlPrefix={urlPrefix} hasOrgBranding={!!orgBranding} {...props} />;
};
export default EventSetupTabWebWrapper;
@@ -0,0 +1,167 @@
"use client";
import { useRef, useState } from "react";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType";
import ManagedEventTypeDialog from "@calcom/features/eventtypes/components/dialogs/ManagedEventDialog";
import type { EventTypeSetupProps, TabMap } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
import { useToast } from "../../src/components/ui/use-toast";
import { useAtomsEventTypeById } from "../hooks/useAtomEventTypeById";
import { useAtomUpdateEventType } from "../hooks/useAtomUpdateEventType";
import { useEventTypeForm } from "../hooks/useEventTypeForm";
import { useHandleRouteChange } from "../hooks/useHandleRouteChange";
import { usePlatformTabsNavigations } from "../hooks/usePlatformTabsNavigations";
import SetupTab from "./EventSetupTabPlatformWrapper";
export type PlatformTabs = keyof Omit<TabMap, "workflows" | "webhooks" | "instant" | "ai" | "apps">;
export type EventTypePlatformWrapperProps = {
id: number;
tabs?: PlatformTabs[];
};
const EventType = ({
tabs = ["setup", "availability", "team", "limits", "advanced"],
...props
}: EventTypeSetupProps & EventTypePlatformWrapperProps) => {
const { t } = useLocale();
const { toast } = useToast();
const isTeamEventTypeDeleted = useRef(false);
const leaveWithoutAssigningHosts = useRef(false);
const [isOpenAssignmentWarnDialog, setIsOpenAssignmentWarnDialog] = useState<boolean>(false);
const [pendingRoute, setPendingRoute] = useState("");
const { eventType, locationOptions, team, teamMembers, destinationCalendar } = props;
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
const updateMutation = useAtomUpdateEventType({
onSuccess: async () => {
const currentValues = form.getValues();
currentValues.children = currentValues.children.map((child) => ({
...child,
created: true,
}));
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
form.reset(currentValues);
toast({ description: t("event_type_updated_successfully", { eventTypeTitle: eventType.title }) });
},
async onSettled() {
return;
},
onError: (err: Error) => {
const message = err?.message;
toast({ description: message ? t(message) : t(err.message) });
},
});
const { form, handleSubmit } = useEventTypeForm({ eventType, onSubmit: updateMutation.mutate });
const slug = form.watch("slug") ?? eventType.slug;
const tabMap = {
setup: tabs.includes("setup") ? (
<SetupTab
eventType={eventType}
locationOptions={locationOptions}
team={team}
teamMembers={teamMembers}
destinationCalendar={destinationCalendar}
/>
) : (
<></>
),
availability: <></>,
team: <></>,
limits: <></>,
advanced: <></>,
instant: <></>,
recurring: <></>,
apps: <></>,
workflows: <></>,
webhooks: <></>,
ai: <></>,
} as const;
useHandleRouteChange({
watchTrigger: null,
isTeamEventTypeDeleted: isTeamEventTypeDeleted.current,
isleavingWithoutAssigningHosts: leaveWithoutAssigningHosts.current,
isTeamEventType: !!team,
assignedUsers: eventType.children,
hosts: eventType.hosts,
assignAllTeamMembers: eventType.assignAllTeamMembers,
isManagedEventType: eventType.schedulingType === SchedulingType.MANAGED,
onError: (url) => {
setIsOpenAssignmentWarnDialog(true);
setPendingRoute(url);
},
onStart: () => {
return;
},
onEnd: () => {
return;
},
});
const onDelete = () => {
isTeamEventTypeDeleted.current = true;
};
const onConflict = (conflicts: ChildrenEventType[]) => {
setSlugExistsChildrenDialogOpen(conflicts);
};
const { tabsNavigation, currentTab } = usePlatformTabsNavigations({
formMethods: form,
eventType,
team,
tabs,
});
return (
<AtomsWrapper>
<EventTypeComponent
{...props}
tabMap={tabMap}
onDelete={onDelete}
onConflict={onConflict}
handleSubmit={handleSubmit}
formMethods={form}
isUpdating={updateMutation.isPending}
isPlatform
tabName={currentTab}
tabsNavigation={tabsNavigation}>
<>
{slugExistsChildrenDialogOpen.length ? (
<ManagedEventTypeDialog
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
isPending={form.formState.isSubmitting}
onOpenChange={() => {
setSlugExistsChildrenDialogOpen([]);
}}
slug={slug}
onConfirm={(e: { preventDefault: () => void }) => {
e.preventDefault();
handleSubmit(form.getValues());
setSlugExistsChildrenDialogOpen([]);
}}
/>
) : null}
</>
</EventTypeComponent>
</AtomsWrapper>
);
};
export const EventTypePlatformWrapper = ({ id, tabs }: EventTypePlatformWrapperProps) => {
const { data: eventTypeQueryData } = useAtomsEventTypeById(id);
if (!eventTypeQueryData) return null;
return <EventType {...eventTypeQueryData} id={id} tabs={tabs} />;
};
@@ -1,27 +1,31 @@
"use client";
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import dynamic from "next/dynamic";
import { usePathname } from "next/navigation";
import { usePathname, useRouter as useAppRouter } from "next/navigation";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import { useRouter as usePageRouter } from "next/router";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import type { NextRouter as NextPageRouter } from "next/router";
import React, { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
import { EventType as EventTypeComponent } from "@calcom/features/eventtypes/components/EventType";
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { SchedulingType } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { showToast } from "@calcom/ui";
import { useEventTypeForm } from "../hooks/useEventTypeForm";
import { useHandleRouteChange } from "../hooks/useHandleRouteChange";
import { useTabsNavigations } from "../hooks/useTabsNavigations";
const ManagedEventTypeDialog = dynamic(
() => import("@calcom/features/eventtypes/components/dialogs/ManagedEventDialog")
@@ -97,6 +101,7 @@ type EventTypeAppComponentProp = {
isAppDir: true;
pathname: string;
pageRouter: null;
appRouter: AppRouterInstance;
};
// discriminative factor: isAppDir
@@ -105,6 +110,7 @@ type EventTypePageComponentProp = {
isAppDir: false;
pageRouter: NextPageRouter;
pathname: null;
appRouter: null;
};
type EventTypeAppPageComponentProp = EventTypeAppComponentProp | EventTypePageComponentProp;
@@ -123,18 +129,31 @@ export const EventTypeWebWrapper = ({ id, isAppDir }: EventTypeWebWrapperProps &
const EventTypePageWrapper = ({ id, ...rest }: EventTypeSetupProps & { id: number }) => {
const router = usePageRouter();
return <EventTypeWeb {...rest} id={id} isAppDir={false} pageRouter={router} pathname={null} />;
return (
<EventTypeWeb {...rest} id={id} isAppDir={false} pageRouter={router} pathname={null} appRouter={null} />
);
};
const EventTypeAppWrapper = ({ id, ...rest }: EventTypeSetupProps & { id: number }) => {
const pathname = usePathname();
return <EventTypeWeb {...rest} id={id} isAppDir={true} pathname={pathname ?? ""} pageRouter={null} />;
const router = useAppRouter();
return (
<EventTypeWeb
{...rest}
id={id}
isAppDir={true}
pathname={pathname ?? ""}
pageRouter={null}
appRouter={router}
/>
);
};
const EventTypeWeb = ({
id,
isAppDir,
pageRouter,
appRouter,
pathname,
...rest
}: EventTypeSetupProps & EventTypeAppPageComponentProp) => {
@@ -305,23 +324,78 @@ const EventTypeWeb = ({
};
}, []);
const onDelete = () => {
isTeamEventTypeDeleted.current = true;
};
const onConflict = (conflicts: ChildrenEventType[]) => {
setSlugExistsChildrenDialogOpen(conflicts);
};
const querySchema = z.object({
tabName: z
.enum([
"setup",
"availability",
"team",
"limits",
"advanced",
"instant",
"recurring",
"apps",
"workflows",
"webhooks",
"ai",
])
.optional()
.default("setup"),
});
const {
data: { tabName },
} = useTypedQuery(querySchema);
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
await utils.viewer.eventTypes.invalidate();
showToast(t("event_type_deleted_successfully"), "success");
isTeamEventTypeDeleted.current = true;
isAppDir ? appRouter.push("/event-types") : pageRouter.push("/event-types");
setSlugExistsChildrenDialogOpen([]);
setIsOpenAssignmentWarnDialog(false);
},
onError: (err) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setSlugExistsChildrenDialogOpen([]);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
const { tabsNavigation } = useTabsNavigations({
formMethods: form,
eventType,
team,
eventTypeApps,
allActiveWorkflows,
});
return (
<EventTypeComponent
{...rest}
allActiveWorkflows={allActiveWorkflows}
tabMap={tabMap}
onDelete={onDelete}
onDelete={(id) => {
deleteMutation.mutate({ id });
}}
isDeleting={deleteMutation.isPending}
onConflict={onConflict}
handleSubmit={handleSubmit}
eventTypeApps={eventTypeApps}
formMethods={form}
isUpdating={updateMutation.isPending}>
isUpdating={updateMutation.isPending}
isPlatform={false}
tabName={tabName}
tabsNavigation={tabsNavigation}>
<>
{slugExistsChildrenDialogOpen.length ? (
<ManagedEventTypeDialog
+1
View File
@@ -24,3 +24,4 @@ export { SelectedCalendarsSettingsPlatformWrapper as SelectedCalendarsSettings }
export { DestinationCalendarSettingsPlatformWrapper as DestinationCalendarSettings } from "./destination-calendar/index";
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/index";
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
+1
View File
@@ -11,3 +11,4 @@ export { DestinationCalendarSettingsWebWrapper } from "./destination-calendar/wr
export * from "./availability";
export { EventTypeWebWrapper as EventType } from "./event-types/wrappers/EventTypeWebWrapper";
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
export { Shell } from "./src/components/ui/shell";
+3
View File
@@ -24,6 +24,9 @@ export default defineConfig(({ mode }) => {
define: {
"process.env.NEXT_PUBLIC_WEBAPP_URL": `"${webAppUrl}"`,
},
ssr: {
noExternal: ["turndown"], // Example if you want to disable SSR for your library
},
build: {
commonjsOptions: {
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
@@ -22,6 +22,9 @@ export function Navbar({ username }: { username?: string }) {
<li>
<Link href="/availability">Availability</Link>
</li>
<li>
<Link href="/event-types">EventTypes</Link>
</li>
<li>
<Link href="/booking">Book Me</Link>
</li>
@@ -0,0 +1,94 @@
import { Navbar } from "@/components/Navbar";
import { Inter } from "next/font/google";
// eslint-disable-next-line @calcom/eslint/deprecated-imports-next-router
import { useRouter } from "next/router";
import { useState } from "react";
import { useEventTypes, useTeamEventTypes, useTeams, EventTypeSettings } from "@calcom/atoms";
const inter = Inter({ subsets: ["latin"] });
export default function Bookings(props: { calUsername: string; calEmail: string }) {
const [eventTypeId, setEventTypeId] = useState<number | null>(null);
const [isTeamEvent, setIsTeamEvent] = useState<boolean>(false);
const router = useRouter();
const { isLoading: isLoadingEvents, data: eventTypes } = useEventTypes(props.calUsername);
const { data: teams } = useTeams();
const { isLoading: isLoadingTeamEvents, data: teamEventTypes } = useTeamEventTypes(teams?.[0]?.id || 0);
const rescheduleUid = (router.query.rescheduleUid as string) ?? "";
return (
<main
className={`flex min-h-screen flex-col ${inter.className} main text-default flex min-h-full w-full flex-col items-center overflow-visible`}>
<Navbar username={props.calUsername} />
<div>
<h1 className="mx-10 my-4 text-2xl font-semibold">
EventTypes{eventTypeId ? `: ${eventTypeId}` : ""}
</h1>
{isLoadingEvents && !eventTypeId && <p>Loading...</p>}
{!isLoadingEvents && !eventTypeId && Boolean(eventTypes?.length) && !rescheduleUid && (
<div className="flex flex-col gap-4">
<h1>User event types</h1>
{eventTypes?.map(
(event: { id: number; slug: string; title: string; lengthInMinutes: number }) => {
const formatEventSlug = event.slug
.split("-")
.map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`)
.join(" ");
return (
<div
onClick={() => {
setEventTypeId(event.id);
setIsTeamEvent(false);
}}
className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4"
key={event.id}>
<h1 className="text-lg font-semibold">{formatEventSlug}</h1>
<p>{`/${event.slug}`}</p>
<span className="border-none bg-gray-800 px-2 text-white">{event?.lengthInMinutes}</span>
</div>
);
}
)}
</div>
)}
{!isLoadingTeamEvents && !eventTypeId && Boolean(teamEventTypes?.length) && !rescheduleUid && (
<div className="flex flex-col gap-4">
<h1>Team event types</h1>
{teamEventTypes?.map(
(event: { id: number; slug: string; title: string; lengthInMinutes: number }) => {
const formatEventSlug = event.slug
.split("-")
.map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`)
.join(" ");
return (
<div
onClick={() => {
setEventTypeId(event.id);
setIsTeamEvent(true);
}}
className="mx-10 w-[80vw] cursor-pointer rounded-md border-[0.8px] border-black px-10 py-4"
key={event.id}>
<h1 className="text-lg font-semibold">{formatEventSlug}</h1>
<p>{`/${event.slug}`}</p>
<span className="border-none bg-gray-800 px-2 text-white">{event?.lengthInMinutes}</span>
</div>
);
}
)}
</div>
)}
{eventTypeId && (
<div>
<EventTypeSettings id={eventTypeId} tabs={["setup"]} />
</div>
)}
</div>
</main>
);
}
+1 -1
View File
@@ -83,7 +83,7 @@ export type { CityTimezones } from "@calcom/lib/cityTimezonesHandler";
export { TRPCError } from "@trpc/server";
export type { TUpdateInputSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.schema";
export type { TUpdateInputSchema as TUpdateEventTypeInputSchema } from "@calcom/trpc/server/routers/viewer/eventTypes/update.schema";
export { createNewUsersConnectToOrgIfExists, sendSignupToOrganizationEmail };
export { getAllUserBookings };
@@ -25,6 +25,7 @@ export type VerticalTabItemProps = {
linkScroll?: boolean;
avatar?: string;
iconClassName?: string;
onClick?: (name: string) => void;
};
const VerticalTabItem = ({
@@ -45,6 +46,12 @@ const VerticalTabItem = ({
{!props.hidden && (
<>
<Link
onClick={(e) => {
if (props.onClick) {
e.preventDefault();
props.onClick(name);
}
}}
key={name}
href={href}
shallow={linkShallow}