From 497b0804d337827a0223048cbc63fc2d5ea6e57a Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:21:23 +0300 Subject: [PATCH] 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 --- ...organizations-teams.controller.e2e-spec.ts | 1 + .../teams/organizations-teams.controller.ts | 13 +- .../outputs/organization-team.output.ts | 9 ++ .../organizations-teams.repository.ts | 2 +- apps/api/v2/swagger/documentation.json | 13 +- docs/platform/atoms/create-event-type.mdx | 36 +++++ .../ee/teams/components/TeamEventTypeForm.tsx | 8 +- .../components/CreateEventTypeForm.tsx | 8 +- packages/lib/hooks/useCreateEventType.ts | 18 ++- .../CreateEventTypePlatformWrapper.tsx | 137 ++++++++++++++++++ .../event-types/private/useCreateEventType.ts | 40 +++++ .../private/useCreateTeamEventType.ts | 39 +++++ .../platform/atoms/hooks/teams/useTeams.ts | 16 +- packages/platform/atoms/index.ts | 1 + .../examples/base/src/pages/event-types.tsx | 48 +++++- 15 files changed, 360 insertions(+), 29 deletions(-) create mode 100644 docs/platform/atoms/create-event-type.mdx create mode 100644 packages/platform/atoms/event-types/wrappers/CreateEventTypePlatformWrapper.tsx create mode 100644 packages/platform/atoms/hooks/event-types/private/useCreateEventType.ts create mode 100644 packages/platform/atoms/hooks/event-types/private/useCreateTeamEventType.ts diff --git a/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.e2e-spec.ts index bf44a3a79d..34e1cf1b5a 100644 --- a/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.e2e-spec.ts +++ b/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.e2e-spec.ts @@ -162,6 +162,7 @@ describe("Organizations Team Endpoints", () => { const responseBody: ApiSuccessResponse = 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"); }); }); diff --git a/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.ts b/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.ts index a79482202b..e9f9b798ed 100644 --- a/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.ts +++ b/apps/api/v2/src/modules/organizations/controllers/teams/organizations-teams.controller.ts @@ -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" } - ) - ), + ); + }), }; } diff --git a/apps/api/v2/src/modules/organizations/outputs/organization-team.output.ts b/apps/api/v2/src/modules/organizations/outputs/organization-team.output.ts index 321aefaacf..a652cd6dfb 100644 --- a/apps/api/v2/src/modules/organizations/outputs/organization-team.output.ts +++ b/apps/api/v2/src/modules/organizations/outputs/organization-team.output.ts @@ -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 { diff --git a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts index 3a4acaf386..91b4d0d94c 100644 --- a/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts +++ b/apps/api/v2/src/modules/organizations/repositories/organizations-teams.repository.ts @@ -97,7 +97,7 @@ export class OrganizationsTeamsRepository { }, }, include: { - members: { select: { accepted: true, userId: true } }, + members: { select: { accepted: true, userId: true, role: true } }, }, skip, take, diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 1c3f7824a5..068fc993b5 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -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": [ diff --git a/docs/platform/atoms/create-event-type.mdx b/docs/platform/atoms/create-event-type.mdx new file mode 100644 index 0000000000..fab59d3756 --- /dev/null +++ b/docs/platform/atoms/create-event-type.mdx @@ -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 ( + <> + { + 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" }, + }} + /> + <> + ) +} +``` + + + Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames. + diff --git a/packages/features/ee/teams/components/TeamEventTypeForm.tsx b/packages/features/ee/teams/components/TeamEventTypeForm.tsx index ec72bea4ab..a55ad13f58 100644 --- a/packages/features/ee/teams/components/TeamEventTypeForm.tsx +++ b/packages/features/ee/teams/components/TeamEventTypeForm.tsx @@ -62,7 +62,7 @@ export const TeamEventTypeForm = ({ {urlPrefix && urlPrefix.length >= 21 ? (
- {isManagedEventType && ( + {isManagedEventType && !isPlatform && (

{t("managed_event_url_clarification")}

)}
) : (
- {isManagedEventType && ( + {isManagedEventType && !isPlatform && (

{t("managed_event_url_clarification")}

)}
diff --git a/packages/features/eventtypes/components/CreateEventTypeForm.tsx b/packages/features/eventtypes/components/CreateEventTypeForm.tsx index 64207958c8..feecea38d7 100644 --- a/packages/features/eventtypes/components/CreateEventTypeForm.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeForm.tsx @@ -55,7 +55,7 @@ export default function CreateEventTypeForm({ {urlPrefix && urlPrefix.length >= 21 ? (
- {isManagedEventType && ( + {isManagedEventType && !isPlatform && (

{t("managed_event_url_clarification")}

)}
) : (
- {isManagedEventType && ( + {isManagedEventType && !isPlatform && (

{t("managed_event_url_clarification")}

)}
diff --git a/packages/lib/hooks/useCreateEventType.ts b/packages/lib/hooks/useCreateEventType.ts index 0f7f39b8eb..e7ea9f0282 100644 --- a/packages/lib/hooks/useCreateEventType.ts +++ b/packages/lib/hooks/useCreateEventType.ts @@ -13,12 +13,7 @@ import { trpc } from "@calcom/trpc/react"; export type CreateEventTypeFormValues = z.infer; -export const useCreateEventType = ( - onSuccessMutation: (eventType: EventType) => void, - onErrorMutation: (message: string) => void -) => { - const utils = trpc.useUtils(); - const { t } = useLocale(); +export const useCreateEventTypeForm = () => { const form = useForm({ 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); diff --git a/packages/platform/atoms/event-types/wrappers/CreateEventTypePlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/CreateEventTypePlatformWrapper.tsx new file mode 100644 index 0000000000..9c6d9e8b68 --- /dev/null +++ b/packages/platform/atoms/event-types/wrappers/CreateEventTypePlatformWrapper.tsx @@ -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 ( +
+ + {onCancel && ( + + )} +
+ ); +}; + +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 ? ( + + { + 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, + }) + } + /> + + ) : ( + + { + 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, + }) + } + /> + + ); +}; diff --git a/packages/platform/atoms/hooks/event-types/private/useCreateEventType.ts b/packages/platform/atoms/hooks/event-types/private/useCreateEventType.ts new file mode 100644 index 0000000000..caec2e1f88 --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/private/useCreateEventType.ts @@ -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>(pathname, body, { + headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_14 }, + }) + .then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/event-types/private/useCreateTeamEventType.ts b/packages/platform/atoms/hooks/event-types/private/useCreateTeamEventType.ts new file mode 100644 index 0000000000..9b79032aea --- /dev/null +++ b/packages/platform/atoms/hooks/event-types/private/useCreateTeamEventType.ts @@ -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>(pathname, body, { + headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_14 }, + }) + .then((res) => { + if (res.data.status === SUCCESS_STATUS) { + return (res.data as ApiSuccessResponse).data; + } + throw new Error(res.data.error.message); + }); + }, + }); +}; diff --git a/packages/platform/atoms/hooks/teams/useTeams.ts b/packages/platform/atoms/hooks/teams/useTeams.ts index 8815869a85..70a628a8a8 100644 --- a/packages/platform/atoms/hooks/teams/useTeams.ts +++ b/packages/platform/atoms/hooks/teams/useTeams.ts @@ -18,12 +18,16 @@ export const useTeams = () => { const event = useQuery({ queryKey: [QUERY_KEY, organizationId], queryFn: async () => { - return http?.get>(pathname).then((res) => { - if (res.data.status === SUCCESS_STATUS) { - return (res.data as ApiSuccessResponse).data; - } - throw new Error(res.data.error.message); - }); + return http + ?.get>(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, }); diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts index aaf7f99261..6d8184e285 100644 --- a/packages/platform/atoms/index.ts +++ b/packages/platform/atoms/index.ts @@ -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"; diff --git a/packages/platform/examples/base/src/pages/event-types.tsx b/packages/platform/examples/base/src/pages/event-types.tsx index 653b494802..38c0070a57 100644 --- a/packages/platform/examples/base/src/pages/event-types.tsx +++ b/packages/platform/examples/base/src/pages/event-types.tsx @@ -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 /> )} + + {!eventTypeId && ( +
+
+

Create Event Type

+ { + refetch(); + }} + /> +
+ +
+

Create Team Event Type

+ {teams?.[0]?.id && ( + { + console.log("cancel team event type creation"); + }} + onSuccess={() => { + refetchTeamEvents(); + }} + /> + )} +
+
+ )} );