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-fallback-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@object-ui/plugin-calendar': patch
'@object-ui/plugin-gantt': patch
'@objectstack/spec': patch
'@object-ui/types': patch
'@object-ui/plugin-list': patch
'@object-ui/plugin-view': patch
---

refactor(plugin-calendar,plugin-gantt)!: the calendar and gantt renderers read their binding from the `calendar` / `gantt` block only; the flat fallback read is retired (#25923)

Clause-②: no (narrowing)
Metadata-protocol: none

<!-- adr-0087: not-required (no-migration-prescription) No `@objectstack/spec` key, accepted value, stored row or stored shape moves. The spec's `ComponentPropsMap['object-calendar']` and `['object-gantt']` rows already refuse every flat key this change stops reading, `@object-ui/types` stopped declaring them in the earlier slice of #25923, and both producers in the console (`ListView` and `ObjectView`) already write the block, so no metadata the spec accepts reaches the retired read. A stored-row census over every metadata source this repository reaches (examples, apps, the console's shipped metadata, seeds, fixtures, platform-objects) found no stored view or node that writes the binding flat, so `objectstack migrate meta` has nothing to rewrite. What narrows is the drawing outcome for a host that mounts `ObjectCalendar` or `ObjectGantt` directly with a flat node: it now reaches the refusal screen. The `@objectstack/spec` half is prose only: the two refusals no longer say the renderer still reads the flat spelling. The other categories are closed on facts: the packages publish (not unpublished); no ADR-0087 id covers this change (not registered or already-registered); the narrowed type members are package-local hand-off types that neither package's entry exports (not type-surface-only or runtime-export-retired). -->

`ObjectCalendar` and `ObjectGantt` kept a second, flat reading of their field binding for a host that mounts them directly with the keys written on the node. The spec refuses those keys on the node, `@object-ui/types` no longer declares them, and no producer writes them, so that read was a second dialect nothing should reach. It is retired.

- **`@object-ui/plugin-calendar`**: `getCalendarConfig` reads the `calendar` block only. A node that writes `startDateField`, `endDateField`, `titleField`, `colorField` or `allDayField` flat, with no block, shows the "Calendar configuration required" screen, whose hint names the `calendar` block. The package-local `ObjectCalendarFlatFields` and `ObjectCalendarHandoff` types are gone, and `ObjectCalendarProps['schema']` is the published `ObjectCalendarSchema`.
- **`@object-ui/plugin-gantt`**: `getGanttConfig` reads the `gantt` block only. A node that writes its `GanttConfig` keys flat, with no block, shows the "Gantt configuration required" screen, which now names the `gantt` block, and in development a warning names every flat key the chart did not read, with or without a block beside it. The legacy singular `dependencyField` is read nowhere (the spec spells it `dependenciesField`, inside the block). The package-local `ObjectGanttFlatFields` type is gone; `ObjectGanttProps['schema']` keeps the two search keys `ListView` writes at runtime, now on `ObjectGanttSearchFields`.
- **`@objectstack/spec`**: the `object-calendar` and `object-gantt` flat-key refusals no longer say the renderer still reads the flat spelling. The prescription is unchanged: write the keys in the `calendar` / `gantt` block.

**Supersedes, in the same release:** `25923-calendar-gantt-flat-keys-handoff.md` (the earlier slice of #25923) says the calendar and gantt renderers are unchanged and still read the flat spelling a host hands them, that `ObjectCalendarProps['schema']` and `ObjectGanttProps['schema']` carry the flat keys, and that the spec refusals call the flat spelling a fallback the renderer still reads. This change retires that read, so those sentences no longer hold: the renderers read the block only, the props types carry no flat binding key, and the refusals no longer say it.

**BREAKING** for code that mounts `<ObjectCalendar>` or `<ObjectGantt>` with a flat node.

- **Before.** `<ObjectGantt schema={{ type: 'object-gantt', objectName: 'task', startDateField: 's', endDateField: 'e' }} />` drew a chart, and the flat keys compiled against `ObjectGanttProps['schema']`.
- **Now.** The flat keys are a compile error on the props type, and the node shows the configuration-required screen.
- **Fix.** Write the binding in the block: `{ type: 'object-gantt', objectName: 'task', gantt: { startDateField: 's', endDateField: 'e', titleField: 'name' } }` (the `gantt` block requires `titleField`; the flat branch used to floor it at `'name'`), and `{ type: 'object-calendar', calendar: { startDateField: 'start' } }` for a calendar.
6 changes: 3 additions & 3 deletions packages/spec/src/ui/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3290,9 +3290,9 @@ describe('object-* block props schemas — declared, so the props gate has a sch
});

