Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .changeset/25923-calendar-gantt-flat-keys-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/spec': minor
'@object-ui/types': minor
'@object-ui/plugin-calendar': minor
'@object-ui/plugin-gantt': minor
'@object-ui/plugin-list': minor
'@object-ui/plugin-view': minor
---

refactor(types,plugin-calendar,plugin-gantt,plugin-list,plugin-view)!: the flat `object-calendar` and `object-gantt` field-mapping keys leave the published schemas; producers emit the `calendar` / `gantt` block (#25923)

Clause-②: yes (narrowing)
Metadata-protocol: narrowed

<!-- adr-0087: not-required (no-migration-prescription) No `@objectstack/spec` key, object definition, stored row or stored shape moves: the spec's `ComponentPropsMap['object-calendar']` and `['object-gantt']` rows already refuse every key this change takes off `@object-ui/types`, so no metadata that the spec accepts carries them, and `objectstack migrate meta` has nothing to rewrite. What narrows is the `@object-ui/types` mirror of those rows: its TypeScript interfaces, its tolerant zod mirrors and, for `object-calendar`, the strict authoring face derived from them, which now refuses the five flat keys by name as the spec does. The affected party is a TypeScript author or a hand-written node, reached by the compiler and by `objectui validate`, and the renderers still read the flat spelling a host hands them. One drawing outcome moves: `getGanttConfig` validates a `gantt` block and never validated the flat spelling, so a pre-door stored view whose `options.gantt` carries a value the spec refuses now shows the refusal screen instead of drawing; that row is outside what the spec accepts, and its conversion belongs to the stored-row census of #25923, not to tolerance in a consumer. The other categories are closed on facts: the five packages publish (not unpublished); no ADR-0087 id covers this change (not registered or already-registered); and the narrowed symbols are zod-backed metadata shapes (not runtime-interface-only, type-surface-only or runtime-export-retired). -->

`@object-ui/types` published, on `ObjectCalendarSchema` and `ObjectGanttSchema`, a flat spelling of the field mapping that the spec refuses on the node. A declared flat key is one the strict authoring face derives from, so `objectui validate` accepted the `object-calendar` spelling the spec refuses. The keys only the host writes now live on each renderer package's own hand-off type (ui/AGENTS.md Commandment #0.1), and the producers write the block.

- **`@object-ui/types`**: `ObjectCalendarSchema` (TypeScript and zod) no longer declares `startDateField`, `endDateField`, `titleField`, `colorField` or `allDayField` flat; they are written in its `calendar` block. The `dateField` / `endField` tombstones stay and now prescribe `calendar.startDateField` / `calendar.endDateField`. `ObjectGanttSchema` no longer declares the 29 `GanttConfig` keys flat (`startDateField`, `endDateField`, `titleField`, `progressField`, `dependenciesField`, `colorField`, `viewMode`, `summaryExtent`, `interactions` and the rest), nor the legacy `dependencyField` alias or the `search` / `searchableFields` pair; they are written in its `gantt` block (the pair is not authored at all). `staticData`, `filter`, `sort`, `gantt` and the other node keys stay. For `object-gantt` the strict authoring face already refused every flat key (its arm is the `properties` bag), so the narrowing there is the published TypeScript and zod export.
- **`@object-ui/plugin-calendar`**: `ObjectCalendarFlatFields` and `ObjectCalendarHandoff` type the flat read `getCalendarConfig` keeps for a host that mounts the renderer directly. The renderer is unchanged: the `calendar` block wins, and the flat spelling is read only without it.
- **`@object-ui/plugin-gantt`**: `ObjectGanttFlatFields` (derived from `GanttConfig`, plus the alias and the search pair) and `ObjectGanttHandoff` type the flat read the same way. The renderer is unchanged.
- **`@objectstack/spec`**: the `object-calendar` and `object-gantt` flat-key refusals no longer say `ObjectView` / `ListView` emit the flat spelling; they hand the binding off in the block, and the flat spelling is only a stored-document fallback the renderer still reads. The prescription itself is unchanged.
- **`@object-ui/plugin-list`** and **`@object-ui/plugin-view`**: `ListView` and `ObjectView`'s `generateViewSchema` emit `calendar: { … }` and `gantt: { … }` on the node they build instead of the flat keys. They emit the block only where the renderer's flat branch drew: a calendar once `startDateField` is declared, a gantt once both dates are, with `titleField` floored at `'name'` as that branch floored it. A gantt binding carrying a value the spec's `GanttConfigSchema` refuses (a pre-door stored `options.gantt`, say) goes out nested too, and now shows the renderer's refusal screen where the unvalidated flat branch drew it.

**BREAKING** for TypeScript code or hand-written nodes that write the flat keys on these two node types.

- **Before.** `const node: ObjectCalendarSchema = { type: 'object-calendar', objectName: 'event', startDateField: 'start' }` compiled, and the `object-calendar` strict face accepted it.
- **Now.** The flat key is a compile error, and `objectui validate` refuses it by name on `object-calendar` (it already did on `object-gantt`). A renderer handed a flat node by a host still draws it.
- **Fix.** Move the keys into the block: FROM `{ type: 'object-calendar', startDateField: 'start', titleField: 'name' }` TO `{ type: 'object-calendar', calendar: { startDateField: 'start', titleField: 'name' } }`, and FROM `{ type: 'object-gantt', startDateField: 's', endDateField: 'e', progressField: 'p' }` TO `{ type: 'object-gantt', gantt: { startDateField: 's', endDateField: 'e', titleField: 'name', progressField: 'p' } }` (the `gantt` block requires `titleField`). Code that hands a flat node straight to `<ObjectCalendar>` or `<ObjectGantt>` types it as `ObjectCalendarProps['schema']` or `ObjectGanttProps['schema']`, which carry the flat keys.
45 changes: 25 additions & 20 deletions packages/spec/src/ui/component.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6195,8 +6195,11 @@ export type ObjectKanbanPropsParsed = z.infer<typeof ObjectKanbanPropsSchema>;

/**
* The flat per-field spellings `ObjectCalendar` keeps reading as a
* backward-compat fallback (`getCalendarConfig`; read points recorded in
* `liveness/page.json`, row `regions`) and that `ObjectView`/`ListView` emit on their runtime handoff. Read, but
* stored-document fallback (`getCalendarConfig`; read points recorded in
* `liveness/page.json`, row `regions`). `ObjectView`/`ListView` hand the
* binding off in the `calendar` block, not flat (objectstack#25923), and the
* flat keys live on `plugin-calendar`'s own hand-off type, not on
* `@object-ui/types`' `ObjectCalendarSchema`. Read, but
* NOT authorable — one composition key per concept (Prime Directive #12, the
* `body` → `children` precedent on {@link PageContainerProps}): the authored
* spelling is the `calendar` object.
Expand All @@ -6209,9 +6212,9 @@ const OBJECT_CALENDAR_FLAT_FIELD_GUIDANCE: readonly KeySetGuidance[] = [
examples: ['startDateField', 'titleField'],
prescription:
'Write this as a key of the `calendar` config object instead — `calendar: { startDateField, '
+ 'endDateField, titleField, colorField, allDayField }`. The flat spelling is the runtime handoff '
+ '`ObjectView`/`ListView` emit and a stored-document fallback the renderer keeps reading; it is '
+ 'not a second authorable spelling (one key per concept, Prime Directive #12).',
+ 'endDateField, titleField, colorField, allDayField }`. `ObjectView`/`ListView` hand the binding '
+ 'off in that block too; the flat spelling is only a stored-document fallback the renderer still '
+ 'reads, not a second authorable spelling (one key per concept, Prime Directive #12).',
},
];

Expand Down Expand Up @@ -7522,21 +7525,22 @@ export type ObjectMapProps = z.input<typeof ObjectMapPropsSchema>;
export type ObjectMapPropsParsed = z.infer<typeof ObjectMapPropsSchema>;

/**
* The flat `GanttConfig` spellings `getGanttConfig`'s branch 2 reads and that
* `ObjectView` / `ListView` EMIT when they flatten `options.gantt` — objectui's
* own `FLAT_GANTT_CONFIG_KEYS` (read points recorded in `liveness/page.json`,
* row `regions`). Read, but
* The flat `GanttConfig` spellings `getGanttConfig`'s branch 2 reads as a
* stored-document fallback — objectui's own `FLAT_GANTT_CONFIG_KEYS` (read
* points recorded in `liveness/page.json`, row `regions`). `ObjectView` /
* `ListView` hand the binding off in the `gantt` block, not flat
* (objectstack#25923). Read, but
* NOT authorable, for the reason the map's twin set above records: objectui#6469
* inherited the objectui#5018 ruling for this block, so the `gantt` block is the
* authoring shape, it is taken WHOLE when present, and every flat key beside it
* is named as ignored in a dev warning.
*
* The flat branch is the HOT path for this block — a hand-authored `gantt` block
* reaching the renderer through either view layer has already been flattened —
* which is exactly why the prescription matters here: an author who learned the
* flat spelling from a view config (or from `@object-ui/types`'
* `ObjectGanttSchema`, which declares the whole flat face) writes it on an SDUI
* node next, where nothing flattens anything.
* The block branch is the path both view layers take — they hand the `gantt`
* block on whole — and the flat branch is only the stored-document fallback.
* The prescription still matters: an author who met the flat spelling in an
* older stored document writes it on an SDUI node next. The flat keys live on
* `plugin-gantt`'s package-local hand-off type (`ObjectGanttFlatFields`), not
* on `@object-ui/types`' `ObjectGanttSchema`, which declares only the block.
*
* Membership is `GanttConfigSchema`'s own shape plus the legacy singular
* `dependencyField` alias the flat branch still reads beside `dependenciesField`
Expand All @@ -7561,9 +7565,10 @@ const OBJECT_GANTT_FLAT_CONFIG_GUIDANCE: readonly KeySetGuidance[] = [
examples: ['startDateField', 'endDateField', 'viewMode'],
prescription:
'Write this as a key of the `gantt` config object instead — `gantt: { startDateField, endDateField, '
+ 'titleField, ... }`. The flat top-level spelling is the internal form `ObjectView`/`ListView` '
+ 'produce when they flatten `options.gantt`, not a second authoring spelling: whenever a `gantt` '
+ 'block is present the renderer takes it WHOLE and names every flat key beside it as ignored. The '
+ 'titleField, ... }`. `ObjectView`/`ListView` hand the binding off in that block too; the flat '
+ 'top-level spelling is only a stored-document fallback the renderer still reads, not a second '
+ 'authoring spelling: whenever a `gantt` block is present the renderer takes it WHOLE and names '
+ 'every flat key beside it as ignored. The '
+ 'legacy singular `dependencyField` is `dependenciesField` inside that block.',
},
];
Expand Down Expand Up @@ -7631,7 +7636,7 @@ function objectGanttMarker() {
* `mobileReadOnly`.
*
* Measured and deliberately NOT declared: the flat `GanttConfig` spellings (the
* flatten product — {@link OBJECT_GANTT_FLAT_CONFIG_GUIDANCE}); `title`, which
* stored-document fallback — {@link OBJECT_GANTT_FLAT_CONFIG_GUIDANCE}); `title`, which
* this renderer never reads (the export-name chain is `gantt.exportFileName` →
* `label` → the OBJECT's label → `objectName`); a row cap — the reload's `$top`
* is the platform ceiling, spread in as core's `nonGridRowCeilingQuery()` (one
Expand All @@ -7642,7 +7647,7 @@ function objectGanttMarker() {
* reads, `search` and `searchableFields` (sent as `$search` / `$searchFields`),
* which `ListView` writes onto the node it generates from its toolbar Search
* box (its `case 'gantt'` arm) — a host-generated key pair in the same position
* as the flatten product, and not declared here.
* as the flat fallback spellings, and not declared here.
*
* VALUE posture: `gantt` is the one config block in this family whose value
* contract is already the SPEC's — the renderer validates it against
Expand Down
14 changes: 8 additions & 6 deletions ui/packages/plugin-calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,14 @@ of its own — the only lever there is `sourceView`, pointed at a view that
declares the block (objectui#8170).

The same five keys written flat on the node are **not** a second way to author
this. That flat spelling is the runtime handoff — `ObjectView` and `ListView`
emit it on the node they build, and `getCalendarConfig` falls back to it only
when the node has no `calendar` block — so `ObjectCalendarSchema` declares it
because the renderer reads it. `@objectstack/spec` refuses it at authoring with
`unrecognized_keys` and names the `calendar` block in the same diagnostic (one
key per concept, its Prime Directive #12).
this. `@objectstack/spec` refuses them at authoring with `unrecognized_keys` and
names the `calendar` block in the same diagnostic (one key per concept, its
Prime Directive #12), and since objectstack#25923 `ObjectCalendarSchema` does
not declare them flat either: `ObjectView` and `ListView` emit the `calendar`
block on the node they build. `getCalendarConfig` still falls back to the flat
spelling when a node has no `calendar` block, for a host that mounts the
renderer directly; that read is typed on this package's own hand-off type,
`ObjectCalendarHandoff`, not on the published schema.

```typescript
import type { ObjectCalendarSchema } from '@object-ui/types';
Expand Down
37 changes: 31 additions & 6 deletions ui/packages/plugin-calendar/src/ObjectCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import type { ObjectCalendarSchema, DataSource, CalendarConfig } from '@object-ui/types';
import type { ObjectCalendarSchema, ObjectCalendarBlockConfig, DataSource, CalendarConfig } from '@object-ui/types';
import { CalendarView, type CalendarViewEvent } from './CalendarView';
import { getVisibleDateRange } from './visibleDays';
import { usePullToRefresh } from '@object-ui/mobile';
Expand Down Expand Up @@ -132,6 +132,26 @@ import {
* Pinned in `__tests__/calendarUnionReads-8651.test.tsx`.
*/

/**
* The five field-name keys written FLAT on the node: the package-local
* hand-off spelling (objectstack#25923, ui/AGENTS.md Commandment #0.1).
*
* `@objectstack/spec` refuses all five on `object-calendar` and prescribes the
* `calendar` block, so the published {@link ObjectCalendarSchema} declares
* none of them, and both producers in this tree (`ListView`, and
* `ObjectView`'s `generateViewSchema`) emit the block. `getCalendarConfig`
* still reads the flat five when a node carries no block, so a host that mounts
* `<ObjectCalendar schema={…}>` with them keeps its calendar; this type is that
* read, kept here, beside the one consumer, and off the authoring face.
* DERIVED from the block's own type, so the two spellings cannot fork.
*/
export type ObjectCalendarFlatFields = Partial<
Pick<ObjectCalendarBlockConfig, 'startDateField' | 'endDateField' | 'titleField' | 'colorField' | 'allDayField'>
>;

/** The node `ObjectCalendar` reads: the published schema plus {@link ObjectCalendarFlatFields}. */
export type ObjectCalendarHandoff = ObjectCalendarSchema & ObjectCalendarFlatFields;

/**
* Props of the `ObjectCalendar` React component.
*
Expand All @@ -149,7 +169,7 @@ import {
* no importer breaks. Tripwire: `__tests__/spec-symbol-4650.test.ts`.
*/
export interface ObjectCalendarComponentProps {
schema: ObjectCalendarSchema;
schema: ObjectCalendarHandoff;
dataSource?: DataSource;
className?: string;
/** Pre-fetched records passed by a parent (e.g. ObjectView). When provided, skips internal data fetching. */
Expand Down Expand Up @@ -320,13 +340,17 @@ type ObjectCalendarConfig = CalendarConfig & {
* `calendar-date-alias-refusal-8355.test.ts` and `plugin-list`'s
* `ListView.calendarAliasRefused-8355.test.tsx`.
*/
function getCalendarConfig(schema: ObjectCalendarSchema): ObjectCalendarConfig | null {
function getCalendarConfig(schema: ObjectCalendarHandoff): ObjectCalendarConfig | null {
// The declared configuration container — read FIRST, as the spec declares it.
if (schema.calendar) {
return schema.calendar as ObjectCalendarConfig;
}

// The flat spelling, which `ObjectView` and `ListView` emit.
// The flat spelling — the package-local hand-off (`ObjectCalendarFlatFields`).
// No producer in this tree emits it since objectstack#25923 (`ListView` and
// `ObjectView` hand the view's block on as `calendar`), and the spec refuses
// it on an authored node; a host that mounts this component with it is the
// reader this branch keeps working.
//
// ⛔ The two alias rungs are spelled as a cast ON `schema` at each read site,
// never through a renamed local. The repo's read census (objectui#6576's
Expand Down Expand Up @@ -1446,8 +1470,9 @@ export const ObjectCalendar: React.FC<ObjectCalendarComponentProps> = ({
* `plugin-view`'s `ObjectView` — and app-shell reaches the first from two
* faces, `ObjectView` (the object-view door) and `InterfaceListPage` (the
* interface-page door). Every one of them hands this component the same
* shape: the shared `baseProps` bag plus whichever of `startDateField` /
* `endDateField` / `titleField` the author declared. NOTHING on the node
* shape: the shared `baseProps` bag plus a `calendar` block carrying whichever
* of `startDateField` / `endDateField` / `titleField` the author declared
* (flat on the node until objectstack#25923). NOTHING on the node
* names the door, so there is nothing to infer from, and a door PROP would
* have to be declared on the node's published schema and threaded through
* four packages.
Expand Down
Loading
Loading