Skip to content

Commit db2c944

Browse files
os-zhuangclaude
andcommitted
feat(spec): interface pages own their view metadata directly (revise ADR-0047 iron rule)
Airtable parity: an interface (list) page now defines its data surface DIRECTLY as metadata — columns, base filter, sort — instead of inheriting them from a referenced object view ("sourceView"). There is no "inherit from a separate view" concept anymore; the page IS the view definition. Spec (packages/spec/src/ui): - page.zod.ts — InterfacePageConfigSchema gains its own `columns` (string[] | ListColumnSchema[]) and `sort`; `filterBy` clarified as the always-on base filter. `sourceView` is now @deprecated, kept only as a runtime back-compat fallback. - page.form.ts — the inspector follows Airtable's IA: Data (Source → Columns → Filter By → Levels) · Appearance · User filters · User actions. The "Source View" picker is removed and replaced by a Columns field picker (widget: field-multi, dependsOn: source). Iron-rule helptext rewritten. Showcase migrated to the new model (define columns directly, drop sourceView): - task-triage.page.ts, task-workbench.page.ts. Pairs with the objectui runtime change that reads page-owned columns/sort with a view→object fallback. Browser-verified: the page editor shows Source/Columns/ Filter By (no Source View); picking columns re-renders the preview live; both showcase pages render their own column sets (workbench shows Estimate (h), which the object default view omits — proving independence). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dbc749e commit db2c944

4 files changed

Lines changed: 33 additions & 17 deletions

File tree

