Skip to content

Commit b42c621

Browse files
fix(lint,metadata-protocol,metadata-core)!: os lint and the react-page props check refuse the retired list-view filterableFields by name, and its residue readers go (#24878)
Closes #24728 Clause-②: yes (narrowing) ## What this does `@objectstack/spec` 18 retired the list view's `filterableFields` with a `retiredKey()` tombstone (#24640), so `os validate` and `os build` refuse it at the parse. `os lint` never parses, and a react page's source is parsed against no list-view schema, so the lint package still treated the key as a live field position. This PR removes every residue reader on the card's list, and turns the two that judge authoring input into a refusal by name that carries the tombstone's prescription. **`os lint` (list views).** `validate-list-view-field-refs.ts` no longer walks `filterableFields` (it is gone from the severity map `POSITIONS` and the dotted-path map `DOTTED_AXIS`). Instead every list-view rung that still carries the key reports a new rule, `list-view-key-retired` (`error`, at `...filterableFields`), whatever the key names and whether or not the object binding resolves. Before, on a JSON/YAML/raw source, a valid name passed `os lint` silently and a stale one was reported as `list-view-field-unknown`. The prescription is READ from `ListViewSchema`'s tombstone description (`[REMOVED] ...`), never retyped: the message carries its verdict head, the hint its "Delete the key" sentence, cut by the existing `retiredKeyRemedy` helper, the way the permission-residue rule already does it. The rule id is exported from the package root as `LIST_VIEW_KEY_RETIRED` (the package's own `rule-id-barrel-exports` test requires it). The keys reported are one frozen list, `LIST_VIEW_RETIRED_KEYS_REPORTED = ['filterableFields']`: the retired keys this rule used to read as field positions. **React-page props (done first, per triage).** `REACT_FIELD_SPECS.ListView.fields` no longer lists `filterableFields`. The ListView block now carries `retiredSchemaProps`, built from the same list and the same tombstone reader. Writing the prop is refused as `react-prop-retired` (`error`) with "is retired and no prop replaces it" plus the tombstone's verdict head and remedy. The old "covers the legacy filterableFields shorthand" test is replaced: a valid name and a stale name both get exactly one `react-prop-retired` and no `page-field-unknown`, the hint equals the remedy read from the tombstone, and a `userFilters` lit control is not refused. **The other residue readers.** - `validate-field-consumers.ts`: `filterableFields` leaves `BEHAVIOUR_SEGMENTS`. - `rule-explanations.ts`: `os explain list-view-field-unknown` and `list-view-field-dotted` no longer name it as a checked position. - `metadata-protocol/src/metadata-diagnostics.ts`: the dead walk is deleted, folded into the `tabs` retirement comment beside it. - `metadata-core/src/object-schema-fls-references.ts`: `LIST_VIEW_NAME_LISTS.filterableFields` is deleted. A body that still carries the key now goes the unclassified way and fails closed. Measured with a one-off probe against `applyObjectSchemaMask` (caller may read `id` and `title`): a list view whose `filterableFields` names the denied `secret_score` is dropped whole, and one naming only `title` is served unchanged. Output: `{"b":{"label":"B","columns":["title"],"filterableFields":["title"]}}`. No denied name leaks. - Fixtures: `objectql/src/metadata-diagnostics.test.ts` (the array-shaped-fields case now uses `userFilters.fields`) and the lint field-refs tests (`FULL_LIST_VIEW`, the per-position case table, the dotted filter-position case). **The ratchet.** `RESIDUE` in `packages/spec/src/ui/view-list-filterable-fields-retirement.test.ts` is now empty. All four entries left in this PR. The new pin file `packages/lint/src/list-view-filterable-fields-retired.test.ts` authors the key as a key on purpose, so it joins `EXCLUDED` by name with its reason, beside the spec pin it twins. The walk's offender list is `[]` and its anti-vacuity floors still hold (38/38 tests pass). ## Classification (changeset `.changeset/24728-filterable-fields-residue.md`) `@objectstack/lint` minor, `@objectstack/metadata-protocol` patch, `@objectstack/metadata-core` patch. **BREAKING (an authoring narrowing)**, shipped `minor` under the launch-window convention. - **Narrowing.** The react-page props check is a member of the reference-integrity suite with its default runtime types, so it runs on `os validate`, `os build` and `os lint` only, not on the page save door. A react page writing the ListView element's `filterableFields` prop used to pass all three commands whenever its names resolved. It is now refused. On list views, `os lint` now fails a raw source that `os validate` already refused. - **Widening of the public surface.** A new exported rule-id constant, `LIST_VIEW_KEY_RETIRED`. - So the line is `Clause-②: yes (narrowing)`. That follows the precedent of the two list-view door refusals that added an exported rule id (`list-view-object-outside-closure`, `list-view-object-unknown`). ⚠️ The claim's line read `Clause-②: no`. It was written before the rule id existed, and this PR carries the corrected line instead of copying it. - **ADR-0087 disposition:** `not-required (already-registered view-list-filterable-fields-removed, list-view-filterable-fields-retired)`. The key's tombstone, D2 conversion and D3 entry already exist (from #24640, same unreleased major), and this PR adds no key, conversion or stored shape. The changeset carries the FROM → TO table and the one-line fix. ## Verification (final head `7077ebc775`, the merge of `origin/main` `81120caac8`) - `pnpm --filter @objectstack/lint test`: 151 files / 6994 tests passed. `typecheck` (tsc + `check:test-typecheck`): pass. - On `5c3a09b412` (before the merge, which touched neither package): `pnpm --filter @objectstack/metadata-core test`: 18 files / 780 passed, plus typecheck. `pnpm --filter @objectstack/metadata-protocol test`: 251 files / 20828 passed (3 files / 19 tests skipped), plus typecheck. `pnpm --filter @objectstack/objectql typecheck`: pass, and `src/metadata-diagnostics.test.ts`: 10/10. - `pnpm --filter @objectstack/spec exec vitest run src/ui/view-list-filterable-fields-retirement.test.ts`: 38/38. `pnpm --filter @objectstack/spec check:generated`: "All 14 generated artifacts are up to date" (spec built first). - Gate families from `node scripts/pm/dispatch-gates.mjs --commands` (92 commands): all 92 run on `7077ebc775`, each exit 0, and `--ran` reconciliation answered "92 derived, 92 run, 0 NOT-MEASURED, 0 UNRUN". Among them: `check:adr-0087-registration` ("ADR-0087 exemption (already-registered)"), `check-changeset-no-major`, `check-empty-changeset`, `check:nul-bytes` and `check:cross-package-test-inputs`. - Reach through the `os lint` registry: `runAuthoringRules('lint', { normalized })` on an unparsed stack whose list view carries `filterableFields: ['status']` returns exactly one `list-view-key-retired` `error` at `views[0].list.filterableFields` (pinned in the new test file). A spawned `os lint` CLI run was NOT MEASURED: the CLI package was not built in this worktree. ## Acceptance notes - `packages/lint/src/reference-integrity-suite.ts` comments (around lines 56, 272 and 300) still list `filterableFields` among the positions the field-refs member walks. They are stale prose outside this claim's file surface, and no behaviour depends on them. Carrier: none. - The other `retiredKey()` tombstones on `ListViewSchema` (`pageName`, `tabs`, `responsive`, `performance`, `striped`, `bordered`, `virtualScroll`) are equally unjudged by `os lint` on a raw source and by the react-page props check. They were never field positions in this rule, so nothing here ever gave them a wrong answer. Whether the renderer still reads any of them as a react prop is NOT MEASURED. Extending to them is one entry each in `LIST_VIEW_RETIRED_KEYS_REPORTED`. Carrier: none. - Open PR #24858 also edits `packages/lint/src/rule-explanations.ts` and `validate-list-view-field-refs.test.ts`. Whichever lands second merges `origin/main` and keeps both sides. No other open PR touches these files (read over all 13 open PRs' file lists). - objectui is not touched. Its reader removal is the #24640 objectui follow-up. --- _Generated by [Claude Code](https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 94842ba commit b42c621

13 files changed

Lines changed: 382 additions & 53 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/lint': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/metadata-core': patch
5+
---
6+
7+
fix(lint)!: `os lint` and the react-page props check report the retired list-view `filterableFields` by name, and the readers that still treated it as a field position go
8+
9+
Clause-②: yes (narrowing)
10+
11+
<!-- adr-0087: not-required (already-registered view-list-filterable-fields-removed, list-view-filterable-fields-retired) The key's retirement, its D2 conversion and its D3 entry are already registered by the change that tombstoned it, which shipped in 18.0.0. This change adds no key, conversion or stored shape: it makes `os lint` and the react-page props check give the verdict `os validate` and `os build` already give the list-view key, with the tombstone's own prescription. -->
12+
13+
**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
`@objectstack/spec` 18 retired the list view's `filterableFields` with a `retiredKey()` tombstone, so `os validate` and `os build` refuse it at the parse. Two authoring checks still read it as a live field position:
16+
17+
- **`os lint`** never parses: it judges the normalized stack, and the conversion that strips the key is retired from the load path. On a JSON, YAML or raw-object source the key reached the list-view field-reference rule as written. A valid field name passed `os lint`, and a stale one was reported as an unknown field (`list-view-field-unknown` / `list-view-field-dotted`), never as the retirement.
18+
- **The react-page props check** read `<ListView filterableFields={[…]}>` as a field-bearing prop. A react page's source is parsed against no list-view schema, so `os validate`, `os build` and `os lint` passed the prop whenever its names resolved.
19+
20+
Both now refuse the key by name, as an `error`, whatever it names:
21+
22+
- a list view (every rung: an object's `listViews`, a `defineView` container's `list` / `listViews`, the flattened list overlay and the view item record) reports `list-view-key-retired` at `….filterableFields`, whether or not its object binding resolves;
23+
- `<ListView filterableFields>` on a react page reports `react-prop-retired`.
24+
25+
The message names the retirement and the hint is the tombstone's own "Delete the key" sentence, read from `ListViewSchema` rather than retyped. The new rule id is exported from the package root as `LIST_VIEW_KEY_RETIRED`.
26+
27+
### FROM → TO
28+
29+
| before | after |
30+
| --- | --- |
31+
| `filterableFields: ['status']` on a list view in a non-`defineStack` source: `os lint` passed | `os lint` fails with `list-view-key-retired` |
32+
| `filterableFields: ['ghost']` on such a list view: `list-view-field-unknown` | `list-view-key-retired` |
33+
| `<ListView filterableFields={['status']} />` on a react page: passed every authoring command | `react-prop-retired` |
34+
35+
**The one-line fix:** delete `filterableFields` (the key or the prop). For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: 'status' }] }`; the Filter panel offers every field the user may read. `os migrate meta --from 17` lists the edits for existing metadata sources; a react page's source is yours to edit by hand.
36+
37+
### The residue readers that go
38+
39+
- **`@objectstack/lint`**: the list-view field-reference rule no longer walks `filterableFields` as a filter position (its severity and dotted-path maps), the field-consumer rule no longer counts it as a behavioural read, and `os explain list-view-field-unknown` / `list-view-field-dotted` no longer name it as a checked position.
40+
- **`@objectstack/metadata-protocol`**: `computeViewReferenceDiagnostics` no longer walks `filterableFields`, the same deletion its retired sibling `tabs` had. A stored body has the key stripped by the conversion replay before it is served, and the write door refuses it.
41+
- **`@objectstack/metadata-core`**: the ADR-0106 field-security scrub no longer classifies `filterableFields` as a list-view name list. A body that still carries the key goes the unclassified way and fails closed: a list view whose `filterableFields` names a field the caller may not read is dropped whole, where the scrub used to remove the name.
42+
43+
⛔ **Untouched:** `userFilters` and `searchableFields` keep their meaning and their checks.

‎packages/lint/src/index.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,12 +496,15 @@ export type {
496496
// [#24668] Its FOURTH: a list view judged against the whole collection (a
497497
// runtime write that names no package) whose bound object exists nowhere —
498498
// refused the same way; `wholeCollection` is the statement.
499+
// Its FIFTH: a list view still carrying a key the spec retired, reported by
500+
// name with the tombstone's prescription (`os lint` never parses).
499501
export {
500502
validateListViewFieldRefs,
501503
LIST_VIEW_FIELD_UNKNOWN,
502504
LIST_VIEW_FIELD_DOTTED,
503505
LIST_VIEW_OBJECT_OUTSIDE_CLOSURE,
504506
LIST_VIEW_OBJECT_UNKNOWN,
507+
LIST_VIEW_KEY_RETIRED,
505508
} from './validate-list-view-field-refs.js';
506509
export type {
507510
ListViewFieldRefFinding,
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `os lint` reports the list view's retired `filterableFields` BY NAME.
5+
*
6+
* @objectstack/spec 18 retired the key with a `retiredKey()` tombstone, so
7+
* `os validate` and `os build` refuse it at the parse. `os lint` never parses:
8+
* it judges the `normalizeStackInput` output, and the D2 conversion that
9+
* strips the key is retired from the load path, so on a non-`defineStack`
10+
* source the key reaches the list-view field-reference rule as written. That
11+
* rule used to walk it as a field position — a valid name passed `os lint`,
12+
* and a stale one was named as an unknown FIELD. Now it reports the
13+
* retirement, once per list view, with the tombstone's own prescription.
14+
*
15+
* This file authors the retired key as a key on purpose, so the spec's
16+
* tree-scoped absence walk (`view-list-filterable-fields-retirement.test.ts`)
17+
* excludes it by name, as a member of the retirement kit.
18+
*/
19+
20+
import { describe, expect, it } from 'vitest';
21+
22+
import { runAuthoringRules } from './authoring-rules.js';
23+
import {
24+
LIST_VIEW_FIELD_DOTTED,
25+
LIST_VIEW_FIELD_UNKNOWN,
26+
LIST_VIEW_KEY_RETIRED,
27+
LIST_VIEW_RETIRED_KEYS_REPORTED,
28+
listViewRetiredKeyPrescription,
29+
validateListViewFieldRefs,
30+
} from './validate-list-view-field-refs.js';
31+
import { retiredKeyRemedy } from './validate-retired-permission-residue.js';
32+
33+
type AnyRec = Record<string, unknown>;
34+
35+
const task = {
36+
name: 'duly_task',
37+
fields: {
38+
title: { type: 'text' },
39+
status: { type: 'select', options: [{ label: 'Open', value: 'open' }] },
40+
owner: { type: 'lookup', reference: 'duly_person' },
41+
},
42+
};
43+
const person = { name: 'duly_person', fields: { name: { type: 'text' } } };
44+
45+
const listWith = (extra: AnyRec): AnyRec => ({
46+
type: 'grid',
47+
data: { provider: 'object', object: 'duly_task' },
48+
columns: ['title'],
49+
...extra,
50+
});
51+
52+
const stackWithList = (list: AnyRec, objects: unknown[] = [task, person]): AnyRec => ({
53+
objects,
54+
views: [{ name: 'task', objectName: 'duly_task', list }],
55+
});
56+
57+
const retiredOf = <F extends { rule: string }>(findings: F[]): F[] => findings.filter((f) => f.rule === LIST_VIEW_KEY_RETIRED);
58+
59+
describe('the list view\'s retired `filterableFields` is reported by name, never as a field position', () => {
60+
const prescription = listViewRetiredKeyPrescription('filterableFields');
61+
62+
it('the tombstone publishes its prescription, and the rule reports exactly the retired key it used to walk', () => {
63+
expect(prescription).not.toBeNull();
64+
expect(LIST_VIEW_RETIRED_KEYS_REPORTED).toEqual(['filterableFields']);
65+
// A key that is not a tombstone has no prescription to read.
66+
expect(listViewRetiredKeyPrescription('columns')).toBeNull();
67+
expect(listViewRetiredKeyPrescription('no_such_key')).toBeNull();
68+
});
69+
70+
it('a VALID field name is reported as the retirement, where it used to pass in silence', () => {
71+
const findings = validateListViewFieldRefs(stackWithList(listWith({ filterableFields: ['status'] })));
72+
expect(findings).toHaveLength(1);
73+
expect(findings[0]).toMatchObject({
74+
rule: LIST_VIEW_KEY_RETIRED,
75+
severity: 'error',
76+
path: 'views[0].list.filterableFields',
77+
});
78+
expect(findings[0]!.message).toContain('`filterableFields`');
79+
// Read from the tombstone, never retyped.
80+
expect(findings[0]!.hint).toBe(retiredKeyRemedy(prescription!));
81+
});
82+
83+
it('a stale or dotted name is reported as the retirement, never as a field reference', () => {
84+
const findings = validateListViewFieldRefs(
85+
stackWithList(listWith({ filterableFields: ['nope_field', 'owner.name'] })),
86+
);
87+
expect(findings.map((f) => f.rule)).toEqual([LIST_VIEW_KEY_RETIRED]);
88+
expect(findings.filter((f) => f.rule === LIST_VIEW_FIELD_UNKNOWN || f.rule === LIST_VIEW_FIELD_DOTTED)).toEqual([]);
89+
});
90+
91+
it('is reported at every list-view rung the rule walks', () => {
92+
const lv = listWith({ filterableFields: [] });
93+
const stack: AnyRec = {
94+
objects: [{ ...task, listViews: { mine: lv } }, person],
95+
views: [
96+
{ name: 'task', objectName: 'duly_task', list: lv, listViews: { open: lv } },
97+
{ name: 'duly_task.flat', object: 'duly_task', viewKind: 'list', ...lv },
98+
{ name: 'duly_task.item', object: 'duly_task', viewKind: 'list', config: lv },
99+
],
100+
};
101+
expect(retiredOf(validateListViewFieldRefs(stack)).map((f) => f.path).sort()).toEqual([
102+
'objects[0].listViews.mine.filterableFields',
103+
'views[0].list.filterableFields',
104+
'views[0].listViews.open.filterableFields',
105+
'views[1].filterableFields',
106+
'views[2].config.filterableFields',
107+
]);
108+
});
109+
110+
it('does not depend on the binding: an unresolvable object, a non-object provider and an empty graph all report it', () => {
111+
const unbound = validateListViewFieldRefs(
112+
stackWithList(listWith({ data: { provider: 'object', object: 'no_such_object' }, filterableFields: ['x'] })),
113+
);
114+
expect(retiredOf(unbound)).toHaveLength(1);
115+
const inline = validateListViewFieldRefs(
116+
stackWithList(listWith({ data: { provider: 'value', items: [] }, filterableFields: ['x'] })),
117+
);
118+
expect(retiredOf(inline)).toHaveLength(1);
119+
const noObjects = validateListViewFieldRefs(stackWithList(listWith({ filterableFields: ['x'] }), []));
120+
expect(retiredOf(noObjects)).toHaveLength(1);
121+
});
122+
123+
it('lit control: a list view without the key reports no retirement, and its surviving filter positions are still judged', () => {
124+
const findings = validateListViewFieldRefs(
125+
stackWithList(listWith({ userFilters: { element: 'dropdown', fields: [{ field: 'nope_field' }] } })),
126+
);
127+
expect(retiredOf(findings)).toEqual([]);
128+
expect(findings.map((f) => f.rule)).toEqual([LIST_VIEW_FIELD_UNKNOWN]);
129+
});
130+
131+
it('reaches `os lint`\'s registry run on the normalized (unparsed) stack, as an error', () => {
132+
const stack = stackWithList(listWith({ filterableFields: ['status'] }));
133+
const findings = runAuthoringRules('lint', { normalized: stack });
134+
const retired = findings.filter((f) => f.rule === LIST_VIEW_KEY_RETIRED);
135+
expect(retired).toHaveLength(1);
136+
expect(retired[0]).toMatchObject({ severity: 'error', path: 'views[0].list.filterableFields' });
137+
});
138+
});

‎packages/lint/src/rule-explanations.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,7 +3123,7 @@ const LIST_VIEW_FIELD_UNKNOWN_EXPLANATION: RuleExplanation = {
31233123
paragraphs: [
31243124
// The positions are held to `listViewWalkedPositions()` by the rule's test.
31253125
'Every field a list view names must be a field of its bound object: its `columns` (and a ' +
3126-
'column\'s `summary` and `prefix`), `filterableFields`, `hiddenFields`, `fieldOrder`, ' +
3126+
'column\'s `summary` and `prefix`), `hiddenFields`, `fieldOrder`, ' +
31273127
'`grouping`, `rowColor` and `userFilters`, the keys of its `filter` and of each ' +
31283128
'`userFilters.tabs[].filter`, and every binding inside a `kanban`, `calendar`, `gantt`, ' +
31293129
'`timeline`, `gallery`, `map` or `tree` block. Nothing resolves the name at render time: the ' +
@@ -3160,7 +3160,7 @@ const LIST_VIEW_FIELD_DOTTED_EXPLANATION: RuleExplanation = {
31603160
'dotted entry whatever its head (`assertProjectionHasNoDottedPaths` on the engine boundary, ' +
31613161
'`assertProjectionFieldsExist` at the REST ingress, both `400 INVALID_FIELD`), so the view\'s ' +
31623162
'FIRST fetch is refused, not rendered.',
3163-
'Filter: the view\'s `filter`, its `userFilters.tabs[].filter`, `filterableFields` and ' +
3163+
'Filter: the view\'s `filter`, its `userFilters.tabs[].filter` and ' +
31643164
'`userFilters.fields` are judged by the head\'s class, through the classifier the door ' +
31653165
'itself uses (`classifyDottedFilterHead`). A relation head stores the related record\'s id, ' +
31663166
'not an embedded document; a virtual head is computed on read, so no driver materialises a ' +

‎packages/lint/src/validate-field-consumers.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@ const PROSE_KEYS: ReadonlySet<string> = new Set([
304304
/** Inside a display root (and everywhere else), these path segments make a site behavioural. */
305305
const BEHAVIOUR_SEGMENTS: ReadonlySet<string> = new Set([
306306
'filter', 'filters', 'runtimeFilter', 'relatedListFilter', 'where', 'criteria', 'conditions',
307-
'condition', 'defaultFilter', 'userFilters', 'quickFilters', 'filterableFields',
307+
// `filterableFields` is not here: @objectstack/spec 18 retired the key and no
308+
// list view reads it, so a field it names is not read by it either.
309+
'condition', 'defaultFilter', 'userFilters', 'quickFilters',
308310
'sort', 'sortBy', 'defaultSort', 'grouping', 'groupBy', 'groupByField', 'groupField',
309311
'startField', 'endField', 'dateField', 'startDateField', 'endDateField', 'coverField',
310312
'titleField', 'colorField', 'latitudeField', 'longitudeField', 'locationField',

‎packages/lint/src/validate-list-view-field-refs.test.ts‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ const FULL_LIST_VIEW: AnyRec = {
9090
{ field: 'period_key', summary: { field: 'estimate' }, prefix: { field: 'status' } },
9191
],
9292
filter: [{ field: 'visible_from', operator: 'equals', value: '2026-01-01' }],
93-
filterableFields: ['status'],
9493
hiddenFields: ['business_unit'],
9594
fieldOrder: ['title', 'status'],
9695
grouping: { fields: [{ field: 'business_unit' }] },
@@ -297,7 +296,6 @@ describe('#14107 — every other walked position', () => {
297296
'views[0].list.columns[0].prefix.field',
298297
'warning',
299298
],
300-
[{ filterableFields: [BAD] }, 'views[0].list.filterableFields[0]', 'error'],
301299
[{ hiddenFields: [BAD] }, 'views[0].list.hiddenFields[0]', 'warning'],
302300
[{ fieldOrder: [BAD] }, 'views[0].list.fieldOrder[0]', 'warning'],
303301
[{ rowColor: { field: BAD } }, 'views[0].list.rowColor.field', 'warning'],
@@ -735,19 +733,19 @@ describe('#14282 — a dotted key the FILTER door refuses, and the ones it serve
735733
expect(findings.every((f) => f.rule === LIST_VIEW_FIELD_DOTTED)).toBe(true);
736734
});
737735

738-
it('the two positions that DECLARE end-user filterable names are judged', () => {
736+
it('the position that DECLARES end-user filterable names is judged', () => {
739737
// objectui folds the resulting conditions into the fetched query
740-
// (`buildEffectiveFilter`), so these names become filter keys.
738+
// (`buildEffectiveFilter`), so these names become filter keys. Its retired
739+
// sibling `filterableFields` is reported by name instead
740+
// (`list-view-filterable-fields-retired.test.ts`).
741741
const findings = validateListViewFieldRefs(
742742
stackWith(
743743
mutate({
744-
filterableFields: ['owner.name'],
745744
userFilters: { fields: [{ field: 'parent.title' }] },
746745
}),
747746
),
748747
);
749748
expect(idsOf(findings).sort()).toEqual([
750-
'views[0].list.filterableFields[0]',
751749
'views[0].list.userFilters.fields[0].field',
752750
]);
753751
expect(findings.every((f) => f.rule === LIST_VIEW_FIELD_DOTTED)).toBe(true);

0 commit comments

Comments
 (0)