Repository navigation
fix(fields): composite and record values read as labelled sub-values, not raw JSON (objectui#11697) - #11749
Merged
objectstack-fleet[bot] merged 2 commits intoOct 6, 2026
Conversation
… the stored JSON (objectui#11697)
`composite` and `record` were registered to JsonCellRenderer, so the record
page (and every grid cell reading the same table) drew
`{"width":10,"height":20}` in a monospace face. They now draw one truncated
line of labelled pairs: a composite key per pair, a record entry per labelled
group. Scalar sub-values read through the package's own faces (the number
formatter, the boolean word); nested objects and arrays stay compact JSON.
The floor and unrecognized shapes answer exactly as JsonCellRenderer does.
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
… move the census rows, document it (objectui#11697) The renderer's contract is pinned through getCellRenderer in `compositeRecordCell-11697`, and the record page end of the path through the real DetailSection in `DetailSection.compositeRecord-11697`. Two existing pins named composite / record as JSON and move with the face: the address test's "structural types stay JSON" control keeps json / object only, and the summary chip census records the new text (still a fit). The object field doc's cell renderer section and the changeset describe the new face. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
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
|
objectstack-fleet
Bot
deleted the
claude/issue-11697-composite-record-display
branch
October 6, 2026 23:02
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 #11697
Clause-②: no
A
compositeorrecordfield value now reads as labelled sub-values on the record page, not as its stored JSON. The audit log actor half of the original card is objectui#11701, which this pull request does not touch and which remains open.What changed
packages/fields/src/index.tsx,buildStandardCellRendererMap:compositemaps toCompositeCellRendererandrecordtoRecordMapCellRenderer. Both are module-local and share one body,StructuredValueCell, which sits besideJsonCellRenderer.jsonandobjectkeepJsonCellRenderer. The stringify insideJsonCellRenderermoved into a helper,compactJsonText, with the same bytes, so the JSON cell and the nested sub-values below use one spelling.block max-w-full truncate) with the full text in itstitle, the same contract asTruncatedText. It is a description list (dl, dt and dd elements). The separators and brackets arearia-hiddenand sit inside the dt and dd elements, so the list stays valid.Width 10 · Height 20Primary (Name A · Score 9) · Backup (Name B · Score 7), one labelled group per entry name. A record entry that is not a populated sub-object is drawn as a pair.formatNumberFieldValue, the callNumberCellRenderermakes, with no declaredscale. A boolean is the locale's word fromuseBooleanValueLabel, which reads the existingcommon.yes/common.nokeys. A string is drawn as itself, and a floor member asEmptyValue. A nested object or array stays compact JSON.null,undefinedand''drawEmptyValue.[]and{}keep their literal, so the objectui#8474 pin and the objectui#8481 json-literal fence pins pass without edits. A non-object value, including a string that holds JSON, is drawn as it is and never parsed (AGENTS.md #0.1).compositeandrecordare inline-excluded and keep the copy affordance.Nothing is added to the package entry: no export, prop, type member or language-pack key.
Why
DetailSection.tsxis untouchedThe record page row already draws its values as one truncated line with a
title: the text and address cells do this. The cell contract expresses the labelled face, so the record page needed no branch of its own. The record-page pin below drives the realDetailSection.Pins that moved with the face
AddressCellRenderer.test.tsx: its control "leaves genuinely structural types stringifying as JSON" now listsjsonandobjectonly.summaryChip.badgeFitCensus-8464.test.tsx(plugin-detail): the composite and record rows now readId acct-1 · Name Acme Corp. They are stillfit, because the face is plain inline text, soCHIP_UNFIT_RENDERER_TYPESis unchanged.Docs: the cell renderer section of
content/docs/fields/object.mdxsaidcompositeandrecordshare the JSON cell. It now describes the new face. Changeset:.changeset/11697-composite-record-display.md(@object-ui/fields: patch).Premise check: the dispatch's mechanism hypotheses, measured
DetailSection, thenresolveCellRendererType, thengetCellRenderer. Confirmed by the ablation below: reverting only the two table entries turns theDetailSectionpin red.@objectstack/spec17.6.0,FieldSchema.safeParseof a composite field refusesfieldsandsubFieldsasunrecognized_keys. The control,relatedListColumns, parses. The schema's 75 keys include nofields,subFields,schemaoritemSchema. objectstack main60ccda5afield.zod.tsis the same. In objectui, theFieldMetadataunion has no composite or record member, andObjectFieldMetadata.schemaexists only fortype: 'object', which stays JSON. So the labels are humanized keys. No spec-side declaration was added.f_composite: { width: 10, height: 20 }, andf_recordwithprimary/backupentries) are the test fixtures. objectstack's SQL driver fidelity test and the dogfood field-zoo matrix write arecordwith scalar entries ({ home: '+1', work: '+2' }). That shape is pinned as pairs.humanizeLabelfrom@object-ui/core. It is already imported here, and it is the key fallback the boolean cell's "Off" badge uses. The camelCase-splitting key convention,humanizeFieldKey, lives in@object-ui/plugin-dashboard, outside this package's dependency edge. See Acceptance notes.addressmade the same move (objectui#4037), with one face for the grid and the detail page.Tests: all at HEAD
22274e7pnpm exec vitest run packages/fields/ --maxWorkers=2:Test Files 238 passed | 1 skipped (239),Tests 3723 passed | 7 skipped (3730), exit 0.pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2:Test Files 245 passed | 1 skipped (246),Tests 2408 passed | 8 skipped (2416), exit 0.packages/fields/src/__tests__/compositeRecordCell-11697.test.tsx: 9 tests. They cover the composite pairs, record groups, scalar record entries, nested JSON, the per-type sub-value faces (the number compared against thenumbercell's own text), the one-line contract with thetitle, and the unchanged floor and fallbacks.jsonandobjectare the control.packages/plugin-detail/src/__tests__/DetailSection.compositeRecord-11697.test.tsx: 2 tests. This is the triage pin: a composite field renders labelled sub-values on the record page, and so does the record field.pnpm --filter @object-ui/fields type-checkandpnpm --filter @object-ui/plugin-detail type-check: exit 0.--listFilesOnlyshows that both programs include the new test files.pnpm exec eslinton the 5 touched source and test files: 0 errors. No warning falls on an added line.pnpm check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations(0 new citations),check:doc-fences,check:doc-types,check:doc-example-ids,docs:check-links, andnode scripts/check-changeset-presence.mjs: all exit 0.check:doc-snippets,check:doc-examples. Reason: their prerequisite is a scoped build of 34 packages, and they exited 2 with "THE GATE COULD NOT RUN". This diff adds no TypeScript fence: the one new block isplaintext. CI runs them.getCellRendererconsumers (grid, kanban, gallery, tree, report, dashboard, console). No test outsidefieldsandplugin-detailnamescompositeorrecordor readslistCellRendererTypes, according togit grep. The published surface is byte-unchanged, and CI runs the farm.Ablation (reverse verification), after committing the fix
ablation-replace.mjsput the two table entries back toJsonCellRenderer(anchor hit once, blob6e801a91c25ebecameff9c729d76ec). On disk,composite: JsonCellRenderercounted 1 andcomposite: CompositeCellRenderercounted 0. The two new pin files then ranTests 8 failed | 3 passed (11). All 6 contract tests and both record-page pins went red. The 3 green tests are the floor and fallback cases and thejson/objectcontrol, which do not depend on the fix. After the restore, the blob equals HEAD6e801a91c25eandgit diff HEADis empty. No build was needed: the vitest alias resolves@object-ui/fieldstosrc.Browser look, not a pin: the built
distwas server-rendered with the built component and fields stylesheets and viewed in Chromium. Each face is one line (20 to 21px high). At 120px and 180px the line ends in an ellipsis (text-overflow: ellipsis,white-space: nowrap), and thetitleholds the full text. This was not checked in the running console app.Acceptance notes
unitPricereadsUnitPrice, becausehumanizeLabelis the value convention. MovinghumanizeFieldKeyinto@object-ui/coreis the convergence that the core docstring says needs its own card. Carrier: none. This is noted here, not filed.ObjectField's read-only branch, the form widget, still shows pretty-printed JSON for these types. The card's "Where" named it, but the triage correction routes the record page through the cell table, and this pull request does not change that widget. Carrier: none. Noted, not filed.Generated by Claude Code