Skip to content

fix(fields): composite and record values read as labelled sub-values, not raw JSON (objectui#11697) - #11749

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11697-composite-record-display
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11697-composite-record-display

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11697

Clause-②: no

A composite or record field 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: composite maps to CompositeCellRenderer and record to RecordMapCellRenderer. Both are module-local and share one body, StructuredValueCell, which sits beside JsonCellRenderer. json and object keep JsonCellRenderer. The stringify inside JsonCellRenderer moved into a helper, compactJsonText, with the same bytes, so the JSON cell and the nested sub-values below use one spelling.
  • The face is one truncated line (block max-w-full truncate) with the full text in its title, the same contract as TruncatedText. It is a description list (dl, dt and dd elements). The separators and brackets are aria-hidden and sit inside the dt and dd elements, so the list stays valid.
    • composite: Width 10 · Height 20
    • record: Primary (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.
    • A number sub-value goes through formatNumberFieldValue, the call NumberCellRenderer makes, with no declared scale. A boolean is the locale's word from useBooleanValueLabel, which reads the existing common.yes / common.no keys. A string is drawn as itself, and a floor member as EmptyValue. A nested object or array stays compact JSON.
  • Unchanged: null, undefined and '' draw EmptyValue. [] 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).
  • Raw view: none existed, so none was added. The record page's copy button still copies the stored JSON, because composite and record are 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.tsx is untouched

The 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 real DetailSection.

Pins that moved with the face

  • AddressCellRenderer.test.tsx: its control "leaves genuinely structural types stringifying as JSON" now lists json and object only.
  • summaryChip.badgeFitCensus-8464.test.tsx (plugin-detail): the composite and record rows now read Id acct-1 · Name Acme Corp. They are still fit, because the face is plain inline text, so CHIP_UNFIT_RENDERER_TYPES is unchanged.

Docs: the cell renderer section of content/docs/fields/object.mdx said composite and record share 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

  1. Path. The record page reaches the value through DetailSection, then resolveCellRendererType, then getCellRenderer. Confirmed by the ablation below: reverting only the two table entries turns the DetailSection pin red.
  2. No sub-field declaration reaches the renderer. On the installed @objectstack/spec 17.6.0, FieldSchema.safeParse of a composite field refuses fields and subFields as unrecognized_keys. The control, relatedListColumns, parses. The schema's 75 keys include no fields, subFields, schema or itemSchema. objectstack main 60ccda5a field.zod.ts is the same. In objectui, the FieldMetadata union has no composite or record member, and ObjectFieldMetadata.schema exists only for type: 'object', which stays JSON. So the labels are humanized keys. No spec-side declaration was added.
  3. Producers. The Field Zoo seed values (f_composite: { width: 10, height: 20 }, and f_record with primary / backup entries) are the test fixtures. objectstack's SQL driver fidelity test and the dogfood field-zoo matrix write a record with scalar entries ({ home: '+1', work: '+2' }). That shape is pinned as pairs.
  4. Humanizer. humanizeLabel from @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.
  5. Precedent. address made the same move (objectui#4037), with one face for the grid and the detail page.

Tests: all at HEAD 22274e7

  • pnpm 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.
  • New pins:
    • 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 the number cell's own text), the one-line contract with the title, and the unchanged floor and fallbacks. json and object are 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-check and pnpm --filter @object-ui/plugin-detail type-check: exit 0. --listFilesOnly shows that both programs include the new test files.
  • pnpm exec eslint on 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, and node scripts/check-changeset-presence.mjs: all exit 0.
  • NOT MEASURED: 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 is plaintext. CI runs them.
  • Not run locally: the other getCellRenderer consumers (grid, kanban, gallery, tree, report, dashboard, console). No test outside fields and plugin-detail names composite or record or reads listCellRendererTypes, according to git grep. The published surface is byte-unchanged, and CI runs the farm.

Ablation (reverse verification), after committing the fix

ablation-replace.mjs put the two table entries back to JsonCellRenderer (anchor hit once, blob 6e801a91c25e became ff9c729d76ec). On disk, composite: JsonCellRenderer counted 1 and composite: CompositeCellRenderer counted 0. The two new pin files then ran Tests 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 the json / object control, which do not depend on the fix. After the restore, the blob equals HEAD 6e801a91c25e and git diff HEAD is empty. No build was needed: the vitest alias resolves @object-ui/fields to src.

Browser look, not a pin: the built dist was 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 the title holds the full text. This was not checked in the running console app.

Acceptance notes

  • camelCase keys are not split. A key such as unitPrice reads UnitPrice, because humanizeLabel is the value convention. Moving humanizeFieldKey into @object-ui/core is the convergence that the core docstring says needs its own card. Carrier: none. This is noted here, not filed.
  • Labels are not translatable. No sub-field declaration exists, so no i18n key exists either. A spec-side sub-field declaration would be a protocol change and is not proposed here (four-axis reasoning in the report).
  • The read-only form face is unchanged. 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.
  • The same face now shows in every host that draws from the cell table: grid cells, kanban and gallery cards, tree rows, report cells, the highlights strip and the summary chip.

Generated by Claude Code

claude added 2 commits October 6, 2026 21:49
… 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>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3328.1 KB 3330.4 KB
Main entry chunk (gzip) 154.8 KB 350 KB
Entry file index-CP4MYGxs.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 579.15KB 139.25KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.15KB 67.31KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.26KB 38.97KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.64KB 22.85KB
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.81KB 3.58KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
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
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 22:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 22:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit db4cb6b Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11697-composite-record-display branch October 6, 2026 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: fields plugin tests

Projects

None yet

2 participants