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
9 changes: 9 additions & 0 deletions .changeset/page-type-honest-enum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@objectstack/spec": patch
---

ui(page): page `type` is the page kind, not a visualization

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.

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.
19 changes: 18 additions & 1 deletion packages/spec/src/ui/page.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,24 @@ export const pageForm = defineForm({
{ field: 'name', required: true, colSpan: 1, helpText: 'Unique identifier (snake_case)' },
{ field: 'label', required: true, colSpan: 1, helpText: 'Page title shown to users' },
{ field: 'icon', colSpan: 1, helpText: 'Icon for navigation menu' },
{ field: 'type', colSpan: 1, helpText: 'Page type — "list" for an interface/list page; also record, home, app, dashboard, etc.' },
{
field: 'type',
colSpan: 1,
// The page KIND, not a visualization. Kanban/Calendar/Gallery/Timeline
// are visualizations OF a List page (set under Interface →
// Appearance → Allowed visualizations), not page types — so they are
// deliberately absent here. Only kinds with a dedicated renderer are
// offered; roadmap kinds (dashboard/form/record_detail/…) are valid in
// the schema but hidden until they render distinctly.
options: [
{ label: 'List / Interface page', value: 'list' },
{ label: 'Record page', value: 'record' },
{ label: 'Home page', value: 'home' },
{ label: 'App page', value: 'app' },
{ label: 'Utility panel', value: 'utility' },
],
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.',
},
{ field: 'template', colSpan: 2, helpText: 'Layout template (e.g., "header-sidebar-main")' },
{ field: 'description', widget: 'textarea', colSpan: 2, helpText: 'Page description for navigation' },
],
Expand Down
31 changes: 19 additions & 12 deletions packages/spec/src/ui/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,7 @@ describe('PageSchema', () => {
it('should accept different page types', () => {
const types: Array<Page['type']> = [
'record', 'home', 'app', 'utility',
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
'timeline', 'form', 'record_detail', 'overview',
'dashboard', 'list', 'form', 'record_detail', 'overview',
];

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

it('should accept all interface page types', () => {
const types = [
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
'dashboard', 'list', 'form', 'record_detail', 'record_review', 'overview', 'blank',
];

types.forEach(type => {
expect(() => PageTypeSchema.parse(type)).not.toThrow();
});
});

it('should reject visualizations as page types (they belong in interfaceConfig.appearance.allowedVisualizations)', () => {
// grid/kanban/calendar/gallery/timeline are visualizations of a `list`
// page, not page kinds — they were removed from PageTypeSchema.
['grid', 'kanban', 'calendar', 'gallery', 'timeline'].forEach(type => {
expect(() => PageTypeSchema.parse(type)).toThrow();
});
});

it('should reject invalid page type', () => {
expect(() => PageTypeSchema.parse('invalid')).toThrow();
});
Expand Down Expand Up @@ -653,8 +659,7 @@ describe('PageSchema with page types', () => {

it('should accept all interface page types', () => {
const basicTypes = [
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
'timeline', 'form', 'record_detail', 'overview',
'dashboard', 'list', 'form', 'record_detail', 'overview',
];

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

it('should accept a grid page bound to an object', () => {
it('should accept a list page bound to an object', () => {
// A grid is a *visualization* of a list page, not a page type — bind the
// object on a `list` page and pick the grid visualization via interfaceConfig.
const page = PageSchema.parse({
name: 'page_grid',
label: 'All Orders',
type: 'grid',
type: 'list',
object: 'order',
regions: [],
});

expect(page.type).toBe('grid');
expect(page.type).toBe('list');
expect(page.object).toBe('order');
});
});
Expand Down Expand Up @@ -1269,9 +1276,9 @@ describe('PageSchema - conditional validation', () => {

it('should not require recordReview for non-record_review types', () => {
expect(() => PageSchema.parse({
name: 'grid_page',
label: 'Grid',
type: 'grid',
name: 'list_page',
label: 'List',
type: 'list',
regions: [],
})).not.toThrow();
});
Expand Down
41 changes: 26 additions & 15 deletions packages/spec/src/ui/page.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ export const BlankPageLayoutSchema = lazySchema(() => z.object({
* Unified page type enum covering both platform pages (Salesforce FlexiPage style)
* and Airtable-inspired interface page types.
*
* **Page type is the page KIND, NOT a visualization.** How an interface (`list`)
* page displays its records — grid / kanban / calendar / gallery / timeline — is a
* *visualization*, configured via `interfaceConfig.appearance.allowedVisualizations`
* and switched at runtime. Those are deliberately NOT page types: a kanban is a `list`
* page shown as a board, not a distinct page kind. (Historically grid/kanban/calendar/
* gallery/timeline appeared here; the runtime never branched on them — it always read
* the visualization from `interfaceConfig` — so they were removed to stop misleading authors.)
*
* **Disambiguation of similar types:**
* - `record` vs `record_detail`: `record` is a component-based layout page (FlexiPage style with regions),
* `record_detail` is a field-display page showing all fields of a single record (Airtable style).
Expand All @@ -152,27 +160,30 @@ export const BlankPageLayoutSchema = lazySchema(() => z.object({
* - `app` vs `utility` vs `blank`: `app` is an app-level page with navigation context,
* `utility` is a floating utility panel (e.g. notes, phone), `blank` is a free-form canvas
* for custom composition. They serve distinct layout purposes.
*
* **Implementation status:** only `record`, `home`, `app`, `utility`, and `list` have
* dedicated renderers today; the remaining interface types (`dashboard`, `form`,
* `record_detail`, `record_review`, `overview`, `blank`) are declared for roadmap parity
* but currently fall back to the list renderer. The authoring form only offers the
* implemented set (see `page.form.ts`) to avoid presenting dead options.
*/
export const PageTypeSchema = lazySchema(() => z.enum([
// Platform page types (Salesforce FlexiPage style)
// Platform page types (Salesforce FlexiPage style) — region/component composition
'record', // Component-based record layout page with regions
'home', // Platform-level home/landing page
'app', // App-level page with navigation context
'utility', // Floating utility panel (e.g. notes, phone dialer)
// Interface page types (Airtable Interface parity)
'dashboard', // KPI summary with charts/metrics
'grid', // Spreadsheet-like data table
'list', // Record list with quick actions
'gallery', // Card-based visual browsing
'kanban', // Status-based board
'calendar', // Date-based scheduling
'timeline', // Gantt-like project timeline
'form', // Data entry form
'record_detail', // Auto-generated single record field display
'record_review', // Sequential record review/approval
'overview', // Interface-level navigation/landing hub
'blank', // Free-form canvas for custom composition
]).describe('Page type — platform or interface page types'));
// Interface page types (Airtable Interface parity) — data-driven surfaces.
// NOTE: grid/kanban/calendar/gallery/timeline are NOT here — they are visualizations
// of a `list` page (interfaceConfig.appearance.allowedVisualizations), not page types.
'list', // Record list/grid surface with switchable visualizations + quick actions
'dashboard', // [roadmap] KPI summary with charts/metrics
'form', // [roadmap] Data entry form
'record_detail', // [roadmap] Auto-generated single record field display
'record_review', // [roadmap] Sequential record review/approval
'overview', // [roadmap] Interface-level navigation/landing hub
'blank', // [roadmap] Free-form canvas for custom composition
]).describe('Page type — the page KIND (platform or interface). Visualizations of a list page live in interfaceConfig, not here.'));

/**
* Record Review Config Schema
Expand Down