feat: create new schedule atom (#24404)
* init: create schedule atom * feat: api v2 endpoints for create schedule atom * fixup * fix: merge conflicts * feat: update platform libraries * chore: add disableToasts prop * fixup: remove classname, not needed * fix: reset form after schedule is created * docs for create schedule atom * better names inside classnames prop * chore: implement PR feedback * implement coderabbit feedback
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
Version,
|
||||
@@ -21,8 +22,11 @@ import {
|
||||
ApiExcludeController as DocsExcludeController,
|
||||
ApiOperation,
|
||||
} from "@nestjs/swagger";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
|
||||
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
|
||||
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
|
||||
import { ApiResponse, UpdateAtomScheduleDto } from "@calcom/platform-types";
|
||||
|
||||
@@ -63,6 +67,7 @@ export class AtomsSchedulesController {
|
||||
data: schedule,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("schedules/:scheduleId")
|
||||
@Permissions([SCHEDULE_WRITE])
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -83,4 +88,20 @@ export class AtomsSchedulesController {
|
||||
data: updatedSchedule,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("schedules/create")
|
||||
@Permissions([SCHEDULE_WRITE])
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Create atom schedule" })
|
||||
async createSchedule(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Body() bodySchedule: z.infer<typeof CreateScheduleSchema>
|
||||
): Promise<ApiResponse<CreateScheduleHandlerReturn>> {
|
||||
const createdSchedule = await createScheduleHandler({ input: bodySchedule, ctx: { user } });
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: createdSchedule,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -219,7 +219,8 @@
|
||||
"platform/atoms/calendar-settings",
|
||||
"platform/atoms/payment-form",
|
||||
"platform/atoms/conferencing-apps",
|
||||
"platform/atoms/calendar-view"
|
||||
"platform/atoms/calendar-view",
|
||||
"platform/atoms/create-schedule"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: "Create schedule"
|
||||
---
|
||||
|
||||
The Create Schedule atom provides a simple dialog interface for users to create new availability
|
||||
schedules. Fully customizable with callback support for handling successful schedule creation.
|
||||
|
||||
Below code snippet can be used to render the Create schedule atom
|
||||
|
||||
```js
|
||||
import { CreateSchedule } from "@calcom/atoms";
|
||||
|
||||
export default function CreateSchedule() {
|
||||
return (
|
||||
<>
|
||||
<CreateSchedule
|
||||
name="Create new schedule"
|
||||
customClassNames={{
|
||||
createScheduleButton: "bg-red-500 border-none my-4 mx-2 rounded-md",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For a demonstration of the Create schedule atom, please refer to the video below.
|
||||
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/2d80dd0dddb64c6fb755b00091bdbced?sid=540f2e02-085e-4240-8b64-ca269cf2bb08"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
></iframe>
|
||||
|
||||
<p></p>
|
||||
|
||||
For developers who don't want the dialog-based interface, we provide a `CreateScheduleForm` atom to integrate the schedule
|
||||
creation form directly into your own UI. This headless approach gives you full flexibility to
|
||||
handle layout, styling, and user flow exactly how you need it.
|
||||
|
||||
Below code snippet can be used to render the Create schedule form atom
|
||||
|
||||
```js
|
||||
import { CreateScheduleForm } from "@calcom/atoms";
|
||||
|
||||
export default function CreateScheduleForm() {
|
||||
return (
|
||||
<>
|
||||
<CreateScheduleForm
|
||||
customClassNames={{
|
||||
atomsWrapper: "border-black border-[1px] w-[500px] my-10 mx-5 rounded-md px-5 py-5",
|
||||
buttons: {
|
||||
continue: "bg-red-400 border-none",
|
||||
container: "justify-start",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
For a demonstration of the Create schedule form, please refer to the video below.
|
||||
|
||||
<p></p>
|
||||
|
||||
<iframe
|
||||
height="315"
|
||||
style={{ width: "100%", maxWidth: "560px" }}
|
||||
src="https://www.loom.com/embed/6f019f1ba49a43439032d2d6b434c17c?sid=448e105f-14f2-4735-979d-30470e7c10d1"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen="true"
|
||||
></iframe>
|
||||
|
||||
<p></p>
|
||||
|
||||
We offer all kinds of customizations to the Create schedule atom via props. Below is a list of props that can be passed to the atom.
|
||||
|
||||
<p></p>
|
||||
|
||||
| Name | Required | Description |
|
||||
| :--------------- | :------- | :----------------------------------------------------------------------- |
|
||||
| name | No | The label for the create schedule button |
|
||||
| customClassNames | No | To pass in custom classnames from outside for styling the atom |
|
||||
| onSuccess | No | Callback function that handles successful creation of schedule |
|
||||
| onError | No | Callback function to handles errors at the time of schedule creation |
|
||||
| disableToasts | No | Boolean value that determines whether the toasts are displayed or not |
|
||||
|
||||
|
||||
Along with the props, create schedule atom accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop.
|
||||
<p></p>
|
||||
| Name | Description |
|
||||
|:---------------------------|:-----------------------------------------------------------------------|
|
||||
| createScheduleButton | Adds styling to the create button |
|
||||
| inputField | Adds styling to the container of the name input field |
|
||||
| formWrapper | Adds styling to the whole form |
|
||||
| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom |
|
||||
|
||||
Similar to the create schedule atom, the create schedule form also offer all kinds of customizations via props. Below is a list of props that can be passed to the atom.
|
||||
<p></p>
|
||||
| customClassNames | No | To pass in custom classnames from outside for styling the atom |
|
||||
| onSuccess | No | Callback function that handles successful creation of schedule |
|
||||
| onError | No | Callback function to handles errors at the time of schedule creation |
|
||||
| disableToasts | No | Boolean value that determines whether the toasts are displayed or not |
|
||||
|
||||
Along with the props, create schedule form also accepts custom styles via the **customClassNames** prop. Below is a list of props that fall under this **customClassNames** prop.
|
||||
|
||||
| Name | Description |
|
||||
|:---------------------------|:-----------------------------------------------------------------------|
|
||||
| formWrapper | Adds styling to the whole form |
|
||||
| inputField | Adds styling to the container of the name input field |
|
||||
| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom |
|
||||
|
||||
|
||||
<Note>
|
||||
Please ensure all custom classnames are valid Tailwind CSS classnames. Note that sometimes the custom
|
||||
classnames may fail to override the styling with the classnames that you might have passed via props. That
|
||||
is because the clsx utility function that we use to override classnames inside our components has some
|
||||
limitations. A simple get around to solve this issue is to just prefix your classnames with ! property just
|
||||
before passing in any classname.
|
||||
</Note>
|
||||
@@ -0,0 +1,108 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { ApiErrorResponse } from "@calcom/platform-types";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Form } from "@calcom/ui/components/form";
|
||||
import { InputField } from "@calcom/ui/components/form";
|
||||
|
||||
import { useAtomCreateSchedule } from "../hooks/schedules/useAtomCreateSchedule";
|
||||
import { AtomsWrapper } from "../src/components/atoms-wrapper";
|
||||
import { useToast } from "../src/components/ui/use-toast";
|
||||
import { cn } from "../src/lib/utils";
|
||||
|
||||
export type ActionButtonsClassNames = {
|
||||
container?: string;
|
||||
continue?: string;
|
||||
close?: string;
|
||||
};
|
||||
|
||||
export const CreateScheduleForm = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
onCancel,
|
||||
customClassNames,
|
||||
disableToasts = false,
|
||||
}: {
|
||||
onSuccess?: (scheduleId: number) => void;
|
||||
onError?: (err: ApiErrorResponse) => void;
|
||||
onCancel?: () => void;
|
||||
customClassNames?: {
|
||||
formWrapper?: string;
|
||||
inputField?: string;
|
||||
actionsButtons?: ActionButtonsClassNames;
|
||||
};
|
||||
disableToasts?: boolean;
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const { t } = useLocale();
|
||||
const form = useForm<{
|
||||
name: string;
|
||||
}>();
|
||||
const { register } = form;
|
||||
|
||||
const { mutateAsync: createSchedule, isPending: isCreateSchedulePending } = useAtomCreateSchedule({
|
||||
onSuccess: (res) => {
|
||||
if (!disableToasts) {
|
||||
toast({
|
||||
description: t("schedule_created_successfully", { scheduleName: res.data.schedule.name }),
|
||||
});
|
||||
}
|
||||
form.reset();
|
||||
onSuccess?.(res.data.schedule.id);
|
||||
},
|
||||
onError: (err) => {
|
||||
onError?.(err);
|
||||
if (!disableToasts) {
|
||||
toast({
|
||||
description: `Could not create schedule: ${err.error.message}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<Form
|
||||
className={customClassNames?.formWrapper}
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
await createSchedule(values);
|
||||
}}>
|
||||
<InputField
|
||||
className={customClassNames?.inputField}
|
||||
label={t("name")}
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
placeholder={t("default_schedule_name")}
|
||||
{...register("name", {
|
||||
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-5 justify-end space-x-2 rtl:space-x-reverse sm:mt-4 sm:flex",
|
||||
customClassNames?.actionsButtons?.container
|
||||
)}>
|
||||
{onCancel && (
|
||||
<Button
|
||||
type="button"
|
||||
color="secondary"
|
||||
onClick={onCancel}
|
||||
className={customClassNames?.actionsButtons?.close}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isCreateSchedulePending}
|
||||
className={customClassNames?.actionsButtons?.continue}>
|
||||
{" "}
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CreateSchedulePlatformWrapper } from "./wrappers/CreateSchedulePlatformWrapper";
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { ApiErrorResponse } from "@calcom/platform-types";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { CreateScheduleForm } from "../CreateScheduleForm";
|
||||
import { ActionButtonsClassNames } from "../CreateScheduleForm";
|
||||
|
||||
export const CreateSchedulePlatformWrapper = ({
|
||||
name,
|
||||
customClassNames,
|
||||
onSuccess,
|
||||
onError,
|
||||
disableToasts = false,
|
||||
}: {
|
||||
name?: string;
|
||||
onSuccess?: (scheduleId: number) => void;
|
||||
onError?: (err: ApiErrorResponse) => void;
|
||||
customClassNames?: {
|
||||
createScheduleButton?: string;
|
||||
inputField?: string;
|
||||
formWrapper?: string;
|
||||
actionsButtons?: ActionButtonsClassNames;
|
||||
};
|
||||
disableToasts?: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid={name}
|
||||
className={customClassNames?.createScheduleButton}
|
||||
StartIcon="plus"
|
||||
onClick={() => setIsDialogOpen(true)}>
|
||||
{name ?? t("new")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="bg-default text-default">
|
||||
<CreateScheduleForm
|
||||
customClassNames={{
|
||||
formWrapper: customClassNames?.formWrapper,
|
||||
inputField: customClassNames?.inputField,
|
||||
actionsButtons: {
|
||||
container: customClassNames?.actionsButtons?.container,
|
||||
continue: customClassNames?.actionsButtons?.continue,
|
||||
close: customClassNames?.actionsButtons?.close,
|
||||
},
|
||||
}}
|
||||
onSuccess={(scheduleId) => {
|
||||
setIsDialogOpen(false);
|
||||
onSuccess?.(scheduleId);
|
||||
}}
|
||||
onError={onError}
|
||||
onCancel={() => setIsDialogOpen(false)}
|
||||
disableToasts={disableToasts}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
import type { CreateScheduleHandlerReturn } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.handler";
|
||||
import { TCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema";
|
||||
|
||||
import http from "../../lib/http";
|
||||
import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule";
|
||||
|
||||
interface useAtomCreateScheduleOptions {
|
||||
onSuccess?: (res: ApiSuccessResponse<CreateScheduleHandlerReturn>) => void;
|
||||
onError?: (err: ApiErrorResponse) => void;
|
||||
}
|
||||
|
||||
export const useAtomCreateSchedule = (
|
||||
{ onSuccess, onError }: useAtomCreateScheduleOptions = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<ApiResponse<CreateScheduleHandlerReturn>, unknown, CreateScheduleSchema>({
|
||||
mutationFn: async (body: CreateScheduleSchema) => {
|
||||
const url = `atoms/schedules/create`;
|
||||
const response = await http.post<ApiResponse<CreateScheduleHandlerReturn>>(url, body);
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.(data);
|
||||
queryClient.invalidateQueries({ queryKey: [ScheduleQueryKey] });
|
||||
} else {
|
||||
onError?.(data);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
onError?.(err as ApiErrorResponse);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -41,4 +41,8 @@ export { useCreateTeamEventType } from "./hooks/event-types/private/useCreateTea
|
||||
|
||||
export { useOrganizationBookings } from "./hooks/organizations/bookings/useOrganizationBookings";
|
||||
export { useOrganizationUserBookings } from "./hooks/organizations/bookings/useOrganizationUserBookings";
|
||||
|
||||
export { CalendarViewPlatformWrapper as CalendarView } from "./calendar-view/index";
|
||||
|
||||
export { CreateSchedulePlatformWrapper as CreateSchedule } from "./create-schedule/index";
|
||||
export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
|
||||
|
||||
@@ -5,3 +5,9 @@ export {
|
||||
|
||||
export { updateSchedule, type UpdateScheduleResponse } from "@calcom/lib/schedules/updateSchedule";
|
||||
export { UserAvailabilityService } from "@calcom/features/availability/lib/getUserAvailability";
|
||||
|
||||
export {
|
||||
createHandler as createScheduleHandler,
|
||||
type CreateScheduleHandlerReturn,
|
||||
} from "@calcom/trpc/server/routers/viewer/availability/schedule/create.handler";
|
||||
export { ZCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema";
|
||||
|
||||
@@ -9,11 +9,13 @@ import type { TCreateInputSchema } from "./create.schema";
|
||||
|
||||
type CreateOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "timeZone" | "defaultScheduleId">;
|
||||
};
|
||||
input: TCreateInputSchema;
|
||||
};
|
||||
|
||||
export type CreateScheduleHandlerReturn = Awaited<ReturnType<typeof createHandler>>;
|
||||
|
||||
export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
const { user } = ctx;
|
||||
if (input.eventTypeId) {
|
||||
|
||||
Reference in New Issue
Block a user