Skip to content

Commit 4bc3bec

Browse files
feat(spec)!: retire the list view's filterableFields — declared as the userFilters.fields shorthand, read as a Filter-panel whitelist; no per-view whitelist replaces it (#24721)
Fixes #24640 Clause-②: yes (narrowing) ## What `filterableFields` retires from the list-view shape under ADR-0049 enforce-or-remove, on the maintainer's ruling C (record 6278975049, verbatim 「C」), pulled into 18.0 (record 6279033298, verbatim 「提前」). The key carried two meanings. The spec described it as the legacy shorthand for `userFilters.fields` (the quick-filter bar), and the Studio view form offered it as "Field names available for filtering". Its one renderer, objectui's `ListView` (the `filterFields` memo), read it as the Filter panel's field whitelist. The ruling is that the platform carries no per-view Filter-panel whitelist, so **no key replaces it**. - **Tombstone.** `filterableFields: retiredKey(LIST_VIEW_FILTERABLE_FIELDS_RETIRED)` sits in `ListViewShapeSchema`, beside `searchableFields`. Every door built from the shape refuses the key with the prescription: `tsc` types it `never`, and the parse raises the prescription instead of a bare unknown-key report. The prescription says: delete the key, no per-view Filter-panel whitelist replaces it; for a curated end-user filter bar declare `userFilters: { element: 'dropdown', fields: [{ field: 'FIELD_NAME' }] }`; the Filter panel offers every field the user may read (field read security decides, and a field the object marks `hidden: true` is not offered). It closes with the house `os migrate meta --from 17` sentence. - **Studio form.** The `filterableFields` input leaves `view.form.ts`, with a comment in its place. The four extracted form-label bundles are regenerated (pure deletion). - **ADR-0087.** The D2 conversion `view-list-filterable-fields-removed` (protocol 18, `retiredFromLoadPath: true`, `retiredAfter: '17.7.0'`) **drops** the key from every list payload in `stack.views[]`, in all three persisted spellings. It never rewrites it to `userFilters.fields`. The D3 entry is `list-view-filterable-fields-retired`. `RETIRED_KEYS_BY_MAJOR[18]` gains `ui/ListView:filterableFields` and `ui/ObjectListView:filterableFields`. The step-18 rationale gains one fragment. - **Ledger.** The `view.json` row `/props/list/children/filterableFields` flips `live` to `dead`. It keeps the tombstone discipline (`verifiedAt` 2026-10-19, `evidenceScope: cross-repo`) and carries a REMOVED note naming the one reader the ruling makes unreachable. `state-counts/view.md` is regenerated: live 79 to 78, dead 11 to 12. - **Docs.** The `os:check` example on `content/docs/concepts/architecture.mdx` stops teaching the key. It declares a `userFilters` dropdown on `industry` instead, because the example meant a user-facing filter. The reference pages are regenerated. - **Changeset.** `.changeset/24640-list-view-filterable-fields-retired.md`: `@objectstack/spec` minor, with a BREAKING banner, a FROM to TO table, the one-line fix and a stored-row paragraph. It carries the Clause-② line and the ADR-0087 marker `registered view-list-filterable-fields-removed, list-view-filterable-fields-retired`. Release arithmetic, measured: `.changeset/pre.json` is `{ "mode": "exit", "tag": "next" }` and the spec label is `18.0.0-next.5`. A minor increment of that prerelease is `18.0.0`, so the next version pass publishes this in 18.0.0. ## Premises, each with its reading (base `bfc1539fe0`; re-read after merging `origin/main` `1c1144fcd8`) **P1 holds.** One declaration exists: `packages/spec/src/ui/view.zod.ts` (was `:2844`) on `ListViewShapeSchema`. `ObjectListViewSchema` is `ListViewShapeSchema.omit({ userFilters }).extend(...)`, so one tombstone covers both. Both `authorable-surface/ui.json` rows (`:757`, `:944`) now read `[RETIRED]`. Inside `packages/spec/src`, `git grep -c filterableFields` at base hit 4 files: the declaration, the form row and two pins. No schema outside the list-view family declares the key. The other carriers are the same family. The flattened overlay arms are built from the shape. `ListViewPageProps` (a page `list-view` node, `ComponentPropsMap['list-view']`) spreads `ListViewSchema.shape`. The react `ListView` block's schema is `ListViewSchema`, and its curated `dataProps` never listed the key (see the Acceptance notes for the react-page boundary). **P2 holds, and the census found more readers than the card listed.** - Zero authors, each with a lit control on the same instrument at base. `git grep -c` for `filterableFields`, then `searchableFields` (control), then `userFilters` (control): | corpus | `filterableFields` | `searchableFields` | `userFilters` | | --- | --- | --- | --- | | `examples/**` | 0 | 2 | 7 | | `packages/apps/**` | 0 | 0 | 0 | | `packages/qa/**` | 0 | 2 | 0 | | `skills/**` | 0 | 20 | 12 | | `packages/platform-objects/**` | 4 | 12 | 21 | The 4 `platform-objects` hits are the four generated translation bundles, regenerated here. - `packages/create-objectstack` reads 0 for all three keys. Its templates author no list view at all, so that zero is uninformative beyond that fact. - Beyond the card's list, these mention the key without authoring it: - `packages/lint`: the list-view field-ref rule's role and severity maps and its fixtures, `REACT_FIELD_SPECS.ListView`, the field-consumer vocabulary, and two `rule-explanations` sentences; - `packages/metadata-core`: the ADR-0106 field-security scrub's `LIST_VIEW_NAME_LISTS`; - `packages/metadata-protocol`: `computeViewReferenceDiagnostics`, a raw-input read; - `packages/objectql`: that read's fixtures. - None of those is an author. All are outside this card's file surface and are left in place (Acceptance notes). The tree-scoped pin carries the four files that spell it as an object key as self-expiring RESIDUE. **P3 holds.** The mechanism is the one the closest 18.x list-view retirement used (`view-list-tabs-removed`), with the strip shape of 17.x `view-list-passthrough-keys-removed`. - **Entries.** One `retired-keys/18.ui__ListView__filterableFields.ts` and one `18.ui__ObjectListView__filterableFields.ts`, registered per key as gate (b) reads them. One D3 `semantic/18.list-view-filterable-fields-retired.ts`, per family. - **Conversion order.** The conversion is inserted into `MAJOR_18_CONVERSIONS` where its identifier sorts, with `order: 71`. It is defined directly above `viewListTabsRemoved`. `scripts/conversions-major18-merge.test.ts` is green. - **Rationale.** The `STEP18_RATIONALE` fragment sorts by id, with `order: 105`. `scripts/step18-rationale-merge.test.ts` is green. - **`retiredAfter: '17.7.0'`** is the newest published stable. `retired-after.census.json` is untouched: it is generated from published tarballs, and its last census release is `17.4.0`. Under the label `18.0.0-next.5`, `retired-after.census.test.ts` admits `[17.4.0, 18.0.0)`. - **The pin's JOIN row.** `retired-key-removal-version.pin.test.ts` requires a JOIN row for every retired entry at the newest stable, so one row is added. - **Disposition.** `check-adr-0087-registration` passes with `registered` (both ids new here). `no-migration-prescription` would refuse itself, because the changeset carries a FROM to TO table. - **Not a semantic mapping.** The machinery asked for a conversion. It is a drop, so this premise did not fail. **P4 holds.** Every generated artifact moved by its own tool: - `pnpm --filter @objectstack/spec build` wrote `authorable-surface/ui.json` (2 rows to `[RETIRED]`); - `gen:docs` wrote 3 reference pages; - `gen:liveness-counts` wrote `state-counts/view.md`; - `node scripts/check-i18n-bundles.mjs --write` wrote the 4 `platform-objects` bundles; - `gen:migration-registry` wrote the gitignored registry. Hand-written, by design: the ledger row (measurement prose, no generator) and the `registry.ts.template` fragment. `authorable-surface.base.json` is untouched. After the merge of `origin/main`, `check:generated` reads "All 14 generated artifacts are up to date". ## Mechanism assumptions G1 to G5, measured - **G1.** `ui/view-list-filterable-fields-retirement.test.ts` pins the refusal at **eight** doors: - `ListViewSchema`; - `ObjectListViewSchema`; - `ViewSchema` `list`; - `ViewSchema` `listViews.*`; - a `ViewItemSchema` record's `config`; - the `view` write door's flattened overlay; - `ObjectSchema` `listViews.*`; - a page `list-view` node's `properties`. Each pin checks issue code `invalid_type`, the path naming the key, and the prescription. Each door has two lit controls: the same body without the key parses, and the same body with `userFilters: { element: 'dropdown', fields: [...] }` plus `searchableFields` parses. The tsc channel is pinned too (`@ts-expect-error` compiles under the test-layer typecheck), and the parse throws the same prescription. - **G2.** The form row is gone, pinned by a walk of `viewForm` with anti-vacuity neighbours. The bundles are regenerated and `check:i18n` passes. - **G3.** `architecture.mdx` is rewritten. `check:skill-examples` reports "262 prose examples type-check". - **G4.** As above. The derived changeset gates were run on the final head (Gates). - **G5.** Every consumer suite is green (Tests). No fixture in those packages authors the key through a parse, so none needed re-truing. The lint and objectql fixtures that spell it feed raw-input walks and still pass. - One out-of-lane pin had to move. It is a count, not a fixture: `packages/platform-objects/src/apps/translations/object-lifecycle-panel-echo-decisions.test.ts`, where the metadata-form catalog drops 674 to 673. The removed input took its label out of all three locales. - That edit is a cross-lane rider, listed for the seat to declare. ## Ablation (the tombstone is what shuts every door) The fix was committed first. Through `scripts/ablation-replace.mjs`, in wrap mode under its own restore trap plus an outer `trap` on the absolute path, the tombstone line was swapped back to the pre-retirement declaration `filterableFields: z.array(z.string()).optional()`. - **Mutation on disk:** anchor count 1 to 0 and replacement count 0 to 1. The blob moved from `1e5e34e05ab8` to `d38c1ae00383`, and an in-command `grep -c` read 1 and 0. - **Run:** `vitest run` of the new pin plus `retired-key-removal-version.pin.test.ts` went **red, the predicted direction**. 13 failed and 71 passed. - **What failed:** - all 8 door refusals; - the empty-array refusal; - the prescription content; - the tsc-channel parse; - the object-`listViews` boundary, whose object now parses; - the removal-version JOIN row. - **What held:** every control and boundary acceptance, and every D2 conversion pin. Those pins are independent of the shape. - **Restore:** the blob after restore equals HEAD (`1e5e34e05ab8`), and `git diff HEAD` is empty. The tests import `src` by relative path, so no `dist` leg applies. ## Tests (consumer packages `--maxWorkers=2`, under `os-verify-lock.sh`) - `@objectstack/spec` `local`: 702 files, 21352 passed (1 todo). Run at `c0c1a8ad33` and again on the merged head `bb71914fc9`. - `@objectstack/spec` `repo` project: 64 of 71 files, 1039 passed, on `bb71914fc9`. The new pin is 1 of the 64. - The other 7 are the build and publish tooling tests (`build-console-objectstack-root`, `build-schemas-check-mode`, `dist-freshness*`, `publish-smoke-*`, `published-projection-choke-point`). They outlast the foreground cap on this shared box, so they are NOT MEASURED locally and left to CI's `Test Core`. - `@objectstack/spec typecheck` passes, on the merged head too. - `@objectstack/lint`: 148 files, 6935 passed, on the merged head. - `@objectstack/platform-objects`: 79 files, 1195 passed, on the merged head; its typecheck passes. - `@objectstack/metadata-protocol`: 249 files (3 skipped), 20771 passed. - `@objectstack/objectql` `local`: 448 files, 8447 passed. - `@objectstack/rest` `local`: 300 files, 6095 passed (326 skipped). - `@objectstack/runtime` `local`: 425 files in three batches, 6671 passed (19 skipped). - `@objectstack/cli` `unit`: 314 files in four batches, 4641 passed. The `integration` layer is declared to CI. - These five were run at `c0c1a8ad33`, before the merge. The merge brought no new `filterableFields` mention: `git grep -c` per file is identical at base and at `origin/main`. - Every example app runs `os validate` and `os build` (`app-crm`, `app-todo`, `app-showcase`, `app-multi-package`), and all pass. `embed-objectql` has no validate script. ## Acceptance notes - **Out-of-lane readers left in place, for the seat's follow-up.** Each reads the key off raw input and none delivers anything once the parse refuses it. This matches the reasoning of the `tabs` retirement's stage 2: the lint suite judges the parsed stack. - `packages/lint/src/validate-list-view-field-refs.ts` (role map and severity map) and its test fixtures; - `packages/lint/src/validate-react-page-props.ts` (`REACT_FIELD_SPECS.ListView`) and its "covers the legacy filterableFields shorthand" test; - `packages/lint/src/validate-field-consumers.ts:307`; - `packages/lint/src/rule-explanations.ts:3114` and `:3149`, which still name the key as a checked position in `os explain` text; - `packages/lint/src/reference-integrity-suite.ts`, in comments; - `packages/metadata-protocol/src/metadata-diagnostics.ts:237` and the `packages/objectql/src/metadata-diagnostics.test.ts` fixtures; - `packages/metadata-core/src/object-schema-fls-references.ts` (`LIST_VIEW_NAME_LISTS.filterableFields`). The tree-scoped pin's RESIDUE set holds the four that spell it as an object key, and goes red when the follow-up deletes them. - **Boundary: a react page.** A react `ListView` element's `filterableFields` prop lives in a react page's opaque `source` string, and no door parses that string against `ListViewSchema`. The lint still treats it as a field-bearing prop ("honored by the renderer"). Once objectui removes its reader, the prop becomes a silent no-op on react pages unless the react-props lint learns to refuse it. The boundary is stated in the schema comment and the ledger note. - **objectui follow-up (the seat files it).** Re-read at the moved pin `12dc7b47`: - `app-shell/src/views/ObjectView.tsx:3448` forwards the key; - `plugin-list/src/ListView.tsx:4656` is the `filterFields` whitelist; - `plugin-view/src/ObjectView.tsx:3124` forwards it, `:1030` keeps a key list, and `plugin-view/src/index.tsx:90` registers an input; - `types/src/objectql.ts:2625` and `:2966` declare it locally; - `types/src/zod/objectql.zod.ts:1300` reads `.shape.filterableFields` by reference. No export is removed and that member still exists as the tombstone, so the pinned objectui build compiles unchanged. The refusal flows into objectui's zod twin on the next pin bump. - **ADR-0047 text is now stale.** Decision 1 says "`filterableFields` stays as the legacy shorthand; loaders auto-upgrade it to `userFilters.fields` entries". That auto-upgrade was never built, and the ruling now forbids it. Its own section 4 calls the alias "one-release courtesies, not commitments". An amendment line is a `docs/adr/**` (Tier H) edit outside this PR's surface. It is raised in the report as an open question for the seat. - **Out-of-repo consumers: NOT MEASURED.** `@objectstack/spec` is published. ## Gates The derivation re-ran on the final head `bb71914fc9` with no paths: `dispatch-gates --commands --repo objectstack-ai/objectstack`, 24 paths against merge base `1c1144fcd`, giving 115 commands. All 115 were run on `bb71914fc9`, each exit code captured before any pipe, and `--ran` reconciles them as "115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED (a DERIVED zero)". - One gate needed its prerequisite first. `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: 7 unrelated packages had no `dist/`). It passed after building them. - Key verdict lines: - `check-adr-0087-registration`: "registered view-list-filterable-fields-removed, list-view-filterable-fields-retired (new here: …)"; - `check:api-surface`: "public API surface + factory signatures unchanged" (key-level narrowing, no export moved); - `check:docs`: "227 generated files in sync"; - `check:spec-changes` and `check:upgrade-guide` generate in memory; - `check:doc-authoring`: clean; - `check:cross-package-test-inputs`: OK; - `check:i18n`: OK; - `check:nul-bytes`: OK; - `check-empty-changeset` and `check-changeset-no-major`: pass. - Also run locally: the local-only `check:dev-prereqs` passes. `check:skill-examples`, `check:liveness` and `check:generated` are in the 115. - CI's own runs (`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`) are read from this PR's check-runs. They are not re-run locally. --- _Generated by [Claude Code](https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0904d85 commit 4bc3bec

