Skip to content

Commit 75bd83d

Browse files
claude[bot]claude
andauthored
feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) (#6472)
* feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) `getGanttConfig` has two branches. When `startDateField` and `endDateField` are both present at the TOP level it builds its config from top-level keys and returns early; otherwise it reads the `gantt` block. Everything the first branch reads was undeclared, and unlike #5903's ten none of it was behind a cast: `BaseSchema` carries `[key: string]: any` and the helper's parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as `any` with no syntax to grep for. The census is therefore an AST enumeration, not a compile-and-observe — an index signature absorbs exactly the evidence an annotation would have produced. - Re-derived on the post-#5903 tree: 47 top-level keys read, 19 declared, 28 residue. #5903 absorbed NONE of the card's 24; the residue is four larger because #5903 retyped `ObjectGanttProps.schema` to `ObjectGanttSchema`, which is correct and is why `staticData`/`filter`/`sort` (declared on `ObjectGridSchema`) now need declaring here, plus `gantt` itself. - The 24 flattened members are DERIVED from `GanttConfig`, so the flat spelling cannot fork from the block spelling; the invariant is pinned in the type system (every `GanttConfig` key is declared at the node's top level). - Nine members move out of `plugin-gantt`'s package-private `GanttConfigEx` into `@object-ui/types`' `GanttConfig` to make that derivation possible. A move, not new vocabulary: the `gantt` block already honoured all nine. - `getGanttConfig(schema: ObjectGanttSchema)` — without it the reads would still land on the index signature. - The zod mirror gains the same 28 at the same requiredness, spec-modelled ones from `GanttConfigSchema.shape` by reference, so `zod-mirror-parity` stays at zero drift and no ledger entry is added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q * refactor(types): sever the `gantt` block declaration to its own card (#6475) PM ruling 2026-08-26 on #6051: `gantt` is the one key of the measured 28 whose declaration would NOT have been additive, and a published CLI's refusal behaviour gets decided on its own card rather than inside a 27-key declaration PR. The other 27 are unchanged and land here. Severed: `gantt?: GanttConfig` on `ObjectGanttSchema` and its `SpecGanttConfigSchema.extend(GanttConfigExtensionFields)` mirror entry. With no mirror entry a block rides through `.passthrough()` unvalidated; with one it is parsed against the spec's `GanttConfigSchema`, which REQUIRES startDateField / endDateField / titleField — and `ObjectGanttSchema` reaches the CLI's `validate`/`check` through `AnyComponentSchema`, so a block missing one of the three would move from "accepted, then warned about at runtime" to "refused at authoring time". - The omission is recorded where someone would look for the key: a comment on the declaration, a comment in the mirror, and the `ObjectGanttProps.schema` JSDoc. All three name #6475 and say `getGanttConfig` still reads and honours the block, so none of them can be read as "nothing reads `gantt`". - Today's behaviour is PINNED rather than left implicit — the test that used to assert the block face accepts the flat vocabulary now asserts the mirror has no `gantt` entry and that a block missing the required trio (and even a wrong-typed block) parses green. The omission is a measured state. - `GanttConfigExtensionFields` stays: it is still the single source the flat face is built from, and the one line #6475 would need. Its doc no longer claims it feeds two faces. This change is now additive-only end to end: 27 new optional keys, no narrowing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011SfZeFWrhGLHmfq61xbz4q --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3777538 commit 75bd83d

5 files changed

