useDocyrusCalendar
Wire a Docyrus data source to a fully configured calendar with auto-detected date / title / user fields, view-aware date-range fetching, color mapping, and drag-and-drop persistence.
Installation
pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-calendarpnpm add @docyrus/app-utils @docyrus/api-client @tanstack/react-queryThis hook is distributed as source. It expects an authenticated RestApiClient from @docyrus/api-client and a QueryClientProvider from @tanstack/react-query somewhere above your component tree.
Overview
useDocyrusCalendar is the calendar-first companion to useDocyrusDataGrid. It maps a Docyrus data source to the Calendar component without forcing you to hand-build the event transformation:
- loads the active Docyrus data source metadata
- auto-detects the date / end-date / title / color / description / user fields, or uses the slugs you provide
- keeps saved-view filtering, sorting, and search wiring from
useDocyrusDataGrid - requests only the columns the calendar actually needs
- transforms records into
IEventobjects ready forCalendarProvider - wires create / update / delete / drag-and-drop callbacks to the Docyrus REST endpoints
- optionally narrows the items query to records overlapping the calendar's visible range
The hook returns a ready-to-spread calendarProviderProps object so the page code stays small.
Usage
Minimal page
'use client';
import { useDocyrusAuth } from '@docyrus/signin';
import {
Calendar,
CalendarProvider
} from '@docyrus/ui/components/calendar';
import { useDocyrusCalendar } from '@docyrus/ui/library/hooks/use-docyrus-calendar';
export function MeetingsCalendarPage() {
const { client } = useDocyrusAuth();
if (!client) return null;
const calendar = useDocyrusCalendar({
client,
appSlug: 'base',
dataSourceSlug: 'meeting',
searchPlaceholder: 'Search meetings'
});
if (!calendar.hasDateField) {
return <div>This data source has no date field.</div>;
}
return (
<div className="space-y-4">
{calendar.toolbar}
<CalendarProvider {...calendar.calendarProviderProps}>
<Calendar />
</CalendarProvider>
</div>
);
}With explicit field slugs and color mapping
const calendar = useDocyrusCalendar({
client,
appSlug: 'base',
dataSourceSlug: 'meeting',
startDateFieldSlug: 'starts_at',
endDateFieldSlug: 'ends_at',
titleFieldSlug: 'subject',
colorFieldSlug: 'meeting_type',
descriptionFieldSlug: 'agenda',
userFieldSlug: 'organizer',
colorMap: {
standup: 'blue',
review: 'purple',
leave: 'red',
'all-hands': 'orange'
},
defaultView: 'week',
defaultEventDurationMinutes: 30
});Visible-range filtering
When events live in a large data source, fetching every record at once is wasteful. Set enableDateRangeFilter: true and the hook narrows the items query to the calendar's currently visible window using an overlap test:
const calendar = useDocyrusCalendar({
client,
appSlug: 'base',
dataSourceSlug: 'meeting',
enableDateRangeFilter: true
});
return (
<CalendarProvider
{...calendar.calendarProviderProps}
onVisibleRangeChange={calendar.setVisibleRange}>
<Calendar />
</CalendarProvider>
);The hook will refetch every time the user navigates the calendar (next month, switch to week view, etc.).
Custom event builder
Use getEvent when the default mapping is not enough — for example to derive the color from multiple fields or to skip records that should not appear on the calendar.
const calendar = useDocyrusCalendar<MeetingRow>({
client,
appSlug: 'base',
dataSourceSlug: 'meeting',
getEvent: (item, ctx) => {
if (item.status === 'cancelled') return null;
const start = new Date(item.starts_at);
const end = new Date(item.ends_at);
return {
id: ctx.index + 1,
recordId: String(item.id),
startDate: start.toISOString(),
endDate: end.toISOString(),
title: `${item.subject} (${item.location ?? 'TBD'})`,
description: item.agenda ?? '',
color: item.priority === 'high' ? 'red' : 'blue',
user: {
id: item.organizer_id,
name: item.organizer_name,
picturePath: item.organizer_avatar ?? null
},
item
};
}
});Using a custom collection adapter
If you maintain a typed collection layer (auto-generated from @docyrus/tanstack-db-generator or hand-written) and want to reuse it for the calendar, pass it via collection. The hook will use the collection's list / create / update / delete methods instead of calling the REST endpoints directly:
const collection = useMeetingCollection();
const calendar = useDocyrusCalendar({
client,
appSlug: 'base',
dataSourceSlug: 'meeting',
collection
});Customizing the calendar UI
calendarProviderProps wires only the minimum required to render events and persist changes to Docyrus (events, users, defaultView, filterItems, onEventCreate, onEventUpdate, onEventDelete, onEventDrop).
Every other CalendarProvider prop is a passthrough — spread calendarProviderProps first, then add the props you need:
<CalendarProvider
{...calendar.calendarProviderProps}
onVisibleRangeChange={calendar.setVisibleRange}
// Replace the built-in event dialog with your own form
onEventClick={(event) => openMeetingDialog(event.recordId)}
// Open a side sheet when an empty time slot is clicked
onSlotClick={(start, end) => openCreateSheet(start, end)}
// Hide UI you don't want
hideUserSelect
hideSettings
// Only show two views in the tab bar
visibleViews={['week', 'month']}
// Custom labels
addButtonLabel="New meeting"
eventCountLabel="meetings"
searchPlaceholder="Search meetings…">
<Calendar />
</CalendarProvider>Event interaction callbacks
Each callback below replaces the built-in dialog for that interaction. Leave it out and the calendar uses its bundled add / details / delete dialogs.
| Prop | Type | Replaces |
|---|---|---|
onCreateEvent | (date: Date) => void | The built-in add-event dialog. Use it to open your own form (e.g. a side sheet bound to the Docyrus form layout). |
onEventClick | (event: IEvent) => void | The built-in event-details dialog. Look up event.recordId to find the underlying Docyrus row. |
onSlotClick | (start: Date, end: Date) => void | Click handler for empty time slots in week/day view. Use it to start a "drag to create" flow. |
onDaySelect | (date: Date) => void | Click handler on day cells (fires regardless of whether a slot or event is hit). |
onVisibleRangeChange | (range, view) => void | Notifies on view switch / date navigation. Wire it to setVisibleRange to drive the date-range filter. |
Header visibility toggles
| Prop | Default | Effect |
|---|---|---|
hideHeaderAddButton | false | Hides the "Add Event" button in the header. |
hideCellAddButton | false | Hides the "+" buttons that appear inside empty day cells. |
hideAddButton | false | Deprecated — hides both header and cell add buttons. Use the two flags above instead. |
hideUserSelect | false | Hides the user / filter dropdown in the header. |
hideFilter | false | Hides the color filter dropdown in the header. |
hideSettings | false | Hides the settings gear (badge style, 24h format, agenda group-by). |
hideEventCount | false | Hides the event count badge that sits next to the date navigator. |
Labels & placeholders
| Prop | Default | Description |
|---|---|---|
addButtonLabel | translated "Add Event" | Custom text for the header add button. |
eventCountLabel | translated "events" | Custom unit for the count badge (e.g. "meetings", "leaves", "entries"). |
searchPlaceholder | translated "Search…" | Custom placeholder for the agenda view search input. |
View configuration
| Prop | Type | Description |
|---|---|---|
visibleViews | Array<TCalendarView> | Which views to surface in the tab bar. Defaults to all five (day, week, month, year, agenda). Use ['week', 'month'] for a leaner board. |
defaultView | TCalendarView | First view shown on mount. Pre-wired by the hook from options.defaultView (defaults to 'month'). |
badge | 'dot' | 'colored' | Event badge style in the month view. Defaults to 'colored'. The user can override via the settings gear unless hideSettings is set. |
Filter customization
The hook auto-derives filterItems from the user field. You can override the entire filter experience with these props:
| Prop | Type | Description |
|---|---|---|
filterItems | Array<CalendarFilterItem> | Replace the auto-derived list (e.g. show statuses, projects, or any other dimension). Each item is { id, label, image? }. |
onFilterChange | (id: string) => void | Called when the user picks a filter item — wire it to your own data-source query filter. |
renderFilterSelect | (context) => ReactNode | Render prop to replace the filter dropdown entirely with custom UI. |
Custom rendering
| Prop | Type | Description |
|---|---|---|
sidebarContent | ReactNode | Custom content rendered alongside the calendar body (e.g. a list of upcoming events, mini month, or filter chips). |
renderEventBadge | (event, defaultBadge) => ReactNode | Replace the badge rendered for events in the month view. The second argument is the default node so you can wrap it. |
Settings the user controls
These live in the settings gear and persist to localStorage under the key calendar-settings. They are not part of CalendarProvider's prop API — show or hide the gear with hideSettings to control whether the user can change them.
| Setting | Values | Default |
|---|---|---|
| Badge style | 'dot' | 'colored' | 'colored' |
| Time format | 12h / 24h | 24h |
| Agenda group-by | 'date' | 'color' | 'date' |
How the query is built
Internally the hook reuses the saved-view state from useDocyrusDataGrid, then creates its own items query with a stronger columns list.
That means a user can save a view that hides the title or date field, but the hook will still request:
id- the resolved start date field
- the resolved end date field (when present)
- the resolved title field
- the resolved color field (when present)
- the resolved description field (when present)
- the resolved user field (when present)
So events keep rendering correctly while the rest of the data-view behavior still follows the active Docyrus view.
Field resolution
Start date field
Resolution order:
startDateFieldSlug- first field with
type === 'field-date'or'field-dateTime'
If no start date field can be resolved, the hook returns hasDateField: false and renders no events.
End date field
Resolution order:
endDateFieldSlug- first matching slug from:
end_date,end_time,end_datetime,finish_date,due_date,deadline,ends_at,ended_at(excluding the start field) - first date / dateTime field different from the start field
When no end date field is available, the hook falls back to defaultEventDurationMinutes (60 minutes by default) to compute each event's end.
Title field
Resolution order:
titleFieldSlug- first matching slug from:
name,title,label,subject,display_name,displayName,commercial_title,short_name
If no title can be resolved, events fall back to Event <index>.
Color field
Resolution order:
colorFieldSlug- field whose slug is
colororcolour
Description field
Resolution order:
descriptionFieldSlug- first matching slug from:
description,notes,content,details,body,summary(excluding the title field)
User field
Resolution order:
userFieldSlug- first field with
type === 'field-userSelect'or'field-userMultiSelect' - field whose slug is
record_owner
User values shape users[] (unique list used by the calendar's filter dropdown) and the filterItems[] array that powers the header filter.
Color resolution
Each record's color is normalized through this pipeline:
- Read the raw value from
colorField(string, enum option name, or object with aname/label/title/valuestring property). - Lower-case and trim the value.
- Look up the value in
colorMap(both original casing and lower-cased keys are tried). - If the value is already one of
'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange', use it as-is. - Otherwise default to
'blue'.
This means a field-select whose option names are Standup, Review, Leave etc. can be mapped to event colors with a small map:
colorMap: {
standup: 'blue',
review: 'purple',
leave: 'red'
}Persistence (CRUD wiring)
calendarProviderProps plugs the calendar's lifecycle events into the Docyrus REST endpoints (or your custom collection adapter when provided):
| Calendar action | Wired to | Effect |
|---|---|---|
| Create event in the UI | onEventCreate | PATCH /items/:id with the new start / end on the matching record (events are created through the UI from existing records — use the explicit createRecord helper to create a brand-new row). |
| Edit event details | onEventUpdate | PATCH /items/:id with the patched start / end date fields. |
| Delete event | onEventDelete | DELETE /items/:id. |
| Drag-and-drop | onEventDrop | PATCH /items/:id with new start / end. |
If you need to create a new Docyrus record from the calendar (for example from an "Add event" toolbar button), call the exposed createRecord(startDate, endDate, extraValues?) helper. It posts to the items endpoint (or your collection.create) and reloads the calendar.
Loading lifecycle
The hook exposes three loading-related fields that map to different UX needs:
| Field | Use it for |
|---|---|
isLoading | true while either the metadata or the items query is still running for the first time. Combine with hasLoadedOnce to render a full-screen skeleton only on first mount. |
isFetching | true while a background refetch is in flight. Good for small spinners on the toolbar. |
hasLoadedOnce | Flips to true after the first successful items fetch and stays true. Use it to keep <Calendar> mounted on subsequent refetches — passing isLoading directly would unmount the provider and reset its internal selectedDate / view state. |
API Reference
useDocyrusCalendar(options)
The hook accepts every useDocyrusDataGrid option except data, enableItemsQuery, showSelectColumn, enableRowMarkers, and collection (replaced by the DocyrusCalendarCollection shape). It also accepts the options below.
| Option | Type | Default | Description |
|---|---|---|---|
client | RestApiClient | — | Authenticated Docyrus API client. |
appSlug | string | — | Target Docyrus app slug (e.g. 'base'). |
dataSourceSlug | string | — | Target data source slug. |
collection | DocyrusCalendarCollection<TData> | — | Optional collection adapter (e.g. typed @docyrus/tanstack-db-generator output). When provided, the hook calls collection.list / create / update / delete instead of hitting REST endpoints directly. |
startDateFieldSlug | string | auto | Slug of the date / dateTime field used as event start. Auto-detected when omitted. |
endDateFieldSlug | string | auto | Slug of the date / dateTime field used as event end. Auto-detected when omitted; falls back to defaultEventDurationMinutes when no end field is found. |
titleFieldSlug | string | auto | Slug of the field used as event title. Auto-detected when omitted. |
colorFieldSlug | string | auto | Slug of the field used to derive the event color. Auto-detected when omitted. |
descriptionFieldSlug | string | auto | Slug of the field used as event description. Auto-detected when omitted. |
userFieldSlug | string | auto | Slug of the user / userSelect field. Drives the calendar's user filter dropdown. Auto-detected when omitted. |
colorMap | Record<string, TEventColor> | — | Map of raw color / enum-option values to TEventColor. Example: { 'Meeting': 'blue', 'Leave': 'red' }. |
defaultEventDurationMinutes | number | 60 | Fallback event length used when the end date field is absent or empty. |
getEvent | (item, context) => DocyrusCalendarEvent | null | — | Override event construction. Return null to skip a record entirely. |
defaultView | TCalendarView | 'month' | Default calendar view. One of 'day' | 'week' | 'month' | 'year' | 'agenda'. |
enableDateRangeFilter | boolean | false | When true, the items query is narrowed to records overlapping the calendar's visible range. Pair with setVisibleRange on CalendarProvider.onVisibleRangeChange to drive it. |
staleTime | number | 30000 | TanStack Query staleTime for the items query (ms). |
enableGroupMenu | boolean | false | Forwarded to the underlying data-grid toolbar. Off by default for calendar pages. |
enableRowHeightMenu | boolean | false | Forwarded to the underlying data-grid toolbar. |
enableDisplayMenu | boolean | false | Forwarded to the underlying data-grid toolbar. |
onReload | () => void | — | Forwarded to the underlying data-grid hook. |
Return value
| Property | Type | Description |
|---|---|---|
calendarProviderProps | Omit<CalendarProviderProps, 'children'> | Ready-to-spread props for <CalendarProvider>. Wires only events, users, defaultView, filterItems, and the CRUD callbacks (onEventCreate, onEventUpdate, onEventDelete, onEventDrop). All other CalendarProvider props (onEventClick, onSlotClick, onDaySelect, visibleViews, hide-flags, custom labels, custom renderers…) are passthroughs you add alongside — see Customizing the calendar UI. |
events | DocyrusCalendarEvent<TData>[] | Transformed events ready for the calendar. Each event carries its original recordId and raw item. |
users | IUser[] | Unique users extracted from the user field. Drives the filter dropdown. |
filterItems | CalendarFilterItem[] | Generic filter items derived from users — already wired into calendarProviderProps.filterItems. |
items | TData[] | All raw records returned by the items query. |
startDateField | DataSourceField | null | Resolved start date field definition. |
endDateField | DataSourceField | null | Resolved end date field definition (may be null when none exists). |
titleField | DataSourceField | null | Resolved title field definition. |
colorField | DataSourceField | null | Resolved color field definition. |
descriptionField | DataSourceField | null | Resolved description field definition. |
userField | DataSourceField | null | Resolved user field definition. |
hasDateField | boolean | true when a usable start date field was resolved. Use it to render a fallback when the data source is incompatible. |
createRecord | (startDate, endDate, extraValues?) => Promise<void> | Persist a brand-new event to Docyrus. Use from custom "Add event" buttons. |
updateRecord | (recordId, data) => Promise<void> | Patch an existing record by its UUID. |
deleteRecord | (recordId) => Promise<void> | Delete a record by its UUID. |
moveRecord | (recordId, newStart, newEnd) => Promise<void> | Move an event (drag-and-drop). Patches the start / end date fields. |
visibleRange | { start: Date; end: Date } | null | Currently active visible range — drives the date-range filter when enabled. |
setVisibleRange | (range | null) => void | Setter wired to CalendarProvider.onVisibleRangeChange. |
requestedColumns | string | Final comma-separated columns string sent to the items endpoint. |
resolvedListParams | DocyrusDataGridListParams | Full query payload (filters, orderBy, limit, offset, columns) passed to the Docyrus items endpoint. |
toolbar | ReactNode | Prebuilt Docyrus data-view toolbar from useDocyrusDataGrid. |
table | Table<TData> | Underlying TanStack table used by the toolbar and saved-view editor. |
views | SavedDataGridView[] | Saved views from useDocyrusDataGrid. |
fields | DataSourceField[] | Full field list from useDocyrusDataGrid. |
dataSource | DataSourceMetadata | null | Resolved Docyrus data source metadata. |
activeViewId | string | null | Currently active saved view id. |
setActiveViewId | (id) => void | Switch the active saved view. |
isLoading | boolean | true until both metadata and items have finished their first fetch. |
isFetching | boolean | true while a refetch is in flight (initial or otherwise). |
hasLoadedOnce | boolean | true after the first successful items fetch — use to keep <Calendar> mounted across refetches. |
error | Error | null | First query error, if any. |
reload | () => void | Refetches the metadata and the items query. |
Type Exports
| Type | Description |
|---|---|
DocyrusCalendarEvent<TData> | Calendar event extended with recordId and raw item. |
DocyrusCalendarCollection<TData> | Shape of an optional collection adapter (list / create / update / delete). |
DocyrusCalendarEventBuilderContext<TData> | Context passed to a custom getEvent builder (resolved fields + item + index). |
UseDocyrusCalendarOptions<TData> | Hook option shape (extends UseDocyrusDataGridOptions). |
UseDocyrusCalendarResult<TData> | Hook return shape. |
IEvent | Calendar event shape (re-exported from @ui/components/calendar). |
IUser | Calendar user shape (re-exported from @ui/components/calendar). |
TCalendarView | 'day' | 'week' | 'month' | 'year' | 'agenda'. |
TEventColor | 'blue' | 'green' | 'red' | 'yellow' | 'purple' | 'orange'. |
CalendarFilterItem | { id, label, image? } filter dropdown item. |