Skip to content

Commit 253b996

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(spec): page form filter-mode widget + ADR-0047 §3.4a (omit-is-none) (#1812)
* feat(spec): page form uses filter-mode widget for userFilters + ADR-0047 §3.4a 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 <noreply@anthropic.com> * refactor(spec): deprecate userFilters element 'toggle' (ADR-0047 §3.4a) 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. --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent c78ebd9 commit 253b996

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

docs/adr/0047-object-ui-run-modes.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,44 @@ have; we do not rely on lint to forbid what the type can simply not express.
153153
| User-created views | Allowed, governed by permissions + view `sharing` | Never |
154154
| Filter state | Session-scoped (URL-param sync is a later, orthogonal step) | Session-scoped |
155155

156+
### 3.4a "No filter bar" is omission, not a literal `element: 'none'`
157+
158+
Airtable's User-filters control is a single tri-state selector
159+
(**None / Tabs / Dropdown**). We deliberately do **not** mirror "none" as a
160+
literal enum value: **"none" is the ABSENCE of `userFilters`**.
161+
162+
Rationale (declarative-metadata hygiene):
163+
164+
- **Consistency.** Every optional capability in the protocol is "off = key
165+
absent" (`kanban`, `grouping`, …). A literal `element: 'none'` would be the
166+
one special case authors and tooling must learn.
167+
- **No dead config.** `element: 'none'` would leave an object whose `fields` /
168+
`tabs` are orphaned — undefined semantics for validation, overlay merge, and
169+
AI generation. Omission has one unambiguous meaning.
170+
- **Cleaner diffs / overlays.** Disabling the bar is a key deletion (ADR-0005
171+
overlay semantics), not a value mutation dragging stale sub-config along.
172+
- **Orthogonal axes.** "Is there a filter bar?" (presence) and "what style?"
173+
(`element`) are independent; one enum would couple them.
174+
175+
**`toggle` is deprecated; authoring offers Airtable's three.** The `element`
176+
enum keeps `dropdown | tabs | toggle` for back-compat (existing configs keep
177+
rendering), but `toggle` is **not** an authoring choice: it overlaps `tabs`
178+
(presets) and `dropdown` (per-field values) without adding expressive power,
179+
needs per-field `defaultValues` to be useful at all, and was the least-
180+
exercised path. A homogeneous "everything is a toggle" bar only fits all-boolean
181+
field sets — a narrow case better served by letting field *type* drive the
182+
control inside `dropdown`. If stackable one-click quick-filters become a
183+
validated need, design them explicitly (à la Linear filter chips), not via the
184+
half-spec'd `toggle`.
185+
186+
**Storage and authoring UI are separate layers.** The Studio editor exposes a
187+
first-class **None / Tabs / Dropdown** segmented selector (the `filter-mode`
188+
widget) — selecting *None* writes `onChange(undefined)`, removing the key.
189+
Authors get Airtable's explicit affordance; the protocol stays clean.
190+
191+
If a "disable but remember the configured fields/tabs" need ever arises, the
192+
right shape is a separate `enabled: false` flag — never `element: 'none'`.
193+
156194
### 3.5 AI authoring rules (inherits ADR-0033's draft gate)
157195

158196
1. **Default output**: objects + list views + navigation → objects. *No pages.*

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,29 @@ export const pageForm = defineForm({
5050
field: 'interfaceConfig',
5151
type: 'composite',
5252
helpText:
53-
'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.',
53+
'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.',
54+
// Explicit sub-fields so `userFilters` can use the dedicated
55+
// filter-mode selector (None / Tabs / Dropdown, ADR-0047 §3.4a).
56+
// None maps to ABSENCE of userFilters — the protocol stores
57+
// "no filter bar" as omission, not a literal element: 'none'.
58+
// (`element: 'toggle'` stays valid but deprecated — not offered.)
59+
// Keep this list in sync with InterfacePageConfigSchema.
60+
fields: [
61+
{ field: 'source' },
62+
{ field: 'sourceView' },
63+
{ field: 'levels' },
64+
{ field: 'filterBy', type: 'repeater' },
65+
{ field: 'appearance', type: 'composite' },
66+
{
67+
field: 'userFilters',
68+
widget: 'filter-mode',
69+
helpText: 'End-user filter bar: None (no bar) / Tabs (named presets) / Dropdown (per-field). None removes the config.',
70+
},
71+
{ field: 'userActions', type: 'composite' },
72+
{ field: 'addRecord', type: 'composite' },
73+
{ field: 'showRecordCount' },
74+
{ field: 'allowPrinting' },
75+
],
5476
},
5577
],
5678
},

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,8 +309,13 @@ export const UserFilterFieldSchema = lazySchema(() => z.object({
309309
* @see Airtable Interface → "User filters" panel (Elements: tabs / dropdowns)
310310
*/
311311
export const UserFiltersSchema = lazySchema(() => z.object({
312+
// `toggle` is DEPRECATED (ADR-0047 §3.4a): it overlaps `tabs` (presets) and
313+
// `dropdown` (per-field values) without adding expressive power, needs
314+
// per-field defaultValues to be useful, and authoring tooling no longer
315+
// offers it (None / Tabs / Dropdown only). Kept in the enum so existing
316+
// configs keep rendering; do not author new `toggle` filters.
312317
element: z.enum(['dropdown', 'tabs', 'toggle']).default('dropdown')
313-
.describe('Filter control style: dropdown selectors per field, tab presets, or on/off toggles'),
318+
.describe('Filter control style: "dropdown" (per-field value selectors) or "tabs" (named presets). "toggle" is deprecated.'),
314319
fields: z.array(UserFilterFieldSchema).optional()
315320
.describe('Fields exposed as quick filters (dropdown/toggle elements)'),
316321
tabs: z.array(ViewTabSchema).optional()

0 commit comments

Comments
 (0)