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
38 changes: 38 additions & 0 deletions docs/adr/0047-object-ui-run-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down
24 changes: 23 additions & 1 deletion packages/spec/src/ui/page.form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
],
},
Expand Down
7 changes: 6 additions & 1 deletion packages/spec/src/ui/view.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down