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
21 changes: 21 additions & 0 deletions .changeset/formview-buttons-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/spec': minor
---

feat(spec): structured `buttons` + `defaults` config on `FormViewSchema` (#2998)

`FormViewSchema` gains two optional top-level keys — the spec home for the flat
renderer-invented form config ObjectUI's `ObjectForm` reads today
(`showSubmit`/`submitText`/`showCancel`/`cancelText`/`showReset`/`initialValues`,
objectui#2545), which the strip-mode container silently discards:

- **`buttons`** — structured action-button config: per-button `{ show, label }`
for `submit` / `cancel` / `reset` (new exported leaf `FormButtonConfigSchema`,
`.strict()` per ADR-0089 D3a so typo'd keys error loudly).
- **`defaults`** — initial field values for create-mode forms, keyed by field
machine name (absorbs ObjectUI's `initialValues`).

Both are marked `[EXPERIMENTAL — NOT ENFORCED]` per ADR-0078's escape hatch
until the ObjectUI renderer reads them (tracked in objectui#2545); authoring
them today is declared, not yet honored. Purely additive — no existing key
changes shape, no tombstone needed.
18 changes: 16 additions & 2 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Migrated to shared/http.zod.ts. Re-exported here for backward compatibility.
## TypeScript Usage

```typescript
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';
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';
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';
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';

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


---

## FormButtonConfig

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **show** | `boolean` | optional | Whether the button is rendered (renderer default applies when omitted) |
| **label** | `string` | optional | Button label (i18n-capable; renderer default when omitted) |


---

## FormField
Expand Down Expand Up @@ -175,6 +187,8 @@ Aggregation function for column footer summary
| **defaultSort** | `Object[]` | optional | Default sort order for related list views within this form |
| **sharing** | `Object` | optional | Public sharing configuration for this form |
| **submitBehavior** | `Object \| Object \| Object \| Object` | optional | Post-submit behavior |
| **buttons** | `Object` | optional | [EXPERIMENTAL — NOT ENFORCED, #2998] Form action-button visibility & labels. Renderer wiring pending in ObjectUI (objectui#2545). |
| **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). |
| **aria** | `Object` | optional | ARIA accessibility attributes for the form view |


Expand Down
2 changes: 2 additions & 0 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3232,6 +3232,8 @@
"FocusManagementSchema (const)",
"FocusTrapConfig (type)",
"FocusTrapConfigSchema (const)",
"FormButtonConfig (type)",
"FormButtonConfigSchema (const)",
"FormField (type)",
"FormFieldSchema (const)",
"FormSection (type)",
Expand Down
1 change: 1 addition & 0 deletions packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1697,6 +1697,7 @@
"ui/EvictionPolicy",
"ui/FocusManagement",
"ui/FocusTrapConfig",
"ui/FormButtonConfig",
"ui/FormField",
"ui/FormSection",
"ui/FormView",
Expand Down
64 changes: 64 additions & 0 deletions packages/spec/src/ui/view.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import {
ViewSchema,
ListViewSchema,
Expand Down Expand Up @@ -1931,6 +1932,69 @@ describe('FormViewSchema - defaultSort', () => {
});
});

// ============================================================================
// FormView structured buttons + defaults (#2998, EXPERIMENTAL until objectui#2545)
// ============================================================================

describe('FormViewSchema - buttons & defaults', () => {
it('should accept structured button config with visibility and labels', () => {
const result = FormViewSchema.parse({
type: 'simple',
sections: [{ fields: ['name'] }],
buttons: {
submit: { show: true, label: 'Save' },
cancel: { show: true, label: 'Discard' },
reset: { show: false },
},
});
expect(result.buttons?.submit).toEqual({ show: true, label: 'Save' });
expect(result.buttons?.reset).toEqual({ show: false });
});

it('should accept a partial buttons block (each button optional)', () => {
const result = FormViewSchema.parse({
type: 'simple',
sections: [{ fields: ['name'] }],
buttons: { submit: { label: 'Create' } },
});
expect(result.buttons?.submit?.label).toBe('Create');
expect(result.buttons?.cancel).toBeUndefined();
});

it('should reject unknown keys inside buttons (strict leaf, ADR-0089 D3a)', () => {
expect(() => FormViewSchema.parse({
type: 'simple',
buttons: { submit: { text: 'Save' } }, // legacy `submitText`-style key
})).toThrow();
expect(() => FormViewSchema.parse({
type: 'simple',
buttons: { showSubmit: true }, // flat renderer-invented key
})).toThrow();
});

it('should accept defaults as a field-name → value record', () => {
const result = FormViewSchema.parse({
type: 'simple',
sections: [{ fields: ['name', 'status', 'priority'] }],
defaults: { status: 'open', priority: 3, tags: ['a', 'b'] },
});
expect(result.defaults).toEqual({ status: 'open', priority: 3, tags: ['a', 'b'] });
});

it('should accept form view without buttons/defaults (optional)', () => {
const result = FormViewSchema.parse({ type: 'simple' });
expect(result.buttons).toBeUndefined();
expect(result.defaults).toBeUndefined();
});

it('marks both keys experimental until the renderer wiring lands (ADR-0078)', () => {
const shape = (FormViewSchema as unknown as z.ZodObject<z.ZodRawShape>).shape;
for (const key of ['buttons', 'defaults'] as const) {
expect(shape[key].description, `${key} .describe()`).toMatch(/EXPERIMENTAL — NOT ENFORCED/);
}
});
});

describe('defineView', () => {
it('should return a parsed view with list config', () => {
const result = defineView({
Expand Down
39 changes: 38 additions & 1 deletion packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,10 +809,21 @@ export const FormSectionSchema = lazySchema(() => z.object({
])),
}, { error: strictVisibilityError }).strict().transform(normalizeVisibleWhen));

/**
* A single form action button (submit / cancel / reset): visibility + label.
* Leaf of the [EXPERIMENTAL] {@link FormViewSchema} `buttons` block (#2998).
* `.strict()` per ADR-0089 D3a so a typo'd key errors instead of vanishing.
*/
export const FormButtonConfigSchema = lazySchema(() => z.object({
show: z.boolean().optional().describe('Whether the button is rendered (renderer default applies when omitted)'),
label: I18nLabelSchema.optional().describe('Button label (i18n-capable; renderer default when omitted)'),
}).strict());
export type FormButtonConfig = z.infer<typeof FormButtonConfigSchema>;

/**
* Form View Schema
* Defines the layout for creating or editing a single record.
*
*
* @example Simple Sectioned Form
* {
* type: "simple",
Expand Down Expand Up @@ -924,6 +935,32 @@ export const FormViewSchema = lazySchema(() => z.object({
z.object({ kind: z.literal('next-record') }),
]).optional().describe('Post-submit behavior'),

/**
* ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Structured action-button
* config: per-button visibility + label for `submit` / `cancel` / `reset`.
* This is the spec home for the flat renderer-invented keys ObjectUI's
* `ObjectForm` reads today (`showSubmit`/`submitText`/`showCancel`/`cancelText`/
* `showReset`, objectui#2545) — those never existed here and are silently
* stripped by this strip-mode container. Experimental until the ObjectUI
* renderer reads `buttons.*` (the ADR-0078 escape hatch: the contract ships
* ahead of its consumer, marked so authoring it is not a false promise).
*/
buttons: z.object({
submit: FormButtonConfigSchema.optional().describe('Submit button'),
cancel: FormButtonConfigSchema.optional().describe('Cancel button'),
reset: FormButtonConfigSchema.optional().describe('Reset button'),
}).strict().optional()
.describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Form action-button visibility & labels. Renderer wiring pending in ObjectUI (objectui#2545).'),

/**
* ⚠️ EXPERIMENTAL — NOT ENFORCED (#2998, ADR-0078). Initial field values for
* create-mode forms, keyed by field machine name. Spec home for ObjectUI's
* renderer-invented `initialValues` (objectui#2545). Experimental until the
* ObjectUI renderer reads it.
*/
defaults: z.record(z.string(), z.unknown()).optional()
.describe('[EXPERIMENTAL — NOT ENFORCED, #2998] Initial field values for create-mode forms (spec home for ObjectUI `initialValues`). Renderer wiring pending (objectui#2545).'),

/** ARIA accessibility attributes */
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the form view'),
}));
Expand Down