Repository navigation
fix(fields): FLS-filter the lookup dropdown's and record picker's drawn columns; gate PeoplePicker's $expand (#10373) - #10411
Conversation
…wn columns; gate PeoplePicker's $expand
Under the renderer-side FLS rulings ("FLS gates the OUTPUT"), RelatedList
filters both its $expand and the columns it draws. In @object-ui/fields only
the $expand lists were gated:
- LookupField: the dropdown's previewed columns are now filtered by
checkField(referenceTo, field, 'read') once the policy has loaded; the id
column is kept, the option label is not a preview column.
- RecordPickerDialog: the drawn columns (header, cells, skeleton, renderGrid
slot) are filtered on objectName; the display and id columns are kept.
- PeoplePicker: the $expand list (derived from dotted subtitle paths, or the
caller's expand) drops relations denied on objectName.
Before the policy loads nothing is filtered; perms is in each memo's deps.
The $expand memos of the dropdown and picker are unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…tches through a click Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…o pending changesets Patch round 1 of objectui#10373 (seat answers on the card): - RecordPickerDialog: the display column is filtered like any other column (keepReadableColumns has no title exemption). The id column never is; when the policy leaves no column to draw, the id column is drawn so every row stays selectable. The display column's titleFormat reads the row with the denied fields removed. - LookupField: recordToOption builds the option label from the row with the denied fields removed (the row FieldMasker serves), so a denied display field or titleFormat token falls through the existing chain. The value, description and carried record are the row as served. - .changeset/10223-lookup-candidates-expand.md and .changeset/lookup-dropdown-cell-renderer-5492.md: one sentence each made false by this change is corrected; frontmatter untouched. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
ℹ️ Console Performance Budget — not measuredThis run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget. This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.
See the workflow run for details. No package size report: it is only generated from a complete package build, so a partial one is never shown. |
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
… the package tsc) The fields build's `tsc` (tsconfig.json) reported TS18047 "'shown' is possibly 'null'" at both copies of the helper: the lazily created copy was narrowed by an `if` inside a loop, which the compiler does not carry to the `delete`. The helper now builds the shown row unconditionally from Object.entries and returns it only when a field was withheld, so the input comes back untouched otherwise — the identity the label override relies on. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
… literal `recordToOption` still emits `description` for fetched records; the option literal now binds to `const option` so the label can be rebuilt from the shown row (objectui#10373). The pin's meaning is unchanged: the widget still reads and emits the declared `description` key. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10373
Clause-②: no
What changes
Head:
2322673f3. The renderer-side FLS rulings objectui#7215 / objectui#7230 say "FLS gates the OUTPUT", and the objectui#7429 sweep applied them.RelatedListgates its$expandand every column it draws (keepReadableColumns), with no exception for the title. In@object-ui/fieldsonly the$expandlists were gated. This PR applies the family's one shape,!perms.isLoaded || perms.checkField(object, f, 'read')withpermsin the memo deps, to everything these three widgets draw or request:LookupFielddropdown columns. A newreadablePreviewColumnsmemo, whichpreviewOfrenders, filters onreferenceTo. That is the objectcandidateExpandalready judges. The id column is kept.LookupFieldoption label.recordToOptionbuilds the label from the row with the policy-denied fields removed (fieldReadGate/withoutDeniedFields). That is the row ObjectStack'sFieldMaskeralready serves, so on ObjectStack no label changes.titleFormattemplate, thengetRecordDisplayName, then the other name-like keys, then the id. AtitleFormattoken naming a denied field renders as an empty slot.recordToOptioncall applies the gate, so the chip and the read-only rendering show the same label.onSelectRecordreceives keeps the served row's other fields; only itslabelchanges.RecordPickerDialog. A newreadableColumnsmemo, filtered onobjectName, feeds the header, skeleton, cells andrenderGridslot.titleFormatreads the row with the denied fields removed.getRecordId), andonSelectRecordsis unchanged.PeoplePicker.effectiveExpanddrops relations denied onobjectName, the object the picker queries. This applies both when the list is derived from subtitle paths and when the caller passesexpand.The
$expandmemos of the dropdown and the picker are unchanged frommain. They read the unfiltered column list and gate their own output, in the objectui#7429 shape. Both lists askcheckFieldabout the same names on the same object.Why the display matters beyond
$expand. A denied relation column was left out of$expandand arrived as a bare key. The lookup cell renderer then resolved that key with its ownfindOne, one per row. The pins assert zero such reads, and that the column is not drawn.Surface additions, declared and approved by the seat on the card, claim amended:
.changeset/10223-lookup-candidates-expand.md: body corrected, frontmatter byte-identical. Its last FLS sentence said a column left out of$expand"is resolved one by one as before". It now says a column the policy denies is not drawn, and a field it denies is not shown in an option's label or the picker's title column..changeset/lookup-dropdown-cell-renderer-5492.md: body corrected, frontmatter byte-identical. Its last paragraph said a slot is dropped "only when the record holds no value". That now holds among the columns the policy lets the user read.packages/types/src/__tests__/field-metadata-rows-option-description-6140.test.ts: one anchor string moved, forced by this diff; the pin's meaning is unchanged. The read site still exists:recordToOptionstill emitsdescription, now inconst option = { value: val, label: String(label), description, ...record };. A repo-wide fixed-string grep of every removed line of the three sources found no other source-text anchor.Two CI fixes on this PR.
346275b07. The packagetscreported "'shown' is possibly 'null'" at both copies ofwithoutDeniedFields. The helper now builds the shown row fromObject.entriesand returns it only when a field was withheld. There is no nullable accumulator and no non-null assertion. Evidence:cd packages/fields && pnpm exec tsc -p tsconfig.json --noEmitexits 0 atdc2488fb3.346275b07sources reproduces both CI errors, exit 2.tsc -p tsconfig.test.jsonon those sources also reports both errors, so the test program would have caught this too. It was not run at346275b07before that push.0e3b8bb13. Fixed as described above.Verification (head
2322673f3)LookupField.displayFls-10373,RecordPickerDialog.displayFls-10373andPeoplePicker.expandFls-10373are part of the targeted run below. EveryLookupField*/RecordPickerDialog*/PeoplePicker*suite, plus the four suites that read these sources as text (6140, 6153,relationalMetaCopySet.derivation,check-control-bytes), gaveTest Files 25 passed (25)andTests 200 passed (200).@object-ui/fieldssuite (pnpm exec vitest run packages/fields/from the repo root):Test Files 184 passed | 1 skipped (185),Tests 3082 passed | 7 skipped (3089).2b1f7fd51, rerun at2322673f3. The two sources were checked out from2b1f7fd51, each blob hash was verified on disk, and a trap restored them fromHEAD. Result:Tests 6 failed | 16 passed (22). The 6 are the new display-field andtitleFormatpins. The dropdown label differs from the stripping backend's; the picker drawsName; eachtitleFormatshowsS-0. After the run,git diff HEADwas empty.721d1e008, with all three sources checked out:Tests 15 failed | 7 passed (22). The 7 greens are the controls whose names start with "control:".pnpm --filter @object-ui/fields build(tsc && vite build && node scripts/build-css.mjs): exit 0.pnpm --filter @object-ui/fields type-check(tsc --noEmit && tsc -p tsconfig.test.json): exit 0, run after the closure build.eslint --no-inline-configon the 7 touched.ts/.tsxfiles gives 0 errors. For each source, the per-rule warning counts equal the base content's (61 / 36 / 17; the 6140 test 0 / 0).eslint.config.jshas no type-aware parser options, so this diff cannot move a verdict on an untouched file. A repo-widepnpm lintis left to CI.check-changeset-presence,check-changeset-no-major,check-changeset-overwrite,check:new-line-citations(0 new),check:changeset-claims,check:control-bytes,check:test-path-roots,check:phantom-deps,check:unused-deps,check:pending-changeset-literals.check-changeset-overwritereports the two corrected bodies as its case 2 (correcting on purpose). It is report-only.check-governed-queue-guard --testreports NOT GOVERNED.Acceptance notes
MePermissionsProvidermounts children only after its first answer, so that path matters only on a later change of policy.PeoplePickerrows draw plain subtitle fields such asemail, and the avatar, without an FLS read.formatRecordTitleconsumers, is the seat's own card..changeset/10373-fields-display-fls.md, apatchfor@object-ui/fields.Generated by Claude Code