Skip to content

Commit 4abe681

Browse files
committed
feat(spec): declare titleField on KanbanConfigSchema
`KanbanConfigSchema` was the one item-titled view config of its family that omitted `titleField`, while Gallery, Timeline, Calendar, Gantt and ListMap all declare the key under the same name and the same `z.string()`. The schema is a `strictObject`, so an author writing the key the board actually reads was refused by name. Declared optional, matching the shape `CalendarConfigSchema` already writes down for this exact key: absence resolves through the ADR-0079 record display-name chain, so requiring it would demand more than the renderer reads. Timeline and Gantt spell it required and are the two siblings this declaration deliberately does not copy. Tests carry the four-leg probe with both controls firing on the same call shape, plus the optionality leg. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 79a046f commit 4abe681

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

‎packages/spec/src/ui/view.test.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,45 @@ describe('KanbanConfigSchema', () => {
182182

183183
expect(() => KanbanConfigSchema.parse(config)).not.toThrow();
184184
});
185+
186+
// [#16894] Director seat, decision batch #87 (objectstack-ai/objectui#8367,
187+
// maintainer 「批 #87 同意」): `titleField` joins this schema as optional
188+
// `z.string()`. The legs below are the card's executable acceptance
189+
// criterion — BOTH controls fire on the same call shape, so the PROBE's flip
190+
// is evidence about the NAME and not about a parser that stopped refusing.
191+
it('ACCEPTS `titleField` as a member, with both controls firing on the same shape', () => {
192+
const canonical = {
193+
groupByField: 'status',
194+
columns: ['name', 'owner'],
195+
};
196+
197+
// CONTROL-1 — the parser CAN refuse, on the named surface.
198+
const control1 = KanbanConfigSchema.safeParse({ ...canonical, zzUnlikelyBogusKey__: true });
199+
expect(control1.success).toBe(false);
200+
const issues = JSON.stringify(control1.error?.issues);
201+
expect(issues).toContain('unrecognized_keys');
202+
expect(issues).toContain('zzUnlikelyBogusKey__');
203+
expect(issues).toContain('this kanban configuration');
204+
205+
// CONTROL-2 — a refusal is about the name: the canonical block is accepted.
206+
expect(KanbanConfigSchema.safeParse(canonical).success).toBe(true);
207+
208+
// PROBE — before this card: `ok=false unrecognized_keys=["titleField"]`.
209+
const probe = KanbanConfigSchema.safeParse({ ...canonical, titleField: 'subject' });
210+
expect(probe.success).toBe(true);
211+
// Accepted as a MEMBER, not merely tolerated: the value survives the parse.
212+
expect(KanbanConfigSchema.parse({ ...canonical, titleField: 'subject' }))
213+
.toMatchObject({ titleField: 'subject' });
214+
});
215+
216+
// ⛔ NOT required. `TimelineConfigSchema` and `GanttConfigSchema` spell
217+
// `titleField` required and are the two siblings this declaration does not
218+
// copy: absence resolves through the ADR-0079 record display-name chain, so
219+
// requiring it would demand more than the renderer reads (#13748).
220+
it('leaves `titleField` OPTIONAL — a board that omits it is a complete config', () => {
221+
const parsed = KanbanConfigSchema.parse({ groupByField: 'status', columns: ['name'] });
222+
expect('titleField' in parsed).toBe(false);
223+
});
185224
});
186225

187226
describe('CalendarConfigSchema', () => {

‎packages/spec/src/ui/view.zod.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,25 @@ export const KanbanConfigSchema = lazySchema(() => strictObject({
13491349
}, {
13501350
groupByField: z.string().describe('Field to group columns by (usually status/select)'),
13511351
summarizeField: z.string().optional().describe('Field to sum at top of column (e.g. amount)'),
1352+
/**
1353+
* [#16894] The one item-titled view config of the family that omitted this
1354+
* key. `GalleryConfigSchema`, `TimelineConfigSchema`, `CalendarConfigSchema`,
1355+
* `GanttConfigSchema` and `ListMapConfigSchema` all declare `titleField`
1356+
* under the same name and the same `z.string()`; this schema is a
1357+
* `strictObject`, so an author writing the key the board actually reads was
1358+
* refused BY NAME while the renderer honoured it. Declared here under the
1359+
* director seat's decision batch #87 (objectstack-ai/objectui#8367).
1360+
*
1361+
* OPTIONAL, deliberately — the shape `CalendarConfigSchema` already writes
1362+
* down for this exact key. Absence resolves through the ADR-0079 record
1363+
* display-name chain (`titleFormat` -> `displayNameField` -> type-aware
1364+
* derivation -> `'Untitled'`), so requiring it would demand more than the
1365+
* renderer reads, the exact shape the #13748 ruling forbids
1366+
* (「不要求超过渲染器真正需要的」). `TimelineConfigSchema` and
1367+
* `GanttConfigSchema` spell it required; they are the two siblings this
1368+
* declaration does NOT copy.
1369+
*/
1370+
titleField: z.string().optional().describe('Field displayed as the card title. Omit to fall back to the record display name (ADR-0079 resolver chain)'),
13521371
columns: z.array(z.string()).describe('Fields to show on cards'),
13531372
}));
13541373

0 commit comments

Comments
 (0)