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:
@@ -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 {
|
||||
|
||||
@@ -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'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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user