Lines changed: 807 additions & 87 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-gantt': minor
4+
---
5+
6+
`ObjectGanttSchema` declares the flattened `GanttConfig` face `ObjectGantt`
7+
actually reads (objectui#6051). `getGanttConfig` has two branches: when
8+
`startDateField` and `endDateField` are both present at the TOP level it builds
9+
its config from top-level keys and returns early; otherwise it reads the `gantt`
10+
block. Everything the first branch reads was undeclared — and unlike
11+
objectui#5903's ten, none of it was hidden behind a cast. `BaseSchema` carries
12+
`[key: string]: any` (objectui#5155's structural ceiling) and the helper's
13+
parameter was `ObjectGridSchema | any`, so `schema.colorField` type-checked as
14+
`any` with no syntax anywhere to grep for. That is also why the census here is an
15+
AST enumeration and not a compile-and-observe: an index signature absorbs every
16+
literal name, so annotating the parameter compiles clean while enforcing nothing.
17+
18+
**27 keys join the declared surface, each additive and each with a live read
19+
site.** 24 flattened `GanttConfig` members — `colorField`, `borderColorField`,
20+
`dependenciesField`, `parentField`, `typeField`, `lockField`, `objectField`,
21+
`summaryExtent`, `defaultCollapsedDepth`, `tooltipFields`, `baselineStartField`,
22+
`baselineEndField`, `groupByField`, `resourceView`, `assigneeField`,
23+
`effortField`, `capacity`, `quickFilters`, `autoZoomToFilter`, `timeSegments`,
24+
`interactions`, `exportFileName`, `timeZone`, `dependencyTypes` — plus the three
25+
query keys the fetch path reads, `staticData`, `filter` and `sort`. Nothing is
26+
declared that the renderer does not consume.
27+
28+
**`GanttConfig` itself gains nine members and is a published type**, exported by
29+
name from `packages/types/src/index.ts`: `lockField`, `objectField`,
30+
`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`,
31+
`timeZone`, `exportFileName`, `interactions`. The entry file's diff is empty only
32+
because the export list already named the type — the widening happened at the
33+
declaration.
34+
35+
**The 28th measured key, `gantt` (the block face), is deliberately NOT declared**
36+
— see the closing section.
37+
38+
The 24 are DERIVED from `GanttConfig` rather than restated, so the flat spelling
39+
cannot fork from the block spelling, and the invariant is pinned in the type
40+
system: every key of `GanttConfig` must be declared at the node's top level.
41+
Making that derivation possible moved nine members — `lockField`, `objectField`,
42+
`summaryExtent`, `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`,
43+
`timeZone`, `exportFileName`, `interactions` — out of `plugin-gantt`'s
44+
package-private `GanttConfigEx` and into `@object-ui/types`' `GanttConfig`. They
45+
are a MOVE, not new vocabulary: the `gantt` block already honoured all nine, and
46+
a type private to the plugin could be referenced by neither authoring face.
47+
48+
Both halves move together, as in objectui#5903: the TS declaration and its zod
49+
mirror gain the same 27 keys at the same requiredness (all optional), the
50+
spec-modelled ones taken from `GanttConfigSchema.shape` by reference, so the
51+
`zod-mirror-parity` ratchet stays at zero drift for this pair and no `KnownDrift`
52+
or `UnmirroredDeclared` entry is added. The mirror builds the flat face and the
53+
`gantt` block from one field map, so they are one schema expressed twice.
54+
55+
Accept-set change, stated plainly. All 27 keys are additive — every one is
56+
optional, and nothing previously legal loses its slot. What changes is that a
57+
**declared** key is now type-validated, so `capacity: 'one'` and
58+
`summaryExtent: 'parent'` are refused where they used to parse green. An
59+
**undeclared** key is still accepted — `BaseSchema` is `.passthrough()`, so this
60+
bought no rejection of misspellings. There is no narrowing anywhere in this
61+
change.
62+
63+
**`gantt` is severed on purpose (objectui#6475), not overlooked.** It is the 28th
64+
key of the measured residue and a genuine read — `getGanttConfig`'s second branch
65+
honours it in full — but it is the one key whose declaration would NOT have been
66+
additive. It has no mirror entry today, so a block rides through `.passthrough()`
67+
unvalidated; declaring it as `GanttConfig` means it gets parsed against the spec's
68+
`GanttConfigSchema`, which REQUIRES `startDateField`, `endDateField` and
69+
`titleField`, and `ObjectGanttSchema` reaches the CLI's `validate` / `check`
70+
through `AnyComponentSchema`. A published CLI's refusal behaviour is decided on
71+
its own card, where reviewers can see what they are approving; objectui#6475
72+
carries the full measurement, including the case FOR enforcing it (the renderer
73+
already feeds that block to `GanttConfigSchema.safeParse` and warns, so enforcing
74+
restores declared = enforced rather than inventing a contract). Today's behaviour
75+
is pinned in the test file so the omission is a measured state, not a silent gap.
76+
`packages/types/src/__tests__/gantt-flat-config-declared-keys.test.ts` pins both
77+
halves so neither can be misread.
78+
79+
Which face WINS is unchanged and was not decided here: the flat branch is checked
80+
first and returns early, so a node carrying both spellings still renders the flat
81+
one. (`plugin-map` had the opposite precedence ruled on in objectui#5018; no
82+
equivalent ruling exists for gantt.)

‎packages/plugin-gantt/src/ObjectGantt.tsx‎

Lines changed: 25 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import React, { useContext, useEffect, useState, useMemo, useCallback, useRef } from 'react';
2626
import { toast } from 'sonner';
27-
import type { ObjectGanttSchema, ObjectGridSchema, DataSource, ViewData, GanttConfig } from '@object-ui/types';
27+
import type { ObjectGanttSchema, DataSource, ViewData, GanttConfig } from '@object-ui/types';
2828
import { GanttConfigSchema } from '@objectstack/spec/ui';
2929
// Aliased on import, following PR #4169's convention: this repo has its OWN
3030
// `resolveI18nLabel` over a DIFFERENT vocabulary (the KEYED `{ key, defaultValue }`
@@ -89,8 +89,19 @@ export interface QuickFilterDef {
8989
}
9090

9191
/**
92-
* Hierarchy/type fields are ObjectUI extensions on top of the spec's
93-
* GanttConfig (not yet in @objectstack/spec GanttConfigSchema).
92+
* The gantt config as THIS renderer consumes it: `GanttConfig` from
93+
* `@object-ui/types` — the spec's `GanttConfigSchema` plus objectui's own
94+
* extensions — with `quickFilters` and `timeSegments` narrowed to the plugin's
95+
* runtime types, and the spec-declared members re-documented with the behaviour
96+
* this renderer gives them.
97+
*
98+
* ⚠️ Nothing here may declare a key `GanttConfig` does not (objectui#6051). Nine
99+
* members that lived ONLY here — `lockField`, `objectField`, `summaryExtent`,
100+
* `defaultCollapsedDepth`, `borderColorField`, `dependencyTypes`, `timeZone`,
101+
* `exportFileName`, `interactions` — were lifted into `@object-ui/types`, because
102+
* a type private to this package can be referenced by neither authoring face.
103+
* Each key is now declared once and both faces derive from it: the `gantt` block
104+
* and the flattened top-level spelling on `ObjectGanttSchema`.
94105
*/
95106
type GanttConfigEx = GanttConfig & {
96107
parentField?: string;
@@ -101,55 +112,9 @@ type GanttConfigEx = GanttConfig & {
101112
* style levels that only group, never schedule.
102113
*/
103114
typeField?: string;
104-
/**
105-
* Record field marking a node as view-only (truthy → locked). A locked
106-
* row's bar can't be dragged/resized, its progress can't be dragged, no
107-
* dependency can be drawn from it, and its inline-edit / context-menu
108-
* edit+delete are hidden — but clicking it (open drawer / jump) still works.
109-
* Independent of the global `readOnly`; use to freeze individual levels (e.g.
110-
* work orders) while siblings stay editable. Maps to {@link GanttTask.locked}.
111-
*/
112-
lockField?: string;
113-
/**
114-
* Record field carrying the row's OBJECT API NAME. Mixed-object
115-
* trees (an `api` provider composing parent-object rows with child-object rows)
116-
* need the detail drawer and its full-page link to follow each row's REAL
117-
* object — otherwise a child row's `→` link builds a URL under the view's bound
118-
* object and 404s. Empty/missing value → falls back to the bound object.
119-
*/
120-
objectField?: string;
121-
/**
122-
* How a summary bar's span is computed. `'children'` (default)
123-
* rolls the bar up from its children — min start / max end / duration-weighted
124-
* progress — and IGNORES the record's own dates. `'self'` renders the bar from
125-
* the record's OWN start/end/progress, falling back to rollup
126-
* only for records without dates (e.g. pure grouping levels). Use `'self'`
127-
* when the parent's schedule is authoritative — e.g. a shift plan whose
128-
* work-order children are locked history: under rollup, dragging the plan
129-
* persists its own dates but the bar snaps back to the children's extent on
130-
* refetch.
131-
*/
132-
summaryExtent?: 'children' | 'self';
133-
/**
134-
* Auto-collapse tree nodes at/below this 0-indexed depth on first render.
135-
* Roots are depth 0. Every node at depth `>= defaultCollapsedDepth`
136-
* with children starts folded; the user can still expand them. Example: a
137-
* project→product→production-plan→work-order tree uses
138-
* `defaultCollapsedDepth: 2` so every production plan (and its work orders)
139-
* starts collapsed. Forwarded to {@link GanttView}.
140-
*/
141-
defaultCollapsedDepth?: number;
142115
/** Baseline (planned) start/end fields → planned-vs-actual reference bars. */
143116
baselineStartField?: string;
144117
baselineEndField?: string;
145-
/**
146-
* Record field carrying a per-task alert stroke color: any CSS color or
147-
* semantic palette name (red/orange/…). When present the bar keeps its fill
148-
* but gets an outline + halo in that color — e.g. red for overdue, orange for
149-
* due-soon — typically a server-computed alert field. Empty/null → no stroke.
150-
* Maps to {@link GanttTask.borderColor}.
151-
*/
152-
borderColorField?: string;
153118
/**
154119
* Dynamic Group by. When set, leaf tasks are bucketed by this
155120
* field and rendered under one synthesized summary row per distinct value
@@ -182,37 +147,6 @@ type GanttConfigEx = GanttConfig & {
182147
* (unfiltered) task set while filtering only hides bars.
183148
*/
184149
autoZoomToFilter?: boolean;
185-
/**
186-
* Whether the backing store persists dependency link TYPES (fs/ss/ff/sf).
187-
* Default true. Set false when dependencies are bare predecessor ids
188-
* (predecessor ids only) — the link menu hides the type switcher (a switch would be
189-
* silently reverted on refetch) and drag-created links are always FS.
190-
* Forwarded to {@link GanttView}.
191-
*/
192-
dependencyTypes?: boolean;
193-
/**
194-
* Business time zone, IANA name like 'Asia/Shanghai'. Renders the
195-
* chart's calendar — shift bands, day columns, snapping, today line, date
196-
* labels — in this zone's wall time for every viewer, instead of the
197-
* browser's zone (which misplaces shift bands for viewers elsewhere). Persisted
198-
* data stays real instants. Forwarded to {@link GanttView}.
199-
*/
200-
timeZone?: string;
201-
/**
202-
* Base name for exported PNG/PDF files, e.g. the view's display
203-
* label — the host's view schema often reaches this component stripped of
204-
* `label`, so views declare it here. Falls back to the object schema label,
205-
* then the object API name. A timestamp suffix is always appended.
206-
*/
207-
exportFileName?: string;
208-
/**
209-
* Per-interaction switches: `move` / `resize` / `progress` / `link`,
210-
* each defaulting to true. Metadata-drivable so a view can e.g. allow bar
211-
* moves but pin durations (`{ resize: false }`) or keep the dependency UI
212-
* read-only (`{ link: false }`). They only narrow what `readOnly` / row locks
213-
* already allow. Forwarded to {@link GanttView}.
214-
*/
215-
interactions?: GanttInteractions;
216150
/**
217151
* Shift segmentation. When set, the day-mode timeline splits each shift-day
218152
* (starting at `dayStart`) into the configured bands (day | night | …):
@@ -289,10 +223,14 @@ export interface ObjectGanttProps {
289223
* that hid even that. Removing the casts without moving the type would have
290224
* changed nothing — the reads would still land on the index signature.
291225
*
292-
* The grid-style `{ gantt: { … } }` block keeps working exactly as before:
293-
* `getGanttConfig` reads it through the same index signature, and the
294-
* registered renderer (`index.tsx`) passes `schema: any`, so no runtime shape
295-
* is turned away.
226+
* objectui#6051 declared what the FLAT branch reads: the 24 flattened
227+
* `GanttConfig` keys `getGanttConfig`'s first branch consumes, plus the
228+
* `staticData` / `filter` / `sort` the fetch path reads. The grid-style
229+
* `{ gantt: { … } }` block keeps working exactly as before and is still read
230+
* through the index signature — declaring it is the one change that would not
231+
* have been additive, and it is severed to objectui#6475. The registered
232+
* renderer (`index.tsx`) still passes `schema: any`, so no runtime shape is
233+
* turned away either way.
296234
*/
297235
schema: ObjectGanttSchema;
298236
dataSource?: DataSource;
@@ -363,10 +301,10 @@ function extractServerMessage(err: unknown): string | null {
363301
/**
364302
* Helper to get gantt configuration from schema
365303
*/
366-
function getGanttConfig(schema: ObjectGridSchema | any): GanttConfigEx | null {
304+
function getGanttConfig(schema: ObjectGanttSchema): GanttConfigEx | null {
367305
let config: GanttConfigEx | null = null;
368306

369-
// 1. Check top-level properties (ObjectGanttSchema style)
307+
// 1. Check top-level properties (the flattened ObjectGanttSchema style)
370308
if (schema.startDateField && schema.endDateField) {
371309
config = {
372310
startDateField: schema.startDateField,
@@ -402,7 +340,7 @@ function getGanttConfig(schema: ObjectGridSchema | any): GanttConfigEx | null {
402340
return config;
403341
}
404342

405-
// 2. Check schema.gantt (ObjectGridSchema style)
343+
// 2. Check schema.gantt (the block face, ObjectGridSchema style)
406344
if (schema.gantt) {
407345
config = schema.gantt as GanttConfigEx;
408346
}

0 commit comments

Comments
 (0)