Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: atoms team booking #14525

Merged
merged 40 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
388b240
feat: setup usePublicEvent with orgSlug and duration
supalarry Apr 10, 2024
f368c55
feat: setup useAvailableSlots with eventTypeSlug and orgSlug
supalarry Apr 10, 2024
72ba838
fix & feat: fix TS issues and allow passing multiple users to Booker
supalarry Apr 10, 2024
0a4f92c
refactor: dont show [15min] [30min] [60min] [1h30min] picker
supalarry Apr 10, 2024
c11b4d4
fix: pass orgSlug to handleNewBooking -> loadUsers -> findUsersByUser…
supalarry Apr 11, 2024
01ab4de
refactor: display attendees in booking confirmation screen
supalarry Apr 11, 2024
d1de780
fix: getting slots get orgSlug from props not event
supalarry Apr 11, 2024
90c5876
revert: Booker username use props instead of hardcoded values
supalarry Apr 11, 2024
a19e5e2
refactor: setup BookerStore org and use it for selectedTime + isDynam…
supalarry Apr 11, 2024
8318741
refactor: hide 'what is this meeting about' input in final booking step
supalarry Apr 11, 2024
b2d330c
fix: TS error
supalarry Apr 11, 2024
114480c
Merge branch 'main' into booker-dynamic
supalarry Apr 11, 2024
ae2aaed
revert: what is the meeting about field hiding
supalarry Apr 11, 2024
259ca13
revert: make org as org? in booker store
supalarry Apr 11, 2024
1cd699d
revert: createNewBooking -> loadUsers -> get forced orgSlug from head…
supalarry Apr 11, 2024
813622c
refactor: useHandleBookEvent get orgSlug from booker store not props
supalarry Apr 11, 2024
4348147
fix: if entity undefined dont access orgSlug
supalarry Apr 11, 2024
ed3b630
Merge branch 'main' into booker-dynamic
ThyMinimalDev Apr 11, 2024
8fa7583
Merge branch 'main' into booker-dynamic
supalarry Apr 12, 2024
b595cc4
refactor: add isDynamic prop as queryKey for usePublicEvent
supalarry Apr 12, 2024
bd9c0ca
refactor: remove unused prop
supalarry Apr 12, 2024
3117e54
Merge branch 'main' into booker-dynamic
supalarry Apr 18, 2024
49e7bd5
re-add docs
supalarry Apr 18, 2024
3113843
Merge branch 'main' into booker-dynamic
supalarry Apr 19, 2024
9dca9e5
fix: typescript error
supalarry Apr 19, 2024
c1e5bda
fix: force platform orgSlug in handleNewBooking.ts
supalarry Apr 23, 2024
c20f36a
Merge branch 'main' into booker-dynamic
supalarry Apr 23, 2024
274801d
Merge branch 'main' into booker-dynamic
supalarry Apr 23, 2024
20c00c0
Merge branch 'main' into booker-dynamic
supalarry Apr 24, 2024
b3cfad1
Merge branch 'main' into booker-dynamic
supalarry Apr 24, 2024
26f51af
Merge branch 'main' into booker-dynamic
supalarry Apr 26, 2024
21dc646
Merge branch 'main' into booker-dynamic
supalarry Apr 26, 2024
c24464e
Merge branch 'main' into booker-dynamic
ThyMinimalDev Apr 30, 2024
1743cf4
fix: remove duplicate setSelectedDuration declaration
ThyMinimalDev Apr 30, 2024
485c521
refactor: destructure properies from body instead of req.body
supalarry May 3, 2024
9576aa1
Merge branch 'main' into booker-dynamic
ThyMinimalDev May 6, 2024
ff91ace
Merge branch 'main' into booker-dynamic
ThyMinimalDev May 7, 2024
872b7e3
fix: entity optional and hide event members
ThyMinimalDev May 7, 2024
d91bbfa
Merge branch 'main' into booker-dynamic
ThyMinimalDev May 7, 2024
334811f
Merge branch 'main' into booker-dynamic
supalarry May 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ export class BookingsController {
@Headers(X_CAL_CLIENT_ID) clientId?: string
): Promise<ApiResponse<unknown>> {
const oAuthClientId = clientId?.toString();
const locationUrl = body.locationUrl;

const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
const booking = await handleNewBooking(
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
Expand Down
4 changes: 4 additions & 0 deletions apps/api/v2/src/ee/bookings/inputs/create-booking.input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export class CreateBookingInput {
@Type(() => Response)
responses!: Response;

@IsString()
@IsOptional()
orgSlug?: string;

@IsString()
@IsOptional()
locationUrl?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export class GetPublicEventTypeQueryParams {
@ApiProperty({ required: false })
@IsString()
@IsOptional()
org?: string;
org?: string | null;
}
4 changes: 4 additions & 0 deletions apps/api/v2/swagger/documentation.json
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@
"required": false,
"in": "query",
"schema": {
"nullable": true,
"type": "string"
}
}
Expand Down Expand Up @@ -4226,6 +4227,9 @@
"responses": {
"$ref": "#/components/schemas/Response"
},
"orgSlug": {
"type": "string"
},
"locationUrl": {
"type": "string"
}
Expand Down
8 changes: 7 additions & 1 deletion packages/features/bookings/Booker/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,13 @@ export type BookerStore = {
* both the slug and the event slug.
*/
isTeamEvent: boolean;
org?: string | null;
seatedEventData: SeatedEventData;
setSeatedEventData: (seatedEventData: SeatedEventData) => void;

isInstantMeeting?: boolean;

org?: string | null;
setOrg: (org: string | null | undefined) => void;
};

/**
Expand Down Expand Up @@ -342,6 +344,10 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
setFormValues: (formValues: Record<string, any>) => {
set({ formValues });
},
org: null,
setOrg: (org: string | null | undefined) => {
set({ org });
},
}));

export const useInitializeBookerStore = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { TFunction } from "next-i18next";
import { useEffect } from "react";

import { useIsPlatform } from "@calcom/atoms/monorepo";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
Expand Down Expand Up @@ -37,6 +38,7 @@ export const getDurationFormatted = (mins: number | undefined, t: TFunction) =>

export const EventDuration = ({ event }: { event: PublicEvent }) => {
const { t } = useLocale();
const isPlatform = useIsPlatform();
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [
state.selectedDuration,
state.setSelectedDuration,
Expand All @@ -52,7 +54,7 @@ export const EventDuration = ({ event }: { event: PublicEvent }) => {
setSelectedDuration(event.length);
}, [selectedDuration, setSelectedDuration, event.metadata?.multipleDuration, event.length, isDynamicEvent]);

if (!event?.metadata?.multipleDuration && !isDynamicEvent)
if ((!event?.metadata?.multipleDuration && !isDynamicEvent) || isPlatform)
return <>{getDurationFormatted(event.length, t)}</>;
ThyMinimalDev marked this conversation as resolved.
Show resolved Hide resolved

const durations = event?.metadata?.multipleDuration || [15, 30, 60, 90];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type BookingOptions = {
bookingUid?: string;
seatReferenceUid?: string;
hashedLink?: string | null;
orgSlug?: string;
};

export const mapBookingToMutationInput = ({
Expand All @@ -34,6 +35,7 @@ export const mapBookingToMutationInput = ({
bookingUid,
seatReferenceUid,
hashedLink,
orgSlug,
}: BookingOptions): BookingCreateBody => {
return {
...values,
Expand All @@ -53,6 +55,7 @@ export const mapBookingToMutationInput = ({
bookingUid,
seatReferenceUid,
hashedLink,
orgSlug,
};
};

Expand Down
13 changes: 12 additions & 1 deletion packages/features/ee/organizations/lib/orgDomains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,17 @@ export function getOrgSlug(hostname: string, forcedSlug?: string) {
}

export function orgDomainConfig(req: IncomingMessage | undefined, fallback?: string | string[]) {
const forPlatform = isPlatformRequest(req);
const forcedSlugHeader = req?.headers?.["x-cal-force-slug"];

const forcedSlug = forcedSlugHeader instanceof Array ? forcedSlugHeader[0] : forcedSlugHeader;

if (forPlatform && forcedSlug) {
return {
isValidOrgDomain: true,
currentOrgDomain: forcedSlug,
};
}

const hostname = req?.headers?.host || "";
return getOrgDomainConfigFromHostname({
hostname,
Expand All @@ -65,6 +72,10 @@ export function orgDomainConfig(req: IncomingMessage | undefined, fallback?: str
});
}

function isPlatformRequest(req: IncomingMessage | undefined) {
return !!req?.headers?.["x-cal-client-id"];
}

export function getOrgDomainConfigFromHostname({
hostname,
fallback,
Expand Down
44 changes: 37 additions & 7 deletions packages/platform/atoms/booker/BookerPlatformWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ import { usePublicEvent } from "../hooks/usePublicEvent";
import { useSlots } from "../hooks/useSlots";
import { AtomsWrapper } from "../src/components/atoms-wrapper";

type BookerPlatformWrapperAtomProps = Omit<BookerProps, "entity"> & {
type BookerPlatformWrapperAtomProps = Omit<BookerProps, "username"> & {
rescheduleUid?: string;
bookingUid?: string;
firstName?: string;
lastName?: string;
guests?: string[];
name?: string;
username: string | string[];
onCreateBookingSuccess?: (data: ApiSuccessResponse<BookingResponse>) => void;
onCreateBookingError?: (data: ApiErrorResponse | Error) => void;
onCreateRecurringBookingSuccess?: (data: ApiSuccessResponse<BookingResponse[]>) => void;
Expand All @@ -59,8 +60,8 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
const setSelectedDate = useBookerStore((state) => state.setSelectedDate);
const setSelectedDuration = useBookerStore((state) => state.setSelectedDuration);
const setBookingData = useBookerStore((state) => state.setBookingData);
const setOrg = useBookerStore((state) => state.setOrg);
const bookingData = useBookerStore((state) => state.bookingData);

const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const setSelectedMonth = useBookerStore((state) => state.setMonth);
const { data: booking } = useGetBookingForReschedule({
Expand All @@ -70,26 +71,45 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
},
});

const username = useMemo(() => {
return formatUsername(props.username);
}, [props.username]);

useEffect(() => {
// reset booker whenever it's unmounted
return () => {
setBookerState("loading");
setSelectedDate(null);
setSelectedTimeslot(null);
setSelectedDuration(null);
setOrg(null);
setSelectedMonth(null);
setSelectedDuration(null);
};
}, []);

const event = usePublicEvent({ username: props.username, eventSlug: props.eventSlug });
setSelectedDuration(props.duration ?? null);
setOrg(props.entity?.orgSlug ?? null);

const isDynamic = useMemo(() => {
return getUsernameList(username ?? "").length > 1;
}, [username]);

const event = usePublicEvent({
username,
eventSlug: props.eventSlug,
isDynamic,
});

const bookerLayout = useBookerLayout(event.data);
useInitializeBookerStore({
...props,
eventId: event.data?.id,
rescheduleUid: props.rescheduleUid ?? null,
bookingUid: props.bookingUid ?? null,
layout: bookerLayout.defaultLayout,
org: event.data?.entity.orgSlug,
org: props.entity?.orgSlug,
username,
bookingData,
});
const [dayCount] = useBookerStore((state) => [state.dayCount, state.setDayCount], shallow);
Expand Down Expand Up @@ -133,20 +153,23 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
prefetchNextMonth,
selectedDate,
});

const schedule = useAvailableSlots({
usernameList: getUsernameList(props.username ?? ""),
usernameList: getUsernameList(username ?? ""),
eventTypeId: event?.data?.id ?? 0,
startTime,
endTime,
timeZone: session?.data?.timeZone,
duration: selectedDuration ?? undefined,
rescheduleUid: props.rescheduleUid,
enabled:
Boolean(props.username) &&
Boolean(username) &&
Boolean(month) &&
Boolean(timezone) &&
// Should only wait for one or the other, not both.
(Boolean(eventSlug) || Boolean(event?.data?.id) || event?.data?.id === 0),
orgSlug: props.entity?.orgSlug ?? undefined,
eventTypeSlug: isDynamic ? "dynamic" : undefined,
});

const bookerForm = useBookingForm({
Expand Down Expand Up @@ -242,7 +265,7 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
<BookerComponent
customClassNames={props.customClassNames}
eventSlug={props.eventSlug}
username={props.username}
username={username}
entity={
event?.data?.entity ?? {
considerUnpublished: false,
Expand Down Expand Up @@ -324,3 +347,10 @@ export const BookerPlatformWrapper = (props: BookerPlatformWrapperAtomProps) =>
</AtomsWrapper>
);
};

function formatUsername(username: string | string[]): string {
if (typeof username === "string") {
return username;
}
return username.join("+");
}
3 changes: 3 additions & 0 deletions packages/platform/atoms/hooks/useHandleBookEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const useHandleBookEvent = ({
const bookingData = useBookerStore((state) => state.bookingData);
const seatedEventData = useBookerStore((state) => state.seatedEventData);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const orgSlug = useBookerStore((state) => state.org);

const handleBookEvent = () => {
const values = bookingForm.getValues();
if (timeslot) {
Expand Down Expand Up @@ -77,6 +79,7 @@ export const useHandleBookEvent = ({
username: username || "",
metadata: metadata,
hashedLink,
orgSlug: orgSlug ? orgSlug : undefined,
};

if (isInstantMeeting) {
Expand Down
17 changes: 15 additions & 2 deletions packages/platform/atoms/hooks/usePublicEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,23 @@ import http from "../lib/http";
export const QUERY_KEY = "get-public-event";
export type UsePublicEventReturnType = ReturnType<typeof usePublicEvent>;

export const usePublicEvent = (props: { username: string; eventSlug: string }) => {
type Props = {
username: string;
eventSlug: string;
isDynamic?: boolean;
};

export const usePublicEvent = (props: Props) => {
const [username, eventSlug] = useBookerStore((state) => [state.username, state.eventSlug], shallow);
const isTeamEvent = useBookerStore((state) => state.isTeamEvent);
const org = useBookerStore((state) => state.org);
const selectedDuration = useBookerStore((state) => state.selectedDuration);

const requestUsername = username ?? props.username;
const requestEventSlug = eventSlug ?? props.eventSlug;

const event = useQuery({
queryKey: [QUERY_KEY, username ?? props.username, eventSlug ?? props.eventSlug],
queryKey: [QUERY_KEY, username ?? props.username, eventSlug ?? props.eventSlug, props.isDynamic],
queryFn: () => {
return http
.get<ApiResponse<PublicEventType>>(
Expand All @@ -34,6 +41,12 @@ export const usePublicEvent = (props: { username: string; eventSlug: string }) =
)
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
if (props.isDynamic && selectedDuration && res.data.data) {
// note(Lauris): Mandatory - In case of "dynamic" event type default event duration returned by the API is 30,
// but we are re-using the dynamic event type as a team event, so we must set the event length to whatever the event length is.
res.data.data.length = selectedDuration;
}

return res.data.data;
}
throw new Error(res.data.error.message);
Expand Down
17 changes: 11 additions & 6 deletions packages/platform/examples/base/src/pages/[bookingUid].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,17 @@ export default function Bookings(props: { calUsername: string; calEmail: string
<p>{booking.user?.email}</p>
</div>
</div>
<div>
<div>
<h4>{`${booking.responses.name}`}</h4>
<p>{`${booking.responses.email}`}</p>
</div>
</div>
{booking.attendees.map((attendee, i) => {
return (
<div key={`${i}-${attendee.name}`}>
<br />
<div>
<h4>{`${attendee.name}`}</h4>
<p>{`${attendee.email}`}</p>
</div>
</div>
);
})}
</div>
</div>
{!!booking.location && booking.location.startsWith("http") && (
Expand Down
Loading
Loading