Uh oh!
There was an error while loading. Please reload this page.
fix(components): record_picker emptyText resolves the inline locale map its contract admits - #5636
Merged
Merged
Conversation
…ap its contract admits
`packages/components/src/renderers/basic/record-picker.tsx` rendered
`props.emptyText` straight into a text node while the contract has admitted
`I18nLabel` (`string | Record< string, string >`) since spec 17.0.0-rc.6 —
measured on the installed 17.0.0 GA pin, where
`ElementRecordPickerPropsSchema.safeParse({ object: 'account', emptyText: { en, 'zh-CN' } })`
succeeds and the member resolves to `optional(union(string, record))`.
React refuses a plain object in a child position rather than stringifying it, so
the dropped arm was not a mis-rendered empty state: the whole picker subtree
threw `Objects are not valid as a React child (found: object with keys {en, zh-CN})`.
The read site now resolves through `pickLocalized` — the objectui-side helper the
sibling text-node sites already read through, `''` on a miss rather than the spec
resolver's `undefined`. The default is applied before resolution, so an absent key
still means "No records" and an authored empty string still renders empty.
The `ComponentMeta` entry, narrowed to a single `'string'` arm precisely because
the renderer dropped the other one, now declares `['string', 'object']`. The
`apps/console` specimen that pinned the narrow arm named this release condition in
its own words ("keeps its single `'string'` arm until the render site catches up")
and is flipped here with its controls kept.
Three comments in this file deferred the gap to objectui#4163, which closed as
completed on 2026-08-15 while the gap was still open; the file now carries no
reference to it. The `ComponentInput.type` doc in `@object-ui/types` cited this
same key as its worked example of an arm deliberately withheld, and is corrected
in the same change so the example stays true.
Fixes#5590Contributor
✅ 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
|
os-sales
marked this pull request as ready for review
August 21, 2026 21:14
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 21, 2026
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 freeto 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#5590
element:record_picker'semptyTextrendered straight into a text node while thecontract has admitted the inline per-locale map since rc.6. This resolves it at the
render site and widens the declaration in the same change, which is the order the
repo's own
ComponentInput.typerule prescribes.Premise re-verified before implementing (triage asked for this explicitly)
The card's line numbers were located by code on
origin/main(f52d36c96), nottaken on trust — all five are exact:
:78emptyText?: string;:213( p className="text-xs text-muted-foreground" ){props.emptyText ?? 'No records'}( /p ):301name: 'emptyText',:308// no locale resolution, so the map form does not render (objectui#4163).:318descriptionending… audit in objectui#4163; this input publishes the string form deliberately …The inherited rationale holds at this site, and the contract half is measured
rather than inherited. Against the installed pin (
@objectstack/spec@17.0.0GA,not rc.6),
ElementRecordPickerPropsSchema:So the declared-vs-enforced gap is real on the pin this repo actually resolves, and
pickLocalizedis the right helper for the reason the ruling gives: the destinationis a text node, and
pickLocalizedspells a miss as''where the spec'sresolveI18nLabelspells itundefined. No fork to report.One correction to the card's framing, which strengthens rather than weakens it: the
dropped arm was not a silent mis-render. React refuses a plain object in a child
position rather than stringifying it, so the map form threw and took the whole picker
subtree with it — see the measured output under Reverse-verification.
What changed
record-picker.tsxread site —pickLocalized(props.emptyText ?? 'No records', language),with
useObjectTranslation()placed above the block's early returns so hook orderstays stable across resolution states. The default is applied before resolution
deliberately: an absent key still means
"No records", and an authored''stillrenders empty rather than being promoted to the default (a
||default would havebroken the second case silently; it has its own test).
emptyText?: string | I18nLabel.ComponentMetaentry —type: 'string'→type: ['string', 'object']. Thenarrowing was correct for exactly as long as it was true; with the map arm now
reaching the screen resolved, withholding it would be the false declaration in the
other direction — the manifest gate reporting
type-mismatchon a legal write thissame input's
descriptionteaches the author to make.apps/console/.../component-input-union-specimens.test.ts— the pin thatasserted the opposite (
toEqual(['string'])+ atype-mismatchon the map) isflipped. It named this exact release condition in its own words — "keeps its single
'string'arm until the render site catches up" — so this is the pin firing asdesigned, not a pin being deleted to go green. Its controls are kept and extended
(
42and['No records']must still reporttype-mismatch), and the property thefile exists to guard — that the widening is per-key, not a blanket "strings may
also be objects" — is still asserted by
element:text_input.defaultValue, whoseI18N_MAP control must stay red because its spec type has no object arm.
Stale-#4163 sweep — the whole class in this file, not the two the card names
The card named
:308and:318. Sweeping the file for the class found a third,about fifty lines above, which names the narrowing without citing the number and would
have survived a grep for
4163:All three are rewritten.
grep -c '4163' record-picker.tsxis now 0 (was 2 — thethird site carried no number).
One site outside this file, named here rather than left false
packages/types/src/base.ts:405cited this very key as theComponentInput.typedoc's worked example of an arm deliberately withheld ("
element:record_picker.emptyTextkeeps a single
'string'arm for exactly that reason — objectui#4163"). Landing thewidening without it would leave a false statement about the key this PR just changed,
authored by this PR. It is corrected in the same change, keeping the teaching and the
attribution intact. This is not a fold of #5591: that finding names
packages/i18n/src/pickLocalized.ts:132andpackages/plugin-dashboard/src/WidgetConfigPanel.tsx:518, and neither is touched here.Reverse-verification — three legs, all mutated on disk and proven both ways
Each leg proves the mutation landed with anchored counts in both directions
(injected text present ≥1, deleted text absent = 0) before any reading is taken, and a
a
trap ... EXIT INT TERMrestores the tree on every exit path including a capSIGTERM. No rebuild leg is owed: both vitest projects alias the workspace packages to
src, notdist(vitest.config.mts:264,apps/console/vite.config.ts:389) — andthe legs going red is itself the proof that the mutated source is what ran.
Leg A — remove
pickLocalizedfrom the read site. Predicted red; observed red,3 failed | 4 passed (7):That message is byte-identical to the pre-fix harm
inline-locale-label-read-sites.test.tsxrecorded for
schema.label, one key over in the same class.Leg B — revert the declared arms to a single
'string'. Predicted red; observedred,
1 failed | 9 passed (10):Leg C — swap
pickLocalizedfor this file's owntoText. This is the leg thatjustifies the test design, and its result is asymmetric by prediction:
toTextreaches
o.enas its fourth fallback, so atoText-based "fix" resolves Englishcorrectly and ignores the active language. Observed
2 failed | 5 passed (7)— thezh-CNcase and the base-language fallback go red whileresolves the SAME map differently for a different language(theencase) stays GREEN. A test assertingonly the English string would have shipped green against the wrong helper; the
non-English case is what pins the helper the ruling names.
Restore leg proven with the same two-way anchoring, and
git statuson both mutatedfiles is empty — byte-identical to the committed fix.
Tests and gates — run at
d91c19dddAffected suites, from the repo root (package-cwd vitest is refused by the repo's own
guard, objectui#3378) —
Test Files 7 passed (7),Tests 251 passed (251), exit 0:the new pin,
record-picker-element-data-source,record-picker-inputs-spec-parity,page-variables,inline-locale-label-read-sites,widget-dom-leak-sweep, and theconsole specimens.
packages/typessuite —Test Files 40 passed (40),Tests 460 passed (460).type-check:packages/components+packages/typesexit 0;apps/consoleexit 0.produced fourteen
TS2307: Cannot find module '@object-ui/*', which is the unbuilt-disttrap and not a finding;
pnpm --filter '@object-ui/console^...' buildthen exit 0.check:control-bytes—✅ check-control-bytes: OK (scanned 4681 tracked text file(s); skipped 85 binary).check:phantom-deps—✅ Every in-scope import is declared by the package that publishes it.(the leg that matters for the two new imports:
@object-ui/i18nand@objectstack/specare both already declared by
packages/components.)check:self-import—✅ No package names itself inside its own src/.check:esm-specifiers—Specifier leg: no un-ledgered package emits an extensionless relative specifier.check:spec-symbols—✅ spec symbol derivation: 1290 files scanned against 4912 spec export names.eslint . --no-inline-configwas run in full rather than narrowed:3505 files, 89 errors across 73 files, none of them in this diff (all
pre-existing on
origin/main). This PR's four files: 0 errors, 35 warnings, all@typescript-eslint/no-explicit-anymatching the surrounding code.packages/componentssuite (177 testfiles) did not finish inside the foreground budget under contention with the sibling
agents on this container (
timeout 540→ exit 124, still collecting). Narrowed to thesuites this diff touches, listed above. CI runs the farm regardless.
DOM-leak ledger (#5574 / PR #5629)
widget-dom-leak-sweep.test.tsxran and is green — it is inside the 251 above. Noledger row needed editing: this change moves the text content of the empty-state
pelement, notrecord-picker's emitted attribute set, so the exact-set-equality assertionin both directions is unmoved. The gate was run, not skipped or worked around.
Published-surface reachability
Yes — the widened declaration is reachable from the package's published exports,
flagged here for the contract-review rule before landing.
packages/componentsexportsexactly
"."and"./style.css";src/index.ts:36isimport './renderers';, so anyconsumer of
@object-ui/componentsgetselement:record_pickerregistered with thewidened
inputs. It does not reachsdui.manifest.json—element:record_pickeris deliberately absent from
PUBLIC_BLOCKS("record picking is a field widget, not apage block") — but the
inputsare a live prop whitelist anyway:renderers/layout/page.tsx:462builds the JSX-page compiler's whitelist from
getKnownTypes()plus these same entries.Net effect for an author: a JSX page or saved node writing the map form stops drawing
type-mismatchon a write the contract accepts and the renderer now resolves.Changeset
.changeset/record-picker-empty-text-i18n-5590.md,@object-ui/components: patch.scripts/check-changeset-presence.mjsverdict, quoted:No
@object-ui/typesentry: that file's change is a doc comment with no emittedbehaviour, and the gate above is satisfied.
Out of scope — filed, not fixed here
labelandplaceholderon this same block carry the samestring | Record< string, string >union (measured in the same probe run) and neither is resolved:
placeholderis passedthrough raw, and
labelgoes through this file'stoText, which reacheso.enunconditionally and so renders English to every locale. Both are separate keys with their
own render-site questions, outside this card's fence — filed unassigned rather than folded
in, and listed in the dev report.
Generated by Claude Code