Skip to content

Commit 0757b00

Browse files
authored
feat: list schedules atom (calcom#24205)
* feat: add api v2 endpoint to fetch all user schedules * chore: update typing * feat: custom hook to fetch all user schedules * refactor: shift logic to transform schedules into function of its own * init: list schedules atom * feat: api endpoints for list schedules atom * refactor: accept redirect url as prop * fix: pass redirect url prop * integrate list schedules atom with availability settings * refactor: extract types to be reused in api v2 endpoints * skip availability settings page for the time being until we have api v2 endpoints in prod * feat: add docs for list schedules atom * fixup * export Schedule type * make sure we always have a default schedule if user deletes his default schedule * chore: implement code rabbit feedback * chore: implement PR feedback * fix: resolve merge conflicts * fix: import path * update platform libraries * fix: type check * resolve merge conflicts * update atoms export * update platform libraries schedule * chore: arrange atoms in alphabetical order * update atoms controller to include endpoints for list schedules atom * add create atom scheule atom * fix: invalidate schedules on new schedule creation * chore: add changesets
1 parent 3402f79 commit 0757b00

21 files changed

Lines changed: 483 additions & 24 deletions

File tree

.changeset/lemon-rice-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calcom/atoms": minor
3+
---
4+
5+
This PR adds an atom to list a user's schedule. Furthermore, there was also another atom introduced which can be used to create a new schedule for a user.

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ import {
2525
import { z } from "zod";
2626

2727
import { SCHEDULE_READ, SCHEDULE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
28+
import type {
29+
GetAvailabilityListHandlerReturn,
30+
DuplicateScheduleHandlerReturn,
31+
} from "@calcom/platform-libraries/schedules";
32+
import { getAvailabilityListHandler, duplicateScheduleHandler } from "@calcom/platform-libraries/schedules";
2833
import type { CreateScheduleHandlerReturn, CreateScheduleSchema } from "@calcom/platform-libraries/schedules";
2934
import { createScheduleHandler } from "@calcom/platform-libraries/schedules";
3035
import { FindDetailedScheduleByIdReturnType } from "@calcom/platform-libraries/schedules";
@@ -68,6 +73,21 @@ export class AtomsSchedulesController {
6873
};
6974
}
7075

76+
@Get("/schedules/all")
77+
@Version(VERSION_NEUTRAL)
78+
@UseGuards(ApiAuthGuard)
79+
@Permissions([SCHEDULE_READ])
80+
async getAllUserSchedules(
81+
@GetUser() user: UserWithProfile
82+
): Promise<ApiResponse<Awaited<GetAvailabilityListHandlerReturn>>> {
83+
const userSchedules = await getAvailabilityListHandler({ ctx: { user } });
84+
85+
return {
86+
status: SUCCESS_STATUS,
87+
data: userSchedules,
88+
};
89+
}
90+
7191
@Patch("schedules/:scheduleId")
7292
@Permissions([SCHEDULE_WRITE])
7393
@UseGuards(ApiAuthGuard)
@@ -104,4 +124,20 @@ export class AtomsSchedulesController {
104124
data: createdSchedule,
105125
};
106126
}
127+
128+
@Post("schedules/:scheduleId/duplicate")
129+
@Permissions([SCHEDULE_WRITE])
130+
@UseGuards(ApiAuthGuard)
131+
@ApiOperation({ summary: "Duplicate existing schedule" })
132+
async duplicateExistingSchedule(
133+
@GetUser() user: UserWithProfile,
134+
@Param("scheduleId", ParseIntPipe) scheduleId: number
135+
): Promise<ApiResponse<Awaited<DuplicateScheduleHandlerReturn>>> {
136+
const duplicatedSchedule = await duplicateScheduleHandler({ ctx: { user }, input: { scheduleId } });
137+
138+
return {
139+
status: SUCCESS_STATUS,
140+
data: duplicatedSchedule,
141+
};
142+
}
107143
}

