Files
calendar/packages/types/Calendar.d.ts
T
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Udit Takkarbot_apk
a4621da2be feat: make source required on EventBusyDetails for Troubleshooter display (#27088)
* feat: make source required on EventBusyDetails for Troubleshooter display

- Make source a required property on EventBusyDetails type
- Update LimitManager to accept and store source when adding busy times
- Add user-friendly source names for all busy time types:
  - 'Booking Limit' for booking limit busy times
  - 'Duration Limit' for duration limit busy times
  - 'Team Booking Limit' for team booking limit busy times
  - 'Buffer Time' for seated event buffer times
  - 'Calendar' for external calendar busy times
- Ensure all entries in detailedBusyTimes have source set
- Cover includeManagedEventsInLimits and teamBookingLimits branches

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add limit value and unit metadata to busy time sources

- Include limit value and unit in source strings for Troubleshooter display
- Booking Limit: shows as 'Booking Limit: 5 per day'
- Duration Limit: shows as 'Duration Limit: 120 min per week'
- Team Booking Limit: shows as 'Team Booking Limit: 10 per month'
- Preserves existing calendar sources (e.g., 'google-calendar')

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: enhance busy time management with new limit sources

- Introduced new limit sources for busy times, including event booking and duration limits, with user-friendly titles for display.
- Updated LimitManager to accept and store detailed busy time information, including title and source.
- Refactored busy time addition logic across various services to utilize the new structure, improving clarity and maintainability.

* fixes

* feat: conditionally include source and translate busy time titles

- Add withSource parameter to conditionally include/exclude source from response
- Translate busy time titles on frontend using useLocale hook
- Source is only included when withSource=true (for Troubleshooter display)

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: enhance user availability service and busy time handling

- Updated LargeCalendar component to include event ID check for enabling busy times.
- Added translation for "busy" in common.json for better user experience.
- Refactored getUserAvailability service to include new method for fetching user availability with busy times from limits.
- Introduced parseLimits function to streamline booking and duration limit parsing.
- Improved error handling in user handler for better user feedback.

* refactor: remove unnecessary timeZone: undefined from addBusyTime calls

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add buffer_time and calendar translation keys for busy times

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add event source to calendar components and improve busy time handling

- Updated EventList component to include event source in data attributes for better tracking.
- Enhanced LargeCalendar component to pass event

* fix: add missing source property to Date Override calendar event

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use 'date-override' as source for Date Override calendar events

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Avoid type assertion

* fix: pass both bookingLimits and durationLimits to getStartEndDateforLimitCheck (#27898)

* test: add unit tests for getUserAvailabilityIncludingBusyTimesFromLimits

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: pass both bookingLimits and durationLimits to getStartEndDateforLimitCheck

- Fix bug where bookingLimits || durationLimits was passed as single param
- Skip getBusyTimesForLimitChecks when eventType has no limits
- Remove as never casts, use proper typing for mock dependencies
- Replace expect.any(String) with exact ISO date assertions

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: replace loose assertions with exact values in tests

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: address review feedback on busy time sources

- Fix t("busy") fallback to t("busy_time.busy") for correct translation lookup
- Add missing title property to buffer time entries for Troubleshooter display
- Use descriptive debug strings for buffer time source field
- Fix import ordering in LargeCalendar.tsx

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-Authored-By: unknown <>

* fix: preserve pre-existing busyTimesFromLimitsBookings from initialData

Address review feedback from @hariombalhara (comment #30, #31):

- Initialize busyTimesFromLimitsBookings from initialData instead of []
  to avoid silently overwriting pre-existing data with an empty array
- Use conditional spread to only include busyTimesFromLimitsBookings
  when it has a value
- Add test verifying pre-existing busyTimesFromLimitsBookings is
  preserved and passed through to _getUserAvailability
- Add test verifying busyTimesFromLimitsBookings is not passed when
  there are no limits and no initialData bookings

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-Authored-By: bot_apk <apk@cognition.ai>

* fix: pass fetched eventType to _getUserAvailability to avoid duplicate DB query

Addresses Devin Review comment r2863593564: the wrapper method fetches
eventType but wasn't passing it through, causing _getUserAvailability to
re-fetch the same eventType from the database.

Also adds a test verifying eventType is forwarded correctly.

Co-Authored-By: bot_apk <apk@cognition.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: bot_apk <apk@cognition.ai>
2026-03-06 14:53:14 -03:00

334 lines
9.1 KiB
TypeScript

import type { calendar_v3 } from "@googleapis/calendar";
import type { Dayjs } from "dayjs";
import type { TFunction } from "i18next";
import type { Time } from "ical.js";
import type { Frequency } from "rrule";
import type z from "zod";
import type { bookingResponse } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import type {
BookingSeat,
DestinationCalendar,
Prisma,
SelectedCalendar as _SelectedCalendar,
} from "@calcom/prisma/client";
import type { SchedulingType } from "@calcom/prisma/enums";
import type { CredentialForCalendarService } from "@calcom/types/Credential";
import type { Ensure } from "./utils";
export type { VideoCallData } from "./VideoApiAdapter";
type PaymentInfo = {
link?: string | null;
reason?: string | null;
id?: string | null;
paymentOption?: string | null;
amount?: number;
currency?: string;
};
export type Person = {
name: string;
email: string;
timeZone: string;
language: { translate: TFunction; locale: string };
username?: string;
usernameInOrg?: string;
id?: number;
bookingId?: number | null;
locale?: string | null;
timeFormat?: TimeFormat;
bookingSeat?: BookingSeat | null;
phoneNumber?: string | null;
};
export type TeamMember = {
id?: number;
name: string;
email: string;
phoneNumber?: string | null;
timeZone: string;
language: { translate: TFunction; locale: string };
};
export type EventBusyDate = {
start: Date | string;
end: Date | string;
source?: string | null;
timeZone?: string;
};
export type EventBusyDetails = EventBusyDate & {
title?: string;
source: string;
userId?: number | null;
};
export type AdditionalInfo = Record<string, unknown> & { calWarnings?: string[] };
export type NewCalendarEventType = {
uid: string;
id: string;
thirdPartyRecurringEventId?: string | null;
type: string;
password: string;
url: string;
additionalInfo: AdditionalInfo;
iCalUID?: string | null;
location?: string | null;
hangoutLink?: string | null;
conferenceData?: ConferenceData;
delegatedToId?: string | null;
};
export type CalendarEventType = {
uid: string;
etag: string;
/** This is the actual caldav event url, not the location url. */
url: string;
summary: string;
description: string;
location: string;
sequence: number;
startDate: Date | Dayjs;
endDate: Date | Dayjs;
duration: {
weeks: number;
days: number;
hours: number;
minutes: number;
seconds: number;
isNegative: boolean;
};
organizer: string;
attendees: unknown[][];
recurrenceId: Time;
timezone: string | object;
};
export type BatchResponse = {
responses: SubResponse[];
};
export type SubResponse = {
body: {
value: {
showAs: "free" | "tentative" | "away" | "busy" | "workingElsewhere";
start: { dateTime: string };
end: { dateTime: string };
}[];
};
};
export interface ConferenceData {
createRequest?: calendar_v3.Schema$CreateConferenceRequest;
}
export interface RecurringEvent {
dtstart?: Date | undefined;
interval: number;
count: number;
freq: Frequency;
until?: Date | undefined;
tzid?: string | undefined;
}
export type { IntervalLimit, IntervalLimitUnit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
export type AppsStatus = {
appName: string;
type: (typeof App)["type"];
success: number;
failures: number;
errors: string[];
warnings?: string[];
};
export type CalEventResponses = Record<
string,
{
label: string;
value: z.infer<typeof bookingResponse>;
isHidden?: boolean;
}
>;
export interface ExistingRecurringEvent {
recurringEventId: string;
}
// If modifying this interface, probably should update builders/calendarEvent files
export interface CalendarEvent {
// Instead of sending this per event.
// TODO: Links sent in email should be validated and automatically redirected to org domain or regular app. It would be a much cleaner way. Maybe use existing /api/link endpoint
bookerUrl?: string;
hashedLink?: string | null;
type: string;
title: string;
startTime: string;
endTime: string;
organizer: Person;
attendees: Person[];
length?: number | null;
additionalNotes?: string | null;
customInputs?: Prisma.JsonObject | null;
description?: string | null;
team?: {
name: string;
members: TeamMember[];
id: number;
};
location?: string | null;
conferenceCredentialId?: number;
conferenceData?: ConferenceData;
additionalInformation?: AdditionalInformation;
uid?: string | null;
existingRecurringEvent?: ExistingRecurringEvent | null;
bookingId?: number;
videoCallData?: VideoCallData;
paymentInfo?: PaymentInfo | null;
requiresConfirmation?: boolean | null;
destinationCalendar?: DestinationCalendar[] | null;
cancellationReason?: string | null;
rejectionReason?: string | null;
hideCalendarNotes?: boolean;
hideCalendarEventDetails?: boolean;
recurrence?: string;
recurringEvent?: RecurringEvent | null;
eventTypeId?: number | null;
appsStatus?: AppsStatus[];
seatsShowAttendees?: boolean | null;
seatsShowAvailabilityCount?: boolean | null;
attendeeSeatId?: string;
seatsPerTimeSlot?: number | null;
schedulingType?: SchedulingType | null;
iCalUID?: string | null;
iCalSequence?: number | null;
hideOrganizerEmail?: boolean;
disableCancelling?: boolean;
disableRescheduling?: boolean;
// It has responses to all the fields(system + user)
responses?: CalEventResponses | null;
// It just has responses to only the user fields. It allows to easily iterate over to show only user fields
userFieldsResponses?: CalEventResponses | null;
platformClientId?: string | null;
platformRescheduleUrl?: string | null;
platformCancelUrl?: string | null;
platformBookingUrl?: string | null;
hideBranding?: boolean;
oneTimePassword?: string | null;
delegationCredentialId?: string | null;
domainWideDelegationCredentialId?: string | null;
customReplyToEmail?: string | null;
rescheduledBy?: string;
organizationId?: number | null;
hasOrganizerChanged?: boolean;
assignmentReason?: {
category: string; // Translated label like "Routed", "Reassigned", etc.
details?: string | null; // The detailed reason string
} | null;
}
export interface EntryPoint {
entryPointType?: string;
uri?: string;
label?: string;
pin?: string;
accessCode?: string;
meetingCode?: string;
passcode?: string;
password?: string;
}
export interface AdditionalInformation {
conferenceData?: ConferenceData;
entryPoints?: EntryPoint[];
hangoutLink?: string;
}
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 | null;
credentialId?: number | null;
integrationTitle?: string;
integration: string;
customCalendarReminder?: DestinationCalendar["customCalendarReminder"];
}
/**
* Mode for calendar fetch operations to control caching behavior:
* - "slots": For getting actual calendar availability (uses cache when available)
* - "overlay": For getting overlay calendar availability (does not use cache)
* - "booking": For booking confirmation (does not use cache)
* - "none": For operations that don't use getAvailability (e.g., deleteEvent, listCalendars)
*/
export type CalendarFetchMode = "slots" | "overlay" | "booking" | "none";
/**
* Parameters for getAvailability and getAvailabilityWithTimeZones methods
*/
export interface GetAvailabilityParams {
dateFrom: string;
dateTo: string;
selectedCalendars: IntegrationCalendar[];
mode: CalendarFetchMode;
fallbackToPrimary?: boolean;
}
/**
* null is to refer to user-level SelectedCalendar
*/
export type SelectedCalendarEventTypeIds = (number | null)[];
export interface CalendarServiceEvent extends CalendarEvent {
calendarDescription: string;
}
export interface Calendar {
getCredentialId?(): number;
createEvent(
event: CalendarServiceEvent,
credentialId: number,
externalCalendarId?: string
): Promise<NewCalendarEventType>;
updateEvent(
uid: string,
event: CalendarServiceEvent,
externalCalendarId?: string | null
): Promise<NewCalendarEventType | NewCalendarEventType[]>;
deleteEvent(uid: string, event: CalendarEvent, externalCalendarId?: string | null): Promise<unknown>;
getAvailability(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
// for OOO calibration (only google calendar for now)
getAvailabilityWithTimeZones?(params: GetAvailabilityParams): Promise<EventBusyDate[]>;
fetchAvailabilityAndSetCache?(selectedCalendars: IntegrationCalendar[]): Promise<unknown>;
listCalendars(event?: CalendarEvent): Promise<IntegrationCalendar[]>;
testDelegationCredentialSetup?(): Promise<void>;
}
/**
* @see [How to inference class type that implements an interface](https://stackoverflow.com/a/64765554/6297100)
*/
type Class<I, Args extends unknown[] = unknown[]> = new (...args: Args) => I;
export type CalendarClass = Class<Calendar, [CredentialForCalendarService]>;
export type SelectedCalendar = Pick<
_SelectedCalendar,
"userId" | "integration" | "externalId" | "credentialId"
>;