fix(plugin-dashboard): feed lookup cells their reference target in ObjectDataTable / RecordDetailDrawer (#6694) - #6710
Merged
Conversation
…jectDataTable / RecordDetailDrawer (#6694) `buildFieldMeta` never wrote `reference_to` / `reference` / `display_field` onto the `FieldMeta` both widgets hand to `renderFieldValue` -> `getCellRenderer` -> `LookupCellRenderer`, so that renderer resolved `undefined` and two things failed silently: `useRefObjectSchema` never loaded the referenced object's schema (the ADR-0079 / #2357 resolution never ran, cells fell back to the generic `.name`/`.title` heuristic), and `ReferencedRecordLink`'s `objectName` was always `undefined` (`navigable` always false, so no lookup cell in either widget ever rendered a real anchor). Adopts `plugin-grid`'s existing `applyRelationalMeta` move, made once in the seam both widgets funnel through. The copy set is 3 of the grid's 9, measured per key: the grid's cells are editable and its extra keys feed the inline picker, while these two widgets are read-only and `packages/fields/src/index.tsx` reads exactly these three off a cell's `field` prop. `FieldMeta` is internal — not re-exported through the barrel, and the `exports` map publishes only ".", so the subpath is refused with ERR_PACKAGE_PATH_NOT_EXPORTED. No published type is widened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Contributor
✅ 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
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6694
plugin-dashboard'sObjectDataTableandRecordDetailDrawerbuild their cell meta withbuildFieldMetaand render it throughrenderFieldValue→getCellRenderer→LookupCellRenderer(@object-ui/fields). That renderer resolves its lookup target fromfield.reference_to || field.referenceand its display field fromfield.display_field.FieldMetacarried none of those spellings, so the renderer resolvedundefinedandtwo things failed — independently, and both silently.
plugin-grid'sObjectGridhas fed exactly these keys all along, viaapplyRelationalMetaat all three of its column-building call sites. This adopts that move; it does not invent a
second one.
The gating measurement —
FieldMetais NOT a published surfaceAnswered before writing the fix, because a published type would put this on the human floor.
Both halves measured, neither inferred from the
exportkeyword:src/index.tsxhas norecordFieldsre-export; the builtdist/index.d.tsnames neitherFieldMetanorrecordFields(zero grep hits).exportsmap?".". Node's own resolver, run from a real dependent (packages/app-shell, which declares the dep and has the workspace symlink), resolves the bare specifier but refuses@object-ui/plugin-dashboard/recordFieldsand.../dist/recordFields.jswithERR_PACKAGE_PATH_NOT_EXPORTED.vite-plugin-dtsdoes emit a per-filedist/recordFields.d.ts, so the type exists on disk —that file is precisely the "an
exportin a module file is not a published surface" trap,and it is unreachable. Structural reachability was checked too: the only exported value that
could carry the type is
ObjectDataTable, typed as aReact.FCofObjectDataTableProps, andObjectDataTablePropsis{ schema; dataSource?; className? }with noFieldMetain it.The three derived types that do reference
keyof FieldMeta(EnrichedColumn,UnheldFieldMetaOverrideKey,AuthoredColumnOverrides) live inObjectDataTable.d.ts,which
index.d.tsonly imports the value from. Repo-wide there are zero cross-packageimporters of either name.
Verdict: internal ⇒ proceed.
The asymmetry, confirmed on the merge-base
ObjectGridstill does what the card says:applyRelationalMeta(ObjectGrid.tsx:456)copies its
RELATIONAL_META_KEYSoff the schema field def, called at lines 2009, 2200 and2283.
plugin-dashboardwrites neither key anywhere: everyreference_to/reference/display_fieldoccurrence inpackages/plugin-dashboard/src/*.tsxis a comment (thereferenceTocolumn override: promised in the README, kept by nothing — make the promise real or withdraw it (enforce-or-remove, from #6425) #6597 retirement notes) or an unrelated word.The copy set — 3 of the grid's 9, measured per key
The card suggested
reference_toplusdisplay_field/id_field/description_field"for parity". That is not what the measurement says. The grid needs all nine because its
cells are editable — its own docblock says the extra keys "drive the inline picker's
query (LookupField reads reference_to/reference, display_field, id_field, description_field,
lookup_filters)". These two widgets are read-only: their render path ends at a cell
renderer, and no field editor is reachable from it.
Measured on
packages/fields/src/index.tsx— the modulegetCellRendererdispatches into —the complete set of relational keys read off a cell's
fieldprop is three:reference_toLookupCellRendererreferenceLookupCellRenderer(the||fallback)display_fieldLookupCellRendererid_field,description_field,lookup_filters,lookupFilterswidgets/LookupField.tsxandwidgets/UserField.tsx, both editorsreference_to_fieldtitleFormatuseRefObjectSchema(reference_to)Copying the other six would mint six members written from the schema def on every call and
read by nothing — exactly what #6625 (
decimals) and #6597 (referenceTo) retired from thissame file weeks ago. The boundary is pinned, not just documented.
Where the fix lands
Once, in
buildFieldMeta— the seam both widgets funnel through, which is whatrecordFields.tsxexists for ("these helpers centralize that logic so the two surfaces neverdrift").
pickCellRelationalMetausesapplyRelationalMeta's own semantics: a key is writtenonly when the def carries it, so a non-relational field's meta gains no keys and an absent key
never lands as an explicit
undefined.Adding three
FieldMetamembers grows both ofObjectDataTable's derived bands — theEnrichedColumnemit tombstones andUnheldFieldMetaOverrideKey's read-side refusal. That isthe intended verdict, not a side effect: an authored column may not source a lookup's
reference target (#6597 measured no authoring story for one), while the schema-derived write
is reached by neither band.
enrich()never spreadsfieldMetaonto the emitted column, sothe emit side stays inert. Both stale docblocks are updated to say so.
Two silent consequences, two separate pins — both RED before, GREEN after
packages/plugin-dashboard/src/__tests__/lookupRelationalMeta-6694.test.tsx, run for eachwidget:
nameField: 'project_code'—deliberately not
name/title, because the generic fallback usually still producesa readable name and a fixture whose display field is
namewould pass before and afterand pin nothing. Each record also carries a
nameholding the wrong answer(
generic-fallback-name), and every case asserts that value is absent.(
/app/project_6694_b/view/p-2), not merely that the cell renders.Red on the merge-base, with the right failure modes (the defect is silent — the cell did
render, showing
generic-fallback-nameas plain text with no anchor):The 2 that pass on the merge-base are the negative-space assertions (the six picker-only keys
are absent, and a non-relational field gains no keys) — correctly green in both directions.
Verification
All commands run from the repo root (
packages/plugin-dashboard-scoped) as one union, oncommit
6172b4419— the head of this branch. (Package-dir vitest is refused by this repo'sobjectui#3378 guard, which is why the root form is used.)
pnpm exec vitest run --maxWorkers=2 packages/plugin-dashboard/—Test Files 84 passed (84),Tests 788 passed (788).pnpm --filter "@object-ui/plugin-dashboard" run type-check—tsc --noEmit && tsc -p tsconfig.test.json, clean.node scripts/check-vi-mock-specifiers.mjs—✅ check-vi-mock-specifiers: OK. Implicatedbecause the new test adds a
vi.mock; the tracked-file count moves 3914 → 3915 andmock-carrying files 479 → 480 with the new file staged, so this reading demonstrably covers
it.
node scripts/check-control-bytes.mjs—✅ check-control-bytes: OK(5561 tracked textfiles).
npx eslint packages/plugin-dashboard— 0 errors, over the 114 files ESLint itselfselected for the package (population read from its own config via
--format json, notguessed). 419 warnings, all pre-existing
no-explicit-any;--max-warningsis deliberatelyunset repo-wide, as
lint.ymlsays in its own comment, so errors are the pass condition.This is a narrowing of the repo-wide
pnpm lint, and the narrowing excludes nothing that mydiff could have moved:
eslint.config.jssets noproject/projectService, so there is nocross-file type program and every file's verdict is computed independently — my diff cannot
change the verdict of a file it does not touch. The rest of the gate farm is left to CI, which
runs it exactly once regardless.
Generated by Claude Code