Repository navigation
fix(fields): password, secret, vector and grid cells draw "No value" when nothing is stored (objectui#8678) - #10529
Conversation
…when nothing is stored (objectui#8678) The four were argument-less arrows in getCellRenderer's standard table that never read `value`, so an unset credential was drawn exactly like a set one and `vector` / `grid` printed their literal over `null`. - password / secret: MaskedCellRenderer, empty = TextCellRenderer's predicate (the floor on the coerced text). A stored value keeps the same mask; the value is never printed. - vector / grid: VectorCellRenderer / GridCellRenderer, empty = the floor exactly. A stored value keeps the same literal. Named module-level components: EmptyValue holds a hook, which an inline arrow would tear down on every resolution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
…ns the fix falsified (objectui#8678)
- cellRenderers.valueIndependent-8678: the census of the four types over
the empty-shaped inputs, the string-class rule against `text`, the
populated faces byte-for-byte, the boundary, and a table-wide identity
pin (no entry of the standard table is a fresh function per resolution).
- 8677: the masked-type exclusion is now measured as "one face for every
stored value, the affordance for none", no longer as value-independent.
- 8441: the rebuild control used `vector` as the last inline arrow; there
is none left, and the identity property is pinned table-wide instead.
- 8596: the census label for the four `{}` rows (faces unchanged).
- docs: grid / vector / password cell-renderer sections name the empty face.
- changeset: @object-ui/fields patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
|
changeset-claim-re-read
|
✅ 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
|
Contract reviewServed-tier: ① Derived judgmentsRead against the card body, the cross-link comment 5592405775, the triage 5619307943 (the binding text: "Masking a populated Behaviour changes, one by one:
Asked and missing: nothing. The triage asked the dev to make the scope judgement explicitly (done in the PR body's per-class table and in the docblocks), to read objectui#8440 first (done; the copy gate reads Done but not asked: the three docs pages ( Existing pins edited:
New pin
② Semver level
Changeset prose, sentence by sentence: the title (an empty cell now says "No value") — true, the aria-label. "These four cell renderers never read their value." — true at the base, argument-less arrows. "drew the six-dot mask ... printed [Vector] and [Grid] for every input, null included." — true. "looked exactly like one that is set wherever the cell is drawn for an empty value, such as an ObjectGrid column." — true, Docs prose added: The PR body's one verbatim spec quote (the ③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #8678
Clause-②: no
What changed
getCellRenderer's standard table registeredpassword,secret,vectorandgridas argument-less arrows that never readvalue. Every input,nullincluded, drew the six-dot mask or the[Vector]/[Grid]literal. Those four entries now point at three named module-level renderers inpackages/fields/src/index.tsx. None of them is exported.MaskedCellRenderer(password,secret): the sharedEmptyValueaffordance (accessible name "No value") when nothing is stored. Otherwise the same mask span as before. The value is never printed.VectorCellRenderer/GridCellRenderer: the affordance when nothing is stored. Otherwise the same literal span as before.They are named rather than inline because
EmptyValueholds a hook. An inline arrow in the table is a new component type on every resolution, so it would tear that hook down on every render. That is the objectui#8441 reason, and the table's docblock now says it.Which inputs count as empty, per value class (read from the spec, not the card)
password,secretSTRING_VALUE_TYPESin@objectstack/specTextCellRenderer's own predicate, the floor on the coerced text:null,undefined,'',[], and any array whose coerced text is''{}(per the objectui#8596 string-class ruling), a whitespace-only string (textkeeps its spaces, and a blank credential is still a set one), any other stringvectorvalueSchemaFor:z.array(z.number())null,undefined,'',[]{}, and every other valuegridFieldType, so the class isGridField's own contract: an array of row objects{}, and every other valueWhy
{}is not added forvector/grid: neither class can produce it. The spec's write seam refuses a non-arrayvector, and that is pinned as a premise below.GridFieldwrites arrays. Also,hasCellValuein@object-ui/plugin-detailcalls every object filled, so a{}clause would draw "No value" inside a row that band has already called filled.The platform contract this restores: the
SECRET_MASKdocblock in@objectstack/spec/data(installed 17.4.0), verbatim: "An UNSET credential reads backnullinstead, never this mask — the masking is presence-preserving on both surfaces, which is what lets a console render "configured" vs "not configured" at all."objectui#8440 (the clipboard half), read before choosing: it is closed (ruling A, landed), and this change leaves it as it was. Its gate reads
hasCellValueplus the masked-type set in plugin-detail. An empty credential row was never copy-interactive, and a populated one still draws the mask. Its pinDetailSection.maskedCopyRefusal-8440passes on this branch.Measurement: objectui#8596's census instrument, taken again
Every registered type (
listCellRendererTypes(), 53 at this base) was rendered throughgetCellRendereragainst 15 inputs:[],{},'',null,undefined, a whitespace string,{a:1},[null],[''],0,false, a string,[1,2,3],[[1]]andSECRET_MASK. The innerHTML was diffed betweenbc4efba(base) anddd068fd(the fix commit).[],'',nullandundefinedfor each of the four, plus['']forpasswordandsecret.{}cell and every stored-value cell render byte-identical.This is a single reading from a probe that was not committed.
cellRenderers.valueIndependent-8678re-derives the four types' rows on every run.Existing pins this change made false, now updated
cellRenderers.signatureValueClass-8677: its census requiredpassword/secretto draw the same face fornullas for a value. That is exactly the defect. The excluded set is nowMASKED_FACES, measured as one face for every stored value,{}included, with the value absent from the DOM and the affordance for nothing stored.cellRenderers.countLabelI18n-8441: a control proved that the resolver rebuilds its table by usingvectoras the last inline arrow. There is no inline entry left, so the control has nothing to test and is retired with a note. The property it protected is now pinned for every registered type by the new file's IDENTITY test.cellRenderers.objectLiteral-8596: census rows unchanged, because the{}column did not move. Only the section label changed ("value-independent" is no longer true).New pin:
packages/fields/src/__tests__/cellRenderers.valueIndependent-8678.test.tsx(37 tests)valueSchemaFor({type:'vector'})accepts a real embedding (the lit half) and refuses{}.gridis absent fromFieldType.options, withvectorpresent as the control.[],{},'',null,undefined. Each case asserts the defect is absent before it asserts the replacement.password/secretrender byte-equal totextfor every inputtextcalls empty, with a control showing thattextreally does separate the inputs.{a:1}keeps the literal.Verification, all run at HEAD
0cf9eaf(the final commit), exit code captured before any pipepnpm exec vitest run packages/fields/ --maxWorkers=2: exit 0,Test Files 195 passed | 1 skipped (196),Tests 3255 passed | 7 skipped (3262).pnpm turbo run type-check --filter @object-ui/fields --concurrency=2: exit 0,Tasks: 12 successful, 12 total.tsc -p tsconfig.test.json --listFileslists all four touched test files.inlineCredentialGate,DetailSection.maskedCopyRefusal-8440,summaryChip.badgeFitCensus-8464, consoleSettingsView.crypto-unavailable): exit 0,Tests 111 passed (111).react-refreshwarnings inindex.tsx). Narrowed, with the evidence for the narrowing:eslint.config.js: the five changed.tsxfiles..md/.mdxget "File ignored because no matching configuration was supplied."--format jsoncount is 5 files.parserOptions.project/projectService), and no rule undereslint-rules/reads another file. So this diff cannot move the verdict for any untouched file.pnpm 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 citation(s)) ·check:changeset-claims·check:pending-changeset-literals·node scripts/check-changeset-presence.mjs: all exit 0.check:changeset-claimslists five pending changesets that nameindex.tsx/grid.mdx. Each paragraph was read (6357, 6625, 6694, 6837, 7166), and none concerns the four entries or the grid cell section..changeset/**andcontent/docs/**:check-changeset-no-major,check-changeset-overwrite,check-changeset-fixed,check-doc-component-types,check-prompt-component-keys,check-doc-example-ids,check-doc-links,check-doc-fence-languages,check-shell-escape-residue: all exit 0.check-doc-snippet-types: needs a filtered build. The docs edits are prose only, with no snippet added or changed. Declared to CI.check-readme-exports: exit 1 with "type entry./dist/index.d.tsis not on disk -- runpnpm buildfirst". The prerequisite is not met, so this is not a red result. No README or export moved.Ablation (one-time; not a permanent test)
Each mutation went through
ablation-replace.mjsfrom the objectstack checkout. The anchor had to hit exactly once, the blob changed, and the restore was proven: blob equals HEAD0b5e83a8bbfdandgit diff HEADis empty. The subject resolves from source (../index), and nodist/is on the path, so there is no build leg.password/secretentries back to the base arrows: 11 of 37 red. That is 8 CENSUS empty rows, 2 RULE and IDENTITY, which namespasswordandsecret.vector/gridback to the base arrows: 9 of 37 red. That is 8 CENSUS rows and IDENTITY, which namesgridandvector.All three turned red, which is the expected direction.
Acceptance notes
standardMapentries, pins beside the census tests, and one changeset. This PR also touches the following, each forced by the change:packages/fields/src/__tests__/(8441, 8596, 8677), which went red or became false.content/docs/fields/grid.mdx,vector.mdxandpassword.mdx(a new "Cell Renderer" section), under AGENTS.md Add automated testing infrastructure and CI/CD workflows #2.MASKED_CELL_FIELD_TYPESdocblock inplugin-detail/src/fieldEnrichment.tsquotes the retired arrow. Its argument still holds:@object-ui/fieldsexports no "is this type masked" predicate.summaryChipRenderers.tsandsummaryChip.badgeFitCensus-8464call the four "value-independent faces". For the object value that census measures, the faces and thefitverdicts are unchanged. Owner: none.vector/gridpopulated face: kept byte-identical, as instructed. For the record, theVectorFieldwidget already draws a preview plus a dimension count for a stored embedding, so a cell could reuse that. That is a separate question.passwordholding['']now draws "No value" in the cell whilehasCellValuecalls it filled.texthas had exactly this behaviour all along, and this change inherits it deliberately so the string class answers one way.Generated by Claude Code