Skip to content

fix(fields)!: the record picker's display column labels a record the way the lookup dropdown does (objectui#10486) - #10590

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10486-picker-column-display-name
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-10486-picker-column-display-name

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10486
Clause-②: yes

The browse-all record picker's display column now labels a record exactly as the lookup dropdown's option label and the read cell do: through @object-ui/core's getRecordDisplayName, with the same inputs and the same row. RecordPickerDialog's published titleFormat prop is replaced by objectSchema (breaking; the changeset is minor under this repo's version policy).

Authority

Ruling C1 on objectui#9436, verbatim (comment 5657441402, ratified in 5814246926):

Ruling recorded — C1: PageHeaderRenderer aligns to the protocol — the declared pointer (nameField / displayNameField) outranks titleFormat in the header exactly as it does in getRecordDisplayName; the pin titleFormat still outranks nameField is rewritten to assert the opposite (director seat class-one adjudication, 2026-09-14)

  1. In PageHeaderRenderer, the unified resolver (getRecordDisplayName) is consulted BEFORE the interpolated titleFormat; the template remains the fallback the contract assigns it — below the declared pointer, above bare record-key guesses.

The lookup branch of that ruling (objectui#10343, landed as PR objectui#10479) put the dropdown's option label on the order: the field's own displayField, then the object's nameField, then the deprecated titleFormat. The triage on this card (comment 5828316879), verbatim: 「route the column through the same getRecordDisplayName call as the dropdown and the read cell; ⛔ not a third copy of the ladder.」

What changed

  • lookupColumnDisplay.tsx: LookupColumnRenderContext carries objectSchema and titleField in place of titleFormat. Step 1 of renderLookupColumnValue renders getRecordDisplayName(objectSchema, row, { titleField }) for the display column whenever a schema is given, not only when a template is declared. It uses recordToOption's floor exclusion: an Untitled or Record #id answer is not a title, so the cell falls through to the column's own renderer. The private single-brace template renderer and its EMPTY_SLOT sentinel are deleted (0 readers left; git grep for EMPTY_SLOT and refTitleFormat under packages/ exits 1, and the control git grep -c renderLookupColumnValue hits).
  • RecordPickerDialog.tsx: the titleFormat prop is removed and objectSchema is added. displayField is now the DECLARED display field; the display column is keyed on displayField ?? 'name', and only the declared value reaches the resolver as titleField. renderCellContent hands step 1 the same row as before (objectui#10223 / objectui#10373): relations collapsed by toPredicateRecord, fields the loaded policy denies removed by withoutDeniedFields. That is the row the dropdown's recordToOption labels from.
  • LookupField.tsx (the one widened surface, in two spots only): the refTitleFormat memo is deleted, and the RecordPickerDialog JSX passes displayField={declaredDisplayField} and objectSchema={refObjectSchema}.
  • The two pins that passed titleFormat= straight to the dialog now pass objectSchema= with unchanged assertions. They also drop their explicit displayField="name": their rows carry a name value, so a declared name would rank above the template, which is what the migrated case means by "no declared display field". The pins are LookupField.candidateExpand-10223 'picker: same title cells, same task cells, same records handed to onSelectRecords' and RecordPickerDialog.displayFls-10373 (its two titleFormat cases).
  • New pins in LookupField.pickerColumnOrder-10486.test.tsx: each reads both surfaces off one mounted LookupField.

Before and after (the new pins)

Fixture: fields contract_no / name / account / short_code; rows c1 = HT-001 / Acme / Globex / AC and c2 = HT-002 / Initech / Umbrella / IN.

case dropdown picker column at base picker column now
nameField: contract_no + titleFormat: '{account} / {contract_no}' (agreement pin) HT-001 Globex / HT-001 HT-001
the same template in its double-brace spelling, no nameField Globex / HT-001 {Globex} / {HT-001} Globex / HT-001
single-brace template alone (lit control) Globex / HT-001 Globex / HT-001 Globex / HT-001
the lookup declares displayField: short_code AC Globex / HT-001 AC
nameField only, rows carry name HT-001 Acme HT-001
nameField only, no name field HT-001 (blank) HT-001

Breaking change

RecordPickerDialog is re-exported from the @object-ui/fields entry, so its props are published. titleFormat is gone, with no dual spelling and no fallback built from it: pass objectSchema, the referenced object's schema, instead. Pass displayField only when the lookup declares one. The column also moves for nameField-only objects. Callers re-checked at 8740e86: only LookupField passed titleFormat (the only non-test titleFormat= hit). AccessExplainPanel, AssignedUsersSection and RelatedList render the dialog without it and pass no objectSchema, so their display columns render exactly as before. PeoplePicker imports only lookupFiltersToRecord, and LocationField only names the dialog in a comment.

Verification (head 3e3ad60)

  • pnpm exec vitest run --maxWorkers=2 packages/fields/ (through the verify lock): Test Files 201 passed, 1 skipped (202); Tests 3315 passed, 7 skipped; exit 0.
  • pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: Tasks 12 successful, 12 total; @object-ui/fields:type-check cache miss, executed; exit 0. tsc -p tsconfig.test.json --listFiles includes the three touched test files.
  • eslint on the 6 changed source and test files: 0 errors, 119 warnings, exit 0. Population: ESLint.isPathIgnored is false for 6 of 6. Count: --format json gives 6 results. Invariance: eslint.config.js sets no parserOptions (no type-aware linting), and no eslint-rules/ rule reads another file.
  • Exit 0 for each of check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new citations), check:changeset-claims (report-only), check:pending-changeset-literals, check:phantom-deps, check-changeset-presence.mjs (6 source files of 1 released package, 1 changeset), check-changeset-no-major, check-changeset-fixed and check-changeset-overwrite.
  • Reverse verification, one-shot, with the fix committed first (20b031b). The three implementation files were swapped to their 8740e86 blobs, and the swap was proven by the disk blob equalling the base blob for each file, plus marker counts: getRecordDisplayName 0 and EMPTY_SLOT 5 in lookupColumnDisplay.tsx. The new pin file plus the two migrated pins then ran: exit 1, 8 failed and 23 passed (31). Red: the five LookupField-mounted cases above other than the lit control, the unit control 'a record with a title renders it in the display column only', the objectui#10223 picker pin, and the objectui#10373 lit-control case. The restore (git checkout HEAD --, under a trap) was proven by the disk blob equalling the HEAD blob for all three files and an empty git diff HEAD. The same files at the fixed head: exit 0, 39 passed (five files). Predicted before the run: 9 red. Observed: 8 (see Acceptance notes).

Acceptance notes

  • The objectui#10373 case 'a titleFormat naming a denied field does not render it in the display column' stayed green under the reverse verification. Its expected cells equal what the column renders with no template at all. Its lit twin in the same file went red, so the pair still bites; the weakness predates this PR.
  • Two pending changesets describe the tree before this change: the last paragraph of the objectui#10343 one (the picker column "still shows the template"), and the objectui#6874 one's titleFormat={refTitleFormat} sentence. This PR's changeset names both. A prose correction of those two files is outside the claimed surface and is left to the seat.
  • The withoutDeniedFields doc comment in LookupField.tsx still says the picker applies the rule to "its display column's titleFormat". It now applies it to the display column's title. That comment sits outside the two fenced spots; noted, not edited.
  • The floor test (Untitled / Record #id) is spelled out beside recordToOption's and the read cell's. It is a floor check, not the ladder. Core exports no floor predicate, and record-title.ts is fenced.
  • A probe of a declared displayField naming a relation, against a backend that does not honour $expand, read [org1, org2] in both the dropdown and the picker, at base and at this head, so no change was measured. The expanding-backend case is NOT MEASURED.
  • The branch was merged with origin/main at 8740e86. Six commits have landed on main since, and none touches packages/fields or the two changesets named above (git diff --stat is empty).

Generated by Claude Code

…way the lookup dropdown does

The browse-all picker's display column rendered a bare titleFormat string
through its own single-brace renderer, above the referenced object's nameField
and above the lookup's declared displayField, and never read nameField when
no template was declared. The dropdown option label already goes through
getRecordDisplayName (the lookup branch of ruling C1 on objectui#9436), so
one record read two ways.

RecordPickerDialog's titleFormat prop is replaced by objectSchema, and
LookupField passes the referenced object's schema and its declared
displayField. The display column now renders
getRecordDisplayName(objectSchema, row, { titleField }) on the row the
dropdown labels from, with the dropdown's floor exclusion. The private
template renderer and its EMPTY_SLOT sentinel are gone.

The two pins that passed titleFormat to the dialog move to objectSchema
with unchanged assertions; new pins read both surfaces off one mounted
LookupField.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…cker column before objectui#10486

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/lookup-dropdown-cell-renderer-5492.md

  • names widgets/lookupColumnDisplay.tsx → packages/fields/src/widgets/lookupColumnDisplay.tsx — edited by this change

    Both surfaces now call one shared module — widgets/lookupColumnDisplay.tsx, which owns column normalisation, the field-descriptor enrichment from the referenced object's schema, and the render itself. The picker's own renderCellContent and columnFieldDescriptors are now thin calls into it, so there is a single renderer left to drift from. The dropdown's extra columns are rendered into the option row itself; the row's title keeps the full option label, which is what a truncated label needs, instead of a raw-value dump.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 21d34d5e2 (merge-base with origin/main): 6 file(s) changed outside .changeset/, read against 1467 pending declaration(s) that publish a body (2050 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3046.9 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-CtppqXB-.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.30KB 130.42KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 258.39KB 65.47KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.59KB 61.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…ribe the picker column as objectui#10486 leaves it

Prose only; both front matters are unchanged. The objectui#10343 upgrade
paragraph's last sentence said the browse-all picker's display column still
showed the template; it now says objectui#10486 moved that column onto the
dropdown's resolver. The objectui#6874 sentence naming the single
titleFormat= pass now names the objectSchema={refObjectSchema} pass, and
keeps its conclusion. This PR's own changeset stops calling them stale.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3047.3 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-BEwAkr4i.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 546.37KB 130.65KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.05KB 65.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.60KB 61.49KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f75aa9aea31de08b089b5379e974c852775a07b4

Reviewed against merge-base 8740e86cedefb8b568011fbc3d158e0d3f504983 (the PR's three commits 20b031b, 3e3ad60, f75aa9a sit directly on it; no merge commit). All line numbers are at the head. Nothing in the os-dev-report comments was taken as evidence; every claim below was re-derived from the diff, the card, ruling C1 (objectui#9436 comment 5657441402, ratified 5814246926), the triage (5828316879), the amended claim (5830222755), AGENTS.md 版本号策略 and .changeset/config.json.

① Derived judgments

Behaviour changes the diff introduces

  1. packages/fields/src/widgets/lookupColumnDisplay.tsx:201-207 — step 1 of renderLookupColumnValue now calls getRecordDisplayName(objectSchema, record, { titleField }) for the display column whenever ctx.objectSchema is given, and returns it unless it is the resolver's floor (Untitled or Record # plus the id). RIGHT. This is the exact call recordToOption makes (LookupField.tsx:152, { titleField: displayField } where that argument is the DECLARED display field) and the read cell makes (packages/fields/src/index.tsx:180). Order per packages/core/src/utils/record-title.ts:579-640: declared titleField, nameField and aliases, titleFormat, type-aware derivation, name-ish record keys, floor. That is the lookup branch of ruling C1 the triage bound: 「route the column through the same getRecordDisplayName call as the dropdown and the read cell; ⛔ not a third copy of the ladder.」 The ladder is not copied; only the three-line floor predicate is (lines 203-205 mirror LookupField.tsx:153-156), which is a floor check, not a rung. Core's record-title.ts is fenced by the claim and is untouched.
  2. lookupColumnDisplay.tsx (base lines 154-208) — the private single-brace template renderer and its EMPTY_SLOT sentinel are deleted. RIGHT, and the caller count the claim asked for holds: git grep for EMPTY_SLOT and refTitleFormat under packages/ and apps/ at the head returns nothing. The double-brace defect the card names is fixed by construction, since core's formatTitleTemplate (record-title.ts:183) normalizes {{field}} to {field}, as the spec 17.4.0 titleFormat describe says every title renderer does.
  3. lookupColumnDisplay.tsx:155,162 — LookupColumnRenderContext gains objectSchema and titleField and loses titleFormat. RIGHT. LookupColumnRenderContext is not re-exported from packages/fields/src/index.tsx (only ./widgets/RecordPickerDialog.js is, at line 4320), so this type is not a published surface. The two other test callers of renderLookupColumnValue (date-carrier-unparsable-8809, fields-date-widget-convention-8194) pass neither key and are unaffected.
  4. packages/fields/src/widgets/RecordPickerDialog.tsx:316 — the published titleFormat prop is removed and objectSchema?: Record of unknown | null is added; no fallback is built from a template. RIGHT under the amended claim: 「⛔ no dual spelling, ⛔ no objectSchema ?? { titleFormat } fallback (option B)」.
  5. RecordPickerDialog.tsx:301,485,502 — displayField is now the DECLARED value; const displayField = declaredDisplayField ?? 'name' keys the column, and only declaredDisplayField reaches the resolver as titleField (line 905). RIGHT. This is what makes the name guess rank below the object's declarations, the same rule LookupField.tsx:331-333 applies to the dropdown. Every other in-file use of displayField (default columns at 541, deps at 910) reads the resolved key, so quick-search, default columns and sort are unchanged.
  6. RecordPickerDialog.tsx:897-899 — the denied-stripped, relation-collapsed row is now handed to step 1 when objectSchema is given rather than when titleFormat was. RIGHT. withoutDeniedFields(toPredicateRecord(record, fieldsMeta), perms, objectName, idField) (line 451-467) is the same rule as LookupField's withoutDeniedFields plus fieldReadGate (LookupField.tsx:197-223), on the same toPredicateRecord(r, refObjectSchema?.fields) row the dropdown labels from (LookupField.tsx:699-701), with fieldsMeta={refObjectSchema?.fields} still passed at LookupField.tsx:1739.
  7. packages/fields/src/widgets/LookupField.tsx:1729-1730 — the RecordPickerDialog JSX passes displayField={declaredDisplayField} and objectSchema={refObjectSchema}; the refTitleFormat memo (base lines 490-498) is deleted. RIGHT, and the diff to this file is exactly the two spots the amended claim fenced (「the refTitleFormat memo and the RecordPickerDialog JSX props only」); nothing in the hydration region is touched.
  8. Column behaviour for nameField-only objects moves too (the column shows the nameField value where it showed name or a blank). RIGHT: that is the resolver's order, the amended claim names probes P6 and P7 as in scope, and the changeset declares it.

Asked and missing / done and not asked

  • Asked, present: agreement pin, double-brace pin, lit control (claim 5829602772's three pins) — all in LookupField.pickerColumnOrder-10486.test.tsx:131-149. Caller census of the deleted renderer — reported in the PR body and verified above (0 readers). One changeset — present.
  • Not asked at claim time, done: f75aa9a edits two pending changesets the PR does not own, prose-only. The card's file surface listed 「one .changeset/10486-…md」. The check-changeset-overwrite gate is report-only and its own header classifies factual prose corrections of a pre-existing changeset as legitimate (12 of 12 modifying commits in its census); both files keep their package names. Both edits are judged sentence by sentence in ② and are true at the head. Not a defect; the widening is confined to prose the PR itself made stale.
  • Not asked, done: the RecordPickerDialog.tsx:562-565 and :884-892 docblocks are reworded to say "title" instead of "template". Accurate at the head.
  • Stale prose the diff leaves behind (outside the fenced spots, not edited): LookupField.tsx:210-211 still says RecordPickerDialog applies the rule to 「its display column's titleFormat」; it now applies it to the display column's title. A comment, not a contract; noted, not a FAIL.

Existing pins the diff edits (no pin is deleted)

  • LookupField.candidateExpand-10223.test.tsx:410 — displayField="name" and titleFormat={TITLE_FORMAT} become objectSchema={{ name, fields: TASK_VERSION_FIELDS, titleFormat: TITLE_FORMAT }}; assertions at 440-452 unchanged. Load-bearing and correct: under the new contract a declared name would be rung 0 and the pin would read TV-0 instead of the template; dropping it is what "no declared display field" now means. With no nameField on the schema the template is still the rung reached, so the pin still measures what it measured (template over the collapsed row, expanded versus bare). Not weakened.
  • RecordPickerDialog.displayFls-10373.test.tsx:123-131 — same migration through the Picker helper; objectSchema is undefined when no titleFormat is given, so the non-template cases exercise the "no schema" path exactly as before. Assertions unchanged (lines 271-272, 281). Not weakened. The case at 261-273 passes with or without the template because the stripped template and the derived name both yield Account 0; that was true at base too (the template rendered Account 0 there as well), so this PR neither created nor widened that weakness.

New pins

  • LookupField.pickerColumnOrder-10486.test.tsx:131-172 — six cases, each reading BOTH surfaces off one mounted LookupField (dropdown option title, then the picker's data-lookup-cell display column). Can they fail: yes. Against the base code the agreement case reads Globex / HT-001 (template first), the double-brace case reads braces, the declared-displayField case reads the template in the short_code column, and the two nameField-only cases read Acme and blank; each asserts the opposite. Negative legs: not.toContain('Globex') (135) and not.toMatch(/[{}]/) (142). Lit control: 145-149, titleFormat alone renders its template on both surfaces (green at base and head, as a control should be).
  • :175-202 — three unit cases on renderLookupColumnValue: the Record #id floor is not a title (column keeps 7; passes at base too, since base ignores an unknown ctx key, so it pins the floor exclusion rather than discriminating from base), a label control rendering Seven in the display column only (red at base: 7), and a no-schema control (green at both).

② Semver level

  • Changed files: 6 source and test files under packages/fields/ (one released package in the fixed group) plus three .changeset/*.md. No content/docs/** change, so no empty-frontmatter docs changeset is owed.
  • .changeset/10486-picker-column-display-name.md declares '@object-ui/fields': minor. Correct under AGENTS.md 版本号策略 (line 271): 「objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)」, and major is forbidden for the fixed group. The body states the breaking change in its first paragraph. The CI checks Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check and Changeset Overwrite Report are all green at the head.
  • Sentence by sentence, .changeset/10486-picker-column-display-name.md, judged against the head tree:
    • Title line (titleFormat prop replaced by objectSchema): TRUE (RecordPickerDialog.tsx:316).
    • "the published RecordPickerDialog component's titleFormat prop is removed. Pass objectSchema … instead": TRUE (index.tsx:4320 re-exports the widget).
    • "the picker half of the lookup branch of ruling C1 … (the field's own displayField, then the object's nameField, then the deprecated titleFormat)": TRUE (record-title.ts:579-601).
    • Before bullet: template rendered above nameField and above the declared displayField, double-brace kept its braces, no template meant the name value or blank, dropdown showed the nameField value: all TRUE of the base (lookupColumnDisplay.tsx base 190-208; LookupField.tsx base 1736-1737 passed displayField={displayField} with the name default).
    • After bullet: the call, the order "declared displayField, then nameField (and its deprecated aliases), then titleFormat, then type-aware derivation", the same row, the floor not used as a title: all TRUE (lookupColumnDisplay.tsx:201-207, RecordPickerDialog.tsx:897-906). The name-ish record-key rung (4b) is omitted from the list; an omission, not a falsehood.
    • "displayField is the declared value … still keyed on 'name' when omitted, but only a declared value ranks above the object's declarations": TRUE (RecordPickerDialog.tsx:502,905).
    • "The column moves for nameField-only objects too": TRUE (pinned at 160-172).
    • "Without objectSchema the display column renders its own value, as before. LookupField passes the schema and the declared display field; no other caller in this repository passed titleFormat": TRUE. At the merge-base the only non-test titleFormat= is LookupField.tsx:1737; at the head RelatedList.tsx:2319, AccessExplainPanel.tsx:623,639 and AssignedUsersSection.tsx:355 pass neither titleFormat nor objectSchema, PeoplePicker.tsx:38 imports only lookupFiltersToRecord, LocationField.tsx:341 is a comment. No apps/ caller.
    • "This change also corrects two pending changesets…": TRUE at f75aa9a.
  • .changeset/10343-lookup-option-label-order.md (front matter byte-identical to main, verified with od -c; the file is identical at main and the merge-base). Edited sentence, lines 37-39: "objectui#10486 moved the browse-all record picker's display column onto the same resolver, so on such an object the picker and the dropdown read the same label." TRUE at the head for both upgrade cases the paragraph names (pinned at 131-136 and 160-164). The replaced sentence ("is a separate renderer and still shows the template") would have been FALSE once this PR lands, so the edit was needed for the release notes to read true. One unedited sentence names the picker column (line 26: the name default "still reads name for quick-create and the picker's display column"): literally TRUE at the head, since LookupField.tsx:333 still keys the picker's display column on name via deriveLookupColumns (line 497) and RecordPickerDialog.tsx:502; its contrast with the option label is now understated, because the title rendered in that column also ranks the guess below the object's declarations. Not false; a touch-up would make the file read cleaner.
  • .changeset/6874-retire-titleformat.md (front matter byte-identical to main; identical at main and the merge-base). Edited sentences, lines 29-31: "RecordPickerDialog and lookupColumnDisplay receive it inside the referenced object's schema, passed as a PROP": TRUE (objectSchema prop / ctx.objectSchema). "the repo's single such pass is objectSchema={refObjectSchema}": TRUE for non-test source (LookupField.tsx:1730 is the only one); the two migrated pins also pass literal schemas, the same looseness the base sentence had with titleFormat= (both pins passed titleFormat= then), so nothing got less true. "So copying reference_to is what makes titleFormat work on this path, and copying titleFormat onto the meta reached nothing": TRUE, the conclusion the 6874 changeset rests on is unchanged. One unedited bullet (lines 20-22) attributes a refObjectSchema?.titleFormat member read to LookupField.tsx; that read was the deleted memo, and at the head no titleFormat member read remains in that file (only comments). The bullet is a past-tense census result and its substance survives (the referenced object's schema is still what decides, now read inside core's record-title.ts, which the same list already names), but the file attribution is stale. Noted, not a FAIL.
  • Is the PR's own changeset enough for the release notes to read true? With f75aa9a the two pending files no longer describe the pre-change tree in the sentences that mattered; the PR's changeset naming them is a pointer, and the edits are what make the notes true. Together they are enough.
  • No docs or README prose is added by the diff. content/docs/fields/lookup.mdx names RecordPickerDialog (lines 138, 146) without its props, and no packages/*/README.md documents the prop, so nothing owed there.

③ Boundary flags

  • PR body flags, answered:
    • Clause-②: yes holds: RecordPickerDialogProps is published through packages/fields/src/index.tsx:4320; titleFormat is gone, objectSchema is new, and displayField's meaning moves. The changeset says all three.
    • "Predicted 9 red, observed 8": consistent with the 10373 denied-field case reading Account 0 with or without the template, as shown in ①. Pre-existing weakness.
    • "left to the seat" (Acceptance notes, on the two pending changesets): stale at the head; f75aa9a made the correction inside this PR. The PR body's "Verification (head 3e3ad60)" carries: git diff --stat 3e3ad60 f75aa9a touches three .changeset files only (the two pending ones, plus a reworded last paragraph in the PR's own changeset that now says it corrects them); no source or test file moved between the verified head and the reviewed head.
    • The withoutDeniedFields doc comment at LookupField.tsx:210-211: confirmed stale, outside the fenced spots, harmless.
    • The floor predicate duplicated beside recordToOption's: a three-line floor check, not the ladder; core is fenced. Accepted.
    • A declared displayField naming a relation against an expanding backend, NOT MEASURED: by construction the column and the dropdown make one call on one collapsed row, so agreement holds there too; what that label reads is the dropdown's existing behaviour, not this PR's.
  • CI at the head (42 check-runs, polled three times over the review, the last a few minutes after the second): 33 success, 3 skipped (Test (coverage), its shard matrix, dependabot), 6 still in progress (Test (shard 1/8), 3/8, 5/8, 6/8, 7/8, Spec Main Shape Gate), 0 failure. Type Check, Lint, Test (dist pins), Test (shard 2/8), 4/8, 8/8, README Export Check, Doc Snippet Type Check, Line Citation Gate, Control Byte Scan and the four changeset checks are green. Convergence of the remaining six is the dispatch seat's to wait on; nothing observed is red.
  • Draft: yes (draft: true), as a dispatched dev's PR must be until the dispatch seat acts.
  • Clause-②: the newest claim (5830222755) declares yes, superseding the first claim's no. The published surface does move (titleFormat removed, objectSchema added, displayField re-meant), so yes is the correct line and the earlier no would not have held.
  • Main under the PR's files: no. 17 commits landed on main after the merge-base 8740e86; git log 8740e86..main over the PR's nine paths is empty, and the two pending changesets are identical at main and the merge-base.

Implemented-by: claude/issue-10486-picker-column-display-name
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 11:52
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 8c0e550 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10486-picker-column-display-name branch September 25, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants