@@ -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 =
;
+ } else if (data?.connectedCalendars.length === 0) {
+ content = (
+
+ );
+ } else {
+ content = data?.connectedCalendars.map((item) => (
+
+ {item.error && !item.calendars && }
+ {item?.error === undefined && item.calendars && (
+
+
+ {item.integration.logo && (
+
+ )}
+
+
+
+ {item.integration.name || item.integration.title}
+
+
+ {item.primary?.email}
+
+
+
+
+ {item.calendars.map((cal, index) => {
+ const id = cal.integrationTitle ?? `calendar-switch-${index}`;
+ return (
+
+ {
+ onToggleConnectedCalendar(cal.externalId, item.credentialId);
+ }}
+ />
+
+ );
+ })}
+
+
+
+ )}
+
+ ));
+ }
return (
- <>
-
-
-
- {isLoading ? (
-
- ) : (
- <>
- {connectedCalendars.length === 0 ? (
-
- ) : (
- <>
- {connectedCalendars.map((item) => (
-
- {item.error && !item.calendars && (
-
- )}
- {item?.error === undefined && item.calendars && (
-
-
- {
- // eslint-disable-next-line @next/next/no-img-element
- item.integration.logo && (
-
- )
- }
-
-
-
- {item.integration.name || item.integration.title}
-
-
- {item.primary.email}
-
-
-
-
- {item.calendars.map((cal, index) => {
- const id = cal.integrationTitle ?? `calendar-switch-${index}`;
- return (
-
- {
- onToggleConnectedCalendar(cal.externalId, item.credentialId);
- }}
- />
- {cal.name}
-
- );
- })}
-
-
-
- )}
-
- ))}
- >
- )}
- >
- )}
-
-
-
- {t("done")}
-
-
-
- >
+
+
+ {content}
+
+ {t("done")}
+
+
+
);
}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 23ec816bb1..59a8a17d91 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -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.",
diff --git a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts
index 016f7a54e1..a0c039b45d 100644
--- a/packages/app-store/ics-feedcalendar/lib/CalendarService.ts
+++ b/packages/app-store/ics-feedcalendar/lib/CalendarService.ts
@@ -1,18 +1,19 @@
/* eslint-disable @typescript-eslint/triple-slash-reference */
///
-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,
};
});
}
diff --git a/packages/features/bookings/Booker/components/OverlayCalendar/store.ts b/packages/features/bookings/Booker/components/OverlayCalendar/store.ts
index 9187e90595..51d3c902f5 100644
--- a/packages/features/bookings/Booker/components/OverlayCalendar/store.ts
+++ b/packages/features/bookings/Booker/components/OverlayCalendar/store.ts
@@ -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
((set) => ({
+export const useOverlayCalendarStore = createWithEqualityFn((set) => ({
overlayBusyDates: undefined,
setOverlayBusyDates: (busyDates: EventBusyDate[]) => {
set({ overlayBusyDates: busyDates });
diff --git a/packages/features/bookings/Booker/components/hooks/useCalendars.ts b/packages/features/bookings/Booker/components/hooks/useCalendars.ts
index d17a79c81e..7f1e26119a 100644
--- a/packages/features/bookings/Booker/components/hooks/useCalendars.ts
+++ b/packages/features/bookings/Booker/components/hooks/useCalendars.ts
@@ -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;
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>(() => {
+ 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,
};
};
diff --git a/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts b/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts
index 92e482fe9d..364f76eab1 100644
--- a/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts
+++ b/packages/features/bookings/Booker/components/hooks/useOverlayCalendar.ts
@@ -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;
-
export const useOverlayCalendar = ({
connectedCalendars,
overlayBusyDates,
onToggleCalendar,
-}: Pick) => {
- 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",
+ []
+ );
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,
diff --git a/packages/features/bookings/Booker/types.ts b/packages/features/bookings/Booker/types.ts
index fe35a33f66..23016cec89 100644
--- a/packages/features/bookings/Booker/types.ts
+++ b/packages/features/bookings/Booker/types.ts
@@ -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;
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) => void;
+ };
bookerForm: UseBookingFormReturnType;
event: BookerEventQuery;
schedule: useScheduleForEventReturnType;
diff --git a/packages/features/calendars/DestinationCalendarSelector.tsx b/packages/features/calendars/DestinationCalendarSelector.tsx
index 7c17ea438a..c093f81669 100644
--- a/packages/features/calendars/DestinationCalendarSelector.tsx
+++ b/packages/features/calendars/DestinationCalendarSelector.tsx
@@ -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")}`
) : (
- Default {" "}
+ {t("default")} {" "}
{destinationCalendar?.name &&
`${destinationCalendar.name} (${destinationCalendar?.integrationTitle} - ${destinationCalendar.primaryEmail})`}
diff --git a/packages/features/calendars/lib/CalendarManager.ts b/packages/features/calendars/lib/CalendarManager.ts
index e3d63da85e..64d6141167 100644
--- a/packages/features/calendars/lib/CalendarManager.ts
+++ b/packages/features/calendars/lib/CalendarManager.ts
@@ -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
+>[number]["integration"] & {
+ credentials?: CredentialPayload[];
+ credential?: CredentialPayload;
+};
+
+type CleanIntegration = Omit;
+
+/**
+ * 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) => {
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 & {
+ primary: boolean | null;
+ isSelected: boolean;
+ readOnly: boolean;
+ credentialId: number;
+ delegationCredentialId: string | null;
+};
+
export const getConnectedCalendars = async (
calendarCredentials: ReturnType,
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>[number]["integration"] & {
- credentials?: Array;
- 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;
-};
diff --git a/packages/features/calendars/lib/getConnectedDestinationCalendars.ts b/packages/features/calendars/lib/getConnectedDestinationCalendars.ts
index 5d26fffb93..39d6fd7231 100644
--- a/packages/features/calendars/lib/getConnectedDestinationCalendars.ts
+++ b/packages/features/calendars/lib/getConnectedDestinationCalendars.ts
@@ -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>;
type ConnectedCalendarsFromGetConnectedCalendars = ReturnTypeGetConnectedCalendars["connectedCalendars"];
-export type UserWithCalendars = Pick & {
- allSelectedCalendars: Pick<
- SelectedCalendar,
- "externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
- >[];
- userLevelSelectedCalendars: Pick<
- SelectedCalendar,
- "externalId" | "integration" | "eventTypeId" | "updatedAt" | "googleChannelId"
- >[];
- destinationCalendar: DestinationCalendar | null;
-};
-
-export type ConnectedDestinationCalendars = Awaited<
- ReturnType
->;
-
/**
* 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["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[] {
+}): 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 & {
+ 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
+>;
+
/**
* 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;
+ connectedCalendars: Awaited>["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>["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 | 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,
},
};
}
diff --git a/packages/features/users/repositories/UserRepository.ts b/packages/features/users/repositories/UserRepository.ts
index 47476a2ab6..9c1a01e94e 100644
--- a/packages/features/users/repositories/UserRepository.ts
+++ b/packages/features/users/repositories/UserRepository.ts
@@ -1110,6 +1110,7 @@ export class UserRepository {
movedToProfileId: true,
selectedCalendars: {
select: {
+ id: true,
eventTypeId: true,
externalId: true,
integration: true,
diff --git a/packages/platform/atoms/destination-calendar/DestinationCalendarSelector.tsx b/packages/platform/atoms/destination-calendar/DestinationCalendarSelector.tsx
index 0f49a39c60..9f7a17c35d 100644
--- a/packages/platform/atoms/destination-calendar/DestinationCalendarSelector.tsx
+++ b/packages/platform/atoms/destination-calendar/DestinationCalendarSelector.tsx
@@ -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")}`
) : (
-
- Default {" "}
+
+ {t("default")} {" "}
{destinationCalendar?.primaryEmail &&
`${destinationCalendar.name} (${destinationCalendar?.integrationTitle} - ${destinationCalendar.primaryEmail})`}
@@ -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);
diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts
index f967479efb..58578f5eda 100644
--- a/packages/platform/libraries/index.ts
+++ b/packages/platform/libraries/index.ts
@@ -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";
diff --git a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts
index 791b278db8..a02b048078 100644
--- a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts
+++ b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts
@@ -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;
+ prisma: PrismaClient;
};
input: TConnectedCalendarsInputSchema;
};
@@ -23,10 +23,9 @@ type ConnectedCalendarsHandlerResult = {
};
export const connectedCalendarsHandler = async ({
- ctx,
+ ctx: { user, prisma },
input,
}: ConnectedCalendarsOptions): Promise => {
- 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,
});
diff --git a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts
index a8caf4d88c..60821783f7 100644
--- a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts
+++ b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts
@@ -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, "externalId"> {
+export interface IntegrationCalendar extends Ensure, "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"];
}
/**
diff --git a/packages/ui/components/form/switch/Switch.tsx b/packages/ui/components/form/switch/Switch.tsx
index b4f4862082..b3374385a4 100644
--- a/packages/ui/components/form/switch/Switch.tsx
+++ b/packages/ui/components/form/switch/Switch.tsx
@@ -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"
)}>