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
15 changes: 15 additions & 0 deletions .changeset/studio-page-regions-properties-optional.md
Original file line number Diff line number Diff line change
@@ -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: {}`.
6 changes: 5 additions & 1 deletion packages/spec/src/ui/page.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
{
Expand Down
31 changes: 20 additions & 11 deletions packages/spec/src/ui/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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({});
});
});

Expand Down
21 changes: 19 additions & 2 deletions packages/spec/src/ui/page.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down