From e7ddd673c30a6684c4c0182db4754ebfcee130c8 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:11:53 +0500 Subject: [PATCH] =?UTF-8?q?docs(adr):=20ADR-0047=20=E2=80=94=20two=20run?= =?UTF-8?q?=20modes=20for=20object=20UI=20(data=20views=20vs=20interface?= =?UTF-8?q?=20pages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formalizes the data-mode / interface-mode split for object list UI, the userFilters + runtime-visualization spec surface, asymmetric defaults, and the AI authoring decision rules. Grounded in live verification against app-showcase + objectui console (2026-06-12). Co-Authored-By: Claude Fable 5 --- docs/adr/0047-object-ui-run-modes.md | 234 +++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 docs/adr/0047-object-ui-run-modes.md diff --git a/docs/adr/0047-object-ui-run-modes.md b/docs/adr/0047-object-ui-run-modes.md new file mode 100644 index 0000000000..31b33df4cf --- /dev/null +++ b/docs/adr/0047-object-ui-run-modes.md @@ -0,0 +1,234 @@ +# ADR-0047: Two run modes for object UI — data views vs interface pages, user filters, and runtime visualization choice + +**Status**: Proposed (2026-06-12) +**Deciders**: ObjectStack Protocol Architects +**Builds on**: [ADR-0005](./0005-metadata-customization-overlay.md) (one Zod source per type, org overlay), [ADR-0017](./0017-object-has-many-view.md) (independent view entities, `viewKind`), [ADR-0019](./0019-app-as-consumer-unit.md) (App is the consumer-facing unit — navigation decides what users see), [ADR-0027](./0027-metadata-authoring-lifecycle.md) (draft · publish lifecycle), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (**AI is the long-term author of metadata — the design center this ADR inherits**) +**Consumers**: `@objectstack/spec` (view + page Zod schemas), `@objectstack/objectql` (registry validation/diagnostics), `../objectui` (console `ObjectView` / `PageView`, `plugin-list`), framework templates (`hotcrm`, `app-showcase`), the `objectstack-ui` authoring skill + +**Premise**: same as ADR-0033 — the platform is pre-launch, the primary author of UI metadata is an AI, and a human confirms. Every design choice below is therefore evaluated against two questions: *does it make the generated-by-default experience correct?* and *can an AI author get it wrong?* + +--- + +## TL;DR + +1. **Two run modes, both first-class.** *Data mode* (navigation → object): every list view of the object renders as a switcher tab; users may create personal views; the toolbar is permissive. *Interface mode* (navigation → page): an author-curated page **references** one view as its source and exposes only the controls the author enabled — filter tabs or dropdowns, a fixed (or whitelisted) visualization, selected user actions. This mirrors Airtable's Data vs Interfaces split and Power Platform's model-driven vs canvas split. +2. **The iron rule: pages reference views, never restate them.** A page's `source` points at an object/view; columns, base filter, and sort are *inherited* from the view definition. The page schema carries presentation policy only — it has no field for columns, so the "page and view each declare columns, then drift" failure mode is unrepresentable. +3. **`userFilters` becomes spec.** The end-user quick-filter surface (Airtable "User filters": element = `tabs | dropdown | toggle`, plus per-field config) is formalized in `ListViewSchema` and `InterfacePageConfig`. The client already implements and renders it (verified live, below); today it works only by accident of raw passthrough, with no type for authors and no Studio form. +4. **Runtime visualization choice is an author-controlled whitelist.** `userActions.visualizations?: boolean | ViewType[]` at view level, `visualizations` at page level (superseding the misplaced `userFilters.elements` enum). Effective options = author whitelist ∩ types whose required field bindings resolve (kanban needs a select `groupBy`, calendar a date field, …). Data mode defaults open; interface mode defaults locked. +5. **Defaults are asymmetric on purpose.** Data mode auto-derives quick filters from select/boolean fields and allows user views; interface mode is closed until the author opens it. An AI that emits *nothing* beyond objects + views + navigation gets a correct, complete system — "omission is correct" is the strongest guardrail we can give a generative author. +6. **AI decision rule, encoded not implied.** Default output is objects + list views + navigation → objects. Interface pages are generated only on explicit signals (persona split, capability narrowing, portal/workspace language). The rule ships in the `objectstack-ui` skill; reference-integrity diagnostics become hard failures in the AI loop (per ADR-0033's draft gate). + +--- + +## 1. Context — what exists (verified live, 2026-06-12) + +The findings below come from running `examples/app-showcase` (:3000) against the +objectui console (:5180), logging in, and exercising the HotCRM `crm_account` +object — plus one runtime experiment: a view written through +`PUT /api/v1/meta/view/:name` carrying `tabs` + `userFilters` that the spec does +not declare. + +**Already working:** + +- **Auto-derived quick filters are live.** The `crm_account` list renders + dropdown filters (类型 / 行业 / 是否活跃 / 更多) although the hotcrm template + contains **zero** `userFilters` metadata — objectui's `ListView` derives them + from the object's select/boolean fields. Selecting 行业=科技 correctly + filters 5 → 2 records through the query pipeline. +- **Metadata-driven `userFilters` round-trips end-to-end.** The experimental + view saved with `userFilters: { element: 'dropdown', fields: [行业, 评级] }` + was stored verbatim (registry validation is warn-only and stores the raw + item), echoed back intact with `_diagnostics.valid: true`, and the console + rendered **exactly the two configured dropdowns**, replacing the auto-derived + set. The client component (`plugin-list/UserFilters.tsx`) supports all three + elements (`dropdown` / `tabs` / `toggle`), lookup-backed options, counts and + defaults. +- **View switcher + user-created views (data mode)** work: 7 artifact views, + overflow menu, "Manage all views…", runtime overlay create/delete (ADR-0005). +- **Runtime visualization switching exists as a dormant component** + (`plugin-list/ViewSwitcher.tsx`), gated by a `showViewSwitcher` prop that + only Studio's view preview enables. + +**Broken or absent:** + +- **`ListViewSchema` has no `userFilters`.** The working client behavior has + no spec type: TS authors get excess-property errors, Studio cannot render a + form for it, and an AI grounded in the Zod schema will never emit it. +- **View-level `tabs` don't reach the renderer.** The experiment's three + filter tabs (全部 / 科技公司 / 金融公司) were stored and served but never + rendered — the console's `ObjectView` forwards `listSchema.tabs` and drops + `viewDef.tabs` (a one-line gap). +- **`InterfacePageConfig` is schema-only.** No page in the running system + carries `interfaceConfig`; nothing renders it. Its + `userFilters.elements: ['grid','gallery','kanban']` enum conflates + *visualization choice* with *filter element type*. +- **No template exercises any of this.** `userFilters`, view `tabs`, + `filterableFields`: zero occurrences across `app-showcase` and the + `hotcrm`/`hr` templates — so the capability is invisible to both humans + and few-shot-grounded AI authors. +- **Registry validation is warn-only** (`registry.validate()` logs and stores + the raw item). Tolerable for humans; for an AI loop it means a misspelled + field name produces no feedback at all. + +## 2. Prior art + +| Platform | Data-mode equivalent | Interface-mode equivalent | Lesson taken | +|---|---|---|---| +| **Airtable** | Data tab: every view user-creatable, grid-first | Interfaces: author-curated pages; *User filters* (Elements: tabs / dropdowns), *Appearance → Visualizations* fixed by author, *User actions* toggles | The panel we are matching feature-for-feature; end users never create views inside an interface | +| **Salesforce** | Object list views (user-creatable, admin-shareable, pinnable) | Lightning App Builder pages (curated regions/components) | List views remain the data asset; pages compose them. Personal vs shared views are permission-governed | +| **Microsoft Power Platform** | Model-driven apps: UI *generated* from Dataverse views/forms | Canvas apps: fully curated | "Generated-by-default is correct" scales; full curation is opt-in because it carries maintenance cost | +| **Notion** | Database with multiple views | Linked database views embedded in pages | Embeds are *references* to the source database — the iron rule in the wild | +| **Retool (counter-example)** | — | Everything is a hand-built page | With no generated data mode, every list costs author time and drifts from schema changes — what we avoid by keeping data mode primary | + +## 3. Decision + +### 3.1 The model: three layers, navigation decides the mode + +| Layer | Metadata | Role | Required? | +|---|---|---|---| +| Data asset | `object` + its list views (ADR-0017 view entities) | Authoritative definition of columns/filter/sort; serves data mode | Yes — every object | +| Entry | `app.navigation` (ADR-0019) | `type: 'object'` → data mode; `type: 'page'` → interface mode | Yes | +| Presentation | `page.interfaceConfig` | Curated end-user surface; references a view, adds policy | Optional, signal-driven | + +The same object may appear behind both entries (ops team gets the object, +business users get a page). The renderer (`plugin-list/ListView`) is shared; +`ObjectView` and `PageView` are two orchestrators feeding it different schema. + +### 3.2 The iron rule + +`interfaceConfig` carries **no column, no base-filter, no sort definitions** — +only: `source` (object or object+view), `userFilters`, `visualizations`, +`userActions`, `addRecord`, `showRecordCount`, and cosmetic appearance. Columns +and data semantics always come from the referenced view. Schema-level +unrepresentability is the strongest anti-drift and anti-AI-error mechanism we +have; we do not rely on lint to forbid what the type can simply not express. + +### 3.3 Spec changes (`@objectstack/spec`) + +1. **`UserFiltersSchema`** (new, `ui/view.zod.ts`), modeled on the proven + objectui client type: + + ```ts + export const UserFilterFieldSchema = z.object({ + field: z.string(), // must exist on the source object + label: I18nLabelSchema.optional(), + type: z.enum(['select','multi-select','boolean','date-range','text']).optional(), // default: infer + options: z.array(OptionSchema).optional(), // default: derive from field def + showCount: z.boolean().optional(), + defaultValues: z.array(z.union([z.string(), z.number(), z.boolean()])).optional(), + }); + + export const UserFiltersSchema = z.object({ + element: z.enum(['dropdown','tabs','toggle']), // Airtable "Elements" + fields: z.array(UserFilterFieldSchema).optional(), // dropdown/toggle + tabs: z.array(ViewTabSchema).optional(), // tabs element reuses ViewTabSchema + showAllRecords: z.boolean().optional(), + }); + ``` + + Attached as `ListViewSchema.userFilters` (view-level convenience) and + `InterfacePageConfigSchema.userFilters` (the primary home). + `filterableFields` stays as the legacy shorthand; loaders auto-upgrade it + to `userFilters.fields` entries. + +2. **Runtime visualization whitelist.** + `UserActionsConfigSchema.visualizations?: z.union([z.boolean(), z.array(ViewTypeEnum)])` + (view level) and `InterfacePageConfigSchema.visualizations` (page level, + full 8-type enum). The existing `userFilters.elements` 3-type enum is kept + one release as a deprecated alias and folded in. + +3. **Semantics, not rendering.** The spec defines *what the author allows*; + capability filtering (does this object have a select field for kanban + `groupBy`? a date field for calendar?) is renderer responsibility and must + hide non-resolvable types rather than render with hardcoded fallbacks + (`groupBy: 'status'` against an object without `status` renders garbage — + observed in the current console code). + +### 3.4 Asymmetric defaults + +| | Data mode | Interface mode | +|---|---|---| +| Quick filters | Auto-derived from select/boolean fields (current live behavior, now blessed) | None until author configures `userFilters` | +| Visualization switching | On (all capability-resolvable types); user preference persists via the per-user view-patch channel | Locked to authored visualization unless whitelisted | +| User-created views | Allowed, governed by permissions + view `sharing` | Never | +| Filter state | Session-scoped (URL-param sync is a later, orthogonal step) | Session-scoped | + +### 3.5 AI authoring rules (inherits ADR-0033's draft gate) + +1. **Default output**: objects + list views + navigation → objects. *No pages.* + Rationale: data mode is a functional superset; a missing page costs polish, + a superfluous page costs a permanently-maintained duplicate asset. The + asymmetry dictates the default. +2. **Generate an interface page only on explicit signals**: persona split + ("销售人员看到…", customer portal), capability narrowing ("users must not + change views", "only filter by industry/type"), or curation language + (workspace / 工作台 / Airtable-interface-like). Ambiguity resolves to *no + page*. +3. **The rule ships as text in the `objectstack-ui` skill** (decision tree + + the iron rule + this ADR as reference), so the constraint binds at + generation time, not only at validation time. +4. **Diagnostics are hard failures for AI.** The metadata write path already + returns `_diagnostics`; it gains reference-integrity checks — `page.source` + resolves; every `userFilters.fields[].field` and `tabs[].filter[].field` + exists on the source object; kanban `groupBy` is a select. Human authors + keep warn-and-store (ADR-0005 tolerance); the ADR-0033 agent loop treats + `valid: false` as a failed apply and self-corrects before draft review. +5. **Templates are the few-shot corpus.** `hotcrm` gains one canonical + interface page (e.g. a "销售工作台" referencing `all_accounts` with + industry/type dropdowns and a locked grid) and one view with filter `tabs`; + `app-showcase` covers the remaining permutations. Zero examples today is + why zero AI generations use these features. + +## 4. Non-goals + +- **Per-user saved filter presets in the cloud** (Airtable lets interface + users pin filter values) — session scope first; a `sys_user_view_state` + channel is a separate decision. +- **Ad-hoc filter-builder spec for end users** — the advanced FilterBuilder + remains a client feature; its output never persists as metadata. +- **Page composition beyond single-source lists** (multi-widget interface + pages, cross-object layouts) — covered by the existing page/component + schemas; this ADR only fixes the list-page run mode. +- **Back-compat machinery** — pre-launch premise; `filterableFields` and + `userFilters.elements` aliases are one-release courtesies, not commitments. + +## 5. Consequences + +**Positive.** The Airtable-parity gap (in-page filter tabs / dropdowns, +author-controlled visualization) closes mostly by *formalizing what already +runs*; one orchestrator line, one schema block, and template examples deliver +the visible feature. AI authors get a closed loop: schema they can see, +diagnostics that push back, examples to imitate, and a default that is correct +when they emit nothing. The two modes stop being implicit — navigation type is +the single, auditable mode switch. + +**Negative / accepted.** Two places can now declare `userFilters` +(view + page); precedence is fixed (page overrides view) and documented, but +it is still a second place. Interface pages remain a thinner v1 than +Airtable's (no per-user saved state). Warn-only validation persists for human +paths — we accept schema-invalid human metadata surviving, as today, until +ADR-0027 promotion tightens it. + +## 6. Implementation plan + +| Phase | Repo | Work | Size | +|---|---|---|---| +| 1 | framework | `UserFiltersSchema` + `userActions.visualizations` + `InterfacePageConfig` rework in spec; reference-integrity diagnostics | S | +| 2 | objectui | `ObjectView`: forward `viewDef.tabs`; import spec types (kill the local duplicate); wire `showViewSwitcher` behind the whitelist + capability filter | S | +| 3 | framework | Template examples (`hotcrm` workbench page, tabbed view); `objectstack-ui` skill decision rules | S | +| 4 | objectui | `PageView` renders `interfaceConfig` (single-source list + `UserFilters` toolbar, locked toolbar policy) | M | +| 5 | both | Studio authoring panel for user filters / visualizations (Airtable right-panel parity); AI loop treats diagnostics as hard failure | M | + +Phases 1–3 are independently shippable and deliver the user-visible feature in +data mode; phase 4 activates interface mode; phase 5 completes the authoring +experience. + +## 7. Open questions + +1. Should interface-mode filter state sync to URL params for shareable links + (Salesforce-style) in v1, or stay purely in memory? +2. Does `userFilters` on the *view* level survive long-term, or do we + eventually deprecate it in favor of page-level-only once PageView ships? +3. Capability filtering for visualizations: renderer-local heuristics now — + does the spec eventually need explicit per-type binding declarations + (`kanban.groupByField` required) surfaced as authoring-time diagnostics?