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:
10 files changed
+95
-37
No files matched your search
@@ -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}
|
||||
|
||||
+8
-2
@@ -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 ?? "");
|
||||
|
||||
Reference in new issue
Block a user