From ebae0f5e9fe10228586e4b41d6f7a12b11c5eecd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 20:30:19 +0000 Subject: [PATCH 1/2] fix(plugin-detail,types): seed new record pages with the `default` template and type `PageNodeSchema.template` by reference to the spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of objectstack-ai/objectstack#23610. `@objectstack/spec` closes `PageSchema.template` to four layout names and retires `full-width` by name (its replacement is `default`); the save path refuses a page carrying it. Studio's new-record-page seed took `template: 'full-width'` from `buildDefaultPageSchema`, so its first save would be refused. The seed now writes `default`, which the renderer's `TEMPLATE_REGISTRY` maps to the same `FullWidthTemplate` — layout unchanged. `PageNodeSchema` re-declared `template` as `string` over the inherited spec member, which TypeScript refuses (TS2430) once the spec closes the set; it is now `SpecPage['template']`, identical under today's spec. The renderer keeps `full-width` so already stored pages keep rendering. Claude-Session: https://claude.ai/code/session_01TCM7ejacXFijr6twN5Ezbj Co-authored-by: Claude --- .changeset/23610-record-page-seed-default.md | 13 +++++++++++++ .../utils/__tests__/pageSchemaIntrospect.test.ts | 4 ++-- .../synth/__tests__/buildDefaultPageSchema.test.ts | 5 +++-- .../src/synth/buildDefaultPageSchema.ts | 8 ++++++-- packages/types/src/layout.ts | 7 ++++++- 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 .changeset/23610-record-page-seed-default.md diff --git a/.changeset/23610-record-page-seed-default.md b/.changeset/23610-record-page-seed-default.md new file mode 100644 index 0000000000..64a9a5e386 --- /dev/null +++ b/.changeset/23610-record-page-seed-default.md @@ -0,0 +1,13 @@ +--- +'@object-ui/plugin-detail': patch +'@object-ui/types': patch +--- + +A new record page seeded in Studio is saved with the `default` layout template instead of `full-width`, and the page node's `template` member is typed by reference to the spec's (part of objectstack#23610). + +`@objectstack/spec` closes `PageSchema.template` to four layout names — `default`, `header-sidebar-main`, `three-column`, `dashboard` — and retires `full-width` by name, naming `default` as its replacement; the save path refuses a page that still carries it. + +- **`buildDefaultPageSchema` seeds `template: 'default'`.** Studio's "new record page" seed takes its template from this synthesis, so with the old value the first save of a new record page was refused. The renderer maps `default` and `full-width` to the same `FullWidthTemplate`, so a seeded page lays out exactly as before. +- **`PageNodeSchema.template` is `SpecPage['template']`.** The member was re-declared as `string` over the spec's inherited one; once the spec closes the set, that widening is a TypeScript error (TS2430) on the interface. Against today's spec, whose member is still `string`, the type is unchanged. + +The renderer still recognises `full-width`, so pages already stored with it keep rendering. diff --git a/packages/app-shell/src/utils/__tests__/pageSchemaIntrospect.test.ts b/packages/app-shell/src/utils/__tests__/pageSchemaIntrospect.test.ts index 79d20e58b9..db0ad764b3 100644 --- a/packages/app-shell/src/utils/__tests__/pageSchemaIntrospect.test.ts +++ b/packages/app-shell/src/utils/__tests__/pageSchemaIntrospect.test.ts @@ -82,7 +82,7 @@ describe('stripDiscussionNodes', () => { type: 'record', pageType: 'record', object: 'account', - template: 'full-width', + template: 'default', regions: [ { name: 'main', @@ -99,7 +99,7 @@ describe('stripDiscussionNodes', () => { expect(types(stripped)).toEqual(['record', 'page:header', 'page:tabs', 'page:tab']); // Everything else about the page survives, including the scalars that are // not nodes at all. - expect(stripped.template).toBe('full-width'); + expect(stripped.template).toBe('default'); expect(stripped.regions[0].name).toBe('main'); expect(stripped.regions[0].width).toBe('full'); }); diff --git a/packages/plugin-detail/src/synth/__tests__/buildDefaultPageSchema.test.ts b/packages/plugin-detail/src/synth/__tests__/buildDefaultPageSchema.test.ts index 771e3d3561..dc326dd77c 100644 --- a/packages/plugin-detail/src/synth/__tests__/buildDefaultPageSchema.test.ts +++ b/packages/plugin-detail/src/synth/__tests__/buildDefaultPageSchema.test.ts @@ -201,12 +201,13 @@ describe('deriveHighlightFields', () => { }); describe('buildDefaultPageSchema', () => { - it('emits a record Page with full-width template + main region', () => { + it('emits a record Page with the default template + main region', () => { const page = buildDefaultPageSchema(leadDef); expect(page.type).toBe('record'); expect(page.pageType).toBe('record'); expect(page.object).toBe('lead'); - expect(page.template).toBe('full-width'); + // The spec retires `full-width` by name; a seed carrying it is refused at save. + expect(page.template).toBe('default'); expect(page.regions).toHaveLength(1); expect(page.regions[0].name).toBe('main'); }); diff --git a/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts b/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts index bc4939b76c..727bb7a7c8 100644 --- a/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts +++ b/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts @@ -989,7 +989,7 @@ export function buildDefaultAttachments(): any { * page. * * Shape: - * { type:'record', template:'full-width', regions:[ { name:'main', + * { type:'record', template:'default', regions:[ { name:'main', * components: [page:header, record:highlights?, record:path?, * page:tabs, record:discussion?] } ] } * For `enable.files: true` objects the tabs node carries a peer @@ -1172,7 +1172,11 @@ export function buildDefaultPageSchema( type: 'record', pageType: 'record', object: def?.name, - template: 'full-width', + // `default`, not `full-width`: the spec's `PageSchema.template` names four + // layouts and retires `full-width` by name (its replacement is `default`), + // so a Studio seed carrying it is refused at save. The renderer maps both + // names to the same `FullWidthTemplate`, so the seed lays out unchanged. + template: 'default', regions, }; } diff --git a/packages/types/src/layout.ts b/packages/types/src/layout.ts index 89b76bcc5e..a6831f0d17 100644 --- a/packages/types/src/layout.ts +++ b/packages/types/src/layout.ts @@ -1643,8 +1643,13 @@ export interface PageNodeSchema extends BaseSchema, Omit Date: Thu, 15 Oct 2026 21:09:17 +0000 Subject: [PATCH 2/2] fix(types): the zod mirror's page `template` takes the spec's member by reference Part of objectstack-ai/objectstack#23610. The zod face's `PageNodeSchema` re-declared `template: z.string()`. Against a spec that closes `PageSchema.template` to four layout names, that member is wider than the TS face (now `SpecPage['template']`), so the `zod-mirror-parity` type assertions red under `tsconfig.test.json`, and `objectui validate` would keep accepting the retired `full-width`. It now takes the spec's own member through `stripImportedDefaults`, the same way `kind` does; against today's spec the accept set and the absent-key behaviour are unchanged. Claude-Session: https://claude.ai/code/session_01TCM7ejacXFijr6twN5Ezbj Co-authored-by: Claude --- .changeset/23610-record-page-seed-default.md | 1 + packages/types/src/zod/layout.zod.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/23610-record-page-seed-default.md b/.changeset/23610-record-page-seed-default.md index 64a9a5e386..5c3efd4878 100644 --- a/.changeset/23610-record-page-seed-default.md +++ b/.changeset/23610-record-page-seed-default.md @@ -9,5 +9,6 @@ A new record page seeded in Studio is saved with the `default` layout template i - **`buildDefaultPageSchema` seeds `template: 'default'`.** Studio's "new record page" seed takes its template from this synthesis, so with the old value the first save of a new record page was refused. The renderer maps `default` and `full-width` to the same `FullWidthTemplate`, so a seeded page lays out exactly as before. - **`PageNodeSchema.template` is `SpecPage['template']`.** The member was re-declared as `string` over the spec's inherited one; once the spec closes the set, that widening is a TypeScript error (TS2430) on the interface. Against today's spec, whose member is still `string`, the type is unchanged. +- **The zod mirror's `template` is the spec's member too.** `PageNodeSchema` in the zod face declared `template: z.string()`, which accepts every name; once the spec closes the set it measures wider than the TS face, so `full-width` would still pass `objectui validate`. It now takes the spec's own member (defaults stripped, as for every imported member), and against today's spec it accepts exactly what it accepted before. The renderer still recognises `full-width`, so pages already stored with it keep rendering. diff --git a/packages/types/src/zod/layout.zod.ts b/packages/types/src/zod/layout.zod.ts index 4abfe12444..bd56d5345c 100644 --- a/packages/types/src/zod/layout.zod.ts +++ b/packages/types/src/zod/layout.zod.ts @@ -1305,7 +1305,12 @@ export const PageNodeSchema = BaseSchema.extend(SpecPageFields.shape).extend({ .optional() .describe('Page override mode (full | slotted | html | react)'), object: z.string().optional().describe('Bound object name (for record pages)'), - template: z.string().optional().describe('Layout template name'), + // objectstack#23610: the spec's own member, by reference, mirroring the TS + // face's `PageNodeSchema.template` (`SpecPage['template']`). A `z.string()` + // here accepts every name, so once the spec closes the set of layout names it + // measures WIDER than the TS face and accepts the `full-width` the spec + // retires. Against a spec whose member is still a string, it is unchanged. + template: stripImportedDefaults(SpecPageSchema).shape.template.describe('Layout template name'), variables: z.array(PageVariableSchema).optional().describe('Local page state variables'), regions: z.array(PageNodeRegionSchema).optional().describe('Page layout regions'), body: aliasKeyRefusal(