it("object-calendar's flat field spellings get the wrong-layer prescription, not a rename", () => {
// Read as back-compat by getCalendarConfig, emitted by ObjectView/ListView
// handoffs — but the authored spelling is the `calendar` object (one key
// per concept; the `body` → `children` precedent).
// Not read by getCalendarConfig since objectstack#25923, and not emitted by
// ObjectView/ListView — the authored spelling is the `calendar` object (one
// key per concept; the `body` → `children` precedent).
const message = refuse(ComponentPropsMap['object-calendar'], { objectName: 'task', startDateField: 'due_date' });
expect(message).toContain('`calendar`');
expect(message).toContain('startDateField');
Expand Down
62 changes: 27 additions & 35 deletions packages/spec/src/ui/component.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6042,15 +6042,13 @@ export type ObjectKanbanProps = z.input<typeof ObjectKanbanPropsSchema>;
export type ObjectKanbanPropsParsed = z.infer<typeof ObjectKanbanPropsSchema>;

/**
* The flat per-field spellings `ObjectCalendar` keeps reading as a
* 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.
* The flat per-field spellings of the `calendar` block, refused on the node.
* `ObjectView`/`ListView` hand the binding off in the `calendar` block, not
* flat, `@object-ui/types`' `ObjectCalendarSchema` declares none of them, and
* `ObjectCalendar`'s `getCalendarConfig` no longer reads them either: its flat
* fallback retired with objectstack#25923. NOT authorable — one composition
* key per concept (Prime Directive #12, the `body` → `children` precedent on
* {@link PageContainerProps}): the authored spelling is the `calendar` object.
*/
const OBJECT_CALENDAR_FLAT_FIELD_GUIDANCE: readonly KeySetGuidance[] = [
...COMPONENT_LEVEL_GUIDANCE,
Expand All @@ -6061,8 +6059,8 @@ const OBJECT_CALENDAR_FLAT_FIELD_GUIDANCE: readonly KeySetGuidance[] = [
prescription:
'Write this as a key of the `calendar` config object instead — `calendar: { startDateField, '
+ '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).',
+ 'off in that block too, and the renderer reads the binding there only: the flat spelling is not '
+ 'read and not a second authorable spelling (one key per concept, Prime Directive #12).',
},
];

Expand Down Expand Up @@ -7373,26 +7371,21 @@ export type ObjectMapProps = z.input<typeof ObjectMapPropsSchema>;
export type ObjectMapPropsParsed = z.infer<typeof ObjectMapPropsSchema>;

/**
* 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 `GanttConfig` spellings, refused on the node — objectui's own
* `FLAT_GANTT_CONFIG_KEYS`. `ObjectView` / `ListView` hand the binding off in
* the `gantt` block, not flat, and `getGanttConfig` reads that block only: its
* flat fallback branch retired with objectstack#25923. 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, and a dev warning names every flat key as not read.
*
* 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.
* older document writes it on an SDUI node next. `@object-ui/types`'
* `ObjectGanttSchema` declares only the block.
*
* Membership is `GanttConfigSchema`'s own shape plus the legacy singular
* `dependencyField` alias the flat branch still reads beside `dependenciesField`
* — the same two halves objectui derives its list from. Spelled out rather than
* `dependencyField` alias of `dependenciesField` — the same two halves objectui
* derives its list from. Spelled out rather than
* read off `GanttConfigSchema.shape` here because that schema is a
* {@link lazySchema} proxy and forcing it at module load would build `view.zod`
* mid-initialisation (the import-cycle footgun `ruleArrayFilterError` defers
Expand All @@ -7413,10 +7406,9 @@ 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, ... }`. `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 '
+ 'titleField, ... }`. `ObjectView`/`ListView` hand the binding off in that block too, and the '
+ 'renderer reads the binding there only: the flat top-level spelling is not read and not a second '
+ 'authoring spelling. The '
+ 'legacy singular `dependencyField` is `dependenciesField` inside that block.',
},
];
Expand Down Expand Up @@ -7483,8 +7475,8 @@ function objectGanttMarker() {
* `showBaselines`, `readOnly` (the chart and the record overlay's lock) and
* `mobileReadOnly`.
*
* Measured and deliberately NOT declared: the flat `GanttConfig` spellings (the
* stored-document fallback — {@link OBJECT_GANTT_FLAT_CONFIG_GUIDANCE}); `title`, which
* Measured and deliberately NOT declared: the flat `GanttConfig` spellings (not
* read since objectstack#25923 — {@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 @@ -7494,8 +7486,8 @@ function objectGanttMarker() {
* props. ⚠️ Recorded, not ruled: objectui#10250 gave the renderer two more
* 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 flat fallback spellings, and not declared here.
* box (its `case 'gantt'` arm) — a host-generated key pair, 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
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const ROWS: NodeRow[] = [
// this block (objectui#8348); its `data` row is the array arm.
withFilter: top({ type: 'object-calendar', calendar: CALENDAR, staticData: [{ ...ROW }] }) },
{ node: 'object-gantt', shape: 'tuple', path: 'dataSource', filterParam: '$filter',
withFilter: top({ type: 'object-gantt', objectName: OBJECT, titleField: 'name', startDateField: 'start', endDateField: 'end' }) },
withFilter: top({ type: 'object-gantt', objectName: OBJECT, gantt: { titleField: 'name', startDateField: 'start', endDateField: 'end' } }) },
{ node: 'object-kanban', shape: 'tuple', path: 'dataSource', filterParam: '$filter',
withFilter: top({ type: 'object-kanban', objectName: OBJECT, groupBy: 'status', columns: [{ id: 'open', title: 'Open' }] }) },
{ node: 'object-map (object)', shape: 'tuple', path: 'dataSource', filterParam: '$filter',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,10 +585,11 @@ const OBJECT_CHART_EXTRAS = {
data: CHART_DATA,
series: CHART_SERIES,
};
// The binding lives in the `calendar` block, the one spelling `ObjectCalendar`
// reads since objectstack#25923 retired its flat fallback.
const CALENDAR_OBJECT_EXTRAS = {
objectName: 'accounts',
startDateField: 'start_at',
titleField: 'name',
calendar: { startDateField: 'start_at', titleField: 'name' },
};

/* ── `packages/components`: the renderer family objectui#5574 widened this to ──
Expand Down
7 changes: 3 additions & 4 deletions ui/packages/plugin-calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,9 @@ 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.
block on the node they build. The renderer does not read the flat spelling
either: a node that carries the five flat keys and no `calendar` block reaches
the "Calendar configuration required" screen, which names the block.

```typescript
import type { ObjectCalendarSchema } from '@object-ui/types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ describe('object-calendar — an AUTHORED `locale` reaches the header popover (o
type: 'plugin-calendar:object-calendar',
id: 'n',
objectName: 'accounts',
startDateField: 'start_at',
titleField: 'name',
calendar: {
startDateField: 'start_at',
titleField: 'name',
},
data: [{ id: 'r1', name: 'Standup', start_at: new Date(2020, 2, 10, 10).toISOString() }],
...extra,
} as never
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const renderBlock = (schema: Record<string, unknown>, ds: ReturnType<typeof make
</SchemaRendererProvider>,
);

const BLOCK = { type: 'object-calendar', objectName: 'event', dateField: 'due', titleField: 'name' };
const BLOCK = { type: 'object-calendar', objectName: 'event', calendar: { startDateField: 'due', titleField: 'name' } };

describe('object-calendar re-reads on the data-invalidation bus (objectui#10572)', () => {
it('an unscoped change (objectName "*") re-runs its query once', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('objectui#12291 — an object quick-create cannot satisfy hands the day
render(
<SchemaRendererProvider dataSource={ds}>
<SchemaRenderer
schema={{ type: 'object-calendar', id: 'n', objectName: 'showcase_task', startDateField: 'due_date', titleField: 'title' } as never}
schema={{ type: 'object-calendar', id: 'n', objectName: 'showcase_task', calendar: { startDateField: 'due_date', titleField: 'title' } } as never}
data={[]}
onEdit={onEdit}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ describe('ObjectCalendar refusal screen — the remedy it states (objectui#8170)
{
type: 'object-calendar',
objectName: 'crm_leave_request',
startDateField: 'start_date',
titleField: 'name',
calendar: {
startDateField: 'start_date',
titleField: 'name',
},
} as any
}
dataSource={makeDataSource()}
Expand All @@ -168,7 +170,9 @@ describe('ObjectCalendar refusal screen — the remedy it states (objectui#8170)
{
type: 'object-calendar',
objectName: 'crm_leave_request',
startDateField: 'start_date',
calendar: {
startDateField: 'start_date',
},
} as any
}
dataSource={makeDataSource()}
Expand Down
Loading
Loading