feat: overlay calendar for Booker atom (#16551)

* add prop to see if overlay calendar is enabled

* fix merge conflicts

* overlay calendar enabled prop

* cleanup

* hide toggle group if platform

* remove isPlatform conditional for overlay calendar

* dont display overlay calendar continue modal for platform

* fix images not appearing for platform

* update booker store

* update booker

* update booker

* shifting variables from booker store to useOverlayCalendar store

* update booker platform wrapper

* update booking page view

* revert changes to overlay calendar store

* update typings

* update toggle set value

* update handler to toggle connected calendar

* fixup

* update booker layout

* fixup! Merge branch 'main' into overlay-calendar-for-booker-atom

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
authored and GitHub committed 2024-09-13 08:13:20 +00:00
1 parent f2b3e620b4
commit 111e2f30c6
10 files changed
+95 -37

No files matched your search

+3 -3
View File
@@ -291,7 +291,9 @@ const BookerComponent = ({
(layout === BookerLayouts.COLUMN_VIEW || layout === BookerLayouts.WEEK_VIEW) &&
"bg-default dark:bg-muted sticky top-0 z-10"
)}>
{!isPlatform ? (
{isPlatform && layout === BookerLayouts.MONTH_VIEW ? (
<></>
) : (
<Header
isMyLink={Boolean(username === sessionUsername)}
eventSlug={eventSlug}
@@ -321,8 +323,6 @@ const BookerComponent = ({
)
}
/>
) : (
<></>
)}
</BookerSection>
)}
@@ -1,6 +1,7 @@
import { useCallback, useMemo } from "react";
import { shallow } from "zustand/shallow";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -170,6 +171,7 @@ const LayoutToggle = ({
enabledLayouts?: BookerLayouts[];
}) => {
const isEmbed = useIsEmbed();
const isPlatform = useIsPlatform();
const { t } = useLocale();
@@ -199,6 +201,10 @@ const LayoutToggle = ({
return null;
}
// just like embed the layout toggle doesn't look rightly placed in platform
// the layout can be toggled via props in the booker atom
if (isPlatform) return null;
return <ToggleGroup onValueChange={onLayoutToggle} defaultValue={layout} options={layoutOptions} />;
};
@@ -1,3 +1,5 @@
import { useIsPlatform } from "@calcom/atoms/monorepo";
import type { UseCalendarsReturnType } from "../hooks/useCalendars";
import { useOverlayCalendar } from "../hooks/useOverlayCalendar";
import { OverlayCalendarContinueModal } from "./OverlayCalendarContinueModal";
@@ -29,6 +31,7 @@ export const OverlayCalendar = ({
handleClickContinue,
hasSession,
}: OverlayCalendarProps) => {
const isPlatform = useIsPlatform();
const {
handleCloseContinueModal,
handleCloseSettingsModal,
@@ -37,6 +40,12 @@ export const OverlayCalendar = ({
handleToggleConnectedCalendar,
checkIsCalendarToggled,
} = useOverlayCalendar({ connectedCalendars, overlayBusyDates, onToggleCalendar });
// on platform we don't handle connecting to third party calendar via booker yet
if (isPlatform && connectedCalendars?.length === 0) {
return <></>;
}
return (
<>
<OverlayCalendarSwitch
@@ -44,11 +53,13 @@ export const OverlayCalendar = ({
hasSession={hasSession}
onStateChange={handleSwitchStateChange}
/>
<OverlayCalendarContinueModal
open={isOpenOverlayContinueModal}
onClose={handleCloseContinueModal}
onContinue={handleClickContinue}
/>
{!isPlatform && (
<OverlayCalendarContinueModal
open={isOpenOverlayContinueModal}
onClose={handleCloseContinueModal}
onContinue={handleClickContinue}
/>
)}
<OverlayCalendarSettingsModal
connectedCalendars={connectedCalendars}
open={isOpenOverlaySettingsModal}
@@ -1,6 +1,7 @@
import Link from "next/link";
import { Fragment } from "react";
import { useIsPlatform } from "@calcom/atoms/monorepo";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import {
@@ -52,6 +53,7 @@ export function OverlayCalendarSettingsModal({
checkIsCalendarToggled,
}: IOverlayCalendarSettingsModalProps) {
const { t } = useLocale();
const isPlatform = useIsPlatform();
return (
<>
@@ -93,8 +95,12 @@ export function OverlayCalendarSettingsModal({
"h-10 w-10",
item.integration.logo.includes("-dark") && "dark:invert"
)}
src={item.integration.logo}
alt={item.integration.title}
src={
isPlatform
? `https://app.cal.com${item.integration.logo}`
: item.integration.logo
}
alt={`${item.integration.title} logo`}
/>
)
}
@@ -14,6 +14,11 @@ export type UseCalendarsReturnType = ReturnType<typeof useCalendars>;
type UseCalendarsProps = {
hasSession: boolean;
};
export type ToggledConnectedCalendars = Set<{
credentialId: number;
externalId: string;
}>;
export const useCalendars = ({ hasSession }: UseCalendarsProps) => {
const searchParams = useSearchParams();
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -64,7 +69,7 @@ export const useCalendars = ({ hasSession }: UseCalendarsProps) => {
isOverlayCalendarEnabled: switchEnabled,
connectedCalendars: data?.connectedCalendars || [],
loadingConnectedCalendar: isPending,
onToggleCalendar: () => {
onToggleCalendar: (data: ToggledConnectedCalendars) => {
utils.viewer.availability.calendarOverlay.reset();
},
};
@@ -32,9 +32,10 @@ export function useLocalSet<T extends HasExternalId>(key: string, initialValue:
});
};
const toggleValue = (value: T) => {
const toggleValue = (value: T): Set<T> => {
let newSet = new Set<T>();
setSet((prevSet) => {
const updatedSet = new Set(prevSet);
const updatedSet = new Set<T>(prevSet);
let itemFound = false;
updatedSet.forEach((item) => {
@@ -48,8 +49,12 @@ export function useLocalSet<T extends HasExternalId>(key: string, initialValue:
updatedSet.add(value);
}
newSet = updatedSet;
return updatedSet;
});
return newSet;
};
const hasItem = (value: T) => {
@@ -66,12 +66,12 @@ export const useOverlayCalendar = ({
}, [hasItem, set, initalised]);
const handleToggleConnectedCalendar = (externalCalendarId: string, credentialId: number) => {
toggleValue({
const calendarsToLoad = toggleValue({
credentialId: credentialId,
externalId: externalCalendarId,
});
setOverlayBusyDates([]);
onToggleCalendar();
onToggleCalendar(calendarsToLoad);
};
return {
@@ -1,11 +1,10 @@
import { useQueryClient } from "@tanstack/react-query";
import { useMemo, useEffect } from "react";
import { useMemo, useEffect, useCallback, useState } from "react";
import { shallow } from "zustand/shallow";
import dayjs from "@calcom/dayjs";
import type { BookerProps } from "@calcom/features/bookings/Booker";
import { Booker as BookerComponent } from "@calcom/features/bookings/Booker";
import { useOverlayCalendarStore } from "@calcom/features/bookings/Booker/components/OverlayCalendar/store";
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
@@ -75,8 +74,11 @@ export type BookerPlatformWrapperAtomProps = Omit<
onDeleteSlotSuccess?: (data: ApiSuccessResponseWithoutData) => void;
onDeleteSlotError?: (data: ApiErrorResponse) => void;
locationUrl?: string;
view?: VIEW_TYPE;
};
type VIEW_TYPE = keyof typeof BookerLayouts;
type BookerPlatformWrapperAtomPropsForIndividual = BookerPlatformWrapperAtomProps & {
username: string | string[];
isTeamEvent?: false;
@@ -91,6 +93,9 @@ type BookerPlatformWrapperAtomPropsForTeam = BookerPlatformWrapperAtomProps & {
export const BookerPlatformWrapper = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => {
const { view = "MONTH_VIEW" } = props;
const layout = BookerLayouts[view];
const { clientId } = useAtomsContext();
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
@@ -101,6 +106,11 @@ export const BookerPlatformWrapper = (
const bookingData = useBookerStore((state) => state.bookingData);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
const setSelectedMonth = useBookerStore((state) => state.setMonth);
const [isOverlayCalendarEnabled, setIsOverlayCalendarEnabled] = useState(
Boolean(localStorage?.getItem?.("overlayCalendarSwitchDefault"))
);
useGetBookingForReschedule({
uid: props.rescheduleUid ?? props.bookingUid ?? "",
onSuccess: (data) => {
@@ -175,7 +185,7 @@ export const BookerPlatformWrapper = (
eventId: event.data?.id,
rescheduleUid: props.rescheduleUid ?? null,
bookingUid: props.bookingUid ?? null,
layout: bookerLayout.defaultLayout,
layout: layout,
org: props.entity?.orgSlug,
username,
bookingData,
@@ -302,12 +312,9 @@ export const BookerPlatformWrapper = (
});
const slots = useSlots(event);
const [calendarSettingsOverlay] = useOverlayCalendarStore(
(state) => [state.calendarSettingsOverlayModal, state.setCalendarSettingsOverlayModal],
shallow
);
const { data: connectedCalendars, isPending: fetchingConnectedCalendars } = useConnectedCalendars({
enabled: !!calendarSettingsOverlay,
enabled: hasSession,
});
const calendars = connectedCalendars as ConnectedDestinationCalendars;
@@ -315,20 +322,21 @@ export const BookerPlatformWrapper = (
credentialId: number;
externalId: string;
}>("toggledConnectedCalendars", []);
const [latestCalendarsToLoad, setLatestCalendarsToLoad] = useState(
Array.from(set).map((item) => ({
credentialId: item.credentialId,
externalId: item.externalId,
}))
);
const { data: overlayBusyDates } = useCalendarsBusyTimes({
loggedInUsersTz: session?.data?.timeZone || "Europe/London",
dateFrom: selectedDate,
dateTo: selectedDate,
calendarsToLoad: Array.from(set).map((item) => ({
credentialId: item.credentialId,
externalId: item.externalId,
})),
calendarsToLoad: latestCalendarsToLoad,
onError: () => {
clearSet();
},
enabled: Boolean(
hasSession && set.size > 0 && localStorage?.getItem("overlayCalendarSwitchDefault") === "true"
),
enabled: Boolean(hasSession && isOverlayCalendarEnabled && latestCalendarsToLoad?.length > 0),
});
const handleBookEvent = useHandleBookEvent({
@@ -342,6 +350,18 @@ export const BookerPlatformWrapper = (
locationUrl: props.locationUrl,
});
const onOverlaySwitchStateChange = useCallback(
(state: boolean) => {
setIsOverlayCalendarEnabled(state);
if (state) {
localStorage.setItem("overlayCalendarSwitchDefault", "true");
} else {
localStorage.removeItem("overlayCalendarSwitchDefault");
}
},
[setIsOverlayCalendarEnabled]
);
useEffect(() => {
// reset booker whenever it's unmounted
return () => {
@@ -361,6 +381,11 @@ export const BookerPlatformWrapper = (
});
setBookingData(null);
}
if (isOverlayCalendarEnabled) {
localStorage.setItem("overlayCalendarSwitchDefault", "true");
} else {
localStorage.removeItem("overlayCalendarSwitchDefault");
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -397,9 +422,7 @@ export const BookerPlatformWrapper = (
onClickOverlayContinue={function (): void {
throw new Error("Function not implemented.");
}}
onOverlaySwitchStateChange={function (): void {
throw new Error("Function not implemented.");
}}
onOverlaySwitchStateChange={onOverlaySwitchStateChange}
extraOptions={extraOptions ?? {}}
bookings={{
handleBookEvent: () => {
@@ -423,10 +446,12 @@ export const BookerPlatformWrapper = (
slots={slots}
calendars={{
overlayBusyDates: overlayBusyDates?.data,
isOverlayCalendarEnabled: false,
isOverlayCalendarEnabled: isOverlayCalendarEnabled,
connectedCalendars: calendars?.connectedCalendars || [],
loadingConnectedCalendar: fetchingConnectedCalendars,
onToggleCalendar: () => {
onToggleCalendar: (data) => {
const calendarsToLoad = Array.from(data ?? []);
setLatestCalendarsToLoad(calendarsToLoad);
return;
},
}}
@@ -14,7 +14,7 @@ export const useCalendarsBusyTimes = ({ onError, enabled, ...rest }: UseCalendar
const availableSlots = useQuery({
queryKey: [
QUERY_KEY,
rest?.calendarsToLoad?.toString() ?? "",
rest?.calendarsToLoad?.length ?? 0,
rest.dateFrom ?? "",
rest.dateTo ?? "",
rest.loggedInUsersTz,
@@ -89,8 +89,8 @@ export default function Bookings(props: { calUsername: string; calEmail: string
{!bookingTitle && eventTypeSlug && !rescheduleUid && (
<>
<h1>{eventTypeSlug}</h1>
<Booker
view="WEEK_VIEW"
eventSlug={eventTypeSlug}
onCreateBookingSuccess={(data) => {
setBookingTitle(data.data.title ?? "");