Skip to content

Commit 4a84c98

Browse files
os-zhuangclaude
andauthored
fix(spec): make page regions and component properties optional (unblock Studio non-list page creation) (#2254)
* fix(spec): make page `regions` and component `properties` optional Two required-but-unauthorable fields made it impossible to create record/home/app pages in the Studio metadata editor: - `PageSchema.regions` was required, but the New Page form has no region editor and list/interface pages don't use regions at all — forcing `regions: []` boilerplate on every page and a hard dead-end for the rest. - `PageComponentSchema.properties` was required, but the create-form seeds a record page's layout from the object's synthesized default page (buildDefaultPageSchema), whose nodes carry props at the top level — so every seeded block failed with "regions.N.components.M.properties: expected record". Many components (record:activity, element:divider, …) also legitimately carry no props. Both become `.optional().default(...)`; an empty full page falls back to the synthesized default layout, slotted pages compose via `slots`, and list pages ignore regions. Also drops the now-unneeded `required` flag on the page form's regions repeater and updates the three tests that asserted the old contract. Verified end-to-end: a record page now creates successfully through the Studio form. 802 spec UI tests + spec/platform-objects typechecks green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: changeset for page regions/properties optional Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 68390c2 commit 4a84c98

4 files changed

Lines changed: 59 additions & 14 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): make page `regions` and component `properties` optional
6+
7+
`PageSchema.regions` and `PageComponentSchema.properties` were required, which
8+
made it impossible to create record/home/app pages in the Studio editor: the
9+
New Page form has no region editor, and the create-form seeds a record page's
10+
default layout from `buildDefaultPageSchema`, whose nodes carry props at the top
11+
level — so every seeded block tripped `regions.N.components.M.properties:
12+
expected record`. Both are now `.optional().default(...)`; an empty full page
13+
falls back to the synthesized default layout, slotted pages compose via `slots`,
14+
list pages ignore regions, and prop-less components (record:activity,
15+
element:divider) no longer need `properties: {}`.

packages/spec/src/ui/page.form.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ export const pageForm = defineForm({
6262
// the region designer is irrelevant here, so hide it for list pages.
6363
visibleOn: "data.type != 'list'",
6464
fields: [
65-
{ field: 'regions', type: 'repeater', required: true, helpText: 'Layout regions (header, main, sidebar, footer) with components' },
65+
// Not required: an empty full page falls back to the synthesized default
66+
// layout, slotted pages compose via `slots`, and list pages ignore regions
67+
// entirely. Marking it required made the form unsatisfiable when no region
68+
// editor was rendered (the metadata form can't yet edit a region tree).
69+
{ field: 'regions', type: 'repeater', helpText: 'Layout regions (header, main, sidebar, footer) with components' },
6670
],
6771
},
6872
{

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,9 @@ describe('PageSchema', () => {
403403
regions: [],
404404
})).toThrow();
405405

406-
expect(() => PageSchema.parse({
407-
name: 'test_page',
408-
label: 'Test Page',
409-
})).toThrow();
406+
// `regions` is intentionally NOT among the required fields — name + label
407+
// alone is a valid page (regions defaults to [], type defaults to 'record').
408+
// See the dedicated "accept page without regions" test below.
410409
});
411410

412411
it('should reject invalid page type', () => {
@@ -1169,11 +1168,17 @@ describe('PageSchema - Negative Validation', () => {
11691168
})).toThrow();
11701169
});
11711170

1172-
it('should reject page without regions', () => {
1173-
expect(() => PageSchema.parse({
1171+
it('should accept page without regions (defaults to [])', () => {
1172+
// Optional with a [] default: list/interface pages render via
1173+
// interfaceConfig, slotted record pages via slots, and an empty full
1174+
// record/home/app page falls back to the synthesized default layout.
1175+
// Requiring regions forced `regions: []` boilerplate everywhere and made
1176+
// the Studio "New Page" form a dead-end for non-list pages.
1177+
const page = PageSchema.parse({
11741178
name: 'no_regions',
11751179
label: 'No Regions',
1176-
})).toThrow();
1180+
});
1181+
expect(page.regions).toEqual([]);
11771182
});
11781183

11791184
it('should reject page with camelCase name', () => {
@@ -1201,10 +1206,14 @@ describe('PageComponentSchema - Negative Validation', () => {
12011206
})).toThrow();
12021207
});
12031208

1204-
it('should reject component without properties', () => {
1205-
expect(() => PageComponentSchema.parse({
1206-
type: 'record:details',
1207-
})).toThrow();
1209+
it('should accept a component without properties (defaults to {})', () => {
1210+
// `properties` is optional with a {} default: many components carry no
1211+
// props (record:activity, element:divider) and the default-page
1212+
// synthesizer emits prop-at-top-level nodes. Requiring it broke seeding a
1213+
// record page's default layout in Studio (every block tripped
1214+
// "components.N.properties: expected record").
1215+
const comp = PageComponentSchema.parse({ type: 'record:details' });
1216+
expect(comp.properties).toEqual({});
12081217
});
12091218
});
12101219

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@ export const PageComponentSchema = lazySchema(() => z.object({
7474

7575
/** Configuration */
7676
label: I18nLabelSchema.optional(),
77-
properties: z.record(z.string(), z.unknown()).describe('Component props passed to the widget. See component.zod.ts for schemas.'),
77+
// Optional with an empty-object default. Many components carry no props
78+
// (record:activity, element:divider, …), and the platform's own default-page
79+
// synthesizer (buildDefaultPageSchema) emits nodes with props at the top
80+
// level rather than under `properties`. Requiring `properties` forced
81+
// `properties: {}` boilerplate and — worse — made every Studio attempt to
82+
// seed a record page from its object's synthesized default layout fail
83+
// validation ("regions.N.components.M.properties: expected record"), which
84+
// was the real reason record/home/app pages couldn't be created in Studio.
85+
properties: z.record(z.string(), z.unknown()).optional().default({}).describe('Component props passed to the widget. See component.zod.ts for schemas.'),
7886

7987
/**
8088
* Event Handlers
@@ -332,7 +340,16 @@ export const PageSchema = lazySchema(() => z.object({
332340
template: z.string().default('default').describe('Layout template name (e.g. "header-sidebar-main")'),
333341

334342
/** Regions & Content */
335-
regions: z.array(PageRegionSchema).describe('Defined regions with components'),
343+
// Optional with an empty-array default. Not every page authors regions:
344+
// • list/interface pages render via `interfaceConfig` (regions unused);
345+
// • `kind: 'slotted'` record pages render via `slots`;
346+
// • a `kind: 'full'` record/home/app page with no regions falls back to
347+
// the synthesized default layout (same surface a slotted page starts from).
348+
// Requiring it forced `regions: []` boilerplate on every list page and made
349+
// the Studio "New Page" form a dead-end for record/home/app pages (the form
350+
// has no region editor, so the required field could never be satisfied).
351+
regions: z.array(PageRegionSchema).optional().default([])
352+
.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.'),
336353

337354
/** Activation */
338355
isDefault: z.boolean().default(false),

0 commit comments

Comments
 (0)