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:
co-authored by
Keith Williams
Morgan
parent
0aa1628925
commit
be128181e2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)}`)
|
||||
|
||||
Reference in New Issue
Block a user