feat: create event type atom (#17142)

* feat: create event type atom

* fixup! feat: create event type atom

* fixup! fixup! feat: create event type atom

* docs: base doc for create event type atom
This commit is contained in:
Morgan
2024-10-17 21:51:23 +05:30
committed by GitHub
parent 8b50f419e5
commit 497b0804d3
15 changed files with 360 additions and 29 deletions
@@ -162,6 +162,7 @@ describe("Organizations Team Endpoints", () => {
const responseBody: ApiSuccessResponse<OrgMeTeamOutputDto[]> = response.body;
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)).toBeDefined();
expect(responseBody.data.some((t) => t.accepted)).toBeTruthy();
expect(responseBody.data.find((t) => t.id === teamCreatedViaApi.id)?.role).toBe("OWNER");
});
});
@@ -67,7 +67,7 @@ export class OrganizationsTeamsController {
}
@Get("/me")
@ApiOperation({ summary: "Get team membership for user" })
@ApiOperation({ summary: "Get teams membership for user" })
@Roles("ORG_MEMBER")
@PlatformPlan("ESSENTIALS")
async getMyTeams(
@@ -84,13 +84,14 @@ export class OrganizationsTeamsController {
);
return {
status: SUCCESS_STATUS,
data: teams.map((team) =>
plainToClass(
data: teams.map((team) => {
const me = team.members.find((member) => member.userId === user.id);
return plainToClass(
OrgMeTeamOutputDto,
{ ...team, accepted: team.members.find((member) => member.userId === user.id)?.accepted ?? false },
me ? { ...team, role: me.role, accepted: me.accepted } : team,
{ strategy: "excludeAll" }
)
),
);
}),
};
}
@@ -1,4 +1,5 @@
import { ApiProperty } from "@nestjs/swagger";
import { MembershipRole } from "@prisma/client";
import { Expose, Type } from "class-transformer";
import { IsEnum, IsString, ValidateNested } from "class-validator";
@@ -9,6 +10,14 @@ export class OrgMeTeamOutputDto extends OrgTeamOutputDto {
@IsString()
@Expose()
readonly accepted!: boolean;
@ApiProperty({
example: MembershipRole.MEMBER,
enum: [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER],
})
@IsEnum([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
@Expose()
readonly role!: MembershipRole;
}
export class OrgTeamsOutputResponseDto {
@@ -97,7 +97,7 @@ export class OrganizationsTeamsRepository {
},
},
include: {
members: { select: { accepted: true, userId: true } },
members: { select: { accepted: true, userId: true, role: true } },
},
skip,
take,
+12 -1
View File
@@ -2005,7 +2005,7 @@
"/v2/organizations/{orgId}/teams/me": {
"get": {
"operationId": "OrganizationsTeamsController_getMyTeams",
"summary": "Get team membership for user",
"summary": "Get teams membership for user",
"parameters": [
{
"name": "orgId",
@@ -12223,6 +12223,12 @@
"type": "string"
}
},
"meetingUrl": {
"type": "string",
"description": "Deprecated - use 'location' instead. Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.",
"example": "https://example.com/meeting",
"deprecated": true
},
"location": {
"type": "string",
"description": "Location for this booking. Displayed in email and calendar event.",
@@ -12234,6 +12240,11 @@
"example": {
"customField": "customValue"
}
},
"recurrenceCount": {
"type": "number",
"description": "The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than\n event type recurrence count",
"example": 5
}
},
"required": [
+36
View File
@@ -0,0 +1,36 @@
---
title: "Create EventType"
---
The create event-type atom enables you to display a form with the required input fields needed to create an event-type.
By specifying a team id as a prop of the component you can create an event-type for a specific team.
```js
import { CreateEventType } from "@calcom/atoms";
export default function EventType() {
return (
<>
<CreateEventType
onSuccess={(eventType) => {
console.log("EventType created", eventType);
}}
onError={(err) => {
console.log("Error while creating eventType", err);
}}
onCancel={() => {
console.log("Clicked on cancel button")
}}
customClassNames={{
atomsWrapper: "border p-4 shadow-md rounded-md",
buttons: { container: "justify-center", submit: "bg-red-500", cancel: "bg-gray-300" },
}}
/>
<>
)
}
```
<Info>
Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames.
</Info>
@@ -62,7 +62,7 @@ export const TeamEventTypeForm = ({
{urlPrefix && urlPrefix.length >= 21 ? (
<div>
<TextField
label={`${t("url")}: ${urlPrefix}`}
label={isPlatform ? "Slug" : `${t("url")}: ${urlPrefix}`}
required
addOnLeading={
!isPlatform ? (
@@ -79,14 +79,14 @@ export const TeamEventTypeForm = ({
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
) : (
<div>
<TextField
label={t("url")}
label={isPlatform ? "Slug" : t("url")}
required
addOnLeading={
!isPlatform ? (
@@ -105,7 +105,7 @@ export const TeamEventTypeForm = ({
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
@@ -55,7 +55,7 @@ export default function CreateEventTypeForm({
{urlPrefix && urlPrefix.length >= 21 ? (
<div>
<TextField
label={`${t("url")}: ${urlPrefix}`}
label={isPlatform ? "Slug" : `${t("url")}: ${urlPrefix}`}
required
addOnLeading={
!isPlatform ? (
@@ -72,14 +72,14 @@ export default function CreateEventTypeForm({
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
) : (
<div>
<TextField
label={t("url")}
label={isPlatform ? "Slug" : t("url")}
required
addOnLeading={
!isPlatform ? (
@@ -96,7 +96,7 @@ export default function CreateEventTypeForm({
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
}}
/>
{isManagedEventType && (
{isManagedEventType && !isPlatform && (
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
)}
</div>
+12 -6
View File
@@ -13,12 +13,7 @@ import { trpc } from "@calcom/trpc/react";
export type CreateEventTypeFormValues = z.infer<typeof createEventTypeInput>;
export const useCreateEventType = (
onSuccessMutation: (eventType: EventType) => void,
onErrorMutation: (message: string) => void
) => {
const utils = trpc.useUtils();
const { t } = useLocale();
export const useCreateEventTypeForm = () => {
const form = useForm<CreateEventTypeFormValues>({
defaultValues: {
length: 15,
@@ -37,6 +32,17 @@ export const useCreateEventType = (
}
}, [schedulingTypeWatch]);
return { form, isManagedEventType };
};
export const useCreateEventType = (
onSuccessMutation: (eventType: EventType) => void,
onErrorMutation: (message: string) => void
) => {
const utils = trpc.useUtils();
const { t } = useLocale();
const { form, isManagedEventType } = useCreateEventTypeForm();
const createMutation = trpc.viewer.eventTypes.create.useMutation({
onSuccess: async ({ eventType }) => {
onSuccessMutation(eventType);
@@ -0,0 +1,137 @@
import { AtomsWrapper } from "@/components/atoms-wrapper";
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
import CreateEventTypeForm from "@calcom/features/eventtypes/components/CreateEventTypeForm";
import { useCreateEventTypeForm } from "@calcom/lib/hooks/useCreateEventType";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { EventType } from "@calcom/prisma/client";
import { Button } from "@calcom/ui";
import { useCreateEventType } from "../../hooks/event-types/private/useCreateEventType";
import { useCreateTeamEventType } from "../../hooks/event-types/private/useCreateTeamEventType";
import { useTeams } from "../../hooks/teams/useTeams";
import { cn } from "../../src/lib/utils";
type ActionButtonsClassNames = {
container?: string;
submit?: string;
cancel?: string;
};
type CreateEventTypeProps = {
teamId?: number;
onSuccess?: (eventType: EventType) => void;
onError?: (error: Error) => void;
customClassNames?: {
atomsWrapper?: string;
buttons?: ActionButtonsClassNames;
};
onCancel?: () => void;
};
const ActionButtons = ({
submitLabel,
cancelLabel,
isPending,
customClassNames,
onCancel,
}: {
submitLabel: string;
cancelLabel: string;
isPending: boolean;
customClassNames?: ActionButtonsClassNames;
onCancel?: () => void;
}) => {
return (
<div className={cn("flex flex-row gap-4", customClassNames?.container)}>
<Button type="submit" loading={isPending} className={customClassNames?.submit}>
{submitLabel}
</Button>
{onCancel && (
<Button
color="secondary"
type="button"
loading={isPending}
className={customClassNames?.cancel}
onClick={() => onCancel()}>
{cancelLabel}
</Button>
)}
</div>
);
};
export const CreateEventTypePlatformWrapper = ({
teamId,
onSuccess,
onError,
customClassNames,
onCancel,
}: CreateEventTypeProps) => {
const { form, isManagedEventType } = useCreateEventTypeForm();
const createEventTypeQuery = useCreateEventType({ onSuccess, onError });
const createTeamEventTypeQuery = useCreateTeamEventType({ onSuccess, onError });
const { data: teams } = useTeams();
const team = teams?.find((t) => t.id === teamId);
const { t } = useLocale();
return teamId && team ? (
<AtomsWrapper customClassName={customClassNames?.atomsWrapper}>
<TeamEventTypeForm
teamSlug={team?.slug}
teamId={teamId}
isTeamAdminOrOwner={team.role === "ADMIN" || team.role === "OWNER"}
urlPrefix=""
isPending={createTeamEventTypeQuery.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createTeamEventTypeQuery.mutate({
lengthInMinutes: values.length,
title: values.title,
slug: values.slug,
description: values.description ?? "",
schedulingType: values.schedulingType ?? "COLLECTIVE",
hosts: [],
teamId,
});
}}
SubmitButton={(isPending) =>
ActionButtons({
isPending,
onCancel,
submitLabel: t("continue"),
cancelLabel: t("cancel"),
customClassNames: customClassNames?.buttons,
})
}
/>
</AtomsWrapper>
) : (
<AtomsWrapper customClassName={customClassNames?.atomsWrapper}>
<CreateEventTypeForm
urlPrefix=""
isPending={createEventTypeQuery.isPending}
form={form}
isManagedEventType={isManagedEventType}
handleSubmit={(values) => {
createEventTypeQuery.mutate({
lengthInMinutes: values.length,
title: values.title,
slug: values.slug,
description: values.description ?? "",
});
}}
SubmitButton={(isPending) =>
ActionButtons({
isPending,
onCancel,
submitLabel: t("continue"),
cancelLabel: t("cancel"),
customClassNames: customClassNames?.buttons,
})
}
/>
</AtomsWrapper>
);
};
@@ -0,0 +1,40 @@
import { useMutation } from "@tanstack/react-query";
import {
V2_ENDPOINTS,
SUCCESS_STATUS,
CAL_API_VERSION_HEADER,
VERSION_2024_06_14,
} from "@calcom/platform-constants";
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { CreateEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
import http from "../../../lib/http";
export const QUERY_KEY = "use-create-event";
export type UseCreateEventTypeProps = {
onSuccess?: (eventType: EventType) => void;
onError?: (err: Error) => void;
onSettled?: () => void;
};
export const useCreateEventType = ({ onSuccess, onError, onSettled }: UseCreateEventTypeProps) => {
return useMutation({
onSuccess,
onError,
onSettled,
mutationFn: (body: CreateEventTypeInput_2024_06_14) => {
const pathname = `/${V2_ENDPOINTS.eventTypes}`;
return http
?.post<ApiResponse<EventType>>(pathname, body, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_14 },
})
.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,39 @@
import { useMutation } from "@tanstack/react-query";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
import type { CreateTeamEventTypeInput_2024_06_14 } from "@calcom/platform-types";
import type { EventType } from "@calcom/prisma/client";
import http from "../../../lib/http";
import { useAtomsContext } from "../../useAtomsContext";
export const QUERY_KEY = "use-create-team-event";
export type UseCreateTeamEventTypeProps = {
onSuccess?: (eventType: EventType) => void;
onError?: (err: Error) => void;
onSettled?: () => void;
};
export const useCreateTeamEventType = ({ onSuccess, onError, onSettled }: UseCreateTeamEventTypeProps) => {
const { organizationId } = useAtomsContext();
return useMutation({
onSuccess,
onError,
onSettled,
mutationFn: ({ teamId, ...body }: CreateTeamEventTypeInput_2024_06_14 & { teamId: number }) => {
if (!teamId) throw new Error("Event type id is required");
const pathname = `/organizations/${organizationId}/teams/${teamId}/event-types`;
return http
?.post<ApiResponse<EventType>>(pathname, body, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_14 },
})
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (res.data as ApiSuccessResponse<EventType>).data;
}
throw new Error(res.data.error.message);
});
},
});
};
@@ -18,12 +18,16 @@ export const useTeams = () => {
const event = useQuery({
queryKey: [QUERY_KEY, organizationId],
queryFn: async () => {
return http?.get<ApiResponse<OrgTeamOutputDto[]>>(pathname).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (res.data as ApiSuccessResponse<OrgTeamOutputDto[]>).data;
}
throw new Error(res.data.error.message);
});
return http
?.get<ApiResponse<(OrgTeamOutputDto & { accepted: boolean; role: string })[]>>(pathname)
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return (
res.data as ApiSuccessResponse<(OrgTeamOutputDto & { accepted: boolean; role: string })[]>
).data;
}
throw new Error(res.data.error.message);
});
},
enabled: !!organizationId,
});
+1
View File
@@ -26,3 +26,4 @@ export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-
export type { UpdateScheduleInput_2024_06_11 as UpdateScheduleBody } from "@calcom/platform-types";
export { EventTypePlatformWrapper as EventTypeSettings } from "./event-types/wrappers/EventTypePlatformWrapper";
export { StripeConnect } from "./connect/stripe/StripeConnect";
export { CreateEventTypePlatformWrapper as CreateEventType } from "./event-types/wrappers/CreateEventTypePlatformWrapper";
@@ -4,7 +4,13 @@ import { Inter } from "next/font/google";
import { useRouter } from "next/router";
import { useState } from "react";
import { useEventTypes, useTeamEventTypes, useTeams, EventTypeSettings } from "@calcom/atoms";
import {
useEventTypes,
useTeamEventTypes,
useTeams,
EventTypeSettings,
CreateEventType,
} from "@calcom/atoms";
const inter = Inter({ subsets: ["latin"] });
@@ -111,6 +117,46 @@ export default function Bookings(props: { calUsername: string; calEmail: string
/>
</div>
)}
{!eventTypeId && (
<div className="mt-8 flex flex-row items-center justify-center gap-24">
<div className="flex w-[30vw] flex-col gap-2">
<h1 className="font-semibold">Create Event Type</h1>
<CreateEventType
customClassNames={{
atomsWrapper: "border p-4 shadow-md rounded-md",
buttons: { container: "justify-center", submit: "bg-red-500", cancel: "bg-gray-300" },
}}
onSuccess={() => {
refetch();
}}
/>
</div>
<div className="flex w-[30vw] flex-col gap-2">
<h1 className="font-semibold">Create Team Event Type</h1>
{teams?.[0]?.id && (
<CreateEventType
customClassNames={{
atomsWrapper: "border shadow-md p-4 rounded-md ",
buttons: {
container: "justify-center flex-row-reverse",
submit: "bg-green-500",
cancel: "bg-stone-300",
},
}}
teamId={teams?.[0]?.id}
onCancel={() => {
console.log("cancel team event type creation");
}}
onSuccess={() => {
refetchTeamEvents();
}}
/>
)}
</div>
</div>
)}
</div>
</main>
);