Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
024c4fd
init: create schedule atom
Ryukemeister Oct 10, 2025
5d26541
feat: api v2 endpoints for create schedule atom
Ryukemeister Oct 10, 2025
ab6699d
fixup
Ryukemeister Oct 10, 2025
78b2fed
fix: merge conflicts
Ryukemeister Oct 10, 2025
7ebbb45
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 10, 2025
1d90396
feat: update platform libraries
Ryukemeister Oct 10, 2025
ad21527
chore: add disableToasts prop
Ryukemeister Oct 10, 2025
dd610ea
fixup: remove classname, not needed
Ryukemeister Oct 10, 2025
ce5a142
fix: reset form after schedule is created
Ryukemeister Oct 10, 2025
5d0339a
docs for create schedule atom
Ryukemeister Oct 10, 2025
53945c0
better names inside classnames prop
Ryukemeister Oct 10, 2025
bbc1f70
chore: implement PR feedback
Ryukemeister Oct 13, 2025
ab24576
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 13, 2025
b5a52da
implement coderabbit feedback
Ryukemeister Oct 13, 2025
67c3851
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 13, 2025
d4520aa
fix: update open api spec for v2
Ryukemeister Oct 13, 2025
494dd9e
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 13, 2025
ba2402f
Revert "fix: update open api spec for v2"
Ryukemeister Oct 13, 2025
e699e02
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 13, 2025
abb4b73
fix: update open api spec
Ryukemeister Oct 13, 2025
354129e
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
supalarry Oct 14, 2025
e9c8e4d
revert open api spec reference
Ryukemeister Oct 14, 2025
3af6064
Merge branch 'main' into rajiv/cal-5507-feature-create-new-schedule-atom
Ryukemeister Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Param,
ParseIntPipe,
Patch,
Post,
Query,
UseGuards,
Version,
Expand All @@ -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";

Expand Down Expand Up @@ -63,6 +67,7 @@ export class AtomsSchedulesController {
data: schedule,
};
}

@Patch("schedules/:scheduleId")
@Permissions([SCHEDULE_WRITE])
@UseGuards(ApiAuthGuard)
Expand All @@ -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,
};
}
}
3 changes: 2 additions & 1 deletion docs/mint.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand Down
127 changes: 127 additions & 0 deletions docs/platform/atoms/create-schedule.mdx
Original file line number Diff line number Diff line change
@@ -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>
108 changes: 108 additions & 0 deletions packages/platform/atoms/create-schedule/CreateScheduleForm.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
1 change: 1 addition & 0 deletions packages/platform/atoms/create-schedule/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CreateSchedulePlatformWrapper } from "./wrappers/CreateSchedulePlatformWrapper";
Original file line number Diff line number Diff line change
@@ -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>
);
};
Loading
Loading