diff --git a/.changeset/studio-page-regions-properties-optional.md b/.changeset/studio-page-regions-properties-optional.md new file mode 100644 index 0000000000..86eee6429b --- /dev/null +++ b/.changeset/studio-page-regions-properties-optional.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): make page `regions` and component `properties` optional + +`PageSchema.regions` and `PageComponentSchema.properties` were required, which +made it impossible to create record/home/app pages in the Studio editor: the +New Page form has no region editor, and the create-form seeds a record page's +default layout from `buildDefaultPageSchema`, whose nodes carry props at the top +level — so every seeded block tripped `regions.N.components.M.properties: +expected record`. Both are now `.optional().default(...)`; an empty full page +falls back to the synthesized default layout, slotted pages compose via `slots`, +list pages ignore regions, and prop-less components (record:activity, +element:divider) no longer need `properties: {}`. diff --git a/packages/spec/src/ui/page.form.ts b/packages/spec/src/ui/page.form.ts index 2d3b1de305..a103e6f908 100644 --- a/packages/spec/src/ui/page.form.ts +++ b/packages/spec/src/ui/page.form.ts @@ -62,7 +62,11 @@ export const pageForm = defineForm({ // the region designer is irrelevant here, so hide it for list pages. visibleOn: "data.type != 'list'", fields: [ - { field: 'regions', type: 'repeater', required: true, helpText: 'Layout regions (header, main, sidebar, footer) with components' }, + // Not required: an empty full page falls back to the synthesized default + // layout, slotted pages compose via `slots`, and list pages ignore regions + // entirely. Marking it required made the form unsatisfiable when no region + // editor was rendered (the metadata form can't yet edit a region tree). + { field: 'regions', type: 'repeater', helpText: 'Layout regions (header, main, sidebar, footer) with components' }, ], }, { diff --git a/packages/spec/src/ui/page.test.ts b/packages/spec/src/ui/page.test.ts index 89b00735d5..4f436bd8f5 100644 --- a/packages/spec/src/ui/page.test.ts +++ b/packages/spec/src/ui/page.test.ts @@ -403,10 +403,9 @@ describe('PageSchema', () => { regions: [], })).toThrow(); - expect(() => PageSchema.parse({ - name: 'test_page', - label: 'Test Page', - })).toThrow(); + // `regions` is intentionally NOT among the required fields — name + label + // alone is a valid page (regions defaults to [], type defaults to 'record'). + // See the dedicated "accept page without regions" test below. }); it('should reject invalid page type', () => { @@ -1169,11 +1168,17 @@ describe('PageSchema - Negative Validation', () => { })).toThrow(); }); - it('should reject page without regions', () => { - expect(() => PageSchema.parse({ + it('should accept page without regions (defaults to [])', () => { + // Optional with a [] default: list/interface pages render via + // interfaceConfig, slotted record pages via slots, and an empty full + // record/home/app page falls back to the synthesized default layout. + // Requiring regions forced `regions: []` boilerplate everywhere and made + // the Studio "New Page" form a dead-end for non-list pages. + const page = PageSchema.parse({ name: 'no_regions', label: 'No Regions', - })).toThrow(); + }); + expect(page.regions).toEqual([]); }); it('should reject page with camelCase name', () => { @@ -1201,10 +1206,14 @@ describe('PageComponentSchema - Negative Validation', () => { })).toThrow(); }); - it('should reject component without properties', () => { - expect(() => PageComponentSchema.parse({ - type: 'record:details', - })).toThrow(); + it('should accept a component without properties (defaults to {})', () => { + // `properties` is optional with a {} default: many components carry no + // props (record:activity, element:divider) and the default-page + // synthesizer emits prop-at-top-level nodes. Requiring it broke seeding a + // record page's default layout in Studio (every block tripped + // "components.N.properties: expected record"). + const comp = PageComponentSchema.parse({ type: 'record:details' }); + expect(comp.properties).toEqual({}); }); }); diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index df45f23f22..6b4ab6b689 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -74,7 +74,15 @@ export const PageComponentSchema = lazySchema(() => z.object({ /** Configuration */ label: I18nLabelSchema.optional(), - properties: z.record(z.string(), z.unknown()).describe('Component props passed to the widget. See component.zod.ts for schemas.'), + // Optional with an empty-object default. Many components carry no props + // (record:activity, element:divider, …), and the platform's own default-page + // synthesizer (buildDefaultPageSchema) emits nodes with props at the top + // level rather than under `properties`. Requiring `properties` forced + // `properties: {}` boilerplate and — worse — made every Studio attempt to + // seed a record page from its object's synthesized default layout fail + // validation ("regions.N.components.M.properties: expected record"), which + // was the real reason record/home/app pages couldn't be created in Studio. + properties: z.record(z.string(), z.unknown()).optional().default({}).describe('Component props passed to the widget. See component.zod.ts for schemas.'), /** * Event Handlers @@ -332,7 +340,16 @@ export const PageSchema = lazySchema(() => z.object({ template: z.string().default('default').describe('Layout template name (e.g. "header-sidebar-main")'), /** Regions & Content */ - regions: z.array(PageRegionSchema).describe('Defined regions with components'), + // Optional with an empty-array default. Not every page authors regions: + // • list/interface pages render via `interfaceConfig` (regions unused); + // • `kind: 'slotted'` record pages render via `slots`; + // • a `kind: 'full'` record/home/app page with no regions falls back to + // the synthesized default layout (same surface a slotted page starts from). + // Requiring it forced `regions: []` boilerplate on every list page and made + // the Studio "New Page" form a dead-end for record/home/app pages (the form + // has no region editor, so the required field could never be satisfied). + regions: z.array(PageRegionSchema).optional().default([]) + .describe('Layout regions (header, main, sidebar, footer) with their components. Optional — list pages use interfaceConfig, slotted pages use slots, and an empty full page falls back to the synthesized default layout.'), /** Activation */ isDefault: z.boolean().default(false),