Skip to content

Commit b5585b3

Browse files
authored
feat: create new schedule atom (calcom#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
1 parent 947b05b commit b5585b3

10 files changed

Lines changed: 387 additions & 2 deletions

File tree

apps/api/v2/src/modules/atoms/controllers/atoms.schedules.controller.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
Param,
1212
ParseIntPipe,
1313
Patch,
14+
Post,
1415
Query,
1516
UseGuards,
1617
Version,
@@ -21,8 +22,11 @@ import {
2122
ApiExcludeController as DocsExcludeController,
2223
ApiOperation,
2324
} from "@nestjs/swagger";
25+
import { z } from "zod";
2426

2527
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
28+
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
29+
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
2630
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
2731
import { ApiResponse, UpdateAtomScheduleDto } from "@calcom/platform-types";
2832

@@ -63,6 +67,7 @@ export class AtomsSchedulesController {
6367
data: schedule,
6468
};
6569
}
70+
6671
@Patch("schedules/:scheduleId")
6772
@Permissions([SCHEDULE_WRITE])
6873
@UseGuards(ApiAuthGuard)
@@ -83,4 +88,20 @@ export class AtomsSchedulesController {
8388
data: updatedSchedule,
8489
};
8590
}
91+
92+
@Post("schedules/create")
93+
@Permissions([SCHEDULE_WRITE])
94+
@UseGuards(ApiAuthGuard)
95+
@ApiOperation({ summary: "Create atom schedule" })
96+
async createSchedule(
97+
@GetUser() user: UserWithProfile,
98+
@Body() bodySchedule: z.infer<typeof CreateScheduleSchema>
99+
): Promise<ApiResponse<CreateScheduleHandlerReturn>> {
100+
const createdSchedule = await createScheduleHandler({ input: bodySchedule, ctx: { user } });
101+
102+
return {
103+
status: SUCCESS_STATUS,
104+
data: createdSchedule,
105+
};
106+
}
86107
}

