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:
Rajiv Sahal
2025-10-14 14:36:25 +00:00
committed by GitHub
parent 947b05bf90
commit b5585b3974
10 changed files with 387 additions and 2 deletions
@@ -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
View File
@@ -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"
]
},
{
+127
View File
@@ -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);
},
});
};
+4
View File
@@ -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";
+6
View File
@@ -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) {