apps/web/app/(use-page-wrapper)/(main-nav)/availability/page.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
99
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
1010
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
1111
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
12+
import { getScheduleListItemData } from "@calcom/lib/schedules/transformers/getScheduleListItemData";
1213
import { MembershipRole } from "@calcom/prisma/enums";
1314
import { availabilityRouter } from "@calcom/trpc/server/routers/viewer/availability/_router";
1415

@@ -56,15 +57,7 @@ const Page = async ({ searchParams: _searchParams }: PageProps) => {
5657
// This is because the data is cached and as a result the data is converted to a string
5758
const availabilities = {
5859
...cachedAvailabilities,
59-
schedules: cachedAvailabilities.schedules.map((schedule) => ({
60-
...schedule,
61-
availability: schedule.availability.map((avail) => ({
62-
...avail,
63-
startTime: new Date(avail.startTime),
64-
endTime: new Date(avail.endTime),
65-
date: avail.date ? new Date(avail.date) : null,
66-
})),
67-
})),
60+
schedules: cachedAvailabilities.schedules.map((schedule) => getScheduleListItemData(schedule)),
6861
};
6962

7063
const organizationId = session?.user?.profile?.organizationId ?? session?.user.org?.id;

apps/web/modules/availability/availability-view.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
141141
<ul className="divide-subtle divide-y" data-testid="schedules" ref={animationParentRef}>
142142
{availabilities.schedules.map((schedule) => (
143143
<ScheduleListItem
144+
redirectUrl={`/availability/${schedule.id}`}
144145
displayOptions={{
145146
hour12: user?.timeFormat ? user.timeFormat === 12 : undefined,
146147
timeZone: user?.timeZone,

docs/mint.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,18 +209,19 @@
209209
"group": "Atoms",
210210
"icon": "atom",
211211
"pages": [
212-
"platform/atoms/cal-provider",
213-
"platform/atoms/google-calendar-connect",
214-
"platform/atoms/outlook-calendar-connect",
215212
"platform/atoms/apple-calendar-connect",
216213
"platform/atoms/availability-settings",
217214
"platform/atoms/booker",
218-
"platform/atoms/event-type",
215+
"platform/atoms/cal-provider",
219216
"platform/atoms/calendar-settings",
220-
"platform/atoms/payment-form",
221-
"platform/atoms/conferencing-apps",
222217
"platform/atoms/calendar-view",
223-
"platform/atoms/create-schedule"
218+
"platform/atoms/conferencing-apps",
219+
"platform/atoms/create-schedule",
220+
"platform/atoms/event-type",
221+
"platform/atoms/google-calendar-connect",
222+
"platform/atoms/list-schedules",
223+
"platform/atoms/outlook-calendar-connect",
224+
"platform/atoms/payment-form"
224225
]
225226
},
226227
{
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
title: "List schedules"
3+
---
4+
5+
The List schedules atom displays all of a user's availability schedules and provides management actions such as delete, duplicate, and edit capabilities for each schedule.
6+
7+
Below code snippet can be used to render the List schedules atom
8+
9+
```js
10+
import { ListSchedules } from "@calcom/atoms";
11+
12+
export default function ListSchedules() {
13+
return (
14+
<>
15+
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
16+
</>
17+
);
18+
}
19+
```
20+
21+
For a demonstration of the List schedules atom, please refer to the video below.
22+
23+
<p></p>
24+
25+
<iframe
26+
height="315"
27+
style={{ width: "100%", maxWidth: "560px" }}
28+
src="https://www.loom.com/embed/8e020a4848d543548c519d3a7cd532c6?sid=e6ccad23-8372-402a-8fd3-c0180bc9113c"
29+
frameborder="0"
30+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
31+
allowfullscreen="true"
32+
></iframe>
33+
34+
<p></p>
35+
36+
Below is a list of props that can be passed to the List schedules atom.
37+
38+
<p></p>
39+
40+
| Name | Required | Description |
41+
| :------------- | :------- | :---------------------------------------------------------------------------------------------------------------- |
42+
| getRedirectUrl | No | Callback function that determines where the user should be redirected when they click on a schedule from the atom |
43+
44+
## Combining list schedules atom with availability settings atom
45+
46+
The List schedules atom works best when combined with the Availability settings atom to provide a seamless experience. In order to combine both of atom, here are the steps we recommend:
47+
48+
<Steps>
49+
<Step title="Setup list schedules atom">
50+
Create a page for demostrating the list schedules atom.
51+
52+
Below code snippet can be used to setup the list schedules atom on a new page
53+
54+
```js
55+
import { ListSchedules } from "@calcom/atoms";
56+
57+
export default function ListSchedules() {
58+
return (
59+
<>
60+
<ListSchedules getRedirectUrl={(scheduleId) => `/availability/${scheduleId}`} />
61+
</>
62+
);
63+
}
64+
```
65+
66+
`getRedirectUrl` prop is a callback function that determines where the user should be redirected when they click on a schedule from the atom. Say if you want to display your availability settings atom on the page `/availability/:scheduleId`, you need to make sure you return the exact same url when you call the `getRedirectUrl` function.
67+
68+
</Step>
69+
<Step title="Setup availability settings atom">
70+
Create a page for demonstrating the availability settings atom. This should be a dynamic route that accepts a schedule id as a query parameter, and then the schedule id gets passed to the availability settings atom.
71+
72+
Below code snippet can be used to obtain the `scheduleId` and display the availability settings atom
73+
74+
```js
75+
import { AvailabilitySettings } from "@calcom/atoms";
76+
import { useRouter } from "next/router";
77+
78+
export default function Availability() {
79+
const router = useRouter();
80+
81+
return (
82+
<>
83+
<AvailabilitySettings
84+
id={router.query.scheduleId as string}
85+
onUpdateSuccess={() => {
86+
console.log("Updated schedule successfully");
87+
}}
88+
/>
89+
</>
90+
)
91+
}
92+
```
93+
94+
</Step>
95+
96+
</Steps>
97+
98+
-Below video shows a demonstration of how we combined the list schedules atom and availability settings atom in one of our example app to create a seamless availability management experience.
99+
100+
<p></p>
101+
102+
<iframe
103+
height="315"
104+
style={{ width: "100%", maxWidth: "560px" }}
105+
src="https://www.loom.com/embed/76b65330e61e47eb95b7072ed3a42ec8?sid=9373ec6f-ba55-431e-98de-5dd3fb252a8a"
106+
frameborder="0"
107+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
108+
allowfullscreen="true"
109+
></iframe>
110+
111+
<p></p>
112+
113+
<Info>
114+
Link to the example app can be found
115+
[here](https://github.com/calcom/cal.com/tree/main/packages/platform/examples/base)
116+
</Info>

packages/features/schedules/components/ScheduleListItem.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function ScheduleListItem({
2626
updateDefault,
2727
isDeletable,
2828
duplicateFunction,
29+
redirectUrl,
2930
}: {
3031
schedule: RouterOutputs["viewer"]["availability"]["list"]["schedules"][number];
3132
deleteFunction: ({ scheduleId }: { scheduleId: number }) => void;
@@ -37,17 +38,15 @@ export function ScheduleListItem({
3738
isDeletable: boolean;
3839
updateDefault: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void;
3940
duplicateFunction: ({ scheduleId }: { scheduleId: number }) => void;
41+
redirectUrl: string;
4042
}) {
4143
const { t, i18n } = useLocale();
4244

4345
return (
4446
<li key={schedule.id}>
4547
<div className="hover:bg-muted flex items-center justify-between px-3 py-5 transition sm:px-4">
4648
<div className="group flex w-full items-center justify-between ">
47-
<Link
48-
href={`/availability/${schedule.id}`}
49-
className="flex-grow truncate text-sm"
50-
title={schedule.name}>
49+
<Link href={redirectUrl} className="flex-grow truncate text-sm" title={schedule.name}>
5150
<div className="space-x-2 rtl:space-x-reverse">
5251
<span className="text-emphasis truncate font-medium">{schedule.name}</span>
5352
{schedule.isDefault && (
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export type Schedule = {
2+
isDefault: boolean;
3+
id: number;
4+
name: string;
5+
timeZone: string | null;
6+
availability: {
7+
id: number;
8+
userId: number | null;
9+
startTime: Date;
10+
endTime: Date;
11+
eventTypeId: number | null;
12+
date: Date | null;
13+
days: number[];
14+
scheduleId: number | null;
15+
}[];
16+
};
17+
18+
export const getScheduleListItemData = (schedule: Schedule) => ({
19+
...schedule,
20+
availability: schedule.availability.map((avail) => ({
21+
...avail,
22+
startTime: new Date(avail.startTime),
23+
endTime: new Date(avail.endTime),
24+
date: avail.date ? new Date(avail.date) : null,
25+
})),
26+
});

packages/platform/atoms/hooks/schedules/useAtomCreateSchedule.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { CreateScheduleHandlerReturn } from "@calcom/trpc/server/routers/vi
66
import { TCreateInputSchema as CreateScheduleSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/create.schema";
77

88
import http from "../../lib/http";
9+
import { QUERY_KEY as SchedulesQueryKey } from "./useAtomGetAllSchedules";
910
import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule";
1011

1112
interface useAtomCreateScheduleOptions {
@@ -35,6 +36,7 @@ export const useAtomCreateSchedule = (
3536
if (data.status === SUCCESS_STATUS) {
3637
onSuccess?.(data);
3738
queryClient.invalidateQueries({ queryKey: [ScheduleQueryKey] });
39+
queryClient.invalidateQueries({ queryKey: [SchedulesQueryKey] });
3840
} else {
3941
onError?.(data);
4042
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useMutation, useQueryClient } from "@tanstack/react-query";
2+
3+
import { SUCCESS_STATUS } from "@calcom/platform-constants";
4+
import type { ApiResponse, ApiErrorResponse } from "@calcom/platform-types";
5+
import type { DuplicateScheduleHandlerReturn } from "@calcom/trpc/server/routers/viewer/availability/schedule/duplicate.handler";
6+
7+
import http from "../../lib/http";
8+
import { QUERY_KEY as ScheduleQueryKey } from "./useAtomSchedule";
9+
10+
interface useAtomDuplicateScheduleOptions {
11+
onSuccess?: (res: ApiResponse<DuplicateScheduleHandlerReturn>) => void;
12+
onError?: (err: ApiErrorResponse) => void;
13+
}
14+
15+
export const useAtomDuplicateSchedule = (
16+
{ onSuccess, onError }: useAtomDuplicateScheduleOptions = {
17+
onSuccess: () => {
18+
return;
19+
},
20+
onError: () => {
21+
return;
22+
},
23+
}
24+
) => {
25+
const queryClient = useQueryClient();
26+
27+
return useMutation<ApiResponse<DuplicateScheduleHandlerReturn>, unknown, { scheduleId: number }>({
28+
mutationFn: async ({ scheduleId }: { scheduleId: number }) => {
29+
const url = `atoms/schedules/${scheduleId}/duplicate`;
30+
const response = await http.post<ApiResponse<DuplicateScheduleHandlerReturn>>(url);
31+
return response.data;
32+
},
33+
onSuccess: (data) => {
34+
if (data.status === SUCCESS_STATUS) {
35+
onSuccess?.(data);
36+
queryClient.invalidateQueries({ queryKey: [ScheduleQueryKey] });
37+
} else {
38+
onError?.(data);
39+
}
40+
},
41+
onError: (err) => {
42+
onError?.(err as ApiErrorResponse);
43+
},
44+
});
45+
};

0 commit comments

Comments
 (0)