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
15 changes: 15 additions & 0 deletions .changeset/element-filter-lint-residue.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
"@objectstack/lint": patch
---

fix(lint): drop the `element:filter` entry from `COMPONENT_FIELD_SPECS` (#9220)

The whole `element:filter` element retired at element grain (ADR-0049 — no
renderer ever shipped for it), so every `ElementFilterProps` key is a
`retiredKey()` tombstone and no spec-conformant page carries `fields` on it.
The field-binding rule's job (resolve a field NAME against the object) is not
the question a retired key raises: an authored key is already reported by name
with the element-retirement prescription through the #5068 props gate, and the
binding entry would only add a second finding about a key that no longer
exists — the #5775/#6629 residue class the package's own
`component-field-specs-liveness` gate refuses.
72 changes: 72 additions & 0 deletions .changeset/element-filter-retired.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
---
"@objectstack/spec": minor
---

feat(spec): retire the `element:filter` element at element grain (#9220, ADR-0049)

**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
launch-window convention ships it as `minor`; the migration prescription is
registered under protocol major 18, where `os migrate meta` users will look).

`element:filter` never had a renderer or reader anywhere. Measured at
retirement (objectstack `2f65b1b42`, objectui `5ffcc14`; cloud per the origin
card's recorded sweep): objectui registers no renderer for it — its
`renderers/basic/elements.tsx` header deferred the element to "owning plugins"
that never materialized — Studio's designer palette carries it as a no-renderer
`PALETTE_EXCLUSIONS` entry ("list surfaces own filtering (userFilters / filter
builder)"), and the 2026-06 page-liveness audit recorded it rendering "Unknown
component type". Every one of its six authorable keys — `object`, `fields`,
`targetVariable`, `layout`, `showSearch`, `aria` — was a capability claim
nothing kept: an author (human or AI) who configured a filter element got a
success receipt for a component that renders nothing (the ADR-0078 shape).
\#9198 retired `targetVariable` per-key on the two input elements and recorded
this wider finding; per-key retirement would have been the wrong grain here, so
the whole element retires at once.

**What is refused:** any authored key on `element:filter` `properties`. All six
keys are `retiredKey()` tombstones — refused at `tsc` (typed `never`) and at
the parse, message carrying the element-grain prescription. The
`ComponentPropsMap` row deliberately STAYS so the #5068 props gate keeps
dispatching on the type and refusing loudly — deleting the row would demote the
type to an unregistered custom string the gate deliberately skips.

**What stays accepted:** a bare `element:filter` node with empty `properties`
(the migrated shape) still parses at the node level — `PageComponentSchema.type`
is an open union, so a node-level refusal is not expressible; the node was
always inert and stays inert. `element:filter` is removed from the
`PageComponentType` enum (de-advertisement — docs, palette derivations, and the
authorable vocabulary), which changes no parse outcome. Filtering on list
surfaces is unchanged and was never this element's: use a view's `userFilters`
quick-filter bar or the list toolbar's filter builder.

The retirement kit:

- tombstones at the schema (`packages/spec/src/ui/component.zod.ts`), enum
removal at `packages/spec/src/ui/page.zod.ts`
- ADR-0087 registration: retired-key entries `ui/ElementFilterProps:object` /
`:fields` / `:targetVariable` / `:layout` / `:showSearch` / `:aria` and the
D2 conversion `element-filter-removed` (protocol 18), wired into the step-18
chain — `os migrate meta --from 17` strips the keys from old sources (pure
lossless deletes; none ever had an effect to lose) and leaves the bare node
- the #9198 conversion's negative-control fixture moves from `element:filter`
to an open-union custom type (same assertion — the strip dispatches on the
component type, not the key name)
- pin tests (`component.test.ts` — refusal carries the prescription; the
bare migrated node parses clean and materializes nothing)
- generated baselines/docs follow the schema (`authorable-surface/`,
`json-schema.manifest/`, spec-changes, upgrade guide, reference docs)

## FROM → TO

```ts
// before — parsed green; nothing anywhere rendered it
{
type: 'element:filter',
properties: { object: 'order', fields: ['status'], layout: 'sidebar' },
}

// after — delete the component; filtering belongs to the list surface
// (view.userFilters / the list toolbar's filter builder)
```

<!-- adr-0087: registered element-filter-removed -->
12 changes: 6 additions & 6 deletions content/docs/references/ui/component.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,12 +61,12 @@ const result = AIChatWindowProps.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **object** | `string` | | Object to filter |
| **fields** | `string[]` | | Filterable field names |
| **targetVariable** | `string` | optional | Page variable to store filter state |
| **layout** | `Enum<'inline' \| 'dropdown' \| 'sidebar'>` | optional (default: `"inline"`) | Filter display layout |
| **showSearch** | `boolean` | optional (default: `true`) | Show search input |
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
| **object** | `never` | optional | [REMOVED] `element:filter` property `object` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |
| **fields** | `never` | optional | [REMOVED] `element:filter` property `fields` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |
| **targetVariable** | `never` | optional | [REMOVED] `element:filter` property `targetVariable` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |
| **layout** | `never` | optional | [REMOVED] `element:filter` property `layout` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |
| **showSearch** | `never` | optional | [REMOVED] `element:filter` property `showSearch` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |
| **aria** | `never` | optional | [REMOVED] `element:filter` property `aria` was removed in @objectstack/spec 17 (#9220, ADR-0049) — the whole `element:filter` element is retired: no renderer for it ever shipped in objectui, framework or cloud (Studio's designer palette lists it as a no-renderer exclusion), so every key on this element was a capability claim nothing kept. Delete the `element:filter` component; list surfaces own their filtering — use a view's `userFilters` quick-filter bar or the list toolbar's filter builder. Run `os migrate meta --from 17` to rewrite existing sources automatically. |


---
Expand Down
3 changes: 1 addition & 2 deletions content/docs/references/ui/page.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,7 +105,7 @@ Interface-level page configuration (Airtable parity)

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:filter' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) |
| **type** | `Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| 'page:card' \| 'page:section' \| 'record:details' \| 'record:highlights' \| 'record:related_list' \| 'record:activity' \| 'record:chatter' \| 'record:discussion' \| 'record:path' \| 'record:alert' \| 'record:quick_actions' \| 'record:reference_rail' \| 'record:history' \| 'app:launcher' \| 'nav:menu' \| 'nav:breadcrumb' \| 'global:search' \| 'global:notifications' \| 'user:profile' \| 'ai:chat_window' \| 'ai:suggestion' \| 'element:text' \| 'element:number' \| 'element:image' \| 'element:divider' \| 'element:button' \| 'element:form' \| 'element:record_picker' \| 'element:text_input'> \| string` | ✅ | Component Type (Standard enum or custom string) |
| **id** | `string` | optional | Unique instance ID |
| **label** | `string \| Record<string, string>` | optional | Display label — the default-language string, or an inline locale map (`{ en, "zh-CN" }`) resolved at render time |
| **properties** | `Record<string, any>` | optional (default: `{}`) | Component props passed to the widget. See component.zod.ts for schemas. |
Expand DownExpand Up@@ -157,7 +157,6 @@ Interface-level page configuration (Airtable parity)
* `element:image`
* `element:divider`
* `element:button`
* `element:filter`
* `element:form`
* `element:record_picker`
* `element:text_input`
Expand Down
2 changes: 1 addition & 1 deletion content/docs/ui/pages.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -148,7 +148,7 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus
- **Navigation:** `app:launcher`, `nav:menu`, `nav:breadcrumb`
- **Utility:** `global:search`, `global:notifications`, `user:profile`
- **AI:** `ai:chat_window`, `ai:suggestion`
- **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:filter`, `element:form`, `element:record_picker`, `element:text_input`
- **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:form`, `element:record_picker`, `element:text_input` (`element:filter` was retired in v17.x — no renderer ever shipped for it; list surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder)

Components may also carry `dataSource` (per-element object binding for multi-object pages), `responsive`, and `aria` configuration. Custom string types are also accepted for project-specific widgets.

Expand Down
7 changes: 6 additions & 1 deletion packages/lint/src/validate-page-field-bindings.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -191,7 +191,12 @@ export const COMPONENT_FIELD_SPECS: Readonly<Record<string, ComponentFieldSpec>>
'record:details': { props: ['fields', 'hideFields'], nestedSections: ['sections'] },
'record:path': { props: ['statusField'] },
'element:number': { props: ['field'] },
'element:filter': { props: ['fields'] },
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
// spec-conformant page carries `fields` on it, and the #5068 props gate
// reports an authored one by name with the element-retirement prescription —
// the same #5775/#6629 residue class as the record-picker entries below.
'element:form': { props: ['fields'] },
// `labelField` is the one field-bearing prop this element declares. Its former
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
Expand Down
2 changes: 0 additions & 2 deletions packages/spec/authorable-defaults/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,8 +34,6 @@
"ui/ElementButtonProps:iconPosition = \"left\"",
"ui/ElementButtonProps:size = \"medium\"",
"ui/ElementButtonProps:variant = \"primary\"",
"ui/ElementFilterProps:layout = \"inline\"",
"ui/ElementFilterProps:showSearch = true",
"ui/ElementFormProps:mode = \"create\"",
"ui/ElementImageProps:fit = \"cover\"",
"ui/ElementMetadataViewerProps:detail = \"business\"",
Expand Down
12 changes: 6 additions & 6 deletions packages/spec/authorable-surface/ui.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -379,12 +379,12 @@
"ui/ElementDataSource:object",
"ui/ElementDataSource:sort",
"ui/ElementDataSource:view",
"ui/ElementFilterProps:aria",
"ui/ElementFilterProps:fields",
"ui/ElementFilterProps:layout",
"ui/ElementFilterProps:object",
"ui/ElementFilterProps:showSearch",
"ui/ElementFilterProps:targetVariable",
"ui/ElementFilterProps:aria [RETIRED]",
"ui/ElementFilterProps:fields [RETIRED]",
"ui/ElementFilterProps:layout [RETIRED]",
"ui/ElementFilterProps:object [RETIRED]",
"ui/ElementFilterProps:showSearch [RETIRED]",
"ui/ElementFilterProps:targetVariable [RETIRED]",
"ui/ElementFormProps:aria",
"ui/ElementFormProps:fields",
"ui/ElementFormProps:mode",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/scripts/format-type.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1353,7 +1353,7 @@ const PAGE_COMPONENT = {
'app:launcher', 'nav:menu', 'nav:breadcrumb', 'global:search',
'global:notifications', 'user:profile', 'ai:chat_window', 'ai:suggestion',
'element:text', 'element:number', 'element:image', 'element:divider',
'element:button', 'element:filter', 'element:form', 'element:record_picker',
'element:button', 'element:form', 'element:record_picker',
'element:text_input',
],
},
Expand Down
Loading
Loading