diff --git a/docs/adr/0047-object-ui-run-modes.md b/docs/adr/0047-object-ui-run-modes.md index 31b33df4cf..6763ba4417 100644 --- a/docs/adr/0047-object-ui-run-modes.md +++ b/docs/adr/0047-object-ui-run-modes.md @@ -153,6 +153,44 @@ have; we do not rely on lint to forbid what the type can simply not express. | 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.4a "No filter bar" is omission, not a literal `element: 'none'` + +Airtable's User-filters control is a single tri-state selector +(**None / Tabs / Dropdown**). We deliberately do **not** mirror "none" as a +literal enum value: **"none" is the ABSENCE of `userFilters`**. + +Rationale (declarative-metadata hygiene): + +- **Consistency.** Every optional capability in the protocol is "off = key + absent" (`kanban`, `grouping`, …). A literal `element: 'none'` would be the + one special case authors and tooling must learn. +- **No dead config.** `element: 'none'` would leave an object whose `fields` / + `tabs` are orphaned — undefined semantics for validation, overlay merge, and + AI generation. Omission has one unambiguous meaning. +- **Cleaner diffs / overlays.** Disabling the bar is a key deletion (ADR-0005 + overlay semantics), not a value mutation dragging stale sub-config along. +- **Orthogonal axes.** "Is there a filter bar?" (presence) and "what style?" + (`element`) are independent; one enum would couple them. + +**`toggle` is deprecated; authoring offers Airtable's three.** The `element` +enum keeps `dropdown | tabs | toggle` for back-compat (existing configs keep +rendering), but `toggle` is **not** an authoring choice: it overlaps `tabs` +(presets) and `dropdown` (per-field values) without adding expressive power, +needs per-field `defaultValues` to be useful at all, and was the least- +exercised path. A homogeneous "everything is a toggle" bar only fits all-boolean +field sets — a narrow case better served by letting field *type* drive the +control inside `dropdown`. If stackable one-click quick-filters become a +validated need, design them explicitly (à la Linear filter chips), not via the +half-spec'd `toggle`. + +**Storage and authoring UI are separate layers.** The Studio editor exposes a +first-class **None / Tabs / Dropdown** segmented selector (the `filter-mode` +widget) — selecting *None* writes `onChange(undefined)`, removing the key. +Authors get Airtable's explicit affordance; the protocol stays clean. + +If a "disable but remember the configured fields/tabs" need ever arises, the +right shape is a separate `enabled: false` flag — never `element: 'none'`. + ### 3.5 AI authoring rules (inherits ADR-0033's draft gate) 1. **Default output**: objects + list views + navigation → objects. *No pages.* diff --git a/packages/spec/src/ui/page.form.ts b/packages/spec/src/ui/page.form.ts index 604a5fc017..4cc437c89d 100644 --- a/packages/spec/src/ui/page.form.ts +++ b/packages/spec/src/ui/page.form.ts @@ -50,7 +50,29 @@ export const pageForm = defineForm({ field: 'interfaceConfig', type: 'composite', helpText: - 'source/sourceView bind the object view (columns, base filter and sort are inherited — the iron rule); userFilters picks the element style (dropdown / tabs / toggle) and exposed fields; appearance.allowedVisualizations whitelists renderers (one entry = locked); userActions toggles the toolbar.', + '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.', + // Explicit sub-fields so `userFilters` can use the dedicated + // filter-mode selector (None / Tabs / Dropdown, ADR-0047 §3.4a). + // None maps to ABSENCE of userFilters — the protocol stores + // "no filter bar" as omission, not a literal element: 'none'. + // (`element: 'toggle'` stays valid but deprecated — not offered.) + // Keep this list in sync with InterfacePageConfigSchema. + fields: [ + { field: 'source' }, + { field: 'sourceView' }, + { field: 'levels' }, + { field: 'filterBy', type: 'repeater' }, + { field: 'appearance', type: 'composite' }, + { + field: 'userFilters', + widget: 'filter-mode', + helpText: 'End-user filter bar: None (no bar) / Tabs (named presets) / Dropdown (per-field). None removes the config.', + }, + { field: 'userActions', type: 'composite' }, + { field: 'addRecord', type: 'composite' }, + { field: 'showRecordCount' }, + { field: 'allowPrinting' }, + ], }, ], }, diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 20540d6f37..57e62aa730 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -309,8 +309,13 @@ export const UserFilterFieldSchema = lazySchema(() => z.object({ * @see Airtable Interface → "User filters" panel (Elements: tabs / dropdowns) */ export const UserFiltersSchema = lazySchema(() => z.object({ + // `toggle` is DEPRECATED (ADR-0047 §3.4a): it overlaps `tabs` (presets) and + // `dropdown` (per-field values) without adding expressive power, needs + // per-field defaultValues to be useful, and authoring tooling no longer + // offers it (None / Tabs / Dropdown only). Kept in the enum so existing + // configs keep rendering; do not author new `toggle` filters. element: z.enum(['dropdown', 'tabs', 'toggle']).default('dropdown') - .describe('Filter control style: dropdown selectors per field, tab presets, or on/off toggles'), + .describe('Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated.'), fields: z.array(UserFilterFieldSchema).optional() .describe('Fields exposed as quick filters (dropdown/toggle elements)'), tabs: z.array(ViewTabSchema).optional()