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
35 changes: 35 additions & 0 deletions docs/adr/0034-unified-runtime-metadata-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,38 @@ Runtime panels gain a small **draft/publish chrome** (Save draft · Publish · "
| ObjectView | `dataSource.create/update('sys_view', toSysViewPayload(...))` (`handleViewConfigSave` / `handleViewCreate`; `persistViewPatch` for incremental toolbar state) | `packages/app-shell/src/views/ObjectView.tsx` |
| ReportView | `adapter.update('sys_report', name, schema)` (`saveSchema`) | `packages/app-shell/src/views/ReportView.tsx` |
| DashboardView | `adapter.updateDashboard(name, schema)` / `adapter.update('sys_dashboard', …)` (`saveSchema`) | `packages/app-shell/src/views/DashboardView.tsx` |

---

## Amendment (2026-06-07): record `page` joins the model (#1541)

Record pages now author through the same per-item `/meta` draft → publish →
version model — via **Studio**, not a bespoke runtime editor. The end-user
runtime "design this page" entry is intentionally deferred; the Studio path is
the supported authoring surface.

What shipped:

- **Seam** — `RuntimeArtifactType` gained `'page'` (#1571), plus pure helpers
`recordPageName(object, existing?)` / `recordPageEnvelope(object, schema, name?)`
in `runtime-metadata-persistence.ts` (forward-looking; for a future runtime
entry — the Studio path uses `ResourceEditPage`'s own draft/publish chrome).
- **Create** — the Studio "New page" form was identity-only (label/name/icon/
description), so a record page couldn't be created or bound to an object. The
`page` resource anchor now exposes **Object / Page type (default `record`) /
Kind**, mirroring `view` (#1572). Root cause was objectui-side config, not the
protocol — the page *edit* form and the protocol schema already carried the
full field set.
- **Seed** — a generic async `createSeed` hook on `MetadataResourceConfig`; the
`page` resource seeds a new record page's `regions` from
`buildDefaultPageSchema(objectDef)` so authoring starts from the auto-generated
default detail layout, not a blank canvas (#1574).
- **Edit + publish** — the existing `PagePreview` WYSIWYG canvas + draft/publish/
version chrome (`MetadataResourceEditPage`) handle composition and release.
- **Render** — `usePageAssignment` already resolves a published record page over
the synthesized default (`RecordDetailView.effectivePage`), so a published page
renders on that object's records.

Net: create (bound + seeded) → design in PagePreview → publish → renders on
records — entirely through Studio, no `sys_page` table, no runtime end-user
editor.
47 changes: 47 additions & 0 deletions e2e/live/studio-record-page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';

/**
* Live e2e for #1541 (record-page authoring via Studio, ADR-0034). Creating a
* `pageType: 'record'` page bound to an object in Studio's "New page" form
* persists it as a draft AND seeds its `regions` from the object's synthesized
* default detail page (via the `page` resource's `createSeed` hook) — so the
* author starts from the auto-generated layout, not a blank canvas.
*
* (The page is then composed in the PagePreview canvas and published through
* the existing ResourceEditPage draft/publish chrome; render-on-records is
* handled by usePageAssignment over the synthesized default.)
*/
test('Studio: a new record page is created bound to its object and seeded from the default layout', async ({ page }) => {
const puts: any[] = [];
page.on('request', (r) => {
if (r.method() === 'PUT' && /\/meta\/page\//.test(r.url())) {
try { puts.push(r.postDataJSON()); } catch { /* ignore */ }
}
});

await page.goto('/apps/showcase_app/metadata/page/new');
// The create form is generated from the page resource's createSchema.
await page.getByLabel(/^Label/i).first().waitFor({ state: 'visible', timeout: 20_000 });

const uniq = Date.now().toString().slice(-6);
// Label slugifies into Name; type defaults to 'record'; bind the object.
await page.getByLabel(/^Label/i).first().fill(`Invoice Page ${uniq}`);
await page.getByLabel(/^Object/i).first().fill('showcase_invoice');
await page.waitForTimeout(500);

await Promise.all([
page.waitForRequest((r) => r.method() === 'PUT' && /\/meta\/page\//.test(r.url()), { timeout: 15_000 }).catch(() => null),
page.locator('button[title^="Save"]').first().click(),
]);
await page.waitForTimeout(800);

expect(puts.length).toBeGreaterThan(0);
const item = (puts[puts.length - 1]?.item ?? puts[puts.length - 1]) as any;
expect(item.type).toBe('record');
expect(item.object).toBe('showcase_invoice');
// Seeded from buildDefaultPageSchema — NOT a blank `regions: []`.
expect(Array.isArray(item.regions)).toBe(true);
const blocks: string[] = item.regions.flatMap((r: any) => (r?.components || []).map((c: any) => c?.type));
expect(blocks.length).toBeGreaterThan(0);
expect(blocks).toContain('record:highlights');
});
Loading