chore: Disables syncing of calendarList on overlay calendar fetch (#27020)
* chore: Disables syncing of calendarList on overlay calendar fetch * Unwrap ToggledConnectedCalendars * Pick the right type for connectedCalendars * further type amends * Type fixes, tons of em * Some further fixups consistent with what was before * fix: resolve type incompatibility in getConnectedDestinationCalendars return type Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: update DestinationCalendarProps to accept tRPC handler return type Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: use generic type for DestinationCalendarProps to accept tRPC enriched types Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: simplify DestinationCalendarProps type for better compatibility Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: export ConnectedCalendar type for consistent type inference Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: use permissive type for connectedCalendars to accept tRPC enriched types Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: export ConnectedCalendar and ConnectedDestinationCalendars types from platform-libraries Co-Authored-By: alex@cal.com <me@alexvanandel.com> * fix: update ConnectedCalendar type to use boolean | null for primary field Co-Authored-By: alex@cal.com <me@alexvanandel.com> * Update ConnectedCalendarItem * Undo some of Devins fixes, more fixes * Fixup the destination calendar return type, historically not null * Change init to undefined to deal with null * Approach to connect selected calendars with the right credential * This return type is used way too much everywhere, not refactoring * Add the selectedCalendars to the type * Actually fix overlay calendar * set calendarsToLoad param as required * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Add new translation for 'Calendar Settings' --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
Devin AI
parent
c62f333337
commit
c9bd18e866
@@ -50,7 +50,7 @@ export const DestinationCalendarSettingsWebWrapper = ({
|
||||
};
|
||||
|
||||
const validatedReminderValue = reminderSchema.safeParse(
|
||||
calendars.data.destinationCalendar.customCalendarReminder
|
||||
calendars.data.destinationCalendar?.customCalendarReminder
|
||||
);
|
||||
let reminderValue: ReminderMinutes = null;
|
||||
if (validatedReminderValue.success) {
|
||||
@@ -63,7 +63,7 @@ export const DestinationCalendarSettingsWebWrapper = ({
|
||||
connectedCalendars={calendars.data.connectedCalendars}
|
||||
isPending={mutation.isPending}
|
||||
destinationCalendar={calendars.data.destinationCalendar}
|
||||
value={calendars.data.destinationCalendar.externalId}
|
||||
value={calendars.data.destinationCalendar?.externalId}
|
||||
hidePlaceholder
|
||||
onChange={mutation.mutate}
|
||||
onReminderChange={handleReminderChange}
|
||||
|
||||
@@ -6,7 +6,7 @@ interface IConnectedCalendarItem {
|
||||
externalId?: string;
|
||||
integrationType: string;
|
||||
calendars?: {
|
||||
primary: true | null;
|
||||
primary: boolean | null;
|
||||
isSelected: boolean;
|
||||
credentialId: number;
|
||||
name?: string | undefined;
|
||||
@@ -18,7 +18,7 @@ interface IConnectedCalendarItem {
|
||||
}[];
|
||||
}
|
||||
|
||||
const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
const ConnectedCalendarItem = (prop: IConnectedCalendarItem): JSX.Element => {
|
||||
const { name, logo, externalId, calendars, integrationType } = prop;
|
||||
|
||||
return (
|
||||
@@ -26,7 +26,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
<div className="flex flex-row items-center p-4">
|
||||
<img src={logo} alt={name} className="m-1 h-8 w-8" />
|
||||
<div className="mx-4">
|
||||
<p className="font-sans text-sm font-bold leading-5">
|
||||
<p className="font-bold font-sans text-sm leading-5">
|
||||
{name}
|
||||
{/* Temporarily removed till we use it on another place */}
|
||||
{/* <span className="mx-1 rounded-[4px] bg-cal-success py-[2px] px-[6px] font-sans text-xs font-medium text-green-600">
|
||||
@@ -36,7 +36,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
<div className="fle-row flex">
|
||||
<span
|
||||
title={externalId}
|
||||
className="max-w-44 text-subtle mt-1 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm">
|
||||
className="mt-1 max-w-44 overflow-hidden text-ellipsis whitespace-nowrap font-sans text-sm text-subtle">
|
||||
{externalId}{" "}
|
||||
</span>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@ const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
{t("edit")}
|
||||
</Button> */}
|
||||
</div>
|
||||
<div className="border-subtle h-px w-full border-b" />
|
||||
<div className="h-px w-full border-subtle border-b" />
|
||||
<div>
|
||||
<ul className="p-4">
|
||||
{calendars?.map((calendar, i) => (
|
||||
|
||||
@@ -93,7 +93,7 @@ const BookerComponent = ({
|
||||
roundRobinHideOrgAndTeam,
|
||||
hideOrgTeamAvatar,
|
||||
showNoAvailabilityDialog,
|
||||
}: BookerProps & WrappedBookerProps) => {
|
||||
}: BookerProps & WrappedBookerProps): JSX.Element | null => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
|
||||
const [bookerState, setBookerState] = useBookerStoreContext(
|
||||
@@ -180,13 +180,7 @@ const BookerComponent = ({
|
||||
isVerificationCodeSending,
|
||||
} = verifyEmail;
|
||||
|
||||
const {
|
||||
overlayBusyDates,
|
||||
isOverlayCalendarEnabled,
|
||||
connectedCalendars,
|
||||
loadingConnectedCalendar,
|
||||
onToggleCalendar,
|
||||
} = calendars;
|
||||
const { overlayBusyDates, isOverlayCalendarEnabled, connectedCalendars, onToggleCalendar } = calendars;
|
||||
|
||||
const scrolledToTimeslotsOnce = useRef(false);
|
||||
const embedUiConfig = useEmbedUiConfig();
|
||||
@@ -276,7 +270,11 @@ const BookerComponent = ({
|
||||
renderConfirmNotVerifyEmailButtonCond ? handleBookEvent(timeSlot) : handleVerifyEmail();
|
||||
|
||||
const EventBooker = useMemo(() => {
|
||||
return bookerState === "booking" ? (
|
||||
if (bookerState !== "booking") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<BookEventForm
|
||||
key={key}
|
||||
timeslot={selectedTimeslot}
|
||||
@@ -312,22 +310,18 @@ const BookerComponent = ({
|
||||
backButton: customClassNames?.confirmStep?.backButton,
|
||||
}}
|
||||
isPlatform={isPlatform}>
|
||||
<>
|
||||
{!isPlatform && (
|
||||
<RedirectToInstantMeetingModal
|
||||
expiryTime={expiryTime}
|
||||
bookingId={parseInt(getQueryParam("bookingId") || "0")}
|
||||
instantVideoMeetingUrl={instantVideoMeetingUrl}
|
||||
onGoBack={() => {
|
||||
onGoBackInstantMeeting();
|
||||
}}
|
||||
orgName={event.data?.entity?.name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{!isPlatform && (
|
||||
<RedirectToInstantMeetingModal
|
||||
expiryTime={expiryTime}
|
||||
bookingId={parseInt(getQueryParam("bookingId") || "0")}
|
||||
instantVideoMeetingUrl={instantVideoMeetingUrl}
|
||||
onGoBack={() => {
|
||||
onGoBackInstantMeeting();
|
||||
}}
|
||||
orgName={event.data?.entity?.name}
|
||||
/>
|
||||
)}
|
||||
</BookEventForm>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}, [
|
||||
bookerFormErrorRef,
|
||||
@@ -418,27 +412,23 @@ const BookerComponent = ({
|
||||
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
||||
isMobile={isMobile}
|
||||
nextSlots={nextSlots}
|
||||
renderOverlay={() =>
|
||||
isEmbed ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<OverlayCalendar
|
||||
isOverlayCalendarEnabled={isOverlayCalendarEnabled}
|
||||
connectedCalendars={connectedCalendars}
|
||||
loadingConnectedCalendar={loadingConnectedCalendar}
|
||||
overlayBusyDates={overlayBusyDates}
|
||||
onToggleCalendar={onToggleCalendar}
|
||||
hasSession={hasSession}
|
||||
handleClickContinue={onClickOverlayContinue}
|
||||
handleSwitchStateChange={onOverlaySwitchStateChange}
|
||||
handleClickNoCalendar={() => {
|
||||
onOverlayClickNoCalendar();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
renderOverlay={() => {
|
||||
if (isEmbed) return null;
|
||||
return (
|
||||
<OverlayCalendar
|
||||
isOverlayCalendarEnabled={isOverlayCalendarEnabled}
|
||||
connectedCalendars={connectedCalendars}
|
||||
overlayBusyDates={overlayBusyDates}
|
||||
onToggleCalendar={onToggleCalendar}
|
||||
hasSession={hasSession}
|
||||
handleClickContinue={onClickOverlayContinue}
|
||||
handleSwitchStateChange={onOverlaySwitchStateChange}
|
||||
handleClickNoCalendar={() => {
|
||||
onOverlayClickNoCalendar();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</BookerSection>
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams, usePathname, useRouter } from "next/navigation";
|
||||
import { useMemo, useCallback, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import { useBookerEmbedEvents } from "@calcom/embed-core/src/embed-iframe/react-hooks";
|
||||
import type { BookerProps } from "@calcom/features/bookings/Booker";
|
||||
import {
|
||||
BookerStoreProvider,
|
||||
useInitializeBookerStoreContext,
|
||||
useBookerStoreContext,
|
||||
useInitializeBookerStoreContext,
|
||||
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
|
||||
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
@@ -22,27 +16,23 @@ import { useSlots } from "@calcom/features/bookings/Booker/components/hooks/useS
|
||||
import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
|
||||
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import {
|
||||
useEvent,
|
||||
useScheduleForEvent,
|
||||
} from "@calcom/features/bookings/Booker/utils/event";
|
||||
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
|
||||
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
|
||||
import {
|
||||
DEFAULT_LIGHT_BRAND_COLOR,
|
||||
DEFAULT_DARK_BRAND_COLOR,
|
||||
WEBAPP_URL,
|
||||
} from "@calcom/lib/constants";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import { Booker as BookerComponent } from "./Booker";
|
||||
|
||||
export type BookerWebWrapperAtomProps = BookerProps & {
|
||||
eventData?: NonNullable<Awaited<ReturnType<typeof getPublicEvent>>>;
|
||||
};
|
||||
|
||||
const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Element => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -64,17 +54,11 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
const isRedirect = searchParams?.get("redirected") === "true" || false;
|
||||
const fromUserNameRedirected = searchParams?.get("username") || "";
|
||||
const rescheduleUid =
|
||||
typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("rescheduleUid")
|
||||
: null;
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduleUid") : null;
|
||||
const rescheduledBy =
|
||||
typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("rescheduledBy")
|
||||
: null;
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("rescheduledBy") : null;
|
||||
const bookingUid =
|
||||
typeof window !== "undefined"
|
||||
? new URLSearchParams(window.location.search).get("bookingUid")
|
||||
: null;
|
||||
typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("bookingUid") : null;
|
||||
const timezone = searchParams?.get("cal.tz") || null;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,10 +86,7 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
timezone,
|
||||
});
|
||||
|
||||
const [dayCount] = useBookerStoreContext(
|
||||
(state) => [state.dayCount, state.setDayCount],
|
||||
shallow
|
||||
);
|
||||
const [dayCount] = useBookerStoreContext((state) => [state.dayCount, state.setDayCount], shallow);
|
||||
|
||||
const { data: session } = useSession();
|
||||
const routerQuery = useRouterQuery();
|
||||
@@ -117,8 +98,7 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
.reduce(
|
||||
(metadata, key) => ({
|
||||
...metadata,
|
||||
[key.substring("metadata[".length, key.length - 1)]:
|
||||
searchParams?.get(key),
|
||||
[key.substring("metadata[".length, key.length - 1)]: searchParams?.get(key),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
@@ -126,11 +106,8 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
return {
|
||||
name:
|
||||
searchParams?.get("name") ||
|
||||
(firstNameQueryParam
|
||||
? `${firstNameQueryParam} ${lastNameQueryParam}`
|
||||
: null),
|
||||
guests:
|
||||
(searchParams?.getAll("guests") || searchParams?.getAll("guest")) ?? [],
|
||||
(firstNameQueryParam ? `${firstNameQueryParam} ${lastNameQueryParam}` : null),
|
||||
guests: (searchParams?.getAll("guests") || searchParams?.getAll("guest")) ?? [],
|
||||
};
|
||||
}, [searchParams, firstNameQueryParam, lastNameQueryParam]);
|
||||
|
||||
@@ -147,13 +124,10 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
const verifyEmail = useVerifyEmail({
|
||||
email: bookerForm.formEmail,
|
||||
name: bookerForm.formName,
|
||||
requiresBookerEmailVerification:
|
||||
event?.data?.requiresBookerEmailVerification,
|
||||
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
|
||||
onVerifyEmail: bookerForm.beforeVerifyEmail,
|
||||
});
|
||||
const slots = useSlots(
|
||||
event?.data ? { id: event.data.id, length: event.data.length } : null
|
||||
);
|
||||
const slots = useSlots(event?.data ? { id: event.data.id, length: event.data.length } : null);
|
||||
|
||||
const isEmbed = useIsEmbed();
|
||||
|
||||
@@ -217,8 +191,7 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps) => {
|
||||
);
|
||||
useBrandColors({
|
||||
brandColor: event.data?.profile.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
darkBrandColor:
|
||||
event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
darkBrandColor: event.data?.profile.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
|
||||
theme: event.data?.profile.theme,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
|
||||
import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useCalendars";
|
||||
import { useOverlayCalendar } from "@calcom/features/bookings/Booker/components/hooks/useOverlayCalendar";
|
||||
import type { WrappedBookerPropsMain } from "@calcom/features/bookings/Booker/types";
|
||||
import { useEffect } from "react";
|
||||
import { OverlayCalendarContinueModal } from "./OverlayCalendarContinueModal";
|
||||
import { OverlayCalendarSettingsModal } from "./OverlayCalendarSettingsModal";
|
||||
import { OverlayCalendarSwitch } from "./OverlayCalendarSwitch";
|
||||
|
||||
type OverlayCalendarProps = Pick<
|
||||
UseCalendarsReturnType,
|
||||
| "connectedCalendars"
|
||||
| "overlayBusyDates"
|
||||
| "onToggleCalendar"
|
||||
| "loadingConnectedCalendar"
|
||||
| "isOverlayCalendarEnabled"
|
||||
WrappedBookerPropsMain["calendars"],
|
||||
"connectedCalendars" | "overlayBusyDates" | "onToggleCalendar" | "isOverlayCalendarEnabled"
|
||||
> & {
|
||||
handleClickNoCalendar: () => void;
|
||||
hasSession: boolean;
|
||||
@@ -27,12 +21,11 @@ export const OverlayCalendar = ({
|
||||
overlayBusyDates,
|
||||
onToggleCalendar,
|
||||
isOverlayCalendarEnabled,
|
||||
loadingConnectedCalendar,
|
||||
handleClickNoCalendar,
|
||||
handleSwitchStateChange,
|
||||
handleClickContinue,
|
||||
hasSession,
|
||||
}: OverlayCalendarProps) => {
|
||||
}: OverlayCalendarProps): JSX.Element | null => {
|
||||
const isPlatform = useIsPlatform();
|
||||
const {
|
||||
handleCloseContinueModal,
|
||||
@@ -70,10 +63,8 @@ export const OverlayCalendar = ({
|
||||
/>
|
||||
)}
|
||||
<OverlayCalendarSettingsModal
|
||||
connectedCalendars={connectedCalendars}
|
||||
open={isOpenOverlaySettingsModal}
|
||||
onClose={handleCloseSettingsModal}
|
||||
isLoading={loadingConnectedCalendar}
|
||||
onToggleConnectedCalendar={handleToggleConnectedCalendar}
|
||||
onClickNoCalendar={() => {
|
||||
handleClickNoCalendar();
|
||||
|
||||
+93
-109
@@ -1,34 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
|
||||
import { Dialog } from "@calcom/features/components/controlled-dialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { DialogContent, DialogClose } from "@calcom/ui/components/dialog";
|
||||
import { DialogClose, DialogContent } from "@calcom/ui/components/dialog";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import { Switch } from "@calcom/ui/components/form";
|
||||
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/components/list";
|
||||
import { SkeletonContainer } from "@calcom/ui/components/skeleton";
|
||||
import { SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useCalendars";
|
||||
|
||||
interface IOverlayCalendarSettingsModalProps {
|
||||
type IOverlayCalendarSettingsModalProps = {
|
||||
open?: boolean;
|
||||
onClose?: (state: boolean) => void;
|
||||
onClickNoCalendar?: () => void;
|
||||
isLoading: boolean;
|
||||
connectedCalendars: UseCalendarsReturnType["connectedCalendars"];
|
||||
onToggleConnectedCalendar: (externalCalendarId: string, credentialId: number) => void;
|
||||
checkIsCalendarToggled: (externalCalendarId: string, credentialId: number) => boolean;
|
||||
}
|
||||
};
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
const SkeletonLoader = (): JSX.Element => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="border-subtle mt-3 stack-y-4 rounded-xl border px-4 py-4 ">
|
||||
<div className="stack-y-4 mt-3 rounded-xl border border-subtle px-4 py-4">
|
||||
<SkeletonText className="h-4 w-full" />
|
||||
<SkeletonText className="h-4 w-full" />
|
||||
<SkeletonText className="h-4 w-full" />
|
||||
@@ -39,109 +35,97 @@ const SkeletonLoader = () => {
|
||||
};
|
||||
|
||||
export function OverlayCalendarSettingsModal({
|
||||
connectedCalendars,
|
||||
isLoading,
|
||||
open,
|
||||
onClose,
|
||||
onClickNoCalendar,
|
||||
onToggleConnectedCalendar,
|
||||
checkIsCalendarToggled,
|
||||
}: IOverlayCalendarSettingsModalProps) {
|
||||
}: IOverlayCalendarSettingsModalProps): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { data, isPending: isLoading } = trpc.viewer.calendars.connectedCalendars.useQuery(undefined, {
|
||||
enabled: !!open,
|
||||
});
|
||||
|
||||
let content: React.ReactNode;
|
||||
if (isLoading) {
|
||||
content = <SkeletonLoader />;
|
||||
} else if (data?.connectedCalendars.length === 0) {
|
||||
content = (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_calendar_installed")}
|
||||
description={t("no_calendar_installed_description")}
|
||||
buttonText={t("add_a_calendar")}
|
||||
buttonOnClick={onClickNoCalendar}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = data?.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.error && !item.calendars && <Alert severity="error" title={item.error.message} />}
|
||||
{item?.error === undefined && item.calendars && (
|
||||
<ListItem className="flex-col rounded-md">
|
||||
<div className="flex w-full flex-1 items-center space-x-3 pb-4 rtl:space-x-reverse">
|
||||
{item.integration.logo && (
|
||||
<Image
|
||||
className={classNames(
|
||||
"h-10 w-10",
|
||||
item.integration.logo.includes("-dark") && "dark:invert"
|
||||
)}
|
||||
width={40}
|
||||
height={40}
|
||||
src={isPlatform ? `https://app.cal.com${item.integration.logo}` : item.integration.logo}
|
||||
alt={`${item.integration.title} logo`}
|
||||
/>
|
||||
)}
|
||||
<div className="grow truncate pl-2">
|
||||
<ListItemTitle component="h3" className="space-x-2 rtl:space-x-reverse">
|
||||
<Link href={`/apps/${item.integration.slug}`}>
|
||||
{item.integration.name || item.integration.title}
|
||||
</Link>
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{item.primary?.email}</ListItemText>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle w-full border-t pt-4">
|
||||
<ul className="stack-y-4">
|
||||
{item.calendars.map((cal, index) => {
|
||||
const id = cal.integrationTitle ?? `calendar-switch-${index}`;
|
||||
return (
|
||||
<li className="flex gap-3" key={id}>
|
||||
<Switch
|
||||
id={id}
|
||||
label={cal.name}
|
||||
checked={checkIsCalendarToggled(cal.externalId, item.credentialId)}
|
||||
onCheckedChange={() => {
|
||||
onToggleConnectedCalendar(cal.externalId, item.credentialId);
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
</Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
type="creation"
|
||||
title="Calendar Settings"
|
||||
className="pb-4"
|
||||
description={t("view_overlay_calendar_events")}>
|
||||
<div className="no-scrollbar max-h-full overflow-y-scroll ">
|
||||
{isLoading ? (
|
||||
<SkeletonLoader />
|
||||
) : (
|
||||
<>
|
||||
{connectedCalendars.length === 0 ? (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_calendar_installed")}
|
||||
description={t("no_calendar_installed_description")}
|
||||
buttonText={t("add_a_calendar")}
|
||||
buttonOnClick={onClickNoCalendar}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.error && !item.calendars && (
|
||||
<Alert severity="error" title={item.error.message} />
|
||||
)}
|
||||
{item?.error === undefined && item.calendars && (
|
||||
<ListItem className="flex-col rounded-md">
|
||||
<div className="flex w-full flex-1 items-center space-x-3 pb-4 rtl:space-x-reverse">
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
item.integration.logo && (
|
||||
<img
|
||||
className={classNames(
|
||||
"h-10 w-10",
|
||||
item.integration.logo.includes("-dark") && "dark:invert"
|
||||
)}
|
||||
src={
|
||||
isPlatform
|
||||
? `https://app.cal.com${item.integration.logo}`
|
||||
: item.integration.logo
|
||||
}
|
||||
alt={`${item.integration.title} logo`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="grow truncate pl-2">
|
||||
<ListItemTitle component="h3" className="space-x-2 rtl:space-x-reverse">
|
||||
<Link href={`/apps/${item.integration.slug}`}>
|
||||
{item.integration.name || item.integration.title}
|
||||
</Link>
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{item.primary.email}</ListItemText>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle w-full border-t pt-4">
|
||||
<ul className="stack-y-4">
|
||||
{item.calendars.map((cal, index) => {
|
||||
const id = cal.integrationTitle ?? `calendar-switch-${index}`;
|
||||
return (
|
||||
<li className="flex gap-3" key={id}>
|
||||
<Switch
|
||||
id={id}
|
||||
checked={checkIsCalendarToggled(cal.externalId, item.credentialId)}
|
||||
onCheckedChange={() => {
|
||||
onToggleConnectedCalendar(cal.externalId, item.credentialId);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={id}>{cal.name}</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex gap-2 self-end">
|
||||
<DialogClose>{t("done")}</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
type="creation"
|
||||
title={t("calendar_settings")}
|
||||
className="pb-4"
|
||||
description={t("view_overlay_calendar_events")}>
|
||||
<div className="no-scrollbar max-h-full overflow-y-scroll">{content}</div>
|
||||
<div className="mt-4 flex gap-2 self-end">
|
||||
<DialogClose>{t("done")}</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2934,6 +2934,7 @@
|
||||
"add_new_user": "Add new user",
|
||||
"this_app_is_not_setup_already": "This app has not been setup yet",
|
||||
"as_csv": "as CSV",
|
||||
"calendar_settings": "Calendar Settings",
|
||||
"overlay_my_calendar": "Overlay my calendar",
|
||||
"overlay_my_calendar_toc": "By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||
"view_overlay_calendar_events": "View your calendar events to prevent clashed booking.",
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
/// <reference path="../../../types/ical.d.ts"/>
|
||||
import ICAL from "ical.js";
|
||||
|
||||
import process from "node:process";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import type {
|
||||
Calendar,
|
||||
IntegrationCalendar,
|
||||
EventBusyDate,
|
||||
CalendarEvent,
|
||||
EventBusyDate,
|
||||
GetAvailabilityParams,
|
||||
IntegrationCalendar,
|
||||
NewCalendarEventType,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import ICAL from "ical.js";
|
||||
|
||||
// for Apple's Travel Time feature only (for now)
|
||||
const getTravelDurationInSeconds = (vevent: ICAL.Component) => {
|
||||
@@ -301,7 +302,7 @@ class ICSFeedCalendarService implements Calendar {
|
||||
name,
|
||||
readOnly: true,
|
||||
externalId: url,
|
||||
integrationName: this.integrationName,
|
||||
integration: this.integrationName,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import { createWithEqualityFn } from "zustand/traditional";
|
||||
|
||||
interface IOverlayCalendarStore {
|
||||
overlayBusyDates: EventBusyDate[] | undefined;
|
||||
@@ -11,7 +10,7 @@ interface IOverlayCalendarStore {
|
||||
setCalendarSettingsOverlayModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export const useOverlayCalendarStore = create<IOverlayCalendarStore>((set) => ({
|
||||
export const useOverlayCalendarStore = createWithEqualityFn<IOverlayCalendarStore>((set) => ({
|
||||
overlayBusyDates: undefined,
|
||||
setOverlayBusyDates: (busyDates: EventBusyDate[]) => {
|
||||
set({ overlayBusyDates: busyDates });
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import { useBookerStore } from "../../store";
|
||||
import type { ToggledConnectedCalendars } from "../../types";
|
||||
import { useOverlayCalendarStore } from "../OverlayCalendar/store";
|
||||
import { useLocalSet } from "./useLocalSet";
|
||||
|
||||
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();
|
||||
@@ -26,11 +19,11 @@ export const useCalendars = ({ hasSession }: UseCalendarsProps) => {
|
||||
const switchEnabled =
|
||||
searchParams?.get("overlayCalendar") === "true" ||
|
||||
localStorage?.getItem("overlayCalendarSwitchDefault") === "true";
|
||||
const { set, clearSet } = useLocalSet<{
|
||||
credentialId: number;
|
||||
externalId: string;
|
||||
}>("toggledConnectedCalendars", []);
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const [set, setSet] = useState<Set<ToggledConnectedCalendars>>(() => {
|
||||
const storedValue = localStorage.getItem("toggledConnectedCalendars");
|
||||
return storedValue ? new Set(JSON.parse(storedValue)) : new Set([]);
|
||||
});
|
||||
|
||||
const [calendarSettingsOverlay] = useOverlayCalendarStore(
|
||||
(state) => [state.calendarSettingsOverlayModal, state.setCalendarSettingsOverlayModal],
|
||||
@@ -55,22 +48,25 @@ export const useCalendars = ({ hasSession }: UseCalendarsProps) => {
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (!isError) return;
|
||||
clearSet();
|
||||
setSet(new Set([]));
|
||||
},
|
||||
[isError]
|
||||
);
|
||||
|
||||
const { data, isPending } = trpc.viewer.calendars.connectedCalendars.useQuery(undefined, {
|
||||
enabled: !!calendarSettingsOverlay || Boolean(searchParams?.get("overlayCalendar")),
|
||||
});
|
||||
const { data, isPending } = trpc.viewer.calendars.connectedCalendars.useQuery(
|
||||
{
|
||||
skipSync: true,
|
||||
},
|
||||
{
|
||||
enabled: !!calendarSettingsOverlay || Boolean(searchParams?.get("overlayCalendar")),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
overlayBusyDates,
|
||||
isOverlayCalendarEnabled: switchEnabled,
|
||||
connectedCalendars: data?.connectedCalendars || [],
|
||||
loadingConnectedCalendar: isPending,
|
||||
onToggleCalendar: (data: ToggledConnectedCalendars) => {
|
||||
utils.viewer.availability.calendarOverlay.reset();
|
||||
},
|
||||
onToggleCalendar: setSet,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
import type { ToggledConnectedCalendars, WrappedBookerPropsMain } from "../../types";
|
||||
import { useOverlayCalendarStore } from "../OverlayCalendar/store";
|
||||
import type { UseCalendarsReturnType } from "./useCalendars";
|
||||
import { useLocalSet } from "./useLocalSet";
|
||||
|
||||
export type UseOverlayCalendarReturnType = ReturnType<typeof useOverlayCalendar>;
|
||||
|
||||
export const useOverlayCalendar = ({
|
||||
connectedCalendars,
|
||||
overlayBusyDates,
|
||||
onToggleCalendar,
|
||||
}: Pick<UseCalendarsReturnType, "connectedCalendars" | "overlayBusyDates" | "onToggleCalendar">) => {
|
||||
const { set, toggleValue, hasItem } = useLocalSet<{
|
||||
credentialId: number;
|
||||
externalId: string;
|
||||
}>("toggledConnectedCalendars", []);
|
||||
}: Pick<
|
||||
WrappedBookerPropsMain["calendars"],
|
||||
"overlayBusyDates" | "onToggleCalendar" | "connectedCalendars"
|
||||
>): {
|
||||
isOpenOverlayContinueModal: boolean;
|
||||
isOpenOverlaySettingsModal: boolean;
|
||||
handleCloseContinueModal: (val: boolean) => void;
|
||||
handleCloseSettingsModal: (val: boolean) => void;
|
||||
handleToggleConnectedCalendar: (externalCalendarId: string, credentialId: number) => void;
|
||||
checkIsCalendarToggled: (externalId: string, credentialId: number) => boolean;
|
||||
} => {
|
||||
const { set, toggleValue, hasItem } = useLocalSet<ToggledConnectedCalendars>(
|
||||
"toggledConnectedCalendars",
|
||||
[]
|
||||
);
|
||||
const [initalised, setInitalised] = useState(false);
|
||||
const [continueWithProvider, setContinueWithProvider] = useOverlayCalendarStore(
|
||||
(state) => [state.continueWithProviderModal, state.setContinueWithProviderModal],
|
||||
@@ -65,7 +71,7 @@ export const useOverlayCalendar = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasItem, set, initalised]);
|
||||
|
||||
const handleToggleConnectedCalendar = (externalCalendarId: string, credentialId: number) => {
|
||||
const handleToggleConnectedCalendar = (externalCalendarId: string, credentialId: number): void => {
|
||||
const calendarsToLoad = toggleValue({
|
||||
credentialId: credentialId,
|
||||
externalId: externalCalendarId,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { UseBookerLayoutType } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
|
||||
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
|
||||
import type { UseBookingsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookings";
|
||||
import type { UseCalendarsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useCalendars";
|
||||
import type { UseSlotsReturnType } from "@calcom/features/bookings/Booker/components/hooks/useSlots";
|
||||
import type { UseVerifyCodeReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyCode";
|
||||
import type { UseVerifyEmailReturnType } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
|
||||
import type { useScheduleForEventReturnType } from "@calcom/features/bookings/Booker/utils/event";
|
||||
import type { BookerEventQuery } from "@calcom/features/bookings/types";
|
||||
import type { IntlSupportedTimeZones } from "@calcom/lib/timeZones";
|
||||
import type { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import type React from "react";
|
||||
import type { GetBookingType } from "../lib/get-booking";
|
||||
|
||||
@@ -105,6 +105,11 @@ export interface BookerProps {
|
||||
useApiV2?: boolean;
|
||||
}
|
||||
|
||||
export type ToggledConnectedCalendars = {
|
||||
credentialId: number;
|
||||
externalId: string;
|
||||
};
|
||||
|
||||
export type WrappedBookerPropsMain = {
|
||||
sessionUsername?: string | null;
|
||||
rescheduleUid: string | null;
|
||||
@@ -121,7 +126,35 @@ export type WrappedBookerPropsMain = {
|
||||
extraOptions: Record<string, string | string[]>;
|
||||
bookings: UseBookingsReturnType;
|
||||
slots: UseSlotsReturnType;
|
||||
calendars: UseCalendarsReturnType;
|
||||
calendars: {
|
||||
overlayBusyDates?: EventBusyDate[];
|
||||
isOverlayCalendarEnabled: boolean;
|
||||
connectedCalendars: {
|
||||
calendars?: {
|
||||
name?: string;
|
||||
integrationTitle?: string;
|
||||
externalId: string;
|
||||
primary: boolean | null;
|
||||
}[];
|
||||
credentialId: number;
|
||||
delegationCredentialId?: string | null;
|
||||
cacheUpdatedAt: null;
|
||||
error?: {
|
||||
message: string | null;
|
||||
};
|
||||
primary?: {
|
||||
email?: string;
|
||||
};
|
||||
integration: {
|
||||
slug: string;
|
||||
name?: string | undefined;
|
||||
title?: string;
|
||||
logo: string;
|
||||
};
|
||||
}[];
|
||||
loadingConnectedCalendar: boolean;
|
||||
onToggleCalendar: (calendarsToLoad: Set<ToggledConnectedCalendars>) => void;
|
||||
};
|
||||
bookerForm: UseBookingFormReturnType;
|
||||
event: BookerEventQuery;
|
||||
schedule: useScheduleForEventReturnType;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import type { SelectClassNames } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
interface Props {
|
||||
onChange: (value: { externalId: string; integration: string }) => void;
|
||||
@@ -88,8 +87,7 @@ const DestinationCalendarSelector = ({
|
||||
|
||||
useEffect(() => {
|
||||
const selected = connectedCalendarsList
|
||||
?.map((connected) => connected.calendars ?? [])
|
||||
.flat()
|
||||
?.flatMap((connected) => connected.calendars ?? [])
|
||||
.find((cal) => cal.externalId === value);
|
||||
|
||||
if (selected) {
|
||||
@@ -140,7 +138,7 @@ const DestinationCalendarSelector = ({
|
||||
`${t("create_events_on")}`
|
||||
) : (
|
||||
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
|
||||
<Badge variant="blue">Default</Badge>{" "}
|
||||
<Badge variant="blue">{t("default")}</Badge>{" "}
|
||||
{destinationCalendar?.name &&
|
||||
`${destinationCalendar.name} (${destinationCalendar?.integrationTitle} - ${destinationCalendar.primaryEmail})`}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { sortBy } from "lodash";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
import { MeetLocationType } from "@calcom/app-store/locations";
|
||||
import getApps from "@calcom/app-store/utils";
|
||||
@@ -7,8 +5,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import getCalendarsEvents, {
|
||||
getCalendarsEventsWithTimezones,
|
||||
} from "@calcom/features/calendars/lib/getCalendarsEvents";
|
||||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import { getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import { getRichDescription, getUid } from "@calcom/lib/CalEventParser";
|
||||
import { CalendarAppDelegationCredentialError } from "@calcom/lib/CalendarAppError";
|
||||
import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants";
|
||||
import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential";
|
||||
@@ -27,9 +24,60 @@ import type {
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService, CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
import { sortBy } from "lodash";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["CalendarManager"] });
|
||||
|
||||
/**
|
||||
* Process the calendar event by generating description and removing attendees if needed
|
||||
*/
|
||||
const processEvent = (calEvent: CalendarEvent): CalendarServiceEvent => {
|
||||
if (calEvent.seatsPerTimeSlot) {
|
||||
calEvent.responses = null;
|
||||
calEvent.userFieldsResponses = null;
|
||||
calEvent.additionalNotes = null;
|
||||
calEvent.customInputs = null;
|
||||
}
|
||||
// Generate the calendar event description
|
||||
const calendarEvent: CalendarServiceEvent = {
|
||||
...calEvent,
|
||||
calendarDescription: getRichDescription(calEvent),
|
||||
};
|
||||
|
||||
const isMeetLocationType = calEvent.location === MeetLocationType;
|
||||
|
||||
// Determine if the calendar event should include attendees
|
||||
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
|
||||
.filter((domain) => domain.trim() !== "")
|
||||
.some((domain) => calEvent.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
|
||||
|
||||
if (calEvent.hideOrganizerEmail && !isOrganizerExempt && !isMeetLocationType) {
|
||||
calendarEvent.attendees = [];
|
||||
}
|
||||
|
||||
return calendarEvent;
|
||||
};
|
||||
|
||||
type IntegrationWithCredentials = Awaited<
|
||||
ReturnType<typeof getCalendarCredentials>
|
||||
>[number]["integration"] & {
|
||||
credentials?: CredentialPayload[];
|
||||
credential?: CredentialPayload;
|
||||
};
|
||||
|
||||
type CleanIntegration = Omit<IntegrationWithCredentials, "credentials" | "credential">;
|
||||
|
||||
/**
|
||||
* Important function to prevent leaking credentials to the client
|
||||
* @param appIntegration
|
||||
* @returns App
|
||||
*/
|
||||
export const cleanIntegrationKeys = (appIntegration: IntegrationWithCredentials): CleanIntegration => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { credentials, credential, ...rest } = appIntegration;
|
||||
return rest;
|
||||
};
|
||||
|
||||
export const getCalendarCredentials = (credentials: Array<CredentialForCalendarService>) => {
|
||||
const calendarCredentials = getApps(credentials, true)
|
||||
.filter((app) => app.type.endsWith("_calendar"))
|
||||
@@ -49,11 +97,31 @@ export const getCalendarCredentialsWithoutDelegation = (credentials: CredentialP
|
||||
return getCalendarCredentials(buildNonDelegationCredentials(credentials));
|
||||
};
|
||||
|
||||
export type ConnectedCalendar = Omit<IntegrationCalendar, "primary"> & {
|
||||
primary: boolean | null;
|
||||
isSelected: boolean;
|
||||
readOnly: boolean;
|
||||
credentialId: number;
|
||||
delegationCredentialId: string | null;
|
||||
};
|
||||
|
||||
export const getConnectedCalendars = async (
|
||||
calendarCredentials: ReturnType<typeof getCalendarCredentials>,
|
||||
selectedCalendars: { externalId: string }[],
|
||||
destinationCalendarExternalId?: string
|
||||
) => {
|
||||
): Promise<{
|
||||
connectedCalendars: {
|
||||
integration: CleanIntegration;
|
||||
calendars?: ConnectedCalendar[];
|
||||
credentialId: number;
|
||||
delegationCredentialId?: string | null;
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
primary?: ConnectedCalendar;
|
||||
}[];
|
||||
destinationCalendar: IntegrationCalendar | undefined;
|
||||
}> => {
|
||||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
try {
|
||||
@@ -82,7 +150,7 @@ export const getConnectedCalendars = async (
|
||||
};
|
||||
}
|
||||
const cals = await calendarInstance.listCalendars();
|
||||
const calendars = sortBy(
|
||||
const calendars: ConnectedCalendar[] = sortBy(
|
||||
cals.map((cal: IntegrationCalendar) => {
|
||||
return {
|
||||
...cal,
|
||||
@@ -165,22 +233,6 @@ export const getConnectedCalendars = async (
|
||||
return { connectedCalendars, destinationCalendar };
|
||||
};
|
||||
|
||||
/**
|
||||
* Important function to prevent leaking credentials to the client
|
||||
* @param appIntegration
|
||||
* @returns App
|
||||
*/
|
||||
const cleanIntegrationKeys = (
|
||||
appIntegration: Awaited<ReturnType<typeof getCalendarCredentials>>[number]["integration"] & {
|
||||
credentials?: Array<CredentialPayload>;
|
||||
credential: CredentialPayload;
|
||||
}
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { credentials, credential, ...rest } = appIntegration;
|
||||
return rest;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function deduplicates credentials based on selected calendars
|
||||
* It removes regular credentials for which corresponding delegation credentials exist which can be identified only associating them with selected calendars
|
||||
@@ -311,7 +363,7 @@ export const createEvent = async (
|
||||
const uid: string = getUid(formattedEvent.uid);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
let success = true;
|
||||
let calError: string | undefined = undefined;
|
||||
let calError: string | undefined;
|
||||
|
||||
log.debug(
|
||||
"Creating calendar event",
|
||||
@@ -410,7 +462,7 @@ export const updateEvent = async (
|
||||
const uid = getUid(calEvent.uid);
|
||||
const calendar = await getCalendar(credential, "booking");
|
||||
let success = false;
|
||||
let calError: string | undefined = undefined;
|
||||
let calError: string | undefined;
|
||||
let calWarnings: string[] | undefined = [];
|
||||
log.debug(
|
||||
"Updating calendar event",
|
||||
@@ -513,33 +565,3 @@ export const deleteEvent = async ({
|
||||
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
/**
|
||||
* Process the calendar event by generating description and removing attendees if needed
|
||||
*/
|
||||
const processEvent = (calEvent: CalendarEvent): CalendarServiceEvent => {
|
||||
if (calEvent.seatsPerTimeSlot) {
|
||||
calEvent.responses = null;
|
||||
calEvent.userFieldsResponses = null;
|
||||
calEvent.additionalNotes = null;
|
||||
calEvent.customInputs = null;
|
||||
}
|
||||
// Generate the calendar event description
|
||||
const calendarEvent: CalendarServiceEvent = {
|
||||
...calEvent,
|
||||
calendarDescription: getRichDescription(calEvent),
|
||||
};
|
||||
|
||||
const isMeetLocationType = calEvent.location === MeetLocationType;
|
||||
|
||||
// Determine if the calendar event should include attendees
|
||||
const isOrganizerExempt = ORGANIZER_EMAIL_EXEMPT_DOMAINS?.split(",")
|
||||
.filter((domain) => domain.trim() !== "")
|
||||
.some((domain) => calEvent.organizer.email.toLowerCase().endsWith(domain.toLowerCase()));
|
||||
|
||||
if (calEvent.hideOrganizerEmail && !isOrganizerExempt && !isMeetLocationType) {
|
||||
calendarEvent.attendees = [];
|
||||
}
|
||||
|
||||
return calendarEvent;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential";
|
||||
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
|
||||
import {
|
||||
cleanIntegrationKeys,
|
||||
getCalendarCredentials,
|
||||
getConnectedCalendars,
|
||||
} from "@calcom/features/calendars/lib/CalendarManager";
|
||||
@@ -12,28 +14,13 @@ import prisma from "@calcom/prisma";
|
||||
import type { DestinationCalendar, SelectedCalendar, User } from "@calcom/prisma/client";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { IntegrationCalendar } from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["getConnectedDestinationCalendarsAndEnsureDefaultsInDb"] });
|
||||
|
||||
type ReturnTypeGetConnectedCalendars = Awaited<ReturnType<typeof getConnectedCalendars>>;
|
||||
type ConnectedCalendarsFromGetConnectedCalendars = ReturnTypeGetConnectedCalendars["connectedCalendars"];
|
||||
|
||||
export type UserWithCalendars = Pick<User, "id" | "email"> & {
|
||||
allSelectedCalendars: Pick<
|
||||
SelectedCalendar,
|
||||
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
|
||||
>[];
|
||||
userLevelSelectedCalendars: Pick<
|
||||
SelectedCalendar,
|
||||
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
|
||||
>[];
|
||||
destinationCalendar: DestinationCalendar | null;
|
||||
};
|
||||
|
||||
export type ConnectedDestinationCalendars = Awaited<
|
||||
ReturnType<typeof getConnectedDestinationCalendarsAndEnsureDefaultsInDb>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Ensures that when DelegationCredential is enabled and there is already a calendar connected for the corresponding domain, we only allow the DelegationCredential calendar to be returned
|
||||
* This is to ensure that duplicate calendar connections aren't shown in UI(apps/installed/calendars). We choose DelegationCredential connection to be shown because we don't want users to be able to work with individual calendars
|
||||
@@ -51,7 +38,7 @@ const _ensureNoConflictingNonDelegatedConnectedCalendar = <
|
||||
connectedCalendars: T[];
|
||||
loggedInUser: { email: string };
|
||||
}) => {
|
||||
return connectedCalendars.filter((connectedCalendar, index, array) => {
|
||||
return connectedCalendars.filter((connectedCalendar, _index, array) => {
|
||||
const allCalendarsWithSameAppSlug = array.filter(
|
||||
(cal) => cal.integration.slug === connectedCalendar.integration.slug
|
||||
);
|
||||
@@ -213,12 +200,13 @@ function findMatchingCalendar({
|
||||
}: {
|
||||
connectedCalendars: ConnectedCalendarsFromGetConnectedCalendars;
|
||||
calendar: DestinationCalendar;
|
||||
}) {
|
||||
}): Required<ConnectedCalendarsFromGetConnectedCalendars[number]>["calendars"][number] | undefined {
|
||||
// Check if destinationCalendar exists in connectedCalendars
|
||||
const allCals = connectedCalendars.flatMap((cal) => cal.calendars ?? []);
|
||||
const matchingCalendar = allCals.find(
|
||||
(cal) => cal.externalId === calendar.externalId && cal.integration === calendar.integration
|
||||
);
|
||||
if (!matchingCalendar) return;
|
||||
return matchingCalendar;
|
||||
}
|
||||
|
||||
@@ -254,13 +242,32 @@ function getSelectedCalendars({
|
||||
}: {
|
||||
user: UserWithCalendars;
|
||||
eventTypeId: number | null;
|
||||
}): Pick<SelectedCalendar, "externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId">[] {
|
||||
}): Pick<
|
||||
SelectedCalendar,
|
||||
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId" | "id"
|
||||
>[] {
|
||||
if (eventTypeId) {
|
||||
return user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventTypeId);
|
||||
}
|
||||
return user.userLevelSelectedCalendars;
|
||||
}
|
||||
|
||||
export type UserWithCalendars = Pick<User, "id" | "email"> & {
|
||||
allSelectedCalendars: Pick<
|
||||
SelectedCalendar,
|
||||
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId" | "id"
|
||||
>[];
|
||||
userLevelSelectedCalendars: Pick<
|
||||
SelectedCalendar,
|
||||
"externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId" | "id"
|
||||
>[];
|
||||
destinationCalendar: DestinationCalendar | null;
|
||||
};
|
||||
|
||||
export type ConnectedDestinationCalendars = Awaited<
|
||||
ReturnType<typeof getConnectedDestinationCalendarsAndEnsureDefaultsInDb>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Fetches the calendars for the authenticated user or the event-type if provided
|
||||
* It also takes care of updating the destination calendar in some edge cases
|
||||
@@ -270,12 +277,17 @@ export async function getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
onboarding,
|
||||
eventTypeId,
|
||||
prisma,
|
||||
skipSync,
|
||||
}: {
|
||||
user: UserWithCalendars;
|
||||
onboarding: boolean;
|
||||
eventTypeId?: number | null;
|
||||
prisma: PrismaClient;
|
||||
}) {
|
||||
skipSync?: boolean;
|
||||
}): Promise<{
|
||||
destinationCalendar: DestinationCalendar & Omit<IntegrationCalendar, "id" | "userId">;
|
||||
connectedCalendars: Awaited<ReturnType<typeof getConnectedCalendars>>["connectedCalendars"];
|
||||
}> {
|
||||
const userCredentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
@@ -284,84 +296,124 @@ export async function getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
select: {
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
...credentialForCalendarServiceSelect,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCalendars = getSelectedCalendars({ user, eventTypeId: eventTypeId ?? null });
|
||||
let connectedCalendars: Awaited<ReturnType<typeof getConnectedCalendars>>["connectedCalendars"] = [];
|
||||
let destinationCalendar: IntegrationCalendar | undefined;
|
||||
|
||||
const { credentials: allCredentials } = await enrichUserWithDelegationCredentialsIncludeServiceAccountKey({
|
||||
user: { id: user.id, email: user.email, credentials: userCredentials },
|
||||
});
|
||||
|
||||
const selectedCalendars = getSelectedCalendars({ user, eventTypeId: eventTypeId ?? null });
|
||||
// get user's credentials + their connected integrations
|
||||
const calendarCredentials = getCalendarCredentials(allCredentials);
|
||||
|
||||
// get all the connected integrations' calendars (from third party)
|
||||
const getConnectedCalendarsResult = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
selectedCalendars,
|
||||
user.destinationCalendar?.externalId
|
||||
);
|
||||
|
||||
let connectedCalendars = getConnectedCalendarsResult.connectedCalendars;
|
||||
const destinationCalendar = getConnectedCalendarsResult.destinationCalendar;
|
||||
|
||||
let calendarToEnsureIsEnabledForConflictCheck: ToggledCalendarDetails | null = null;
|
||||
|
||||
if (connectedCalendars.length === 0) {
|
||||
user = await handleNoConnectedCalendars(user);
|
||||
} else if (!user.destinationCalendar) {
|
||||
({ user, calendarToEnsureIsEnabledForConflictCheck, connectedCalendars } =
|
||||
await handleNoDestinationCalendar({
|
||||
user,
|
||||
connectedCalendars,
|
||||
onboarding,
|
||||
}));
|
||||
} else {
|
||||
/* There are connected calendars and a destination calendar */
|
||||
log.debug(
|
||||
`There are connected calendars and a destination calendar, so check if destinationCalendar exists in connectedCalendars for user ${user.id}`
|
||||
if (!skipSync) {
|
||||
// get all the connected integrations' calendars (from third party)
|
||||
const getConnectedCalendarsResult = await getConnectedCalendars(
|
||||
calendarCredentials,
|
||||
selectedCalendars,
|
||||
user.destinationCalendar?.externalId
|
||||
);
|
||||
|
||||
const destinationCal = findMatchingCalendar({ connectedCalendars, calendar: user.destinationCalendar });
|
||||
if (!destinationCal) {
|
||||
connectedCalendars = getConnectedCalendarsResult.connectedCalendars;
|
||||
destinationCalendar = getConnectedCalendarsResult.destinationCalendar;
|
||||
|
||||
let calendarToEnsureIsEnabledForConflictCheck: ToggledCalendarDetails | null = null;
|
||||
|
||||
if (connectedCalendars.length === 0) {
|
||||
user = await handleNoConnectedCalendars(user);
|
||||
} else if (!user.destinationCalendar) {
|
||||
({ user, calendarToEnsureIsEnabledForConflictCheck, connectedCalendars } =
|
||||
await handleDestinationCalendarNotInConnectedCalendars({
|
||||
await handleNoDestinationCalendar({
|
||||
user,
|
||||
connectedCalendars,
|
||||
onboarding,
|
||||
}));
|
||||
} else if (onboarding && !destinationCal.isSelected) {
|
||||
} else {
|
||||
/* There are connected calendars and a destination calendar */
|
||||
log.debug(
|
||||
`Onboarding:Destination calendar is not selected, but in connectedCalendars, so mark it as selected in the calendar list for user ${user.id}`
|
||||
`There are connected calendars and a destination calendar, so check if destinationCalendar exists in connectedCalendars for user ${user.id}`
|
||||
);
|
||||
// Mark the destination calendar as selected in the calendar list
|
||||
// We use every so that we can exit early once we find the matching calendar
|
||||
connectedCalendars.every((cal) => {
|
||||
const index = (cal.calendars || []).findIndex(
|
||||
(calendar) =>
|
||||
calendar.externalId === destinationCal.externalId &&
|
||||
calendar.integration === destinationCal.integration
|
||||
);
|
||||
if (index >= 0 && cal.calendars) {
|
||||
cal.calendars[index].isSelected = true;
|
||||
calendarToEnsureIsEnabledForConflictCheck = {
|
||||
externalId: destinationCal.externalId,
|
||||
integration: destinationCal.integration || "",
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
const destinationCal = findMatchingCalendar({ connectedCalendars, calendar: user.destinationCalendar });
|
||||
if (!destinationCal) {
|
||||
({ user, calendarToEnsureIsEnabledForConflictCheck, connectedCalendars } =
|
||||
await handleDestinationCalendarNotInConnectedCalendars({
|
||||
user,
|
||||
connectedCalendars,
|
||||
onboarding,
|
||||
}));
|
||||
} else if (onboarding && !destinationCal.isSelected) {
|
||||
log.debug(
|
||||
`Onboarding:Destination calendar is not selected, but in connectedCalendars, so mark it as selected in the calendar list for user ${user.id}`
|
||||
);
|
||||
// Mark the destination calendar as selected in the calendar list
|
||||
// We use every so that we can exit early once we find the matching calendar
|
||||
connectedCalendars.every((cal) => {
|
||||
const index = (cal.calendars || []).findIndex(
|
||||
(calendar) =>
|
||||
calendar.externalId === destinationCal.externalId &&
|
||||
calendar.integration === destinationCal.integration
|
||||
);
|
||||
if (index >= 0 && cal.calendars) {
|
||||
cal.calendars[index].isSelected = true;
|
||||
calendarToEnsureIsEnabledForConflictCheck = {
|
||||
externalId: destinationCal.externalId,
|
||||
integration: destinationCal.integration || "",
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the newly toggled record to the DB
|
||||
if (calendarToEnsureIsEnabledForConflictCheck) {
|
||||
await ensureSelectedCalendarIsInDb({
|
||||
user,
|
||||
selectedCalendar: calendarToEnsureIsEnabledForConflictCheck,
|
||||
eventTypeId: eventTypeId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
// very explicit about skipping sync.
|
||||
if (skipSync) {
|
||||
// TODO: Make calendar types more flexible so this isn't needed
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { integration } = item;
|
||||
// TODO: Make calendar types more flexible somehow so this isn't needed
|
||||
const credential: typeof item.credential & { selectedCalendars: { id: string }[] } =
|
||||
item.credential as CredentialDataWithTeamName & { selectedCalendars: { id: string }[] };
|
||||
|
||||
// Insert the newly toggled record to the DB
|
||||
if (calendarToEnsureIsEnabledForConflictCheck) {
|
||||
await ensureSelectedCalendarIsInDb({
|
||||
user,
|
||||
selectedCalendar: calendarToEnsureIsEnabledForConflictCheck,
|
||||
eventTypeId: eventTypeId ?? null,
|
||||
const safeToSendIntegration = cleanIntegrationKeys(integration);
|
||||
connectedCalendars.push({
|
||||
integration: safeToSendIntegration,
|
||||
credentialId: credential.id,
|
||||
delegationCredentialId: credential.delegationCredentialId,
|
||||
calendars: selectedCalendars
|
||||
.filter((cal) =>
|
||||
credential.selectedCalendars.some((appSelectedCal) => appSelectedCal.id === cal.id)
|
||||
)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
isSelected: true,
|
||||
readOnly: false,
|
||||
primary: null,
|
||||
credentialId: credential.id,
|
||||
delegationCredentialId: credential.delegationCredentialId,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -369,11 +421,18 @@ export async function getConnectedDestinationCalendarsAndEnsureDefaultsInDb({
|
||||
connectedCalendars,
|
||||
loggedInUser: { email: user.email },
|
||||
});
|
||||
let destinationCalendarWithoutIdAndUserId: Omit<IntegrationCalendar, "id" | "userId"> | null = null;
|
||||
if (destinationCalendar) {
|
||||
// ID and userID will be provided by user.destinationCalendar
|
||||
const { id: _id, userId: _userId, ...partialDestCal } = destinationCalendar;
|
||||
destinationCalendarWithoutIdAndUserId = partialDestCal;
|
||||
}
|
||||
return {
|
||||
connectedCalendars: noConflictingNonDelegatedConnectedCalendars,
|
||||
destinationCalendar: {
|
||||
...(user.destinationCalendar as DestinationCalendar),
|
||||
...destinationCalendar,
|
||||
// biome-ignore lint/style/noNonNullAssertion: destinationCalendar is guaranteed to be non null here
|
||||
...user.destinationCalendar!,
|
||||
...destinationCalendarWithoutIdAndUserId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1110,6 +1110,7 @@ export class UserRepository {
|
||||
movedToProfileId: true,
|
||||
selectedCalendars: {
|
||||
select: {
|
||||
id: true,
|
||||
eventTypeId: true,
|
||||
externalId: true,
|
||||
integration: true,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
import { SingleValueComponent } from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { OptionComponent } from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import {
|
||||
OptionComponent,
|
||||
SingleValueComponent,
|
||||
} from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import type { ConnectedDestinationCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Select } from "@calcom/ui/components/form";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getPlaceholderContent } from "../lib/getPlaceholderContent";
|
||||
|
||||
export type DestinationCalendarProps = {
|
||||
@@ -43,8 +43,7 @@ export const DestinationCalendarSelector = ({
|
||||
|
||||
useEffect(() => {
|
||||
const selected = connectedCalendars
|
||||
.map((connected) => connected.calendars ?? [])
|
||||
.flat()
|
||||
.flatMap((connected) => connected.calendars ?? [])
|
||||
.find((cal) => cal.externalId === value);
|
||||
|
||||
if (selected) {
|
||||
@@ -52,15 +51,16 @@ export const DestinationCalendarSelector = ({
|
||||
integration.calendars?.some((calendar) => calendar.externalId === selected.externalId)
|
||||
);
|
||||
|
||||
const label = selected.name ? `${selected.name} ` : "";
|
||||
setSelectedOption({
|
||||
value: `${selected.integration}:${selected.externalId}`,
|
||||
label: selected.name ? `${selected.name} ` : "",
|
||||
label,
|
||||
subtitle: `(${selectedIntegration?.integration.title?.replace(/calendar/i, "")} - ${
|
||||
selectedIntegration?.primary?.name
|
||||
})`,
|
||||
});
|
||||
}
|
||||
}, [connectedCalendars]);
|
||||
}, [connectedCalendars, value]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return (
|
||||
@@ -95,8 +95,8 @@ export const DestinationCalendarSelector = ({
|
||||
!hidePlaceholder ? (
|
||||
`${t("create_events_on")}`
|
||||
) : (
|
||||
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
|
||||
<Badge variant="blue">Default</Badge>{" "}
|
||||
<span className="min-w-0 overflow-hidden truncate whitespace-nowrap text-default">
|
||||
<Badge variant="blue">{t("default")}</Badge>{" "}
|
||||
{destinationCalendar?.primaryEmail &&
|
||||
`${destinationCalendar.name} (${destinationCalendar?.integrationTitle} - ${destinationCalendar.primaryEmail})`}
|
||||
</span>
|
||||
@@ -119,7 +119,7 @@ export const DestinationCalendarSelector = ({
|
||||
}}
|
||||
isSearchable={false}
|
||||
className={classNames(
|
||||
"border-default my-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm text-sm"
|
||||
"my-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-default text-sm"
|
||||
)}
|
||||
onChange={(newValue) => {
|
||||
setSelectedOption(newValue);
|
||||
|
||||
@@ -51,7 +51,9 @@ export type {
|
||||
} from "@calcom/features/bookings/types";
|
||||
|
||||
export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager";
|
||||
export type { ConnectedCalendar } from "@calcom/features/calendars/lib/CalendarManager";
|
||||
export { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||
export type { ConnectedDestinationCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||
export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler";
|
||||
|
||||
export { cityTimezonesHandler } from "@calcom/features/cityTimezones/cityTimezonesHandler";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import type { PrismaClient } from "@prisma/client";
|
||||
import type { TConnectedCalendarsInputSchema } from "./connectedCalendars.schema";
|
||||
|
||||
type ConnectedCalendarsOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
prisma: PrismaClient;
|
||||
};
|
||||
input: TConnectedCalendarsInputSchema;
|
||||
};
|
||||
@@ -23,10 +23,9 @@ type ConnectedCalendarsHandlerResult = {
|
||||
};
|
||||
|
||||
export const connectedCalendarsHandler = async ({
|
||||
ctx,
|
||||
ctx: { user, prisma },
|
||||
input,
|
||||
}: ConnectedCalendarsOptions): Promise<ConnectedCalendarsHandlerResult> => {
|
||||
const { user } = ctx;
|
||||
const onboarding = input?.onboarding || false;
|
||||
|
||||
const { connectedCalendars, destinationCalendar } =
|
||||
@@ -34,6 +33,7 @@ export const connectedCalendarsHandler = async ({
|
||||
user,
|
||||
onboarding,
|
||||
eventTypeId: input?.eventTypeId ?? null,
|
||||
skipSync: input?.skipSync ?? false,
|
||||
prisma,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export type TConnectedCalendarsInputSchema =
|
||||
// Fetches the calendars for this event-type only if present
|
||||
// Otherwise, fetches the calendars for the authenticated user
|
||||
eventTypeId?: number | null;
|
||||
skipSync?: boolean;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
@@ -15,5 +16,6 @@ export const ZConnectedCalendarsInputSchema: z.ZodType<TConnectedCalendarsInputS
|
||||
// Fetches the calendars for this event-type only if present
|
||||
// Otherwise, fetches the calendars for the authenticated user
|
||||
eventTypeId: z.number().nullish(),
|
||||
skipSync: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
Vendored
+4
-2
@@ -249,15 +249,17 @@ export interface AdditionalInformation {
|
||||
hangoutLink?: string;
|
||||
}
|
||||
|
||||
export interface IntegrationCalendar extends Ensure<Partial<_SelectedCalendar>, "externalId"> {
|
||||
export interface IntegrationCalendar extends Ensure<Partial<_SelectedCalendar>, "externalId" | "integration"> {
|
||||
primary?: boolean;
|
||||
name?: string;
|
||||
readOnly?: boolean;
|
||||
// For displaying the connected email address
|
||||
email?: string;
|
||||
primaryEmail?: string;
|
||||
primaryEmail?: string | null;
|
||||
credentialId?: number | null;
|
||||
integrationTitle?: string;
|
||||
integration: string;
|
||||
customCalendarReminder?: DestinationCalendar["customCalendarReminder"];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import cx from "@calcom/ui/classNames";
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import * as Label from "@radix-ui/react-label";
|
||||
import * as PrimitiveSwitch from "@radix-ui/react-switch";
|
||||
import type React from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
|
||||
import cx from "@calcom/ui/classNames";
|
||||
|
||||
import { Tooltip } from "../../tooltip";
|
||||
|
||||
const Wrapper = ({ children, tooltip }: { tooltip?: string; children: React.ReactNode }) => {
|
||||
@@ -76,7 +74,7 @@ export const Switch = (
|
||||
htmlFor={id}
|
||||
className={cx(
|
||||
"text-emphasis font-medium",
|
||||
size === "sm" ? "m-1 text-xs" : labelOnLeading ? "mr-2 mt-1 text-sm" : "ml-2 mt-1 text-sm",
|
||||
size === "sm" ? "m-1 text-xs" : labelOnLeading ? "mr-2 text-sm" : "ml-2 text-sm",
|
||||
primitiveProps.disabled ? "cursor-not-allowed opacity-25" : "cursor-pointer",
|
||||
labelOnLeading && "flex-1"
|
||||
)}>
|
||||
|
||||
Reference in New Issue
Block a user