From 82485010f483aab39f3af1f443ba1da29a2a0d88 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 13 Jun 2026 12:58:20 +0500 Subject: [PATCH 1/2] =?UTF-8?q?feat(spec):=20page=20form=20uses=20filter-m?= =?UTF-8?q?ode=20widget=20for=20userFilters=20+=20ADR-0047=20=C2=A73.4a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit page.form.ts: the Interface section's interfaceConfig composite now lists its sub-fields explicitly so userFilters can use the dedicated 'filter-mode' selector widget (None / Tabs / Dropdown / Toggle, objectui). ADR-0047 §3.4a records the design decision: 'no filter bar' is the ABSENCE of userFilters, not a literal element: 'none'. Rationale — declarative-metadata hygiene (off = key absent, like every other optional capability), no orphaned sub-config, clean overlay diffs, presence and style are orthogonal axes. The authoring UI provides the explicit None/Tabs/Dropdown/Toggle affordance; storage stays clean. Pairs with objectui#1694 (the filter-mode widget). An unknown widget name degrades gracefully to the prior composite rendering, so this is independently mergeable. Co-Authored-By: Claude Fable 5 --- docs/adr/0047-object-ui-run-modes.md | 29 ++++++++++++++++++++++++++++ packages/spec/src/ui/page.form.ts | 23 +++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/docs/adr/0047-object-ui-run-modes.md b/docs/adr/0047-object-ui-run-modes.md index 31b33df4cf..53f03041b8 100644 --- a/docs/adr/0047-object-ui-run-modes.md +++ b/docs/adr/0047-object-ui-run-modes.md @@ -153,6 +153,35 @@ 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 that as a +literal enum value. The `userFilters.element` enum stays +`dropdown | tabs | toggle`; **"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, and we carry four + styles (`+toggle`) to Airtable's three. + +**Storage and authoring UI are separate layers.** The Studio editor exposes a +first-class **None / Tabs / Dropdown / Toggle** 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..2161404680 100644 --- a/packages/spec/src/ui/page.form.ts +++ b/packages/spec/src/ui/page.form.ts @@ -50,7 +50,28 @@ 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 / Toggle, ADR-0047). + // None maps to ABSENCE of userFilters — the protocol stores + // "no filter bar" as omission, not a literal element: 'none'. + // 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) / Toggle. None removes the config.', + }, + { field: 'userActions', type: 'composite' }, + { field: 'addRecord', type: 'composite' }, + { field: 'showRecordCount' }, + { field: 'allowPrinting' }, + ], }, ], }, From 1b8d384c1daad4333db32ac9c43e88ec1c04c524 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:33:30 +0500 Subject: [PATCH 2/2] =?UTF-8?q?refactor(spec):=20deprecate=20userFilters?= =?UTF-8?q?=20element=20'toggle'=20(ADR-0047=20=C2=A73.4a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Authoring offers None/Tabs/Dropdown only (Airtable parity). The element enum keeps 'toggle' for back-compat (existing configs keep rendering), documented deprecated in the schema + ADR §3.4a; page.form helpText and the §3.4a rationale updated to drop Toggle from the offered modes. --- docs/adr/0047-object-ui-run-modes.md | 25 +++++++++++++++++-------- packages/spec/src/ui/page.form.ts | 5 +++-- packages/spec/src/ui/view.zod.ts | 7 ++++++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/docs/adr/0047-object-ui-run-modes.md b/docs/adr/0047-object-ui-run-modes.md index 53f03041b8..6763ba4417 100644 --- a/docs/adr/0047-object-ui-run-modes.md +++ b/docs/adr/0047-object-ui-run-modes.md @@ -156,9 +156,8 @@ have; we do not rely on lint to forbid what the type can simply not express. ### 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 that as a -literal enum value. The `userFilters.element` enum stays -`dropdown | tabs | toggle`; **"none" is the ABSENCE of `userFilters`**. +(**None / Tabs / Dropdown**). We deliberately do **not** mirror "none" as a +literal enum value: **"none" is the ABSENCE of `userFilters`**. Rationale (declarative-metadata hygiene): @@ -171,13 +170,23 @@ Rationale (declarative-metadata hygiene): - **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, and we carry four - styles (`+toggle`) to Airtable's three. + (`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 / Toggle** segmented selector (the -`filter-mode` widget) — selecting *None* writes `onChange(undefined)`, removing -the key. Authors get Airtable's explicit affordance; the protocol stays clean. +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'`. diff --git a/packages/spec/src/ui/page.form.ts b/packages/spec/src/ui/page.form.ts index 2161404680..4cc437c89d 100644 --- a/packages/spec/src/ui/page.form.ts +++ b/packages/spec/src/ui/page.form.ts @@ -52,9 +52,10 @@ export const pageForm = defineForm({ helpText: '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 / Toggle, ADR-0047). + // 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' }, @@ -65,7 +66,7 @@ export const pageForm = defineForm({ { field: 'userFilters', widget: 'filter-mode', - helpText: 'End-user filter bar: None (no bar) / Tabs (named presets) / Dropdown (per-field) / Toggle. None removes the config.', + 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' }, 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()