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:
+1
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+7
-6
@@ -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 {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ export class OrganizationsTeamsRepository {
|
||||
},
|
||||
},
|
||||
include: {
|
||||
members: { select: { accepted: true, userId: true } },
|
||||
members: { select: { accepted: true, userId: true, role: true } },
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user