🏷️(front) add DAV service type definitions

Add TypeScript type definitions for CalDAV service including
calendar, event, addressbook and options types. Add global
type declarations for tsdav and ical.js libraries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Panchout
2026-01-25 20:33:36 +01:00
parent 1182400fb2
commit 1a26d9aac3
6 changed files with 989 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
import type { DAVAddressBook } from 'tsdav'
import ICAL from 'ical.js'
export type AddressBook = DAVAddressBook & {
headers?: Record<string, string>
uid?: unknown
}
export type AddressBookObject = {
data: ICAL.Component
etag?: string
url: string
addressBookUrl: string
}
export type VCard = {
name: string
email: string | null
}
export type AddressBookVCard = {
// INFO - 2025-07-24 - addressBookUrl is undefined when the contact is from a VCardProvider
addressBookUrl?: string
vCard: VCard
}
export type Contact = {
name?: string
email: string
}

View File

@@ -0,0 +1,280 @@
/**
* Types for CalDavService - Pure CalDAV operations
*
* Reuses types from tsdav and ts-ics where possible to avoid duplication.
*/
import type { DAVCalendar, DAVCalendarObject } from 'tsdav'
import type {
IcsCalendar,
IcsEvent,
IcsAttendee,
IcsOrganizer,
IcsAttendeePartStatusType,
FreeBusyType as IcsFreeBusyType,
} from 'ts-ics'
// ============================================================================
// Re-exports from libraries (avoid duplication)
// ============================================================================
/** Attendee type from ts-ics */
export type CalDavAttendee = IcsAttendee
/** Organizer type from ts-ics */
export type CalDavOrganizer = IcsOrganizer
/** Attendee participation status from ts-ics */
export type AttendeeStatus = IcsAttendeePartStatusType
/** FreeBusy type from ts-ics */
export type FreeBusyType = IcsFreeBusyType
// ============================================================================
// Connection & Authentication
// ============================================================================
export type CalDavCredentials = {
serverUrl: string
username?: string
password?: string
headers?: Record<string, string>
fetchOptions?: RequestInit
}
export type CalDavAccount = {
serverUrl: string
rootUrl?: string
principalUrl?: string
homeUrl?: string
headers?: Record<string, string>
fetchOptions?: RequestInit
}
// ============================================================================
// Calendar Types (extends tsdav types)
// ============================================================================
/** Calendar type extending DAVCalendar with additional properties */
export type CalDavCalendar = Pick<DAVCalendar, 'url' | 'ctag' | 'syncToken' | 'components' | 'timezone'> & {
displayName: string
description?: string
color?: string
resourcetype?: string[]
headers?: Record<string, string>
fetchOptions?: RequestInit
}
export type CalDavCalendarCreate = {
displayName: string
description?: string
color?: string
timezone?: string
components?: ('VEVENT' | 'VTODO' | 'VJOURNAL')[]
}
export type CalDavCalendarUpdate = {
displayName?: string
description?: string
color?: string
timezone?: string
}
// ============================================================================
// Event Types (extends tsdav types)
// ============================================================================
/** Event type extending DAVCalendarObject with parsed ICS data */
export type CalDavEvent = Pick<DAVCalendarObject, 'url' | 'etag'> & {
calendarUrl: string
data: IcsCalendar
}
export type CalDavEventCreate = {
calendarUrl: string
event: IcsEvent
}
export type CalDavEventUpdate = {
eventUrl: string
event: IcsEvent
etag?: string
}
// ============================================================================
// Time Range & Filters
// ============================================================================
export type TimeRange = {
start: string | Date
end: string | Date
}
export type EventFilter = {
timeRange?: TimeRange
expand?: boolean
componentType?: 'VEVENT' | 'VTODO' | 'VJOURNAL'
}
// ============================================================================
// Sharing Types (CalDAV Scheduling & ACL)
// ============================================================================
export type SharePrivilege = 'read' | 'read-write' | 'read-write-noacl' | 'admin'
export type ShareStatus = 'pending' | 'accepted' | 'declined'
export type CalDavSharee = {
href: string // mailto:email or principal URL
displayName?: string
privilege: SharePrivilege
status?: ShareStatus
}
export type CalDavShareInvite = {
calendarUrl: string
sharees: CalDavSharee[]
summary?: string
comment?: string
}
export type CalDavShareResponse = {
success: boolean
sharees: CalDavSharee[]
errors?: { href: string; error: string }[]
}
export type CalDavInvitation = {
uid: string
calendarUrl: string
ownerHref: string
ownerDisplayName?: string
summary?: string
privilege: SharePrivilege
status: ShareStatus
}
// ============================================================================
// Scheduling (iTIP) Types
// ============================================================================
export type SchedulingMethod = 'REQUEST' | 'REPLY' | 'CANCEL' | 'ADD' | 'REFRESH' | 'COUNTER' | 'DECLINECOUNTER'
export type SchedulingRequest = {
method: SchedulingMethod
organizer: CalDavOrganizer
attendees: CalDavAttendee[]
event: IcsEvent
}
export type SchedulingResponse = {
success: boolean
responses: {
recipient: string
status: 'delivered' | 'failed' | 'pending'
error?: string
}[]
}
// ============================================================================
// FreeBusy Types
// ============================================================================
export type FreeBusyPeriod = {
start: Date
end: Date
type: FreeBusyType
}
export type FreeBusyRequest = {
attendees: string[] // email addresses
timeRange: TimeRange
organizer?: CalDavOrganizer
}
export type FreeBusyResponse = {
attendee: string
periods: FreeBusyPeriod[]
}
// ============================================================================
// ACL Types
// ============================================================================
export type AclPrivilege =
| 'all'
| 'read'
| 'write'
| 'write-properties'
| 'write-content'
| 'unlock'
| 'bind'
| 'unbind'
| 'read-acl'
| 'write-acl'
| 'read-current-user-privilege-set'
export type AclPrincipal = {
href?: string
all?: boolean
authenticated?: boolean
unauthenticated?: boolean
self?: boolean
}
export type AclEntry = {
principal: AclPrincipal
privileges: AclPrivilege[]
grant: boolean
protected?: boolean
inherited?: string
}
export type CalendarAcl = {
calendarUrl: string
entries: AclEntry[]
ownerHref?: string
}
// ============================================================================
// Sync Types
// ============================================================================
export type SyncReport = {
syncToken: string
changed: CalDavEvent[]
deleted: string[] // URLs of deleted events
}
export type SyncOptions = {
syncToken?: string
syncLevel?: 1 | 'infinite'
}
// ============================================================================
// Principal Types
// ============================================================================
export type CalDavPrincipal = {
url: string
displayName?: string
email?: string
calendarHomeSet?: string
addressBookHomeSet?: string
}
// ============================================================================
// Response Types
// ============================================================================
export type CalDavResponse<T = void> = {
success: boolean
data?: T
error?: string
status?: number
}
export type CalDavMultiResponse<T> = {
success: boolean
results: { url: string; data?: T; error?: string }[]
}

