Repository navigation
fix(fields): PeoplePicker rows, its tray and the user cell draw a person's name and avatar from the fields the viewer may read (objectui#10535) - #10624
Conversation
…son's name and avatar from the fields the viewer may read PeoplePicker read the person name down its ladder (display field, then name, username, label) straight off the served row, so a name the loaded policy denies on the queried object printed in the row, the tray chip, the avatar alt and the chip's remove label. The read-only user cell drew name and image of an expanded person the same way, single and multi. - personDisplay: getPersonNameFields names the ladder; getPersonName also takes the ladder, already filtered. - PeoplePicker: readableNameFields filters that ladder with the same gate the subtitle and avatar use; PersonRow and SelectionTray take it as nameFields. Records stay as served; the name is derived per render. - UserCellRenderer: reads each person through the module's existing withoutDeniedFields on reference_to / reference / sys_user, with usePermissions called before any early return. The denied-name fallback is the lookup option label's: skip the denied rung, fall through to the next readable one, as for a stripping backend's row. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…known in the 10535 pin The objectui types declare only `reference_to` on a user field; the pin also reads the spec's `reference`, the key a grid column forwards. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ 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
|
…CellRenderer sentence
The pending 7166 entry says `UserCellRenderer` destructures `{ value }`
and reads no field meta. After objectui#10535 it destructures
`{ value, field }` and reads `reference_to` / `reference` to name the
person's object; it still reads none of the three retired keys. An
insert-only in-release note, frontmatter byte-identical.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ 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
|
Fixes #10535
Clause-②: no
What changes
Head
78005aa7e, base21d34d5e2. Session:https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC(domain:uiseat 1, claim5831720599).The renderer-side FLS rulings objectui#7215 / objectui#7230 say "FLS gates the OUTPUT". objectui#10433 gated the subtitle and the avatar a person row and the tray draw. Two outputs of the same family were still drawn straight off the served row, and this PR gates both.
getPersonNamereads the name down a ladder: the display field, thenname,username,label. Since objectui#10433, PeoplePicker gates each subtitle path and the avatar field with!perms.isLoaded || perms.checkField(objectName, f.split('.')[0], 'read'), withpermsin the memo deps.readableNameFields).PersonRowandSelectionTrayread the name from that ladder, through a newnameFieldsprop.getPersonNameFieldsinpersonDisplay.ts.getPersonNamealso accepts an already-filtered ladder, and its existing single-field calls are unchanged.onSelectandonSelectRecordsreceive the same values as before. Nothing caches a resolved name: the name is derived on every render, so a policy that arrives later relabels the rows and chips already on screen.alt, the initials and the chip's remove label, so those follow the gate.UserCellRenderer, single and multi. Each person row goes through the module-privatewithoutDeniedFields(record, policy, objectName)that PR objectui#10592 added to the same file. The filter runs before anything is drawn: the empty-row check, the name (name, thenusername, thenUser), the initials, and theimagesrc.usePermissions()is called first, before any early return.reference_to, thenreference, thensys_user.objectNameisLookupField'sreferenceTo(reference_toorreference), read from the meta thatUserFieldnormalises toreference || reference_to || 'sys_user'. That resolves to the same object.LookupCellRendererin the same file reads the same two spellings.The denied-name fallback (the triage's condition)
I checked it in
LookupField.tsxrecordToOption(objectui#10411). The option label is built fromwithoutDeniedFields(record, readable), and the existing label chain falls through "exactly as it does for that row", meaning the row ObjectStack'sFieldMaskerserves. The lookup cell's name ladder from objectui#10501 uses the same wording. That fallback works by skipping each denied rung, so the label falls to the next readable one. It does not add a new rung.The person surfaces now follow the same fallback. A denied rung is skipped and the next readable one shows. When nothing is readable, each surface shows the floor it already had: the row and the tray show
—with?initials, and the cell showsUser. Each surface draws exactly what it already draws today for a stripping backend's row, and the pins assert that byte for byte (gated equals stripped).No new export, and no new copy of the row filter (objectui#10594)
personDisplay.ts,PersonRow,SelectionTrayorPeoplePicker: itsexport *list names none of them, and onlyLookupField.tsximportsPeoplePicker.getPersonNameFieldsis package-internal.index.tsx. PeoplePicker's name gate is a field-LIST filter, the shape objectui#10594 puts out of scope.git grep -c -E "function (withoutDeniedFields|readableRow|fieldReadGate)"overpackages/*/src(tests excluded) counts 7 at the base and 7 at the head.Pins (real
PermissionProvider, backends that do not strip)packages/fields/src/widgets/PeoplePicker.nameFls-10535.test.tsx, onsys_user:name;packages/fields/src/__tests__/userCell.readGate-10535.test.tsx:reference_to/reference, and asys_userpolicy says nothing about a field that points elsewhere (the defaultsys_userleg is the single and multi tests, whose field names no object);LoadedImagereports every image as loaded (the objectui#10433 technique), so animgrenders exactly when a URL reaches the Avatar. The controls assert thatimg.Verification
Tests 8 failed | 5 passed (13). The row drewalt="Amy Lin" src="http://x/amy.png"and the textAmy Lin. The cell drewtitle="Amy Lin"andsrc="http://x/amy.png".@object-ui/fieldssuite at78005aa7e, run aspnpm exec vitest run --maxWorkers=2 packages/fields/from the repo root:Test Files 205 passed | 1 skipped (206),Tests 3360 passed | 7 skipped (3367).6afa54c6e(the fix commit; later commits change only the two pin files' typing). The ablation removed both gates throughablation-replace.mjs, which checks every leg on disk:PeoplePicker.tsx: thereadableNameFieldsfilter became the unfiltered ladder. Anchor count went 1 to 0, and the blob went1b3a98f4515atocf58c6203ebd.index.tsx: bothwithoutDeniedFields(..., perms, personObject)calls were handed a not-loaded policy. Anchor count went 2 to 0, and the blob went6fdf855a20d7toec6d0e5f19b0.Tests 8 failed | 5 passed (13). The 8 red tests are every denial and relabel pin. The 5 green are the 4 controls plus "can still be chosen".blob after restore == blob at HEAD,git diff HEADempty), andgit status --porcelainwas empty afterwards.pnpm --workspace-concurrency=2 --filter '@object-ui/fields^...' build(exit 0). Thenpnpm --filter @object-ui/fields type-checkexited 0 (tsc --noEmit && tsc -p tsconfig.test.json), andtsc -p tsconfig.test.jsonre-ran clean on the head's bytes.--listFilesshows both new pin files in the test program.--no-inline-config, per-rule counts, head vs base) over the 5 edited sources: identical on every file.react-hooks/exhaustive-depsis 0 at both. The two new pin files have 0 messages.78005aa7e:pnpm check:control-bytes✅,pnpm check:new-line-citations(0 new),node scripts/check-changeset-presence.mjs✅ (1 changeset),pnpm check:changeset-claimsexit 0 (see Acceptance notes). Also green:check:pending-changeset-literals,check:test-path-roots,check:vi-mock-specifiers,check:unreferenced-sources,check:shell-escape-residue,check:i18n-keys,check:handler-key-reads..changeset/10535-person-name-read-gate.md,@object-ui/fieldspatch.Acceptance notes
16d126a58)..changeset/7166-retire-inert-fieldmeta-copies.mdsaid: "ausercolumn resolves toUserCellRenderer, which destructures{ value }and reads no field meta at all." After this PR, the renderer destructures{ value, field }and readsreference_to/reference.check-changeset-overwritecase 2). The paragraph's point still holds: none of the three keys it retires has a reader on the cell path.6625-retire-fieldmeta-decimals,6694-dashboard-lookup-reference-metaand6837-reference-to-arm-deletionstay true.PersonRow/SelectionTrayinPeoplePicker.tsx. They live inPersonRow.tsx/SelectionTray.tsx, and this PR touches them for the name path only.personDisplay.ts's module header says its helpers are shared by "the read-only user cell renderer".UserCellRendererimports none of them; its own ladder isname || username || 'User'. That line predates this PR and I left it as is.referenceis the spec's spelling and the key a grid column forwards.UserFieldMetadatain the objectui types declares neither spelling, so the pin casts. These are the same two spellingsLookupCellRendererreads, and objectui#6837 left thoseFieldMetadatareaders alone.MePermissionsProviderrenders itsloadingFallbackuntil it has loaded, so in the console these surfaces mount under a policy that has already loaded. The pins follow the objectui#10501 precedent, a policy that changes after mount (from open to denying) on the same mounted nodes.FieldMaskeralready strips denied fields server-side, and on its rows nothing changes.Generated by Claude Code