Skip to content

Commit 663e7d6

Browse files
os-zhuangclaude
andauthored
feat(spec): structured buttons + defaults config on FormViewSchema (#2998) (#3014)
Track A of #2998: spec home for the renderer-invented flat form keys ObjectUI's ObjectForm reads today (showSubmit/submitText/showCancel/ cancelText/showReset/initialValues — objectui#2545), which the strip-mode FormViewSchema container silently discards (ADR-0078's prohibited shape). - New optional `buttons` block: per-button { show, label } for submit/cancel/reset via exported FormButtonConfigSchema (.strict() per ADR-0089 D3a). - New optional `defaults` record: initial field values for create-mode forms (absorbs objectui initialValues). - Both marked [EXPERIMENTAL — NOT ENFORCED, #2998] per ADR-0078's escape hatch until the ObjectUI renderer wiring lands (objectui#2545). - Additive only: minor changeset; api-surface +2 exports; json-schema.manifest +ui/FormButtonConfig; ui/view.mdx regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Ue47V8zZ5QhcYMPiRQDA7
1 parent a16972b commit 663e7d6

6 files changed

Lines changed: 142 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: 16 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, FormField, FormSection, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, NavigationMode, ObjectListView, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
20-
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, FormField, FormSection, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, NavigationMode, ObjectListView, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
19+
import { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, NavigationMode, ObjectListView, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
20+
import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnSummary, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, GroupingField, KanbanConfig, ListChartConfig, ListColumn, ListView, NavigationConfig, NavigationMode, ObjectListView, ObjectUserFilters, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, 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
## FormField
@@ -175,6 +187,8 @@ Aggregation function for column footer summary
175187
| **defaultSort** | `Object[]` | optional | Default sort order for related list views within this form |
176188
| **sharing** | `Object` | optional | Public sharing configuration for this form |
177189
| **submitBehavior** | `Object \| Object \| Object \| Object` | optional | Post-submit behavior |
190+
| **buttons** | `Object` | optional | [EXPERIMENTAL — NOT ENFORCED, #2998] Form action-button visibility & labels. Renderer wiring pending in ObjectUI (objectui#2545). |
191+
| **defaults** | `Record<string, any>` | optional | [EXPERIMENTAL — NOT ENFORCED, #2998] Initial field values for create-mode forms (spec home for ObjectUI `initialValues`). Renderer wiring pending (objectui#2545). |
178192
| **aria** | `Object` | optional | ARIA accessibility attributes for the form view |
179193

180194

packages/spec/api-surface.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,6 +3232,8 @@
32323232
"FocusManagementSchema (const)",
32333233
"FocusTrapConfig (type)",
32343234
"FocusTrapConfigSchema (const)",
3235+
"FormButtonConfig (type)",
3236+
"FormButtonConfigSchema (const)",
32353237
"FormField (type)",
32363238
"FormFieldSchema (const)",
32373239
"FormSection (type)",

packages/spec/json-schema.manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1697,6 +1697,7 @@
16971697
"ui/EvictionPolicy",
16981698
"ui/FocusManagement",
16991699
"ui/FocusTrapConfig",
1700+
"ui/FormButtonConfig",
17001701
"ui/FormField",
17011702
"ui/FormSection",
17021703
"ui/FormView",

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)