docs/mint.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,8 @@
219219
"platform/atoms/calendar-settings",
220220
"platform/atoms/payment-form",
221221
"platform/atoms/conferencing-apps",
222-
"platform/atoms/calendar-view"
222+
"platform/atoms/calendar-view",
223+
"platform/atoms/create-schedule"
223224
]
224225
},
225226
{
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
title: "Create schedule"
3+
---
4+
5+
The Create Schedule atom provides a simple dialog interface for users to create new availability
6+
schedules. Fully customizable with callback support for handling successful schedule creation.
7+
8+
Below code snippet can be used to render the Create schedule atom
9+
10+
```js
11+
import { CreateSchedule } from "@calcom/atoms";
12+
13+
export default function CreateSchedule() {
14+
return (
15+
<>
16+
<CreateSchedule
17+
name="Create new schedule"
18+
customClassNames={{
19+
createScheduleButton: "bg-red-500 border-none my-4 mx-2 rounded-md",
20+
}}
21+
/>
22+
</>
23+
)
24+
}
25+
```
26+
27+
For a demonstration of the Create schedule atom, please refer to the video below.
28+
29+
<p></p>
30+
31+
<iframe
32+
height="315"
33+
style={{ width: "100%", maxWidth: "560px" }}
34+
src="https://www.loom.com/embed/2d80dd0dddb64c6fb755b00091bdbced?sid=540f2e02-085e-4240-8b64-ca269cf2bb08"
35+
frameborder="0"
36+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
37+
allowfullscreen="true"
38+
></iframe>
39+
40+
<p></p>
41+
42+
For developers who don't want the dialog-based interface, we provide a `CreateScheduleForm` atom to integrate the schedule
43+
creation form directly into your own UI. This headless approach gives you full flexibility to
44+
handle layout, styling, and user flow exactly how you need it.
45+
46+
Below code snippet can be used to render the Create schedule form atom
47+
48+
```js
49+
import { CreateScheduleForm } from "@calcom/atoms";
50+
51+
export default function CreateScheduleForm() {
52+
return (
53+
<>
54+
<CreateScheduleForm
55+
customClassNames={{
56+
atomsWrapper: "border-black border-[1px] w-[500px] my-10 mx-5 rounded-md px-5 py-5",
57+
buttons: {
58+
continue: "bg-red-400 border-none",
59+
container: "justify-start",
60+
},
61+
}}
62+
/>
63+
</>
64+
)
65+
}
66+
```
67+
68+
For a demonstration of the Create schedule form, please refer to the video below.
69+
70+
<p></p>
71+
72+
<iframe
73+
height="315"
74+
style={{ width: "100%", maxWidth: "560px" }}
75+
src="https://www.loom.com/embed/6f019f1ba49a43439032d2d6b434c17c?sid=448e105f-14f2-4735-979d-30470e7c10d1"
76+
frameborder="0"
77+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
78+
allowfullscreen="true"
79+
></iframe>
80+
81+
<p></p>
82+
83+
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.
84+
85+
<p></p>
86+
87+
| Name | Required | Description |
88+
| :--------------- | :------- | :----------------------------------------------------------------------- |
89+
| name | No | The label for the create schedule button |
90+
| customClassNames | No | To pass in custom classnames from outside for styling the atom |
91+
| onSuccess | No | Callback function that handles successful creation of schedule |
92+
| onError | No | Callback function to handles errors at the time of schedule creation |
93+
| disableToasts | No | Boolean value that determines whether the toasts are displayed or not |
94+
95+
96+
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.
97+
<p></p>
98+
| Name | Description |
99+
|:---------------------------|:-----------------------------------------------------------------------|
100+
| createScheduleButton | Adds styling to the create button |
101+
| inputField | Adds styling to the container of the name input field |
102+
| formWrapper | Adds styling to the whole form |
103+
| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom |
104+
105+
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.
106+
<p></p>
107+
| customClassNames | No | To pass in custom classnames from outside for styling the atom |
108+
| onSuccess | No | Callback function that handles successful creation of schedule |
109+
| onError | No | Callback function to handles errors at the time of schedule creation |
110+
| disableToasts | No | Boolean value that determines whether the toasts are displayed or not |
111+
112+
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.
113+
114+
| Name | Description |
115+
|:---------------------------|:-----------------------------------------------------------------------|
116+
| formWrapper | Adds styling to the whole form |
117+
| inputField | Adds styling to the container of the name input field |
118+
| actionsButtons | Object containing classnames for the submit, cancel buttons and container inside the create schedule atom |
119+
120+
121+
<Note>
122+
Please ensure all custom classnames are valid Tailwind CSS classnames. Note that sometimes the custom
123+
classnames may fail to override the styling with the classnames that you might have passed via props. That
124+
is because the clsx utility function that we use to override classnames inside our components has some
125+
limitations. A simple get around to solve this issue is to just prefix your classnames with ! property just
126+
before passing in any classname.
127+
</Note>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useForm } from "react-hook-form";
2+
3+
import { useLocale } from "@calcom/lib/hooks/useLocale";
4+
import type { ApiErrorResponse } from "@calcom/platform-types";
5+
import { Button } from "@calcom/ui/components/button";
6+
import { Form } from "@calcom/ui/components/form";
7+
import { InputField } from "@calcom/ui/components/form";
8+
9+
import { useAtomCreateSchedule } from "../hooks/schedules/useAtomCreateSchedule";
10+
import { AtomsWrapper } from "../src/components/atoms-wrapper";
11+
import { useToast } from "../src/components/ui/use-toast";
12+
import { cn } from "../src/lib/utils";
13+
14+
export type ActionButtonsClassNames = {
15+
container?: string;
16+
continue?: string;
17+
close?: string;
18+
};
19+
20+
export const CreateScheduleForm = ({
21+
onSuccess,
22+
onError,
23+
onCancel,
24+
customClassNames,
25+
disableToasts = false,
26+
}: {
27+
onSuccess?: (scheduleId: number) => void;
28+
onError?: (err: ApiErrorResponse) => void;
29+
onCancel?: () => void;
30+
customClassNames?: {
31+
formWrapper?: string;
32+
inputField?: string;
33+
actionsButtons?: ActionButtonsClassNames;
34+
};
35+
disableToasts?: boolean;
36+
}) => {
37+
const { toast } = useToast();
38+
const { t } = useLocale();
39+
const form = useForm<{
40+
name: string;
41+
}>();
42+
const { register } = form;
43+
44+
const { mutateAsync: createSchedule, isPending: isCreateSchedulePending } = useAtomCreateSchedule({
45+
onSuccess: (res) => {
46+
if (!disableToasts) {
47+
toast({
48+
description: t("schedule_created_successfully", { scheduleName: res.data.schedule.name }),
49+
});
50+
}
51+
form.reset();
52+
onSuccess?.(res.data.schedule.id);
53+
},
54+
onError: (err) => {
55+
onError?.(err);
56+
if (!disableToasts) {
57+
toast({
58+
description: `Could not create schedule: ${err.error.message}`,
59+
});
60+
}
61+
},
62+
});
63+
64+
return (
65+
<AtomsWrapper>
66+
<Form
67+
className={customClassNames?.formWrapper}
68+
form={form}
69+
handleSubmit={async (values) => {
70+
await createSchedule(values);
71+
}}>
72+
<InputField
73+
className={customClassNames?.inputField}
74+
label={t("name")}
75+
type="text"
76+
id="name"
77+
required
78+
placeholder={t("default_schedule_name")}
79+
{...register("name", {
80+
setValueAs: (v) => (!v || v.trim() === "" ? null : v),
81+
})}
82+
/>
83+
<div
84+
className={cn(
85+
"mt-5 justify-end space-x-2 rtl:space-x-reverse sm:mt-4 sm:flex",
86+
customClassNames?.actionsButtons?.container
87+
)}>
88+
{onCancel && (
89+
<Button
90+
type="button"
91+
color="secondary"
92+
onClick={onCancel}
93+
className={customClassNames?.actionsButtons?.close}>
94+
{t("close")}
95+
</Button>
96+
)}
97+
<Button
98+
type="submit"
99+
loading={isCreateSchedulePending}
100+
className={customClassNames?.actionsButtons?.continue}>
101+
{" "}
102+
{t("continue")}
103+
</Button>
104+
</div>
105+
</Form>
106+
</AtomsWrapper>
107+
);
108+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { CreateSchedulePlatformWrapper } from "./wrappers/CreateSchedulePlatformWrapper";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
2+
import { useState } from "react";
3+
4+
import { useLocale } from "@calcom/lib/hooks/useLocale";
5+
import type { ApiErrorResponse } from "@calcom/platform-types";
6+
import { Button } from "@calcom/ui/components/button";
7+
8+
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
9+
import { CreateScheduleForm } from "../CreateScheduleForm";
10+
import { ActionButtonsClassNames } from "../CreateScheduleForm";
11+
12+
export const CreateSchedulePlatformWrapper = ({
13+
name,
14+
customClassNames,
15+
onSuccess,
16+
onError,
17+
disableToasts = false,
18+
}: {
19+
name?: string;
20+
onSuccess?: (scheduleId: number) => void;
21+
onError?: (err: ApiErrorResponse) => void;
22+
customClassNames?: {
23+
createScheduleButton?: string;
24+
inputField?: string;
25+
formWrapper?: string;
26+
actionsButtons?: ActionButtonsClassNames;
27+
};
28+
disableToasts?: boolean;
29+
}) => {
30+
const { t } = useLocale();
31+
const [isDialogOpen, setIsDialogOpen] = useState(false);
32+
33+
return (
34+
<AtomsWrapper>
35+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
36+
<DialogTrigger asChild>
37+
<Button
38+
type="button"
39+
data-testid={name}
40+
className={customClassNames?.createScheduleButton}
41+
StartIcon="plus"
42+
onClick={() => setIsDialogOpen(true)}>
43+
{name ?? t("new")}
44+
</Button>
45+
</DialogTrigger>
46+
<DialogContent className="bg-default text-default">
47+
<CreateScheduleForm
48+
customClassNames={{
49+
formWrapper: customClassNames?.formWrapper,
50+
inputField: customClassNames?.inputField,
51+
actionsButtons: {
52+
container: customClassNames?.actionsButtons?.container,
53+
continue: customClassNames?.actionsButtons?.continue,
54+
close: customClassNames?.actionsButtons?.close,
55+
},
56+
}}
57+
onSuccess={(scheduleId) => {
58+
setIsDialogOpen(false);
59+
onSuccess?.(scheduleId);
60+
}}
61+
onError={onError}
62+
onCancel={() => setIsDialogOpen(false)}
63+
disableToasts={disableToasts}
64+
/>
65+
</DialogContent>
66+
</Dialog>
67+
</AtomsWrapper>
68+
);
69+
};

0 commit comments

Comments
 (0)