feat: booker atom custom location (#14635)

* frontend: handle booking locationUrl

* backend: pass booking locationUrl to handleNewBooking

* examples app: display booking location if provided

* fix: v2 not starting: contains -> includes

* backend: pass booking locationUrl to handleNewBooking

* swagger docs update

* examples app: dont display where of booking if does not start with http

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Lauris Skraucis
2024-04-18 08:26:36 +00:00
committed by GitHub
co-authored by Keith Williams Morgan
parent 0aa1628925
commit be128181e2
9 changed files with 53 additions and 14 deletions
@@ -57,6 +57,7 @@ type OAuthRequestParams = {
platformRescheduleUrl: string;
platformCancelUrl: string;
platformBookingUrl: string;
platformBookingLocation?: string;
};
const DEFAULT_PLATFORM_PARAMS = {
@@ -65,6 +66,7 @@ const DEFAULT_PLATFORM_PARAMS = {
platformRescheduleUrl: "",
platformBookingUrl: "",
areEmailsEnabled: true,
platformBookingLocation: undefined,
};
@Controller({
@@ -141,12 +143,15 @@ export class BookingsController {
@Post("/")
async createBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput,
@Body() body: CreateBookingInput,
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<unknown>> {
const oAuthClientId = clientId?.toString();
const locationUrl = body.locationUrl;
try {
const booking = await handleNewBooking(await this.createNextApiBookingRequest(req, oAuthClientId));
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
);
return {
status: SUCCESS_STATUS,
data: booking,
@@ -258,13 +263,14 @@ export class BookingsController {
async createNextApiBookingRequest(
req: BookingRequest,
oAuthClientId?: string
oAuthClientId?: string,
platformBookingLocation?: string
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const userId = (await this.getOwnerId(req)) ?? -1;
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(req, oAuthClientId)
: DEFAULT_PLATFORM_PARAMS;
Object.assign(req, { userId, ...oAuthParams });
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
req.body = { ...req.body, areEmailsEnabled: oAuthParams.areEmailsEnabled };
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -98,4 +98,8 @@ export class CreateBookingInput {
@Type(() => Response)
responses!: Response;
@IsString()
@IsOptional()
locationUrl?: string;
}
+4 -1
View File
@@ -4225,6 +4225,9 @@
},
"responses": {
"$ref": "#/components/schemas/Response"
},
"locationUrl": {
"type": "string"
}
},
"required": [
@@ -4270,4 +4273,4 @@
}
}
}
}
}
@@ -909,10 +909,18 @@ async function handler(
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
platformBookingLocation?: string;
},
bookingDataSchemaGetter: BookingDataSchemaGetter = getBookingDataSchema
) {
const { userId, platformClientId, platformCancelUrl, platformBookingUrl, platformRescheduleUrl } = req;
const {
userId,
platformClientId,
platformCancelUrl,
platformBookingUrl,
platformRescheduleUrl,
platformBookingLocation,
} = req;
// handle dynamic user
let eventType =
@@ -1501,7 +1509,7 @@ async function handler(
responses: reqBody.calEventResponses || null,
userFieldsResponses: reqBody.calEventUserFieldsResponses || null,
attendees: attendeesList,
location: bookingLocation, // Will be processed by the EventManager later.
location: platformBookingLocation ?? bookingLocation, // Will be processed by the EventManager later.
conferenceCredentialId,
destinationCalendar,
hideCalendarNotes: eventType.hideCalendarNotes,
@@ -51,6 +51,7 @@ type BookerPlatformWrapperAtomProps = Omit<BookerProps, "entity"> & {
onReserveSlotError?: (data: ApiErrorResponse) => void;
onDeleteSlotSuccess?: (data: ApiSuccessResponseWithoutData) => void;
onDeleteSlotError?: (data: ApiErrorResponse) => void;
locationUrl?: string;
};
export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) => {
@@ -227,6 +228,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
handleBooking: createBooking,
handleInstantBooking: createInstantBooking,
handleRecBooking: createRecBooking,
locationUrl: props.locationUrl,
});
return (
@@ -7,6 +7,8 @@ import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
import http from "../lib/http";
export type UseCreateBookingInput = BookingCreateBody & { locationUrl?: string };
interface IUseCreateBooking {
onSuccess?: (res: ApiSuccessResponse<BookingResponse>) => void;
onError?: (err: ApiErrorResponse | Error) => void;
@@ -21,7 +23,7 @@ export const useCreateBooking = (
},
}
) => {
const createBooking = useMutation<ApiResponse<BookingResponse>, Error, BookingCreateBody>({
const createBooking = useMutation<ApiResponse<BookingResponse>, Error, UseCreateBookingInput>({
mutationFn: (data) => {
return http.post<ApiResponse<BookingResponse>>("/bookings", data).then((res) => {
if (res.data.status === SUCCESS_STATUS) {
@@ -9,14 +9,17 @@ import {
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { BookingCreateBody } from "@calcom/prisma/zod-utils";
import type { UseCreateBookingInput } from "./useCreateBooking";
type UseHandleBookingProps = {
bookingForm: UseBookingFormReturnType["bookingForm"];
event: useEventReturnType;
metadata: Record<string, string>;
hashedLink?: string | null;
handleBooking: (input: BookingCreateBody) => void;
handleBooking: (input: UseCreateBookingInput) => void;
handleInstantBooking: (input: BookingCreateBody) => void;
handleRecBooking: (input: BookingCreateBody[]) => void;
locationUrl?: string;
};
export const useHandleBookEvent = ({
@@ -27,6 +30,7 @@ export const useHandleBookEvent = ({
handleBooking,
handleInstantBooking,
handleRecBooking,
locationUrl,
}: UseHandleBookingProps) => {
const setFormValues = useBookerStore((state) => state.setFormValues);
const timeslot = useBookerStore((state) => state.selectedTimeslot);
@@ -80,7 +84,7 @@ export const useHandleBookEvent = ({
} else if (event.data?.recurringEvent?.freq && recurringEventCount && !rescheduleUid) {
handleRecBooking(mapRecurringBookingToMutationInput(bookingInput, recurringEventCount));
} else {
handleBooking(mapBookingToMutationInput(bookingInput));
handleBooking({ ...mapBookingToMutationInput(bookingInput), locationUrl });
}
// Clears form values stored in store, so old values won't stick around.
setFormValues({});
@@ -87,10 +87,10 @@ export default function Bookings(props: { calUsername: string; calEmail: string
<div>
<div>
<h4>
{booking.user.name}{" "}
{booking.user?.name}{" "}
<span className="rounded-md bg-blue-800 px-2 text-sm text-white">Host</span>
</h4>
<p>{booking.user.email}</p>
<p>{booking.user?.email}</p>
</div>
</div>
<div>
@@ -101,6 +101,16 @@ export default function Bookings(props: { calUsername: string; calEmail: string
</div>
</div>
</div>
{!!booking.location && booking.location.startsWith("http") && (
<div className="flex gap-[70px]">
<div>
<h4>Where</h4>
</div>
<div>
<p>{booking.location}</p>
</div>
</div>
)}
{booking.responses.notes && (
<div className="flex gap-[70px]">
<div className="w-[40px]">
@@ -133,7 +143,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
className="underline"
onClick={() => {
cancelBooking({
id: parseInt(booking.id),
id: booking.id,
uid: booking.uid,
cancellationReason: "User request",
allRemainingBookings: true,
@@ -26,7 +26,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
{!isLoadingEvents && !eventTypeSlug && Boolean(eventTypes?.length) && !rescheduleUid && (
<div className="flex flex-col gap-4">
{eventTypes?.map((event: { id: number; slug: string; title: string }) => {
{eventTypes?.map((event: { id: number; slug: string; title: string; length: number }) => {
const formatEventSlug = event.slug
.split("-")
.map((item) => `${item[0].toLocaleUpperCase()}${item.slice(1)}`)