View File

@@ -0,0 +1,39 @@
import type { IcsCalendar, IcsEvent, IcsRecurrenceId } from 'ts-ics'
import type { DAVCalendar } from 'tsdav'
// TODO - CJ - 2025-07-03 - add <TCalendarUid = any> generic
// TODO - CJ - 2025-07-03 - add options to support IcsEvent custom props
export type Calendar = DAVCalendar & {
// INFO - CJ - 2025-07-03 - Useful fields from 'DAVCalendar'
// ctag?: string
// description?: string;
// displayName?: string | Record<string, unknown>;
// calendarColor?: string
// url: string
// fetchOptions?: RequestInit
headers?: Record<string, string>
uid?: unknown
}
export type CalendarObject = {
data: IcsCalendar
etag?: string
url: string
calendarUrl: string
}
export type CalendarEvent = {
calendarUrl: string
event: IcsEvent
}
export type EventUid = {
uid: string
recurrenceId?: IcsRecurrenceId
}
export type DisplayedCalendarEvent = {
calendarUrl: string
event: IcsEvent
recurringEvent?: IcsEvent
}

View File

@@ -0,0 +1,434 @@
/**
* Types for EventCalendar adapter (vkurko/calendar)
*
* Based on: https://github.com/vkurko/calendar
* These types represent the EventCalendar library format
*/
// ============================================================================
// Duration Types
// ============================================================================
export type EventCalendarDuration = {
years?: number
months?: number
weeks?: number
days?: number
hours?: number
minutes?: number
seconds?: number
}
export type EventCalendarDurationInput =
| EventCalendarDuration
| string // 'hh:mm:ss' or 'hh:mm'
| number // total seconds
// ============================================================================
// Event Types
// ============================================================================
export type EventCalendarEvent = {
id: string | number
resourceId?: string | number
resourceIds?: (string | number)[]
allDay?: boolean
start: Date | string
end?: Date | string
title?: EventCalendarContent
editable?: boolean
startEditable?: boolean
durationEditable?: boolean
display?: 'auto' | 'background' | 'ghost' | 'preview' | 'pointer'
backgroundColor?: string
textColor?: string
color?: string
classNames?: string | string[]
styles?: string | string[]
extendedProps?: Record<string, unknown>
}
export type EventCalendarEventInput = Omit<EventCalendarEvent, 'id'> & {
id?: string | number
}
// ============================================================================
// Resource Types
// ============================================================================
export type EventCalendarResource = {
id: string | number
title?: EventCalendarContent
eventBackgroundColor?: string
eventTextColor?: string
extendedProps?: Record<string, unknown>
children?: EventCalendarResource[]
}
// ============================================================================
// View Types
// ============================================================================
export type EventCalendarView =
| 'dayGridMonth'
| 'dayGridWeek'
| 'dayGridDay'
| 'timeGridWeek'
| 'timeGridDay'
| 'listDay'
| 'listWeek'
| 'listMonth'
| 'listYear'
| 'resourceTimeGridDay'
| 'resourceTimeGridWeek'
| 'resourceTimelineDay'
| 'resourceTimelineWeek'
| 'resourceTimelineMonth'
export type EventCalendarViewInfo = {
type: EventCalendarView
title: string
currentStart: Date
currentEnd: Date
activeStart: Date
activeEnd: Date
}
// ============================================================================
// Content Types
// ============================================================================
export type EventCalendarContent =
| string
| { html: string }
| { domNodes: Node[] }
// ============================================================================
// Callback/Handler Types
// ============================================================================
// Note: jsEvent uses Event (not MouseEvent) for compatibility with @event-calendar/core DomEvent type
export type EventCalendarEventClickInfo = {
el: HTMLElement
event: EventCalendarEvent
jsEvent: Event
view: EventCalendarViewInfo
}
export type EventCalendarDateClickInfo = {
date: Date
dateStr: string
allDay: boolean
dayEl: HTMLElement
jsEvent: Event
view: EventCalendarViewInfo
resource?: EventCalendarResource
}
export type EventCalendarEventDropInfo = {
event: EventCalendarEvent
oldEvent: EventCalendarEvent
oldResource?: EventCalendarResource
newResource?: EventCalendarResource
delta: EventCalendarDuration
revert: () => void
jsEvent: Event
view: EventCalendarViewInfo
}
export type EventCalendarEventResizeInfo = {
event: EventCalendarEvent
oldEvent: EventCalendarEvent
startDelta: EventCalendarDuration
endDelta: EventCalendarDuration
revert: () => void
jsEvent: Event
view: EventCalendarViewInfo
}
export type EventCalendarSelectInfo = {
start: Date
end: Date
startStr: string
endStr: string
allDay: boolean
jsEvent: Event
view: EventCalendarViewInfo
resource?: EventCalendarResource
}
export type EventCalendarDatesSetInfo = {
start: Date
end: Date
startStr: string
endStr: string
view: EventCalendarViewInfo
}
export type EventCalendarEventMountInfo = {
el: HTMLElement
event: EventCalendarEvent
view: EventCalendarViewInfo
timeText: string
}
export type EventCalendarEventContentInfo = {
event: EventCalendarEvent
view: EventCalendarViewInfo
timeText: string
}
// ============================================================================
// Options Types
// ============================================================================
export type EventCalendarOptions = {
// View configuration
view?: EventCalendarView
views?: Record<string, EventCalendarViewOptions>
headerToolbar?: EventCalendarToolbar
footerToolbar?: EventCalendarToolbar
// Date configuration
date?: Date | string
firstDay?: 0 | 1 | 2 | 3 | 4 | 5 | 6
hiddenDays?: number[]
// Time configuration
slotMinTime?: EventCalendarDurationInput
slotMaxTime?: EventCalendarDurationInput
slotDuration?: EventCalendarDurationInput
slotLabelInterval?: EventCalendarDurationInput
slotHeight?: number
scrollTime?: EventCalendarDurationInput
// Display configuration
allDaySlot?: boolean
allDayContent?: EventCalendarContent
dayMaxEvents?: boolean | number
nowIndicator?: boolean
locale?: string
// Event configuration
events?: EventCalendarEvent[] | EventCalendarEventFetcher
eventSources?: EventCalendarEventSource[]
eventColor?: string
eventBackgroundColor?: string
eventTextColor?: string
eventClassNames?: string | string[] | ((info: EventCalendarEventMountInfo) => string | string[])
eventContent?: EventCalendarContent | ((info: EventCalendarEventContentInfo) => EventCalendarContent)
displayEventEnd?: boolean
// Resource configuration
resources?: EventCalendarResource[] | EventCalendarResourceFetcher
datesAboveResources?: boolean
filterResourcesWithEvents?: boolean
// Interaction configuration
editable?: boolean
selectable?: boolean
dragScroll?: boolean
eventStartEditable?: boolean
eventDurationEditable?: boolean
eventDragMinDistance?: number
longPressDelay?: number
// Callbacks
dateClick?: (info: EventCalendarDateClickInfo) => void
eventClick?: (info: EventCalendarEventClickInfo) => void
eventDrop?: (info: EventCalendarEventDropInfo) => void
eventResize?: (info: EventCalendarEventResizeInfo) => void
select?: (info: EventCalendarSelectInfo) => void
datesSet?: (info: EventCalendarDatesSetInfo) => void
eventDidMount?: (info: EventCalendarEventMountInfo) => void
// Button text
buttonText?: {
today?: string
dayGridMonth?: string
dayGridWeek?: string
dayGridDay?: string
listDay?: string
listWeek?: string
listMonth?: string
listYear?: string
resourceTimeGridDay?: string
resourceTimeGridWeek?: string
resourceTimelineDay?: string
resourceTimelineWeek?: string
resourceTimelineMonth?: string
timeGridDay?: string
timeGridWeek?: string
}
// Theme
theme?: EventCalendarTheme
}
export type EventCalendarViewOptions = Partial<EventCalendarOptions> & {
titleFormat?: ((start: Date, end: Date) => string) | Intl.DateTimeFormatOptions
duration?: EventCalendarDurationInput
dayHeaderFormat?: Intl.DateTimeFormatOptions
slotLabelFormat?: Intl.DateTimeFormatOptions
}
export type EventCalendarToolbar = {
start?: string
center?: string
end?: string
}
export type EventCalendarTheme = {
allDay?: string
active?: string
bgEvent?: string
bgEvents?: string
body?: string
button?: string
buttonGroup?: string
calendar?: string
compact?: string
content?: string
day?: string
dayFoot?: string
dayHead?: string
daySide?: string
days?: string
draggable?: string
dragging?: string
event?: string
eventBody?: string
eventTag?: string
eventTime?: string
eventTitle?: string
events?: string
extra?: string
ghost?: string
handle?: string
header?: string
hiddenScroll?: string
hiddenTimes?: string
highlight?: string
icon?: string
line?: string
lines?: string
list?: string
month?: string
noEvents?: string
nowIndicator?: string
otherMonth?: string
pointer?: string
popup?: string
preview?: string
resizer?: string
resource?: string
resourceTitle?: string
sidebar?: string
today?: string
time?: string
title?: string
toolbar?: string
view?: string
week?: string
withScroll?: string
}
// ============================================================================
// Event Source Types
// ============================================================================
export type EventCalendarEventSource = {
events?: EventCalendarEvent[] | EventCalendarEventFetcher
url?: string
method?: string
extraParams?: Record<string, unknown> | (() => Record<string, unknown>)
eventDataTransform?: (event: unknown) => EventCalendarEventInput
backgroundColor?: string
textColor?: string
color?: string
classNames?: string | string[]
editable?: boolean
}
export type EventCalendarFetchInfo = {
start: Date
end: Date
startStr: string
endStr: string
}
export type EventCalendarEventFetcher = (
fetchInfo: EventCalendarFetchInfo,
successCallback: (events: EventCalendarEventInput[]) => void,
failureCallback: (error: Error) => void
) => void | Promise<EventCalendarEventInput[]>
export type EventCalendarResourceFetcher = (
fetchInfo: EventCalendarFetchInfo,
successCallback: (resources: EventCalendarResource[]) => void,
failureCallback: (error: Error) => void
) => void | Promise<EventCalendarResource[]>
// ============================================================================
// Calendar Instance Types
// ============================================================================
export type EventCalendarInstance = {
// Navigation
getDate(): Date
setOption(name: string, value: unknown): void
getOption(name: string): unknown
getView(): EventCalendarViewInfo
prev(): void
next(): void
today(): void
gotoDate(date: Date | string): void
// Events
getEvents(): EventCalendarEvent[]
getEventById(id: string | number): EventCalendarEvent | null
addEvent(event: EventCalendarEventInput): EventCalendarEvent
updateEvent(event: EventCalendarEvent): void
removeEventById(id: string | number): void
refetchEvents(): void
// Resources
getResources(): EventCalendarResource[]
getResourceById(id: string | number): EventCalendarResource | null
addResource(resource: EventCalendarResource): void
refetchResources(): void
// Rendering
unselect(): void
destroy(): void
}
// ============================================================================
// Conversion Options
// ============================================================================
export type CalDavToEventCalendarOptions = {
/** Default color for events without a color */
defaultEventColor?: string
/** Default text color for events */
defaultTextColor?: string
/** Whether to include recurring event instances */
includeRecurringInstances?: boolean
/** Custom ID generator for events */
eventIdGenerator?: (event: unknown, calendarUrl: string) => string | number
/** Custom extended props extractor */
extendedPropsExtractor?: (event: unknown) => Record<string, unknown>
/** Map calendar URLs to colors */
calendarColors?: Map<string, string>
}
export type EventCalendarToCalDavOptions = {
/** Default calendar URL for new events */
defaultCalendarUrl?: string
/** Default timezone for events */
defaultTimezone?: string
/** Custom UID generator */
uidGenerator?: () => string
/** Preserve extended props in ICS custom properties */
preserveExtendedProps?: boolean
}

