diff --git a/.changeset/calendar-view-schema-converge.md b/.changeset/calendar-view-schema-converge.md new file mode 100644 index 000000000..61f9a3c1d --- /dev/null +++ b/.changeset/calendar-view-schema-converge.md @@ -0,0 +1,33 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-calendar': patch +--- + +`CalendarViewSchema` (TS interface and zod mirror) converges on the registered +`calendar-view` renderer's measured read set (objectui#5667, maintainer ruling +option A — the renderer is authoritative). + +**Breaking for consumers of the published type** (deliberate; per-repo policy +breaking changes ship as `minor` — the fixed group's `major` tracks +`@objectstack`): + +- Nine inert keys are retired: `events` (the interface's only required key, + which the renderer deliberately drops — objectui#4433), `defaultView`, + `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + `onEventUpdate`, `onDateChange`. None had a read site on the authored-node + path and no measured app authors them (ADR-0049 enforce-or-remove). +- The type now declares what the renderer actually reads: `data`, `titleField`, + `startDateField`, `endDateField`, `allDayField`, `colorField`, `view`, + `currentDate`, `allowCreate`, `className`, plus the two host-only function + hatches it forwards (`onEventClick`, `onViewChange`). +- Practical radius, measured: `BaseSchema` carries an index signature and the + zod `BaseSchema` is `.passthrough()`, so nodes still authoring retired keys + neither fail to compile nor get rejected at validation — they are simply no + longer declared, documented, or type-checked. The material accept change is + that zod no longer **requires** `events`: a `{ "type": "calendar-view" }` + node without it now validates (previously the one key validation demanded + was the one key guaranteed to do nothing). + +Runtime renderer behaviour is unchanged. `@object-ui/plugin-calendar`'s README +and `content/docs/api/schema-reference.md` are repaired to the converged +surface in the same change, so no copy of the old contradiction survives. diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index 70cd5ba2a..6a65a83a7 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -1036,15 +1036,17 @@ Each widget supports `colSpan` and `rowSpan` to control its size in the grid. Th ### CalendarViewSchema -A multi-view calendar for displaying and managing events. +A multi-view calendar computed from the node's `data` records. There is no +authorable `events` key: the renderer builds one event per record in `data`, +reading the fields the field-name properties point at (objectui#5667; an +authored `events` is dropped by design, objectui#4433). ```json { "type": "calendar-view", - "defaultView": "month", - "views": ["month", "week", "day", "agenda"], - "editable": true, - "events": [ + "view": "month", + "currentDate": "2024-03-15T12:00:00.000Z", + "data": [ { "id": "evt-1", "title": "Team Standup", @@ -1060,21 +1062,30 @@ A multi-view calendar for displaying and managing events. "color": "#8b5cf6", "allDay": false } - ] + ], + "allowCreate": true, + "className": "h-[600px] border rounded-lg" } ``` | Property | Type | Description | |----------|------|-------------| -| `events` | `CalendarEvent[]` | **Required.** Events with `id`, `title`, `start`, `end`, optional `color` and `allDay`. | -| `defaultView` | `CalendarViewMode` | Initial view: `"month"`, `"week"`, `"day"`, `"agenda"`. | -| `views` | `CalendarViewMode[]` | Available view modes the user can switch between. | -| `editable` | `boolean` | Allow creating and modifying events. | -| `defaultDate` | `string \| Date` | Initial calendar date. | -| `onEventClick` | `function` | Callback when an event is clicked. | -| `onEventCreate` | `function` | Callback for new event creation: `(start, end)`. | -| `onEventUpdate` | `function` | Callback when an event is modified. | -| `onDateChange` | `function` | Callback when the visible date range changes. | +| `data` | `any` | Records rendered as events — an array, or a binding expression that resolves to one. | +| `titleField` | `string` | Record field for the event title. Default `"title"`. | +| `startDateField` | `string` | Record field for the event start date/time. Default `"start"`. | +| `endDateField` | `string` | Record field for the event end date/time. Default `"end"`. | +| `allDayField` | `string` | Record field for the all-day flag. Default `"allDay"`. | +| `colorField` | `string` | Record field for the event color. Default `"color"`. | +| `view` | `CalendarViewMode` | View mode: `"month"`, `"week"`, `"day"`. Any other value falls back to `"month"`. | +| `currentDate` | `string \| Date` | Initial calendar date — an ISO date string when authored as JSON. | +| `allowCreate` | `boolean` | Show the "New event" affordance; clicking it dispatches a `create` action. Default `false`. | +| `onEventClick` | `function` | Host-only: forwarded when a React host supplies a function; authored JSON cannot produce one. | +| `onViewChange` | `function` | Host-only: same rule as `onEventClick`. | + +Nine formerly declared keys — `events` (was required, and dropped by the +renderer), `defaultView`, `defaultDate`, `date`, `views`, `editable`, +`onEventCreate`, `onEventUpdate`, `onDateChange` — were retired in +objectui#5667: nothing read them on the authored-node path. **Related:** [ObjectViewSchema](#objectviewschema), [DashboardComponentSchema](#dashboardcomponentschema) diff --git a/packages/plugin-calendar/README.md b/packages/plugin-calendar/README.md index 16b2dce16..372802c8c 100644 --- a/packages/plugin-calendar/README.md +++ b/packages/plugin-calendar/README.md @@ -89,7 +89,7 @@ import '@object-ui/plugin-calendar'; // Now you can use calendar types in your schemas const schema = { type: 'calendar-view', - events: [ + data: [ { id: '1', title: 'Team Meeting', @@ -148,60 +148,64 @@ ComponentRegistry.register('my-calendar', ObjectCalendarRenderer); ### CalendarView -Display a monthly calendar with events. This is a **partial summary**: the full -contract is `CalendarViewSchema` in `@object-ui/types`, which declares 13 keys of -its own on top of the common `BaseSchema` keys (`className`, `id`, `data`, -`visible`, ...). +Display a calendar computed from the node's `data` records. This is the full +authored surface: `CalendarViewSchema` in `@object-ui/types` declares 13 keys of +its own, converged on what the registered `calendar-view` renderer actually +reads (objectui#5667). Two of them — `data` and `className` — refine common +`BaseSchema` keys; the rest of `BaseSchema` (`id`, `visible`, ...) applies as on +any node. ```typescript { type: 'calendar-view', - events: CalendarEvent[], // REQUIRED — the only required key besides `type` - defaultView?: CalendarViewMode, // 'month' | 'week' | 'day' | 'agenda' (default 'month') - view?: CalendarViewMode, // controlled - views?: CalendarViewMode[], // default ['month', 'week', 'day'] - defaultDate?: string | Date, - date?: string | Date, // controlled - editable?: boolean, // default false - onEventClick?: (event: CalendarEvent) => void, - onDateChange?: (date: Date) => void, - onViewChange?: (view: CalendarViewMode) => void, - className?: string // from `BaseSchema`, not `CalendarViewSchema` + data?: any, // records rendered as events (array, or a binding expression) + titleField?: string, // default 'title' + startDateField?: string, // default 'start' + endDateField?: string, // default 'end' + allDayField?: string, // default 'allDay' + colorField?: string, // default 'color' + view?: CalendarViewMode, // 'month' | 'week' | 'day' (default 'month') + currentDate?: string | Date, // ISO string authored; Date from a React host + allowCreate?: boolean, // default false — shows the "New event" button + className?: string, // Tailwind classes for the container + onEventClick?: (event: CalendarEvent) => void, // HOST-ONLY (see below) + onViewChange?: (view: CalendarViewMode) => void // HOST-ONLY (see below) } ``` -`onEventCreate` and `onEventUpdate` complete the 13; see `CalendarViewSchema` for -their signatures. `onDateClick` is **not** on this schema — it is a -`CalendarViewProps` component prop (see [Drag-and-Drop](#drag-and-drop) and -[Click-to-Create](#click-to-create)). - -> **The type is not the renderer.** `CalendarViewSchema` is the shape -> `@object-ui/types` publishes for this node, not a description of what the -> registered `calendar-view` renderer reads. That renderer builds its events from -> the node's `data` array plus the `titleField` / `startDateField` / -> `endDateField` / `colorField` / `allDayField` inputs, and **drops an authored -> `events` key** (objectui#4433) — so the type's one required key does nothing -> when written as JSON. Of the keys above it also reads `view` and `className`; -> the handlers only ever arrive from a React host -> (``), because JSON cannot carry a -> function. +There is deliberately **no authorable `events` key**: the renderer computes its +events from `data` plus the field-name keys, and drops an authored `events` +(objectui#4433). Nine formerly declared keys — `events`, `defaultView`, +`defaultDate`, `date`, `views`, `editable`, `onEventCreate`, `onEventUpdate`, +`onDateChange` — were retired in objectui#5667 because nothing read them on the +authored-node path and no measured app authors them. `onDateClick` is **not** +on this schema — it is a `CalendarViewProps` component prop (see +[Drag-and-Drop](#drag-and-drop) and [Click-to-Create](#click-to-create)). + +> **The handlers are host-only.** `onEventClick` and `onViewChange` are +> forwarded to the component only when the value really is a function, which +> authored JSON can never produce — supply them from a React host +> (``). Authored as JSON strings they +> are dropped, same as absent. ### Calendar Event Structure -The authored event shape, declared by `@object-ui/types`: - -```typescript -interface CalendarEvent { - id: string; - title: string; - start: string; // ISO datetime string - end: string; // ISO datetime string - description?: string; - color?: string; // Tailwind color class - allDay?: boolean; -} +Events are not authored directly — the renderer computes one event per record +in the node's `data` array, reading the fields the field-name keys point at: + +```text +id: record.id // falls back to record._id, then the array index +title: record[titleField] // default field 'title' +start: record[startDateField] // default field 'start' (ISO datetime string) +end: record[endDateField] // default field 'end' (optional) +allDay: record[allDayField] // default field 'allDay' +color: record[colorField] // default field 'color' +data: record // the whole record rides along ``` +`@object-ui/types` still exports the `CalendarEvent` interface — it is the +declared payload type of the host-only `onEventClick` callback. + ## Examples ### Basic Calendar @@ -209,7 +213,7 @@ interface CalendarEvent { ```typescript const schema = { type: 'calendar-view', - events: [ + data: [ { id: '1', title: 'Product Launch', @@ -228,6 +232,11 @@ const schema = { }; ``` +The records above already use the default field names (`title`, `start`, `end`, +`color`), so no field-name keys are needed; point `titleField` / +`startDateField` / `endDateField` / `allDayField` / `colorField` at your own +fields when they differ. + ### With ObjectQL Integration ```typescript @@ -243,21 +252,28 @@ const schema = { ### Interactive Calendar +The handlers are host-only — a function can only come from a React host +building the node in code, never from authored JSON: + ```typescript const schema = { type: 'calendar-view', - events: [], + data: [], onEventClick: (event) => { console.log('Event clicked:', event); // Open event details modal }, - onDateChange: (date) => { - console.log('Visible date changed:', date); - // React to the calendar moving to another month/week/day + onViewChange: (view) => { + console.log('View changed:', view); + // React to the calendar switching between month/week/day } }; ``` +Authored JSON reacts to clicks through the node's action channel instead +(`allowCreate: true` dispatches `{ type: 'create' }`; event clicks dispatch +`{ type: 'event-click' }`). + ## ObjectQL Integration When using with ObjectStack, the calendar can automatically fetch and display events: @@ -291,7 +307,7 @@ Style the calendar with Tailwind classes: const schema = { type: 'calendar-view', className: 'border rounded-lg shadow-lg', - events: [...] + data: [...] }; ``` @@ -301,30 +317,30 @@ The **authored** (JSON metadata) types live in `@object-ui/types`, not in this package — this package imports them too, and does not re-export them: ```typescript -import type { CalendarViewSchema, CalendarEvent } from '@object-ui/types'; - -const event: CalendarEvent = { - id: '1', - title: 'Meeting', - start: '2024-01-15T10:00:00', - end: '2024-01-15T11:00:00' -}; +import type { CalendarViewSchema } from '@object-ui/types'; const schema: CalendarViewSchema = { type: 'calendar-view', - events: [event] + data: [ + { id: '1', name: 'Meeting', begins: '2024-01-15T10:00:00', ends: '2024-01-15T11:00:00' } + ], + titleField: 'name', + startDateField: 'begins', + endDateField: 'ends', + view: 'week', + allowCreate: true }; ``` -> **Two different `CalendarEvent` types, same name.** The one above, from -> `@object-ui/types`, is the *authored* event: `id: string` and -> `start` / `end` accept ISO strings — that is the shape documented under -> [Calendar Event Structure](#calendar-event-structure) and the one a -> `calendar-view` schema carries. This package also exports a `CalendarEvent` -> (`CalendarViewProps['events']`), which is the `CalendarView` **component's -> runtime** type: `id: string | number` and `start: Date` / `end?: Date`. Use -> the `@object-ui/types` one for metadata and the plugin one when you render -> `CalendarView` yourself in React; they are not interchangeable. +> **Two different `CalendarEvent` types, same name.** `@object-ui/types` +> exports a `CalendarEvent` (`id: string`, `start` / `end` accept ISO strings) +> — since objectui#5667 it is no longer part of the authored surface (a +> `calendar-view` node carries `data` records, not events; see +> [Calendar Event Structure](#calendar-event-structure)), but it remains the +> declared payload type of the host-only `onEventClick` callback. This package +> also exports a `CalendarEvent` (`CalendarViewProps['events']`), which is the +> `CalendarView` **component's runtime** type: `id: string | number` and +> `start: Date` / `end?: Date`. They are not interchangeable. ## Links diff --git a/packages/types/src/complex.ts b/packages/types/src/complex.ts index 24af60d12..b272f3ac0 100644 --- a/packages/types/src/complex.ts +++ b/packages/types/src/complex.ts @@ -170,59 +170,91 @@ export interface CalendarEvent { } /** - * Calendar view component + * Calendar view component. + * + * The authored surface of the registered `calendar-view` renderer, converged + * on the renderer's measured read set (objectui#5667): the calendar's events + * are COMPUTED from the node's `data` array plus the five field-name keys + * below — there is no authorable `events` key. An authored `events` is dropped + * by design (objectui#4433); this repo's action metadata rides + * `properties.action`, not a calendar prop. + * + * Nine formerly declared keys — `events` (the interface's only required key + * besides `type`, and the one the renderer refuses), `defaultView`, + * `defaultDate`, `date`, `views`, `editable`, `onEventCreate`, + * `onEventUpdate`, `onDateChange` — are RETIRED rather than implemented: + * none had a read site on the authored-node path, and no measured app authors + * any of them (ADR-0049 enforce-or-remove, objectui#5667). */ export interface CalendarViewSchema extends BaseSchema { type: 'calendar-view'; /** - * Calendar events + * Records to display as events — the renderer computes its events from this + * array plus the field-name keys below. + * + * Redeclared from `BaseSchema` (same `any` type) because a binding + * expression string is also legal here and resolves to the array before the + * renderer reads it. A non-array value renders an empty calendar. */ - events: CalendarEvent[]; + data?: any; /** - * Default view mode - * @default 'month' + * Record field to use for the event title. + * @default 'title' */ - defaultView?: CalendarViewMode; + titleField?: string; /** - * Controlled view mode + * Record field containing the event start date/time. + * @default 'start' */ - view?: CalendarViewMode; + startDateField?: string; /** - * Default date + * Record field containing the event end date/time. + * @default 'end' */ - defaultDate?: string | Date; + endDateField?: string; /** - * Controlled date + * Record field indicating an all-day event. + * @default 'allDay' */ - date?: string | Date; + allDayField?: string; /** - * Available views - * @default ['month', 'week', 'day'] + * Record field to use for the event color. + * @default 'color' */ - views?: CalendarViewMode[]; + colorField?: string; /** - * Enable event creation - * @default false + * Calendar view mode. + * + * The registered renderer renders `'month' | 'week' | 'day'` and falls back + * to `'month'` for any other value — including `'agenda'`, which + * {@link CalendarViewMode} still names. + * @default 'month' */ - editable?: boolean; + view?: CalendarViewMode; /** - * Event click handler + * Initial calendar date — an ISO date string when authored as JSON; a + * `Date` instance is accepted from a React host. */ - onEventClick?: (event: CalendarEvent) => void; + currentDate?: string | Date; /** - * Event create handler + * Show the "New event" affordance; clicking it dispatches a + * `{ type: 'create' }` action on the node's action channel (objectui#4454). + * @default false */ - onEventCreate?: (start: Date, end: Date) => void; + allowCreate?: boolean; /** - * Event update handler + * Tailwind classes for the calendar container. Redeclared from `BaseSchema` + * as part of the converged authored surface. */ - onEventUpdate?: (event: CalendarEvent) => void; + className?: string; /** - * Date change handler + * Event click handler — HOST-ONLY. The renderer forwards it only when the + * value is a function, which authored JSON can never produce; supply it from + * a React host (``). */ - onDateChange?: (date: Date) => void; + onEventClick?: (event: CalendarEvent) => void; /** - * View change handler + * View change handler — HOST-ONLY, same rule as {@link CalendarViewSchema.onEventClick}. */ onViewChange?: (view: CalendarViewMode) => void; } diff --git a/packages/types/src/zod/complex.zod.ts b/packages/types/src/zod/complex.zod.ts index ea4f9b148..2f437c2b2 100644 --- a/packages/types/src/zod/complex.zod.ts +++ b/packages/types/src/zod/complex.zod.ts @@ -86,21 +86,51 @@ export const CalendarEventSchema = z.object({ /** * Calendar View Schema - Calendar component + * + * Mirrors `CalendarViewSchema` in `../complex.ts`, converged on the registered + * `calendar-view` renderer's measured read set (objectui#5667): events are + * computed from `data` plus the field-name keys; the formerly required + * `events` and the eight other inert keys (`defaultView`, `defaultDate`, + * `date`, `views`, `editable`, `onEventCreate`, `onEventUpdate`, + * `onDateChange`) are retired (ADR-0049 enforce-or-remove). + * + * `BaseSchema` is `.passthrough()`, so the retired keys are not REJECTED here + * — they are simply no longer declared or type-checked. The material accept + * change is that `events` is no longer required. */ export const CalendarViewSchema = BaseSchema.extend({ type: z.literal('calendar-view'), - events: z.array(CalendarEventSchema).describe('Calendar events'), - defaultView: CalendarViewModeSchema.optional().describe('Default view mode'), - view: CalendarViewModeSchema.optional().describe('Controlled view mode'), - defaultDate: z.union([z.string(), z.date()]).optional().describe('Default date'), - date: z.union([z.string(), z.date()]).optional().describe('Controlled date'), - views: z.array(CalendarViewModeSchema).optional().describe('Available views'), - editable: z.boolean().optional().describe('Whether events are editable'), - onEventClick: z.function().optional().describe('Event click handler'), - onEventCreate: z.function().optional().describe('Event create handler'), - onEventUpdate: z.function().optional().describe('Event update handler'), - onDateChange: z.function().optional().describe('Date change handler'), - onViewChange: z.function().optional().describe('View change handler'), + data: z + .any() + .optional() + .describe( + 'Records to display as events (computed with the field-name keys; binding expressions resolve before the renderer reads it)', + ), + titleField: z.string().optional().describe("Record field for the event title (default 'title')"), + startDateField: z + .string() + .optional() + .describe("Record field for the event start date/time (default 'start')"), + endDateField: z + .string() + .optional() + .describe("Record field for the event end date/time (default 'end')"), + allDayField: z.string().optional().describe("Record field for the all-day flag (default 'allDay')"), + colorField: z.string().optional().describe("Record field for the event color (default 'color')"), + view: CalendarViewModeSchema.optional().describe( + "View mode (the renderer renders 'month' | 'week' | 'day'; other values fall back to 'month')", + ), + currentDate: z + .union([z.string(), z.date()]) + .optional() + .describe('Initial calendar date (ISO string authored; Date instance from a React host)'), + allowCreate: z.boolean().optional().describe('Show the "New event" affordance (default false)'), + className: z.string().optional().describe('Tailwind classes for the calendar container'), + onEventClick: z + .function() + .optional() + .describe('Host-only event click handler (authored JSON cannot produce a function)'), + onViewChange: z.function().optional().describe('Host-only view change handler'), }); /**