Skip to content

Commit 048e568

Browse files
ThyMinimalDevhbjORbjSomayChauhansupalarrykeithwillcode
authored
feat: useApiV2AvailableSlots (#21138)
* feat: useApiV2AvailableSlots * feat: useApiV2AvailableSlots * fix type check * remove log * fix unit tests * simplify * feat: add missing params to the slots endpoint * updated documentation * _shouldServeCache is correct * fix types * fix enabled prop * fix type * add embedConnectVersion to query key * add teamId to slot type * fix * add fallback to trpc query if apiv2 fails * add comment * remove logs * fix * refactor: default to isTeamEvent in query params * fix: only return v2 slots once fetch is success * fix last e2e failing test * add feature flag * add migration sql * Update packages/prisma/migrations/20250512153630_add_use_api_v2_for_team_slots_feature_flag/migration.sql --------- Co-authored-by: hbjORbj <[email protected]> Co-authored-by: Somay Chauhan <[email protected]> Co-authored-by: supalarry <[email protected]> Co-authored-by: Keith Williams <[email protected]>
1 parent ade64c0 commit 048e568

File tree

16 files changed

+218
-46
lines changed

16 files changed

+218
-46
lines changed

apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ export class SlotsController_2024_04_15 {
157157
@Query() query: GetAvailableSlotsInput_2024_04_15,
158158
@Req() req: ExpressRequest
159159
): Promise<ApiResponse<{ slots: TimeSlots["slots"] | RangeSlots["slots"] }>> {
160-
const isTeamEvent = await this.slotsService.checkIfIsTeamEvent(query.eventTypeId);
160+
const isTeamEvent =
161+
query.isTeamEvent === undefined
162+
? await this.slotsService.checkIfIsTeamEvent(query.eventTypeId)
163+
: query.isTeamEvent;
161164
const availableSlots = await getAvailableSlots({
162165
input: {
163166
...query,

apps/api/v2/swagger/documentation.json

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18293,19 +18293,19 @@
1829318293
"type": {
1829418294
"type": "string"
1829518295
},
18296-
"value": {
18296+
"option": {
1829718297
"type": "string"
1829818298
},
18299-
"valueId": {
18299+
"optionId": {
1830018300
"type": "string"
1830118301
}
1830218302
},
1830318303
"required": [
1830418304
"id",
1830518305
"name",
1830618306
"type",
18307-
"value",
18308-
"valueId"
18307+
"option",
18308+
"optionId"
1830918309
]
1831018310
},
1831118311
"NumberAttribute": {
@@ -18320,19 +18320,19 @@
1832018320
"type": {
1832118321
"type": "string"
1832218322
},
18323-
"value": {
18323+
"option": {
1832418324
"type": "number"
1832518325
},
18326-
"valueId": {
18326+
"optionId": {
1832718327
"type": "string"
1832818328
}
1832918329
},
1833018330
"required": [
1833118331
"id",
1833218332
"name",
1833318333
"type",
18334-
"value",
18335-
"valueId"
18334+
"option",
18335+
"optionId"
1833618336
]
1833718337
},
1833818338
"SingleSelectAttribute": {
@@ -18347,34 +18347,34 @@
1834718347
"type": {
1834818348
"type": "string"
1834918349
},
18350-
"value": {
18350+
"option": {
1835118351
"type": "string"
1835218352
},
18353-
"valueId": {
18353+
"optionId": {
1835418354
"type": "string"
1835518355
}
1835618356
},
1835718357
"required": [
1835818358
"id",
1835918359
"name",
1836018360
"type",
18361-
"value",
18362-
"valueId"
18361+
"option",
18362+
"optionId"
1836318363
]
1836418364
},
18365-
"MultiSelectAttributeValue": {
18365+
"MultiSelectAttributeOption": {
1836618366
"type": "object",
1836718367
"properties": {
18368-
"valueId": {
18368+
"optionId": {
1836918369
"type": "string"
1837018370
},
18371-
"value": {
18371+
"option": {
1837218372
"type": "string"
1837318373
}
1837418374
},
1837518375
"required": [
18376-
"valueId",
18377-
"value"
18376+
"optionId",
18377+
"option"
1837818378
]
1837918379
},
1838018380
"MultiSelectAttribute": {
@@ -18389,18 +18389,18 @@
1838918389
"type": {
1839018390
"type": "string"
1839118391
},
18392-
"values": {
18392+
"options": {
1839318393
"type": "array",
1839418394
"items": {
18395-
"$ref": "#/components/schemas/MultiSelectAttributeValue"
18395+
"$ref": "#/components/schemas/MultiSelectAttributeOption"
1839618396
}
1839718397
}
1839818398
},
1839918399
"required": [
1840018400
"id",
1840118401
"name",
1840218402
"type",
18403-
"values"
18403+
"options"
1840418404
]
1840518405
},
1840618406
"MembershipUserOutputDto": {

apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
66
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
77
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
88
import { getOrganizationSEOSettings } from "@calcom/features/ee/organizations/lib/orgSettings";
9+
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
910
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
1011
import slugify from "@calcom/lib/slugify";
1112
import prisma from "@calcom/prisma";
@@ -113,8 +114,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
113114
const organizationSettings = getOrganizationSEOSettings(team);
114115
const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false;
115116

117+
const featureRepo = new FeaturesRepository();
118+
const useApiV2 = await featureRepo.checkIfTeamHasFeature(team.id, "use-api-v2-for-team-slots");
119+
116120
return {
117121
props: {
122+
useApiV2,
118123
eventData: {
119124
eventTypeId,
120125
entity: {

apps/web/modules/team/type-view.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ function Type({
3535
crmOwnerRecordType,
3636
crmAppSlug,
3737
isEmbed,
38+
useApiV2,
3839
}: PageProps) {
3940
const searchParams = useSearchParams();
4041

4142
return (
4243
<BookingPageErrorBoundary>
4344
<main className={getBookerWrapperClasses({ isEmbed: !!isEmbed })}>
4445
<Booker
46+
useApiV2={useApiV2}
4547
username={user}
4648
eventSlug={slug}
4749
bookingData={booking}

docs/api-reference/v2/openapi.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16992,14 +16992,14 @@
1699216992
"type": {
1699316993
"type": "string"
1699416994
},
16995-
"value": {
16995+
"option": {
1699616996
"type": "string"
1699716997
},
16998-
"valueId": {
16998+
"optionId": {
1699916999
"type": "string"
1700017000
}
1700117001
},
17002-
"required": ["id", "name", "type", "value", "valueId"]
17002+
"required": ["id", "name", "type", "option", "optionId"]
1700317003
},
1700417004
"NumberAttribute": {
1700517005
"type": "object",
@@ -17013,14 +17013,14 @@
1701317013
"type": {
1701417014
"type": "string"
1701517015
},
17016-
"value": {
17016+
"option": {
1701717017
"type": "number"
1701817018
},
17019-
"valueId": {
17019+
"optionId": {
1702017020
"type": "string"
1702117021
}
1702217022
},
17023-
"required": ["id", "name", "type", "value", "valueId"]
17023+
"required": ["id", "name", "type", "option", "optionId"]
1702417024
},
1702517025
"SingleSelectAttribute": {
1702617026
"type": "object",
@@ -17034,26 +17034,26 @@
1703417034
"type": {
1703517035
"type": "string"
1703617036
},
17037-
"value": {
17037+
"option": {
1703817038
"type": "string"
1703917039
},
17040-
"valueId": {
17040+
"optionId": {
1704117041
"type": "string"
1704217042
}
1704317043
},
17044-
"required": ["id", "name", "type", "value", "valueId"]
17044+
"required": ["id", "name", "type", "option", "optionId"]
1704517045
},
17046-
"MultiSelectAttributeValue": {
17046+
"MultiSelectAttributeOption": {
1704717047
"type": "object",
1704817048
"properties": {
17049-
"valueId": {
17049+
"optionId": {
1705017050
"type": "string"
1705117051
},
17052-
"value": {
17052+
"option": {
1705317053
"type": "string"
1705417054
}
1705517055
},
17056-
"required": ["valueId", "value"]
17056+
"required": ["optionId", "option"]
1705717057
},
1705817058
"MultiSelectAttribute": {
1705917059
"type": "object",
@@ -17067,14 +17067,14 @@
1706717067
"type": {
1706817068
"type": "string"
1706917069
},
17070-
"values": {
17070+
"options": {
1707117071
"type": "array",
1707217072
"items": {
17073-
"$ref": "#/components/schemas/MultiSelectAttributeValue"
17073+
"$ref": "#/components/schemas/MultiSelectAttributeOption"
1707417074
}
1707517075
}
1707617076
},
17077-
"required": ["id", "name", "type", "values"]
17077+
"required": ["id", "name", "type", "options"]
1707817078
},
1707917079
"MembershipUserOutputDto": {
1708017080
"type": "object",

packages/features/bookings/Booker/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface BookerProps {
9696
areInstantMeetingParametersSet?: boolean | null;
9797
userLocale?: string | null;
9898
hasValidLicense?: boolean;
99+
useApiV2?: boolean;
99100
}
100101

101102
export type WrappedBookerPropsMain = {

packages/features/bookings/Booker/utils/event.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const useScheduleForEvent = ({
7171
orgSlug,
7272
teamMemberEmail,
7373
isTeamEvent,
74+
useApiV2 = true,
7475
}: {
7576
prefetchNextMonth?: boolean;
7677
username?: string | null;
@@ -85,6 +86,7 @@ export const useScheduleForEvent = ({
8586
teamMemberEmail?: string | null;
8687
fromRedirectOfNonOrgLink?: boolean;
8788
isTeamEvent?: boolean;
89+
useApiV2?: boolean;
8890
} = {}) => {
8991
const { timezone } = useBookerTime();
9092
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStore(
@@ -95,7 +97,7 @@ export const useScheduleForEvent = ({
9597
const searchParams = useCompatSearchParams();
9698
const rescheduleUid = searchParams?.get("rescheduleUid");
9799

98-
const schedule = useSchedule({
100+
const scheduleUsingApiV2 = useSchedule({
99101
username: usernameFromStore ?? username,
100102
eventSlug: eventSlugFromStore ?? eventSlug,
101103
eventId,
@@ -110,8 +112,32 @@ export const useScheduleForEvent = ({
110112
isTeamEvent,
111113
orgSlug,
112114
teamMemberEmail,
115+
useApiV2: useApiV2,
113116
});
114117

118+
const scheduleNotUsingApiV2 = useSchedule({
119+
username: usernameFromStore ?? username,
120+
eventSlug: eventSlugFromStore ?? eventSlug,
121+
eventId,
122+
timezone,
123+
selectedDate,
124+
prefetchNextMonth,
125+
monthCount,
126+
dayCount,
127+
rescheduleUid,
128+
month: monthFromStore ?? month,
129+
duration: durationFromStore ?? duration,
130+
isTeamEvent,
131+
orgSlug,
132+
teamMemberEmail,
133+
useApiV2: false,
134+
// only run this query if the one using Api v2 fails
135+
// Network error does not trigger `isError` flag, so we are instead using `failureReason` here
136+
enabled: isTeamEvent && !!scheduleUsingApiV2?.failureReason,
137+
});
138+
139+
const schedule = scheduleUsingApiV2?.isSuccess ? scheduleUsingApiV2 : scheduleNotUsingApiV2;
140+
115141
return {
116142
data: schedule?.data,
117143
isPending: schedule?.isPending,

packages/features/embed/Embed.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ const EmailEmbed = ({
278278
eventId: eventType?.id,
279279
isTeamEvent,
280280
duration: selectedDuration,
281+
useApiV2: false,
281282
});
282283
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
283284

packages/features/flags/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export type AppFlags = {
2020
"salesforce-crm-tasker": boolean;
2121
"workflow-smtp-emails": boolean;
2222
"cal-video-log-in-overlay": boolean;
23+
"use-api-v2-for-team-slots": boolean;
2324
};

packages/features/flags/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const initialData: AppFlags = {
1919
"salesforce-crm-tasker": false,
2020
"workflow-smtp-emails": false,
2121
"cal-video-log-in-overlay": false,
22+
"use-api-v2-for-team-slots": false,
2223
};
2324

2425
if (process.env.NEXT_PUBLIC_IS_E2E) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import axios from "axios";
3+
4+
import type { AvailableSlotsType } from "@calcom/platform-libraries";
5+
import type {
6+
GetAvailableSlotsInput_2024_04_15,
7+
ApiResponse,
8+
ApiSuccessResponse,
9+
} from "@calcom/platform-types";
10+
11+
export const QUERY_KEY = "get-available-slots";
12+
13+
export const useApiV2AvailableSlots = ({
14+
enabled,
15+
...rest
16+
}: GetAvailableSlotsInput_2024_04_15 & { enabled: boolean }) => {
17+
const availableSlots = useQuery({
18+
queryKey: [
19+
QUERY_KEY,
20+
rest.startTime,
21+
rest.endTime,
22+
rest.eventTypeId,
23+
rest.eventTypeSlug,
24+
rest.isTeamEvent ?? false,
25+
rest.teamId ?? false,
26+
rest.usernameList,
27+
rest.routedTeamMemberIds,
28+
rest.skipContactOwner,
29+
rest._shouldServeCache,
30+
rest.teamMemberEmail,
31+
rest.embedConnectVersion ?? false,
32+
],
33+
queryFn: () => {
34+
return axios
35+
.get<ApiResponse<AvailableSlotsType>>(`${process.env.NEXT_PUBLIC_API_V2_URL}/slots/available`, {
36+
params: rest,
37+
})
38+
.then((res) => {
39+
if (res.data.status === "success") {
40+
return (res.data as ApiSuccessResponse<AvailableSlotsType>).data;
41+
}
42+
throw new Error(res.data.error.message);
43+
});
44+
},
45+
enabled: enabled,
46+
});
47+
return availableSlots;
48+
};

0 commit comments

Comments
 (0)