Repository navigation
Commit db4cb6b
fix(fields): composite and record values read as labelled sub-values, not raw JSON (objectui#11697) (#11749)
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](https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 055d350 commit db4cb6b
7 files changed
Lines changed: 549 additions & 23 deletions
File tree
- .changeset
- content/docs/fields
- packages
- fields/src
- __tests__
- plugin-detail/src/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
134 | | - | |
135 | | - | |
| 134 | + | |
136 | 135 | | |
137 | 136 | | |
138 | 137 | | |
| |||
142 | 141 | | |
143 | 142 | | |
144 | 143 | | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
145 | 162 | | |
146 | 163 | | |
147 | 164 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
158 | 158 | | |
159 | 159 | | |
160 | 160 | | |
161 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
162 | 164 | | |
163 | 165 | | |
164 | 166 | | |
| |||
Lines changed: 212 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
0 commit comments