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
7 changes: 7 additions & 0 deletions .changeset/b1-page-seam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@object-ui/app-shell": minor
---

Runtime persistence seam: add `'page'` artifact type (record-page draft/publish).

`RuntimeArtifactType` now includes `'page'`, so a record `PageSchema` stages and publishes through the same ADR-0034 `/meta` draft model as views/reports/dashboards (#1541). New pure helpers `recordPageName(objectName, existing?)` (prefers an assigned page name, else mints `<object>_record`) and `recordPageEnvelope(objectName, schema, name?)` (sets the `name`/`object`/`pageType:'record'`/`kind:'full'` identity fields the resolver matches on) — foundation for the record-page edit loop.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
readRuntimeDraft,
discardRuntimeDraft,
unwrapDraftBody,
recordPageName,
recordPageEnvelope,
} from './runtime-metadata-persistence';

/**
Expand Down Expand Up @@ -44,18 +46,58 @@ describe('runtime-metadata-persistence seam (ADR-0034)', () => {
});
});

it('persistRuntimeMetadata works for view / dashboard too', async () => {
it('persistRuntimeMetadata works for view / dashboard / page too', async () => {
const metadataClient = makeMetadataClient();

await persistRuntimeMetadata('view', 'my_view', { columns: ['name'] }, { metadataClient });
await persistRuntimeMetadata('dashboard', 'my_dash', { widgets: [] }, { metadataClient });
await persistRuntimeMetadata('page', 'invoice_record', { regions: [] }, { metadataClient });

expect(metadataClient.save).toHaveBeenCalledWith('view', 'my_view', { columns: ['name'] }, {
mode: 'draft',
});
expect(metadataClient.save).toHaveBeenCalledWith('dashboard', 'my_dash', { widgets: [] }, {
mode: 'draft',
});
expect(metadataClient.save).toHaveBeenCalledWith('page', 'invoice_record', { regions: [] }, {
mode: 'draft',
});
});

describe('record page helpers (#1541)', () => {
it('recordPageName prefers an existing name, else mints <object>_record', () => {
expect(recordPageName('invoice')).toBe('invoice_record');
expect(recordPageName('invoice', 'custom_invoice_page')).toBe('custom_invoice_page');
expect(recordPageName('invoice', null)).toBe('invoice_record');
});

it('recordPageEnvelope sets the record-page identity fields the resolver matches on', () => {
const env = recordPageEnvelope('invoice', { type: 'page', title: 'Invoice', regions: [{ name: 'main' }] });
expect(env).toMatchObject({
type: 'page',
name: 'invoice_record',
object: 'invoice',
pageType: 'record',
kind: 'full',
title: 'Invoice',
regions: [{ name: 'main' }],
});
});

it('recordPageEnvelope keeps an explicit/existing page name', () => {
expect(recordPageEnvelope('invoice', { name: 'inv_v2' }).name).toBe('inv_v2');
expect(recordPageEnvelope('invoice', {}, 'inv_v3').name).toBe('inv_v3');
});

it('a page draft round-trips through the seam', async () => {
const metadataClient = makeMetadataClient();
metadataClient.get.mockResolvedValue({ type: 'page', name: 'invoice_record', item: { regions: [{ name: 'x' }] } });
const draft = await readRuntimeDraft('page', 'invoice_record', { metadataClient });
expect(metadataClient.get).toHaveBeenCalledWith('page', 'invoice_record', { state: 'draft' });
expect(draft).toEqual({ regions: [{ name: 'x' }] });
await publishRuntimeMetadata('page', 'invoice_record', { metadataClient });
expect(metadataClient.publish).toHaveBeenCalledWith('page', 'invoice_record');
});
});

it('createRuntimeMetadata → save draft and returns the name', async () => {
Expand Down
38 changes: 36 additions & 2 deletions packages/app-shell/src/views/runtime-metadata-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,42 @@
* a legacy write path here.
*/

/** The three runtime-editable artifact types ADR-0034 unifies. */
export type RuntimeArtifactType = 'view' | 'report' | 'dashboard';
/** The runtime-editable artifact types ADR-0034 unifies. `page` (a record
* `PageSchema`) joins the original three (#1541): a record page is edited in
* the browser and staged/published through the same `/meta` draft model. */
export type RuntimeArtifactType = 'view' | 'report' | 'dashboard' | 'page';

/**
* The metadata `name` (the `:name` in `/meta/page/:name`) for an object's
* record page. Prefer an already-assigned record page's name; otherwise mint
* the convention `<object>_record`. Editing the synthesized default for the
* first time materialises a real named page under this key so it has something
* to draft / publish / version against.
*/
export function recordPageName(objectName: string, existingName?: string | null): string {
return existingName || `${objectName}_record`;
}

/**
* Wrap an edited `PageSchema` as a persistable **record page** body: ensures the
* `name` / `object` / `pageType: 'record'` / `kind: 'full'` identity fields the
* resolver (`usePageAssignment`) matches on, so a published page overrides the
* synthesized default for that object on the next render.
*/
export function recordPageEnvelope(
objectName: string,
schema: Record<string, any>,
name?: string,
): Record<string, any> {
return {
...schema,
type: 'page',
name: recordPageName(objectName, name ?? (schema?.name as string | undefined)),
object: objectName,
pageType: 'record',
kind: 'full',
};
}

/** Everything the seam needs to persist any of the three artifact types. */
export interface RuntimePersistCtx {
Expand Down
Loading