examples/app-showcase/src/pages/task-triage.page.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export const TaskTriagePage: Page = {
2424
regions: [],
2525
interfaceConfig: {
2626
source: 'showcase_task',
27-
// Inherit columns/filter/sort from the object's default view (ADR-0047).
28-
sourceView: 'default',
27+
// ADR-0047 (revised): columns defined directly on the page (no inheritance).
28+
columns: ['title', 'project', 'assignee', 'status', 'priority', 'due_date', 'progress'],
2929

3030
// End-user filter element: a row of preset tabs. `showAllRecords` adds the
3131
// leading unfiltered "All" tab automatically.

examples/app-showcase/src/pages/task-workbench.page.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export const TaskWorkbenchPage: Page = {
3434
interfaceConfig: {
3535
source: 'showcase_task',
3636

37-
// ADR-0047 iron rule: the page inherits columns/filter/sort from the
38-
// referenced view and adds presentation policy only.
39-
sourceView: 'default',
37+
// ADR-0047 (revised): the page defines its own columns directly — no
38+
// inheriting from a separate view.
39+
columns: ['title', 'project', 'assignee', 'status', 'priority', 'estimate_hours'],
4040

4141
// End-user quick filters — the only filtering surface on this page.
4242
userFilters: {

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

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const pageForm = defineForm({
6868
{
6969
name: 'interface',
7070
label: 'Interface (list pages)',
71-
description: 'ADR-0047 interface mode: bind a source view and curate the end-user surface — quick filters, locked visualizations, toolbar actions (Airtable Interfaces parity).',
71+
description: 'Interface mode (Airtable parity): the page defines its own data surface directly — columns, filters, visualizations and toolbar — no inheriting from a separate view.',
7272
collapsible: true,
7373
// Primary content for a list page — open by default (still collapsible).
7474
collapsed: false,
@@ -78,7 +78,7 @@ export const pageForm = defineForm({
7878
field: 'interfaceConfig',
7979
type: 'composite',
8080
helpText:
81-
'source/sourceView bind the object view (columns, base filter and sort are inherited — the iron rule); appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar.',
81+
'The page IS the view: source picks the object, columns/filterBy are defined directly here; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar.',
8282
// Order: common authoring controls first, rarely-used ones last.
8383
// Explicit sub-fields so `userFilters` can use the dedicated
8484
// filter-mode selector (None / Tabs / Dropdown, ADR-0047 §3.4a).
@@ -87,22 +87,25 @@ export const pageForm = defineForm({
8787
// (`element: 'toggle'` stays valid but deprecated — not offered.)
8888
// Keep this list in sync with InterfacePageConfigSchema.
8989
fields: [
90-
{ field: 'source', widget: 'ref:object', helpText: 'Object this list reads from' },
91-
// Pick from the source object's views instead of typing a name.
92-
// `dependsOn: 'source'` tells the picker which object's views to list.
93-
{ field: 'sourceView', widget: 'view-ref', dependsOn: 'source', helpText: 'Named list view to inherit columns/filter/sort from (blank = object default)' },
90+
// ── Data ── the page defines its own data surface directly.
91+
{ field: 'source', widget: 'ref:object', helpText: 'Object this page reads from' },
92+
// Columns are defined ON the page (no view inheritance). `dependsOn:
93+
// 'source'` tells the picker which object's fields to offer.
94+
{ field: 'columns', widget: 'field-multi', dependsOn: 'source', helpText: 'Columns to show — defined directly on the page (blank = all object fields)' },
95+
{ field: 'filterBy', type: 'repeater', helpText: 'Always-on base filter for the page' },
96+
{ field: 'levels', helpText: 'Hierarchy levels to display (tree-like sources)' },
97+
// ── Appearance ──
9498
{ field: 'appearance', type: 'composite', helpText: 'Allowed visualizations (Grid / Kanban / Calendar / …) and description visibility' },
99+
// ── User filters ──
95100
{
96101
field: 'userFilters',
97102
widget: 'filter-mode',
98103
helpText: 'End-user filter bar: None (no bar) / Tabs (named presets) / Dropdown (per-field). None removes the config.',
99104
},
105+
// ── User actions ──
100106
{ field: 'userActions', type: 'composite', helpText: 'Toolbar toggles (search, sort, filter, row height)' },
101107
{ field: 'addRecord', type: 'composite', helpText: 'Add-record entry point' },
102108
{ field: 'showRecordCount', helpText: 'Show the record count bar' },
103-
// Less-common — kept last.
104-
{ field: 'filterBy', type: 'repeater', helpText: 'Always-on page filter (in addition to the source view)' },
105-
{ field: 'levels', helpText: 'Hierarchy levels to display (tree-like sources)' },
106109
{ field: 'allowPrinting', helpText: 'Allow users to print this page' },
107110
],
108111
},

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
UserFiltersSchema,
1414
ViewFilterRuleSchema,
1515
AddRecordConfigSchema,
16+
ListColumnSchema,
1617
} from './view.zod';
1718

1819
/**
@@ -226,10 +227,22 @@ export const RecordReviewConfigSchema = lazySchema(() => z.object({
226227
export const InterfacePageConfigSchema = lazySchema(() => z.object({
227228
/** Data binding (ADR-0047: pages REFERENCE views, never restate them) */
228229
source: z.string().optional().describe('Source object name for the page'),
229-
sourceView: z.string().optional()
230-
.describe('Named list view on the source object to inherit columns/filter/sort from (ADR-0047 iron rule: the page adds presentation policy only). Omit to use the object default view'),
230+
231+
// ADR-0047 (revised): the page carries its OWN view metadata — columns, sort
232+
// and base filter are defined directly here (Airtable parity: there is no
233+
// "inherit from a named view" concept). The page IS the view definition.
234+
columns: z.union([z.array(z.string()), z.array(ListColumnSchema)]).optional()
235+
.describe('Columns shown by the page. Blank = all object fields. Defined directly on the page (no view inheritance).'),
236+
sort: z.array(SortItemSchema).optional()
237+
.describe('Default sort order for the page, defined directly on the page.'),
238+
filterBy: z.array(ViewFilterRuleSchema).optional().describe('Always-on page filter (base filter).'),
231239
levels: z.number().int().min(1).optional().describe('Number of hierarchy levels to display'),
232-
filterBy: z.array(ViewFilterRuleSchema).optional().describe('Page-level filter criteria'),
240+
241+
/** @deprecated Back-compat only. Pre-revision pages inherited columns/filter/sort
242+
* from a named object view; new pages define `columns`/`sort`/`filterBy` directly.
243+
* Still honored at runtime as a fallback when the page has no own `columns`. */
244+
sourceView: z.string().optional()
245+
.describe('@deprecated Legacy named-view inheritance. Define columns/sort/filterBy on the page instead.'),
233246

234247
/** Appearance — `appearance.allowedVisualizations` is the runtime visualization whitelist */
235248
appearance: AppearanceConfigSchema.optional().describe('Appearance and visualization configuration'),

0 commit comments

Comments
 (0)