24 files changed

Lines changed: 944 additions & 37 deletions
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: retire the list view's `filterableFields` — declared as the `userFilters.fields` shorthand, read as a Filter-panel whitelist; no per-view whitelist replaces it
6+
7+
**BREAKING** — `filterableFields` is removed from the list view
8+
(`ListViewSchema`, `ObjectListViewSchema` — a `defineView` container's
9+
`list` / `listViews`, an object's `listViews` — a view item record's list
10+
`config`, the flattened list overlay the `PUT /api/v1/meta/view` door accepts,
11+
and a page `list-view` node's `properties`). ADR-0049 enforce-or-remove;
12+
maintainer ruling C: the platform carries no per-view Filter-panel whitelist.
13+
14+
The key carried two meanings, and the declared one was never delivered. The
15+
spec described it as the legacy shorthand for `userFilters.fields` — the
16+
quick-filter bar above a list — and the Studio view form offered it as "Field
17+
names available for filtering". Its one renderer, objectui's list view, read it
18+
as something else: the whitelist of fields the Filter panel's "Add filter"
19+
offers, a field the object marks `hidden` included when named. No quick-filter
20+
chip ever came of it. Zero list views in this repo's examples, app packages,
21+
platform objects, scaffolding templates or QA apps authored the key.
22+
23+
### FROM → TO
24+
25+
| removed | what to write instead |
26+
| --- | --- |
27+
| `filterableFields: ['status', 'priority']` meant as a curated end-user filter bar | `userFilters: { element: 'dropdown', fields: [{ field: 'status' }, { field: 'priority' }] }` |
28+
| `filterableFields: [...]` meant to narrow the Filter panel | nothing — the Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered |
29+
30+
**The one-line fix: delete `filterableFields:` from every list view; if you
31+
wanted a curated filter bar, declare `userFilters` with those fields.** `os
32+
migrate meta --from 17` lists the mechanical edits for existing sources;
33+
`--write` applies the ones it can prove.
34+
35+
```ts
36+
// before — parsed clean; the console used it to narrow "Add filter"
37+
defineView({
38+
list: {
39+
type: 'grid', columns: ['subject', 'status'],
40+
filterableFields: ['status'],
41+
},
42+
});
43+
// after — a quick-filter bar for `status`; "Add filter" offers every readable field
44+
defineView({
45+
list: {
46+
type: 'grid', columns: ['subject', 'status'],
47+
userFilters: { element: 'dropdown', fields: [{ field: 'status' }] },
48+
},
49+
});
50+
```
51+
52+
**Stored rows.** A stored `view` row that carries the key replays without it at
53+
every rehydration seam (the D2 conversion below DROPS it), so it re-saves
54+
through the strict write door, and its Filter panel then offers every field the
55+
user may read — the ruled end state, not a rewrite. ⛔ No conversion turns the
56+
key into `userFilters.fields`: that would keep nothing an author saw (the
57+
whitelist would vanish and quick-filter chips would appear). An object row
58+
whose own `listViews` carries the key is reached by no conversion and is
59+
refused at its door until edited by hand.
60+
61+
⛔ **Untouched:** `userFilters` and `searchableFields` keep their meaning at
62+
every door.
63+
64+
### The retirement kit
65+
66+
- **A `retiredKey()` tombstone on the list-view shape**, beside
67+
`searchableFields`. Every door built from it refuses: `tsc` types the key
68+
`never`, and the parse raises the prescription (delete the key; `userFilters`
69+
for a curated bar; what the Filter panel offers) instead of a bare
70+
unknown-key report.
71+
- **D2 conversion `view-list-filterable-fields-removed`** (protocol 18, retired
72+
from the load path): strips `filterableFields` from every list payload in
73+
`stack.views[]`, in all three persisted spellings.
74+
- **D3 entry `list-view-filterable-fields-retired`** beside it, carrying the
75+
part no conversion can decide: whether the author wanted a curated
76+
`userFilters` bar.
77+
- **`RETIRED_KEYS_BY_MAJOR[18]`**: `ui/ListView:filterableFields`,
78+
`ui/ObjectListView:filterableFields`; both `authorable-surface/ui.json` rows
79+
become `[RETIRED]`.
80+
- **The Studio view form's `filterableFields` input** leaves with the key, and
81+
the extracted form-label bundles are regenerated.
82+
- **The liveness row flips `live` → `dead`**, re-measured, with a REMOVED note
83+
naming the one reader the ruling makes unreachable.
84+
- **The architecture concept page's list-view example** now declares a
85+
`userFilters` bar instead of the retired key.
86+
- **Pins** (`ui/view-list-filterable-fields-retirement.test.ts`): the refusal,
87+
its issue code, path and prescription at eight doors, each with a lit
88+
control and a `userFilters` / `searchableFields` boundary; the tsc channel;
89+
the conversion's reach, its never-a-rewrite guarantee, boundary and
90+
idempotence; the D2/D3 registration; and a tree-scoped absence walk over the
91+
declared radius.
92+
- **No deprecation window**, per the project's startup-stage posture.
93+
94+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec`
95+
is published, so this is breaking for consumers no telemetry was consulted for.
96+
97+
Clause-②: yes (narrowing)
98+
99+
<!-- adr-0087: registered view-list-filterable-fields-removed, list-view-filterable-fields-retired -->

‎content/docs/concepts/architecture.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ export const CustomerView = defineView({
242242
{ field: 'annual_revenue' },
243243
{ field: 'primary_contact' },
244244
],
245-
filterableFields: ['industry', 'annual_revenue'],
245+
// A quick-filter bar above the list: end users pick an industry to narrow the rows.
246+
userFilters: { element: 'dropdown', fields: [{ field: 'industry' }] },
246247
sort: [{ field: 'name', order: 'asc' }],
247248
},
248249
});

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,7 +1831,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
18311831
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) |
18321832
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | |
18331833
| **searchableFields** | `string[]` | optional | Fields enabled for search |
1834-
| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
1834+
| **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '<field_name>' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
18351835
| **resizable** | `boolean` | optional | Enable column resizing |
18361836
| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover |
18371837
| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration |
@@ -1917,7 +1917,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
19171917
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) |
19181918
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | |
19191919
| **searchableFields** | `string[]` | optional | Fields enabled for search |
1920-
| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
1920+
| **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '<field_name>' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
19211921
| **resizable** | `boolean` | optional | Enable column resizing |
19221922
| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover |
19231923
| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration |

‎content/docs/references/data/object.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ const result = ApiMethod.parse(data);
377377
| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) |
378378
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | |
379379
| **searchableFields** | `string[]` | optional | Fields enabled for search |
380-
| **filterableFields** | `string[]` | optional | Legacy shorthand for userFilters.fields — bare field names enabled for end-user filtering. Prefer userFilters |
380+
| **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '<field_name>' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
381381
| **resizable** | `boolean` | optional | Enable column resizing |
382382
| **compactToolbar** | `boolean` | optional | Collapse Group/Color/Density/Hide-fields into a single View settings popover |
383383
| **selection** | `{ type?: Enum<'none' \| 'single' \| 'multiple'> }` | optional | Row selection configuration |

0 commit comments

Comments
 (0)