diff --git a/apps/web/components/apps/CalendarListContainer.tsx b/apps/web/components/apps/CalendarListContainer.tsx index 69836304ced798..b27585e5b24672 100644 --- a/apps/web/components/apps/CalendarListContainer.tsx +++ b/apps/web/components/apps/CalendarListContainer.tsx @@ -1,27 +1,32 @@ "use client"; -import { useEffect, Suspense } from "react"; - import { InstallAppButton } from "@calcom/app-store/InstallAppButton"; -import { DestinationCalendarSettingsWebWrapper } from "./DestinationCalendarSettingsWebWrapper"; -import { SelectedCalendarsSettingsWebWrapper } from "@calcom/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper"; -import AppListCard from "@calcom/web/modules/apps/components/AppListCard"; -import { SkeletonLoader } from "@calcom/web/modules/apps/components/SkeletonLoader"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; import { Button } from "@calcom/ui/components/button"; import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { ShellSubHeading } from "@calcom/ui/components/layout"; import { List } from "@calcom/ui/components/list"; import { showToast } from "@calcom/ui/components/toast"; import { revalidateSettingsCalendars } from "@calcom/web/app/cache/path/settings/my-account"; +import AppListCard from "@calcom/web/modules/apps/components/AppListCard"; +import { SkeletonLoader } from "@calcom/web/modules/apps/components/SkeletonLoader"; +import { SelectedCalendarsSettingsWebWrapper } from "@calcom/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper"; +import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections"; +import useRouterQuery from "@lib/hooks/useRouterQuery"; import { QueryCell } from "@lib/QueryCell"; -import useRouterQuery from "@lib/hooks/useRouterQuery"; +import { Suspense, useEffect } from "react"; +import { DestinationCalendarSettingsWebWrapper } from "./DestinationCalendarSettingsWebWrapper"; -import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections"; +type CalendarListContainerProps = { + connectedCalendars: RouterOutputs["viewer"]["calendars"]["connectedCalendars"]; + installedCalendars: RouterOutputs["viewer"]["apps"]["integrations"]; + heading?: boolean; + fromOnboarding?: boolean; +}; type Props = { onChanged: () => unknown | Promise; @@ -30,7 +35,7 @@ type Props = { isPending?: boolean; }; -function CalendarList(props: Props) { +function CalendarList(props: Props): JSX.Element { const { t } = useLocale(); const query = trpc.viewer.apps.integrations.useQuery({ variant: "calendar", onlyInstalled: false }); @@ -66,18 +71,16 @@ function CalendarList(props: Props) { ); } -const AddCalendarButton = () => { +const AddCalendarButton = (): JSX.Element => { const { t } = useLocale(); return ( - <> - - + ); }; -export const CalendarListContainerSkeletonLoader = () => { +export const CalendarListContainerSkeletonLoader = (): JSX.Element => { const { t } = useLocale(); return ( { ); }; -type CalendarListContainerProps = { - connectedCalendars: RouterOutputs["viewer"]["calendars"]["connectedCalendars"]; - installedCalendars: RouterOutputs["viewer"]["apps"]["integrations"]; - heading?: boolean; - fromOnboarding?: boolean; -}; - export function CalendarListContainer({ connectedCalendars: data, installedCalendars, heading = true, fromOnboarding, -}: CalendarListContainerProps) { +}: CalendarListContainerProps): JSX.Element { const { t } = useLocale(); const { error, setQuery: setError } = useRouterQuery("error"); @@ -113,7 +109,7 @@ export function CalendarListContainer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const utils = trpc.useUtils(); - const onChanged = () => + const onChanged = (): void => { Promise.allSettled([ utils.viewer.apps.integrations.invalidate( { variant: "calendar", onlyInstalled: true }, @@ -124,7 +120,7 @@ export function CalendarListContainer({ utils.viewer.calendars.connectedCalendars.invalidate(), revalidateSettingsCalendars(), ]); - + }; const mutation = trpc.viewer.calendars.setDestinationCalendar.useMutation({ onSuccess: () => { utils.viewer.calendars.connectedCalendars.invalidate(); @@ -132,51 +128,61 @@ export function CalendarListContainer({ }, }); + let content = null; + if (!!data.connectedCalendars.length || !!installedCalendars?.items.length) { + let headingContent = null; + if (heading) { + headingContent = ( + <> + + }> + + + + ); + } + content = headingContent; + } else if (fromOnboarding) { + content = ( + <> + {!!data?.connectedCalendars.length && ( + } + /> + )} + + + ); + } else { + content = ( + + {t(`connect_calendar_apps`)} + + } + /> + ); + } + return ( }> - {!!data.connectedCalendars.length || !!installedCalendars?.items.length ? ( - <> - {heading && ( - <> - - }> - - - - )} - - ) : fromOnboarding ? ( - <> - {!!data?.connectedCalendars.length && ( - } - /> - )} - - - ) : ( - - {t(`connect_calendar_apps`)} - - } - /> - )} + {content} ); } diff --git a/apps/web/components/apps/DestinationCalendarSettingsWebWrapper.tsx b/apps/web/components/apps/DestinationCalendarSettingsWebWrapper.tsx index ccdde41a5278b9..4df517248853f7 100644 --- a/apps/web/components/apps/DestinationCalendarSettingsWebWrapper.tsx +++ b/apps/web/components/apps/DestinationCalendarSettingsWebWrapper.tsx @@ -1,17 +1,22 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; +import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { - reminderSchema, type ReminderMinutes, + reminderSchema, } from "@calcom/trpc/server/routers/viewer/calendars/setDestinationReminder.schema"; import { showToast } from "@calcom/ui/components/toast"; - -import { AtomsWrapper } from "../../../../packages/platform/atoms/src/components/atoms-wrapper"; import { DestinationCalendarSettings } from "../../../../packages/platform/atoms/destination-calendar/DestinationCalendar"; - -export const DestinationCalendarSettingsWebWrapper = () => { +import { AtomsWrapper } from "../../../../packages/platform/atoms/src/components/atoms-wrapper"; +export const DestinationCalendarSettingsWebWrapper = ({ + connectedCalendars, +}: { + connectedCalendars?: RouterOutputs["viewer"]["calendars"]["connectedCalendars"]; +}): JSX.Element | null => { const { t } = useLocale(); - const calendars = trpc.viewer.calendars.connectedCalendars.useQuery(); + const calendars = trpc.viewer.calendars.connectedCalendars.useQuery(undefined, { + initialData: connectedCalendars, + }); const utils = trpc.useUtils(); const mutation = trpc.viewer.calendars.setDestinationCalendar.useMutation({ onSuccess: () => { @@ -33,7 +38,7 @@ export const DestinationCalendarSettingsWebWrapper = () => { return null; } - const handleReminderChange = (value: ReminderMinutes) => { + const handleReminderChange = (value: ReminderMinutes): void => { const destCal = calendars.data.destinationCalendar; if (destCal?.credentialId) { reminderMutation.mutate({ @@ -47,9 +52,10 @@ export const DestinationCalendarSettingsWebWrapper = () => { const validatedReminderValue = reminderSchema.safeParse( calendars.data.destinationCalendar.customCalendarReminder ); - const reminderValue: ReminderMinutes = validatedReminderValue.success - ? validatedReminderValue.data - : null; + let reminderValue: ReminderMinutes = null; + if (validatedReminderValue.success) { + reminderValue = validatedReminderValue.data; + } return ( diff --git a/apps/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper.tsx b/apps/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper.tsx index 4770405eb59f24..678e5603ce80e3 100644 --- a/apps/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper.tsx +++ b/apps/web/modules/calendars/components/SelectedCalendarsSettingsWebWrapper.tsx @@ -1,8 +1,4 @@ -import Link from "next/link"; -import React from "react"; - -import AppListCard from "@calcom/web/modules/apps/components/AppListCard"; -import CredentialActionsDropdown from "@calcom/web/modules/apps/components/CredentialActionsDropdown"; +import { SelectedCalendarsSettings } from "@calcom/atoms/selected-calendars/SelectedCalendarsSettings"; import AdditionalCalendarSelector from "@calcom/features/calendars/AdditionalCalendarSelector"; import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -11,8 +7,10 @@ import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/components/alert"; import { Select } from "@calcom/ui/components/form"; import { List } from "@calcom/ui/components/list"; - -import { SelectedCalendarsSettings } from "@calcom/atoms/selected-calendars/SelectedCalendarsSettings"; +import AppListCard from "@calcom/web/modules/apps/components/AppListCard"; +import CredentialActionsDropdown from "@calcom/web/modules/apps/components/CredentialActionsDropdown"; +import Link from "next/link"; +import React from "react"; export enum SelectedCalendarSettingsScope { User = "user", @@ -30,6 +28,7 @@ type SelectedCalendarsSettingsWebWrapperProps = { scope?: SelectedCalendarSettingsScope; setScope?: (scope: SelectedCalendarSettingsScope) => void; disableConnectionModification?: boolean; + connectedCalendars?: RouterOutputs["viewer"]["calendars"]["connectedCalendars"]; }; const ConnectedCalendarList = ({ @@ -92,7 +91,12 @@ const ConnectedCalendarList = ({ isChecked={cal.isSelected} destination={cal.externalId === destinationCalendarId} credentialId={cal.credentialId} - eventTypeId={shouldUseEventTypeScope ? eventTypeId : null} + eventTypeId={(() => { + if (shouldUseEventTypeScope) { + return eventTypeId; + } + return null; + })()} delegationCredentialId={connectedCalendar.delegationCredentialId || null} /> ))} @@ -145,19 +149,30 @@ export const SelectedCalendarsSettingsWebWrapper = (props: SelectedCalendarsSett eventTypeId = null, } = props; - const query = trpc.viewer.calendars.connectedCalendars.useQuery( - { - eventTypeId: scope === SelectedCalendarSettingsScope.EventType ? eventTypeId! : null, - }, - { - suspense: true, - refetchOnWindowFocus: false, - } - ); + let queryInput: { eventTypeId: number } | undefined; + if (scope === SelectedCalendarSettingsScope.EventType) { + queryInput = { eventTypeId: eventTypeId! }; + } + + let initialData: RouterOutputs["viewer"]["calendars"]["connectedCalendars"] | undefined; + if (scope === SelectedCalendarSettingsScope.User && props.connectedCalendars) { + initialData = props.connectedCalendars; + } - const { isPending } = props; + const query = trpc.viewer.calendars.connectedCalendars.useQuery(queryInput, { + initialData, + suspense: true, + refetchOnWindowFocus: false, + }); + + const isPending = props.isPending; const showScopeSelector = !!props.eventTypeId; - const isDisabled = disabledScope ? disabledScope === scope : false; + + let isDisabled = false; + if (disabledScope) { + isDisabled = disabledScope === scope; + } + const shouldDisableConnectionModification = isDisabled || disableConnectionModification; return (
@@ -170,7 +185,7 @@ export const SelectedCalendarsSettingsWebWrapper = (props: SelectedCalendarsSett scope={scope} shouldDisableConnectionModification={shouldDisableConnectionModification} /> - {query.data?.connectedCalendars && query.data?.connectedCalendars.length > 0 ? ( + {!!(query.data?.connectedCalendars && query.data?.connectedCalendars.length > 0) && ( - ) : null} + )}
); diff --git a/packages/features/availability/lib/getUserAvailability.ts b/packages/features/availability/lib/getUserAvailability.ts index 596897ed7dd68c..41dab58c6d99f1 100644 --- a/packages/features/availability/lib/getUserAvailability.ts +++ b/packages/features/availability/lib/getUserAvailability.ts @@ -1,6 +1,3 @@ -import * as Sentry from "@sentry/nextjs"; -import { z } from "zod"; - import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; @@ -10,16 +7,14 @@ import { getBusyTimesFromTeamLimits, } from "@calcom/features/busyTimes/lib/getBusyTimesFromLimits"; import { getBusyTimesService } from "@calcom/features/di/containers/BusyTimes"; -import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; +import type { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import type { PrismaHolidayRepository } from "@calcom/features/holidays/repositories/PrismaHolidayRepository"; import type { PrismaOOORepository } from "@calcom/features/ooo/repositories/PrismaOOORepository"; import type { IRedisService } from "@calcom/features/redis/IRedisService"; -import type { WorkingHours as WorkingHoursWithUserId } from "@calcom/types/schedule"; import type { DateOverride, WorkingHours } from "@calcom/features/schedules/lib/date-ranges"; import { buildDateRanges, subtract } from "@calcom/features/schedules/lib/date-ranges"; import { getWorkingHours } from "@calcom/lib/availability"; import { stringToDayjsZod } from "@calcom/lib/dayjs"; -import { detectEventTypeScheduleForUser } from "./detectEventTypeScheduleForUser"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { getHolidayService } from "@calcom/lib/holidays"; import { getHolidayEmoji } from "@calcom/lib/holidays/getHolidayEmoji"; @@ -31,20 +26,23 @@ import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { withReporting } from "@calcom/lib/sentryWrapper"; import type { + Availability, Booking, - User, OutOfOfficeEntry, OutOfOfficeReason, EventType as PrismaEventType, - Availability, SelectedCalendar, TravelSchedule, + User, } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { CalendarFetchMode, EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar"; -import type { TimeRange } from "@calcom/types/schedule"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; +import type { TimeRange, WorkingHours as WorkingHoursWithUserId } from "@calcom/types/schedule"; +import * as Sentry from "@sentry/nextjs"; +import { z } from "zod"; +import { detectEventTypeScheduleForUser } from "./detectEventTypeScheduleForUser"; const log = logger.getSubLogger({ prefix: ["getUserAvailability"] }); @@ -407,7 +405,7 @@ export class UserAvailabilityService { const getBusyTimesEnd = dateTo.toISOString(); const selectedCalendars = eventType?.useEventLevelSelectedCalendars - ? EventTypeRepository.getSelectedCalendarsFromUser({ user, eventTypeId: eventType.id }) + ? user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventType.id) : user.userLevelSelectedCalendars; let calendarTimezone: string | null = null; diff --git a/packages/features/calendars/lib/getConnectedDestinationCalendars.ts b/packages/features/calendars/lib/getConnectedDestinationCalendars.ts index 4945531231d29b..5d26fffb938596 100644 --- a/packages/features/calendars/lib/getConnectedDestinationCalendars.ts +++ b/packages/features/calendars/lib/getConnectedDestinationCalendars.ts @@ -4,7 +4,6 @@ import { getConnectedCalendars, } from "@calcom/features/calendars/lib/CalendarManager"; import { DestinationCalendarRepository } from "@calcom/features/calendars/repositories/DestinationCalendarRepository"; -import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository"; import { isDelegationCredential } from "@calcom/lib/delegationCredential"; import logger from "@calcom/lib/logger"; import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; @@ -44,7 +43,7 @@ const _ensureNoConflictingNonDelegatedConnectedCalendar = < integration: { slug: string }; primary?: { email?: string | null | undefined } | undefined; delegationCredentialId?: string | null | undefined; - } + }, >({ connectedCalendars, loggedInUser, @@ -216,7 +215,7 @@ function findMatchingCalendar({ calendar: DestinationCalendar; }) { // Check if destinationCalendar exists in connectedCalendars - const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat(); + const allCals = connectedCalendars.flatMap((cal) => cal.calendars ?? []); const matchingCalendar = allCals.find( (cal) => cal.externalId === calendar.externalId && cal.integration === calendar.integration ); @@ -255,14 +254,10 @@ function getSelectedCalendars({ }: { user: UserWithCalendars; eventTypeId: number | null; -}) { +}): Pick[] { if (eventTypeId) { - return EventTypeRepository.getSelectedCalendarsFromUser({ - user, - eventTypeId: eventTypeId ?? null, - }); + return user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventTypeId); } - return user.userLevelSelectedCalendars; } diff --git a/packages/features/eventtypes/repositories/eventTypeRepository.ts b/packages/features/eventtypes/repositories/eventTypeRepository.ts index 7aa4bbcde59706..273a70520e2813 100644 --- a/packages/features/eventtypes/repositories/eventTypeRepository.ts +++ b/packages/features/eventtypes/repositories/eventTypeRepository.ts @@ -1408,16 +1408,6 @@ export class EventTypeRepository { }; } - static getSelectedCalendarsFromUser({ - user, - eventTypeId, - }: { - user: UserWithSelectedCalendars; - eventTypeId: number; - }) { - return user.allSelectedCalendars.filter((calendar) => calendar.eventTypeId === eventTypeId); - } - async findByIdForUserAvailability({ id }: { id: number }) { const eventType = await this.prismaClient.eventType.findUnique({ where: { id }, diff --git a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts index 68d2ea8c575913..791b278db87415 100644 --- a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts +++ b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.handler.ts @@ -11,7 +11,21 @@ type ConnectedCalendarsOptions = { input: TConnectedCalendarsInputSchema; }; -export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalendarsOptions) => { +type GetConnectedDestinationCalendarsAndEnsureDefaultsInDbResult = Awaited< + ReturnType +>; + +type ConnectedCalendarsHandlerResult = { + destinationCalendar: GetConnectedDestinationCalendarsAndEnsureDefaultsInDbResult["destinationCalendar"]; + connectedCalendars: (GetConnectedDestinationCalendarsAndEnsureDefaultsInDbResult["connectedCalendars"][number] & { + cacheUpdatedAt: null; + })[]; +}; + +export const connectedCalendarsHandler = async ({ + ctx, + input, +}: ConnectedCalendarsOptions): Promise => { const { user } = ctx; const onboarding = input?.onboarding || false; diff --git a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts index e2fd71a64c0591..a8caf4d88c7a13 100644 --- a/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts +++ b/packages/trpc/server/routers/viewer/calendars/connectedCalendars.schema.ts @@ -1,12 +1,19 @@ import { z } from "zod"; -export const ZConnectedCalendarsInputSchema = z +export type TConnectedCalendarsInputSchema = + | { + onboarding?: boolean; + // Fetches the calendars for this event-type only if present + // Otherwise, fetches the calendars for the authenticated user + eventTypeId?: number | null; + } + | undefined; + +export const ZConnectedCalendarsInputSchema: z.ZodType = z .object({ onboarding: z.boolean().optional(), // Fetches the calendars for this event-type only if present // Otherwise, fetches the calendars for the authenticated user - eventTypeId: z.number().nullable(), + eventTypeId: z.number().nullish(), }) .optional(); - -export type TConnectedCalendarsInputSchema = z.infer;