Skip to content

Commit 596caeb

Browse files
committed
feat(spec): structured buttons + defaults config on FormViewSchema (#2998)
Track A of #2998: give the renderer-invented flat form keys ObjectUI's ObjectForm reads today (showSubmit/submitText/showCancel/cancelText/ showReset/initialValues — objectui#2545) a real home in the protocol, instead of being silently stripped by the strip-mode FormViewSchema container (the "parsed -> silently inert" shape ADR-0078 prohibits). - New optional `buttons` block: per-button `{ show, label }` for submit/cancel/reset via a new exported FormButtonConfigSchema leaf, `.strict()` per ADR-0089 D3a so typo'd keys error loudly. - New optional `defaults` record: initial field values for create-mode forms, keyed by field machine name (absorbs objectui `initialValues`). - Both marked [EXPERIMENTAL — NOT ENFORCED, #2998] per ADR-0078's escape hatch: the framework has no runtime consumer of FormViewSchema, so the contract ships ahead of its ObjectUI consumer without a false promise. The marker matches the liveness ledger's experimental regex, ready for Track B (enrolling `view` in GOVERNED — separate PR per the issue). - Purely additive (strip-mode container): minor changeset, no tombstone. - Tests: parse round-trips, strict-leaf rejection of legacy flat keys, experimental-marker assertion. api-surface.json regenerated (+2 exports); ui/view.mdx reference regenerated (scoped hunks only). Refs ADR-0078, ADR-0089 D3a, objectui#2545, objectui#1763. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Ue47V8zZ5QhcYMPiRQDA7
1 parent 084c618 commit 596caeb

5 files changed

Lines changed: 139 additions & 3 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): structured `buttons` + `defaults` config on `FormViewSchema` (#2998)
6+
7+
`FormViewSchema` gains two optional top-level keys — the spec home for the flat
8+
renderer-invented form config ObjectUI's `ObjectForm` reads today
9+
(`showSubmit`/`submitText`/`showCancel`/`cancelText`/`showReset`/`initialValues`,
10+
objectui#2545), which the strip-mode container silently discards:
11+
12+
- **`buttons`** — structured action-button config: per-button `{ show, label }`
13+
for `submit` / `cancel` / `reset` (new exported leaf `FormButtonConfigSchema`,
14+
`.strict()` per ADR-0089 D3a so typo'd keys error loudly).
15+
- **`defaults`** — initial field values for create-mode forms, keyed by field
16+
machine name (absorbs ObjectUI's `initialValues`).
17+
18+
Both are marked `[EXPERIMENTAL — NOT ENFORCED]` per ADR-0078's escape hatch
19+
until the ObjectUI renderer reads them (tracked in objectui#2545); authoring
20+
them today is declared, not yet honored. Purely additive — no existing key
21+
changes shape, no tombstone needed.

content/docs/references/ui/view.mdx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Migrated to shared/http.zod.ts. Re-exported here for backward compatibility.
1616
## TypeScript Usage
1717

1818
```typescript
19-
import { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
20-
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
19+
import { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, FormButtonConfig, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
20+
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, FormButtonConfig, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, NavigationConfig, NavigationMode, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, ViewData, ViewFilterRule, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
2121

2222
// Validate data
2323
const result = AddRecordConfig.parse(data);
@@ -88,6 +88,18 @@ Aggregation function for column footer summary
8888
* `max`
8989

9090

91+
---
92+
93+
## FormButtonConfig
94+
95+
### Properties
96+
97+
| Property | Type | Required | Description |
98+
| :--- | :--- | :--- | :--- |
99+
| **show** | `boolean` | optional | Whether the button is rendered (renderer default applies when omitted) |
100+
| **label** | `string` | optional | Button label (i18n-capable; renderer default when omitted) |
101+
102+
91103
---
92104

93105
## GalleryConfig

packages/spec/api-surface.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,6 +3227,8 @@
32273227
"FocusManagementSchema (const)",
32283228
"FocusTrapConfig (type)",
32293229
"FocusTrapConfigSchema (const)",
3230+
"FormButtonConfig (type)",
3231+
"FormButtonConfigSchema (const)",
32303232
"FormField (type)",
32313233
"FormFieldSchema (const)",
32323234
"FormSection (type)",

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2+
import { z } from 'zod';
23
import {
34
ViewSchema,
45
ListViewSchema,
@@ -1931,6 +1932,69 @@ describe('FormViewSchema - defaultSort', () => {
19311932
});
19321933
});
19331934

1935+
// ============================================================================
1936+
// FormView structured buttons + defaults (#2998, EXPERIMENTAL until objectui#2545)
1937+
// ============================================================================
1938+
1939+
describe('FormViewSchema - buttons & defaults', () => {
1940+
it('should accept structured button config with visibility and labels', () => {
1941+
const result = FormViewSchema.parse({
1942+
type: 'simple',
1943+
sections: [{ fields: ['name'] }],
1944+
buttons: {
1945+
submit: { show: true, label: 'Save' },
1946+
cancel: { show: true, label: 'Discard' },
1947+
reset: { show: false },
1948+
},
1949+
});
1950+
expect(result.buttons?.submit).toEqual({ show: true, label: 'Save' });
1951+
expect(result.buttons?.reset).toEqual({ show: false });
1952+
});
1953+
1954+
it('should accept a partial buttons block (each button optional)', () => {
1955+
const result = FormViewSchema.parse({
1956+
type: 'simple',
1957+
sections: [{ fields: ['name'] }],
1958+
buttons: { submit: { label: 'Create' } },
1959+
});
1960+
expect(result.buttons?.submit?.label).toBe('Create');
1961+
expect(result.buttons?.cancel).toBeUndefined();
1962+
});
1963+
1964+
it('should reject unknown keys inside buttons (strict leaf, ADR-0089 D3a)', () => {
1965+
expect(() => FormViewSchema.parse({
1966+
type: 'simple',
1967+
buttons: { submit: { text: 'Save' } }, // legacy `submitText`-style key
1968+
})).toThrow();
1969+
expect(() => FormViewSchema.parse({
1970+
type: 'simple',
1971+
buttons: { showSubmit: true }, // flat renderer-invented key
1972+
})).toThrow();
1973+
});
1974+
1975+
it('should accept defaults as a field-name → value record', () => {
1976+
const result = FormViewSchema.parse({
1977+
type: 'simple',
1978+
sections: [{ fields: ['name', 'status', 'priority'] }],
1979+
defaults: { status: 'open', priority: 3, tags: ['a', 'b'] },
1980+
});
1981+
expect(result.defaults).toEqual({ status: 'open', priority: 3, tags: ['a', 'b'] });
1982+
});
1983+
1984+
it('should accept form view without buttons/defaults (optional)', () => {
1985+
const result = FormViewSchema.parse({ type: 'simple' });
1986+
expect(result.buttons).toBeUndefined();
1987+
expect(result.defaults).toBeUndefined();
1988+
});
1989+
1990+
it('marks both keys experimental until the renderer wiring lands (ADR-0078)', () => {
1991+
const shape = (FormViewSchema as unknown as z.ZodObject<z.ZodRawShape>).shape;
1992+
for (const key of ['buttons', 'defaults'] as const) {
1993+
expect(shape[key].description, `${key} .describe()`).toMatch(/EXPERIMENTAL NOT ENFORCED/);
1994+
}
1995+
});
1996+
});
1997+
19341998
describe('defineView', () => {
19351999
it('should return a parsed view with list config', () => {
19362000
const result = defineView({

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -809,10 +809,21 @@ export const FormSectionSchema = lazySchema(() => z.object({
809809
])),
810810
}, { error: strictVisibilityError }).strict().transform(normalizeVisibleWhen));
811811

812+
/**
813+
* A single form action button (submit / cancel / reset): visibility + label.
814+
* Leaf of the [EXPERIMENTAL] {@link FormViewSchema} `buttons` block (#2998).
815+
* `.strict()` per ADR-0089 D3a so a typo'd key errors instead of vanishing.
816+
*/
817+
export const FormButtonConfigSchema = lazySchema(() => z.object({
818+
show: z.boolean().optional().describe('Whether the button is rendered (renderer default applies when omitted)'),
819+
label: I18nLabelSchema.optional().describe('Button label (i18n-capable; renderer default when omitted)'),
820+
}).strict());
821+
export type FormButtonConfig = z.infer<typeof FormButtonConfigSchema>;
822+
812823
/**
813824
* Form View Schema
814825
* Defines the layout for creating or editing a single record.
815-
*
826+
*
816827
* @example Simple Sectioned Form
817828
* {
818829
* type: "simple",
@@ -924,6 +935,32 @@ export const FormViewSchema = lazySchema(() => z.object({
924935
z.object({ kind: z.literal('next-record') }),
925936
]).optional().describe('Post-submit behavior'),
926937

938+
/**
939+
* ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Structured action-button
940+
* config: per-button visibility + label for `submit` / `cancel` / `reset`.
941+
* This is the spec home for the flat renderer-invented keys ObjectUI's
942+
* `ObjectForm` reads today (`showSubmit`/`submitText`/`showCancel`/`cancelText`/
943+
* `showReset`, objectui#2545) — those never existed here and are silently
944+
* stripped by this strip-mode container. Experimental until the ObjectUI
945+
* renderer reads `buttons.*` (the ADR-0078 escape hatch: the contract ships
946+
* ahead of its consumer, marked so authoring it is not a false promise).
947+
*/
948+
buttons: z.object({
949+
submit: FormButtonConfigSchema.optional().describe('Submit button'),
950+
cancel: FormButtonConfigSchema.optional().describe('Cancel button'),
951+
reset: FormButtonConfigSchema.optional().describe('Reset button'),
952+
}).strict().optional()
953+
.describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Form action-button visibility & labels. Renderer wiring pending in ObjectUI (objectui#2545).'),
954+
955+
/**
956+
* ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Initial field values for
957+
* create-mode forms, keyed by field machine name. Spec home for ObjectUI's
958+
* renderer-invented `initialValues` (objectui#2545). Experimental until the
959+
* ObjectUI renderer reads it.
960+
*/
961+
defaults: z.record(z.string(), z.unknown()).optional()
962+
.describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Initial field values for create-mode forms (spec home for ObjectUI `initialValues`). Renderer wiring pending (objectui#2545).'),
963+
927964
/** ARIA accessibility attributes */
928965
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the form view'),
929966
}));

0 commit comments

Comments
 (0)