Skip to content

Commit d222663

Browse files
os-zhuangclaude
andcommitted
fix(spec): page type is the page kind, not a visualization (ADR-0047)
Remove grid/kanban/calendar/gallery/timeline from PageTypeSchema — they are visualizations of a `list` interface page (interfaceConfig.appearance. allowedVisualizations), switched at runtime, never distinct page kinds. The renderer never branched on them as page types, so they only misled authors (selecting page type "kanban" did nothing). VisualizationTypeSchema is unchanged and remains the home for those values. The page authoring form (page.form.ts) now offers only the kinds with a dedicated renderer — list / record / home / app / utility — via explicit labelled options, so the dropdown stops presenting dead options. Roadmap kinds (dashboard/form/record_detail/record_review/overview/blank) remain valid in the schema but are hidden from the form until they render distinctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 80833a4 commit d222663

4 files changed

Lines changed: 72 additions & 28 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
ui(page): page `type` is the page kind, not a visualization
6+
7+
Removed `grid` / `kanban` / `calendar` / `gallery` / `timeline` from `PageTypeSchema`. They are visualizations of a `list` (interface) page — configured via `interfaceConfig.appearance.allowedVisualizations` and switched at runtime — never distinct page kinds. The runtime never branched on them as page types (it always read the visualization from `interfaceConfig`), so they only misled authors (e.g. selecting page type "kanban" did nothing). `VisualizationTypeSchema` is unchanged and remains the home for those values.
8+
9+
The roadmap interface kinds (`dashboard`, `form`, `record_detail`, `record_review`, `overview`, `blank`) stay valid in the schema but the page authoring form (`page.form.ts`) now offers only the kinds with a dedicated renderer — `list`, `record`, `home`, `app`, `utility` — with explicit labels, so the dropdown stops presenting dead options.

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,24 @@ export const pageForm = defineForm({
1919
{ field: 'name', required: true, colSpan: 1, helpText: 'Unique identifier (snake_case)' },
2020
{ field: 'label', required: true, colSpan: 1, helpText: 'Page title shown to users' },
2121
{ field: 'icon', colSpan: 1, helpText: 'Icon for navigation menu' },
22-
{ field: 'type', colSpan: 1, helpText: 'Page type — "list" for an interface/list page; also record, home, app, dashboard, etc.' },
22+
{
23+
field: 'type',
24+
colSpan: 1,
25+
// The page KIND, not a visualization. Kanban/Calendar/Gallery/Timeline
26+
// are visualizations OF a List page (set under Interface →
27+
// Appearance → Allowed visualizations), not page types — so they are
28+
// deliberately absent here. Only kinds with a dedicated renderer are
29+
// offered; roadmap kinds (dashboard/form/record_detail/…) are valid in
30+
// the schema but hidden until they render distinctly.
31+
options: [
32+
{ label: 'List / Interface page', value: 'list' },
33+
{ label: 'Record page', value: 'record' },
34+
{ label: 'Home page', value: 'home' },
35+
{ label: 'App page', value: 'app' },
36+
{ label: 'Utility panel', value: 'utility' },
37+
],
38+
helpText: 'Page kind. "List / Interface" binds a source view into a curated surface — how it looks (grid / kanban / calendar / …) is a visualization set under Interface, not a page type.',
39+
},
2340
{ field: 'template', colSpan: 2, helpText: 'Layout template (e.g., "header-sidebar-main")' },
2441
{ field: 'description', widget: 'textarea', colSpan: 2, helpText: 'Page description for navigation' },
2542
],

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

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,7 @@ describe('PageSchema', () => {
202202
it('should accept different page types', () => {
203203
const types: Array<Page['type']> = [
204204
'record', 'home', 'app', 'utility',
205-
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
206-
'timeline', 'form', 'record_detail', 'overview',
205+
'dashboard', 'list', 'form', 'record_detail', 'overview',
207206
];
208207

209208
types.forEach(type => {
@@ -487,15 +486,22 @@ describe('PageTypeSchema', () => {
487486

488487
it('should accept all interface page types', () => {
489488
const types = [
490-
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
491-
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
489+
'dashboard', 'list', 'form', 'record_detail', 'record_review', 'overview', 'blank',
492490
];
493491

494492
types.forEach(type => {
495493
expect(() => PageTypeSchema.parse(type)).not.toThrow();
496494
});
497495
});
498496

497+
it('should reject visualizations as page types (they belong in interfaceConfig.appearance.allowedVisualizations)', () => {
498+
// grid/kanban/calendar/gallery/timeline are visualizations of a `list`
499+
// page, not page kinds — they were removed from PageTypeSchema.
500+
['grid', 'kanban', 'calendar', 'gallery', 'timeline'].forEach(type => {
501+
expect(() => PageTypeSchema.parse(type)).toThrow();
502+
});
503+
});
504+
499505
it('should reject invalid page type', () => {
500506
expect(() => PageTypeSchema.parse('invalid')).toThrow();
501507
});
@@ -653,8 +659,7 @@ describe('PageSchema with page types', () => {
653659

654660
it('should accept all interface page types', () => {
655661
const basicTypes = [
656-
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
657-
'timeline', 'form', 'record_detail', 'overview',
662+
'dashboard', 'list', 'form', 'record_detail', 'overview',
658663
];
659664

660665
basicTypes.forEach(type => {
@@ -984,16 +989,18 @@ describe('Page end-to-end', () => {
984989
expect(page.recordReview?.actions).toHaveLength(3);
985990
});
986991

987-
it('should accept a grid page bound to an object', () => {
992+
it('should accept a list page bound to an object', () => {
993+
// A grid is a *visualization* of a list page, not a page type — bind the
994+
// object on a `list` page and pick the grid visualization via interfaceConfig.
988995
const page = PageSchema.parse({
989996
name: 'page_grid',
990997
label: 'All Orders',
991-
type: 'grid',
998+
type: 'list',
992999
object: 'order',
9931000
regions: [],
9941001
});
9951002

996-
expect(page.type).toBe('grid');
1003+
expect(page.type).toBe('list');
9971004
expect(page.object).toBe('order');
9981005
});
9991006
});
@@ -1269,9 +1276,9 @@ describe('PageSchema - conditional validation', () => {
12691276

12701277
it('should not require recordReview for non-record_review types', () => {
12711278
expect(() => PageSchema.parse({
1272-
name: 'grid_page',
1273-
label: 'Grid',
1274-
type: 'grid',
1279+
name: 'list_page',
1280+
label: 'List',
1281+
type: 'list',
12751282
regions: [],
12761283
})).not.toThrow();
12771284
});

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ export const BlankPageLayoutSchema = lazySchema(() => z.object({
142142
* Unified page type enum covering both platform pages (Salesforce FlexiPage style)
143143
* and Airtable-inspired interface page types.
144144
*
145+
* **Page type is the page KIND, NOT a visualization.** How an interface (`list`)
146+
* page displays its records — grid / kanban / calendar / gallery / timeline — is a
147+
* *visualization*, configured via `interfaceConfig.appearance.allowedVisualizations`
148+
* and switched at runtime. Those are deliberately NOT page types: a kanban is a `list`
149+
* page shown as a board, not a distinct page kind. (Historically grid/kanban/calendar/
150+
* gallery/timeline appeared here; the runtime never branched on them — it always read
151+
* the visualization from `interfaceConfig` — so they were removed to stop misleading authors.)
152+
*
145153
* **Disambiguation of similar types:**
146154
* - `record` vs `record_detail`: `record` is a component-based layout page (FlexiPage style with regions),
147155
* `record_detail` is a field-display page showing all fields of a single record (Airtable style).
@@ -152,27 +160,30 @@ export const BlankPageLayoutSchema = lazySchema(() => z.object({
152160
* - `app` vs `utility` vs `blank`: `app` is an app-level page with navigation context,
153161
* `utility` is a floating utility panel (e.g. notes, phone), `blank` is a free-form canvas
154162
* for custom composition. They serve distinct layout purposes.
163+
*
164+
* **Implementation status:** only `record`, `home`, `app`, `utility`, and `list` have
165+
* dedicated renderers today; the remaining interface types (`dashboard`, `form`,
166+
* `record_detail`, `record_review`, `overview`, `blank`) are declared for roadmap parity
167+
* but currently fall back to the list renderer. The authoring form only offers the
168+
* implemented set (see `page.form.ts`) to avoid presenting dead options.
155169
*/
156170
export const PageTypeSchema = lazySchema(() => z.enum([
157-
// Platform page types (Salesforce FlexiPage style)
171+
// Platform page types (Salesforce FlexiPage style) — region/component composition
158172
'record', // Component-based record layout page with regions
159173
'home', // Platform-level home/landing page
160174
'app', // App-level page with navigation context
161175
'utility', // Floating utility panel (e.g. notes, phone dialer)
162-
// Interface page types (Airtable Interface parity)
163-
'dashboard', // KPI summary with charts/metrics
164-
'grid', // Spreadsheet-like data table
165-
'list', // Record list with quick actions
166-
'gallery', // Card-based visual browsing
167-
'kanban', // Status-based board
168-
'calendar', // Date-based scheduling
169-
'timeline', // Gantt-like project timeline
170-
'form', // Data entry form
171-
'record_detail', // Auto-generated single record field display
172-
'record_review', // Sequential record review/approval
173-
'overview', // Interface-level navigation/landing hub
174-
'blank', // Free-form canvas for custom composition
175-
]).describe('Page type — platform or interface page types'));
176+
// Interface page types (Airtable Interface parity) — data-driven surfaces.
177+
// NOTE: grid/kanban/calendar/gallery/timeline are NOT here — they are visualizations
178+
// of a `list` page (interfaceConfig.appearance.allowedVisualizations), not page types.
179+
'list', // Record list/grid surface with switchable visualizations + quick actions
180+
'dashboard', // [roadmap] KPI summary with charts/metrics
181+
'form', // [roadmap] Data entry form
182+
'record_detail', // [roadmap] Auto-generated single record field display
183+
'record_review', // [roadmap] Sequential record review/approval
184+
'overview', // [roadmap] Interface-level navigation/landing hub
185+
'blank', // [roadmap] Free-form canvas for custom composition
186+
]).describe('Page type — the page KIND (platform or interface). Visualizations of a list page live in interfaceConfig, not here.'));
176187

177188
/**
178189
* Record Review Config Schema

0 commit comments

Comments
 (0)