View File

@@ -0,0 +1,161 @@
import type { IcsEvent } from 'ts-ics'
import type { Calendar, CalendarEvent } from './calendar'
import type { AddressBookVCard, Contact, VCard } from './addressbook'
import { attendeeRoleTypes, availableViews } from '../constants'
export type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>
}
export type DomEvent = GlobalEventHandlersEventMap[keyof GlobalEventHandlersEventMap]
export type ServerSource = {
serverUrl: string
headers?: Record<string, string>
fetchOptions?: RequestInit
}
export type CalendarSource = {
calendarUrl: string
calendarUid?: unknown
headers?: Record<string, string>
fetchOptions?: RequestInit
}
export type AddressBookSource = {
addressBookUrl: string
addressBookUid?: unknown
headers?: Record<string, string>
fetchOptions?: RequestInit
}
export type VCardProvider = {
fetchContacts: () => Promise<VCard[]>
}
export type View = typeof availableViews[number]
export type IcsAttendeeRoleType = typeof attendeeRoleTypes[number]
export type SelectedCalendar = {
url: string
selected: boolean
}
export type SelectCalendarCallback = (calendar: SelectedCalendar) => void
export type SelectCalendarsClickInfo = {
jsEvent: DomEvent
calendars: Calendar[]
selectedCalendars: Set<string>
handleSelect: SelectCalendarCallback
}
export type SelectCalendarHandlers = {
onClickSelectCalendars: (info: SelectCalendarsClickInfo) => void,
}
export type EventBodyInfo = {
calendar: Calendar
vCards: AddressBookVCard[]
event: IcsEvent
view: View
userContact?: Contact
}
export type BodyHandlers = {
getEventBody: (info: EventBodyInfo) => Node[]
}
export type EventEditCallback = (event: CalendarEvent) => Promise<Response>
export type EventEditCreateInfo = {
jsEvent: DomEvent
userContact?: Contact,
event: IcsEvent
calendars: Calendar[]
vCards: AddressBookVCard[]
handleCreate: EventEditCallback
}
export type EventEditSelectInfo = {
jsEvent: DomEvent
userContact?: Contact,
calendarUrl: string
event: IcsEvent
recurringEvent?: IcsEvent
calendars: Calendar[]
vCards: AddressBookVCard[]
handleUpdate: EventEditCallback
handleDelete: EventEditCallback
}
export type EventEditMoveResizeInfo = {
jsEvent: DomEvent
calendarUrl: string
userContact?: Contact,
event: IcsEvent
recurringEvent?: IcsEvent,
start: Date,
end: Date,
handleUpdate: EventEditCallback
}
export type EventEditDeleteInfo = {
jsEvent: DomEvent
userContact?: Contact,
calendarUrl: string
event: IcsEvent
recurringEvent?: IcsEvent
handleDelete: EventEditCallback
}
export type EventEditHandlers = {
onCreateEvent: (info: EventEditCreateInfo) => void,
onSelectEvent: (info: EventEditSelectInfo) => void,
onMoveResizeEvent: (info: EventEditMoveResizeInfo) => void,
onDeleteEvent: (info: EventEditDeleteInfo) => void,
}
export type EventChangeInfo = {
calendarUrl: string
event: IcsEvent
ical: string
}
export type EventChangeHandlers = {
onEventCreated?: (info: EventChangeInfo) => void
onEventUpdated?: (info: EventChangeInfo) => void
onEventDeleted?: (info: EventChangeInfo) => void
}
export type CalendarElementOptions = {
view?: View
views?: View[]
locale?: string
date?: Date
editable?: boolean
}
export type CalendarClientOptions = {
userContact?: Contact
}
export type DefaultComponentsOptions = {
hideVCardEmails?: boolean
}
export type CalendarOptions =
// NOTE - CJ - 2025-07-03
// May define individual options or not
CalendarElementOptions
// May define individual options or not
& CalendarClientOptions
// Must define all handlers or none
& (SelectCalendarHandlers | Record<never, never>)
// Must define all handlers or none
& (EventEditHandlers | Record<never, never>)
// May define individual handlers or not
& EventChangeHandlers
// May define handlers or not, but they will be assigned a default value if they are not
& Partial<BodyHandlers>
& DefaultComponentsOptions
export type CalendarResponse = {
response: Response
ical: string
}

45
src/frontend/apps/calendars/types.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
declare module "mustache" {
const Mustache: {
render: (template: string, view: unknown) => string;
};
export default Mustache;
}
declare module "email-addresses" {
interface ParsedAddress {
type: "mailbox" | "group";
name?: string;
address?: string;
local?: string;
domain?: string;
}
export function parseOneAddress(input: string): ParsedAddress | null;
}
declare module "@event-calendar/core" {
export interface Calendar {
setOption: (name: string, value: unknown) => void;
getOption: (name: string) => unknown;
refetchEvents: () => void;
addEvent: (event: unknown) => void;
updateEvent: (event: unknown) => void;
removeEventById: (id: string) => void;
unselect: () => void;
$destroy?: () => void;
}
export function createCalendar(
el: HTMLElement,
plugins: unknown[],
options: Record<string, unknown>
): Calendar;
export const TimeGrid: unknown;
export const DayGrid: unknown;
export const List: unknown;
export const Interaction: unknown;
export const ResourceTimeGrid: unknown;
export const ResourceTimeline: unknown;
}
declare module "@event-calendar/core/index.css";