Repository navigation
fix(app-shell): the Interfaces pillar's nav rail renders a locale-map label instead of crashing, and resolveSurface resolves it once (objectui#11158) - #11179
Conversation
…nd resolveSurface resolves it once (objectui#11158)
The spec types a nav item's label as I18nLabel (a string or an inline
locale map). NavTree rendered it raw, so a map threw "Objects are not
valid as a React child" and took the pillar's rail down; resolveSurface
read String(label), so the canvas caption printed [object Object].
- NavTree reads the group heading, the entry text and the title
attribute through navItemLabelText in the designer locale, the helper
the canvas card and the nav-item inspector already share.
- resolveSurface takes the locale and carries the resolved text in
Surface.label, so the rail, the first-leaf auto-open and the
?surface= restore (findSurfaceInTree) carry the same text.
findSurfaceInTree still matches on {type,name} alone.
- NavNode.label is typed as the spec's I18nLabel, so the compiler
refuses the next raw render.
Pins: the real InterfacesPillar under en and zh (rail rows, caption,
breadcrumb, ?surface= restore, no React child error, the plain-string
control), and unit pins on resolveSurface / findSurfaceInTree.
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
|
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: Inputs: card #11158 (its body and its four comments: triage ① Derived judgmentsEach direction line of
Other derived checks: the Check-runs on the head (43 with ② Semver level
③ Boundary flags
Deviations (five):
(a) Enumeration for the closure card's nail: every remaining site in Sites that read an
(b) Caption staleness after a language switch or a rename — real by reading, not measured, not a blocker; escalated as the follow-up the dev named. Mechanism in ① item 4. The rename half predates this PR: at the base Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11158
Clause-②: no
What was wrong
The spec types a nav item's
labelasI18nLabel: a plain string or an inline locale map. In the Studio's Interfaces pillar:NavTreerendered the label raw in three places: the group heading, the entry text, and thetitleattribute of an entry with no surface. A map made React throwObjects are not valid as a React child (found: object with keys {en, zh-CN}), and the pillar's whole rail was gone.resolveSurfacereadString(node.label ?? '')intoSurface.label, so the canvas caption and the breadcrumb printed[object Object].What changed
NavTreereads the heading, the entry text and thetitleattribute throughnavItemLabelText(label, locale)(previews/navItemLabel.ts), in the designer locale fromuseMetadataLocale(). It is the helper the canvas card and the nav-item inspector already share. There is no second resolver.resolveSurfaceresolves once. It takes the locale and carries the resolved text inSurface.label. All three callers pass the designer locale: the rail, the first-leaf auto-open in the load effect, andfindSurfaceInTreebehind the?surface=restore.findSurfaceInTreestill matches on{ type, name }alone. NoString()of a label is left innavSurface.ts.NavNode.labelis typed as the spec'sI18nLabel, sotscrefuses a raw render (measured below).localeis deliberately not a dependency of the load effect. A language switch must not re-run the load, because the load rehydrates the nav edit buffer. A comment at the call says so.NavNode,Surface,resolveSurfaceandfindSurfaceInTreeare internal to app-shell.packages/app-shell/src/index.tsexports none of them, and the package'sexportsmap has only.and./styles.css. So the type change moves no published contract, which matches the claim'sClause-②: no.Pins
StudioDesignSurface.interfacesRailLabelMap-11158.test.tsxmounts the realInterfacesPillarover a mocked metadata client, under en and under zh. Every nav item in the fixture parses againstNavigationItemSchema. The fixture has a map-labelled first leaf, a map-labelled group, a plain-string leaf (the control), a nested map-labelled leaf, and a map-labelledurlitem. The cases:urlrow is disabled and titled by its text. Noconsole.errorsays "not valid as a React child", and no[object Object]appears.?surface=page:reportsopens the nested map-labelled leaf, with its caption in the designer locale.navSurface.test.ts:resolveSurfacecarries the text underen-USand underzh-CN.'', as before.findSurfaceInTreefinds the same leaf in both locales.expectTypeOfpinsNavNode['label']toI18nLabel | undefined.LOCALE.Reverse verification (one-time; the fix was committed first)
Each leg changed one anchor through
ablation-replace.mjs, which checks the anchor count and the blob hash on disk. It then ran the pin files (ortsc) and restored the file. Each restore was proven by a blob equal toHEADand an emptygit diff HEAD. All legs ran ate591e1dfd.node.labelresolveSurfaceback toString(node.label ?? '')node.labeltitleback tonode.labelNavNode.labelback tostringtsc -p tsconfig.test.jsonredexpectTypeOfpin; TS2322 on the map fixtures{node.label}tsc --noEmitredtsc -p tsconfig.test.json --listFilesOnlylists both test files (2 of 4802), so the test type-check compiles them. The pillar pin imports the component by relative path fromsrc/, so nodist/is on its path.Verification at
e591e1dfdpnpm exec vitest run packages/app-shell/src/views/studio-design/ packages/app-shell/src/views/metadata-admin/previews/: 156 files and 1532 tests passed, including both pin files.pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json): exit 0. Its dependency closure was built first withturbo run build --filter=@object-ui/app-shell^...(28 of 28 tasks).pnpm exec eslinton the four changed source and test files: 0 errors. There were 18 warnings before and 18 after, the same set. The load effect's existingexhaustive-depswarning now also nameslocale, which is deliberate (see above).node scripts/check-changeset-presence.mjspnpm check:control-bytespnpm check:new-line-citations(0 new citations)check:pending-changeset-literalscheck:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shapecheck:test-path-rootscheck:esm-specifiers(the specifier leg)check:self-importcheck-governed-queue-guard.mjs --testover the five changed paths: NOT GOVERNEDcheck:changeset-claims(report-only) names one pending changeset,6681-declared-lazy-marketplace-routes.md, which citesStudioDesignSurface.tsxabout its static import ofSuggestedBindingsPanel. This diff adds no import to that file, so the paragraph still holds.Declared narrowing. The repo-wide
pnpm lintandpnpm testbelong to CI. The eslint run is narrowed to the changed files, and that narrowing is itself a measurement:--format jsonreturns 4 results, none of them ignored.parserOptions.projectorprojectService, so linting is not type-aware.eslint-rules/reads from disk. A grep for fs reads matches none of them, and the same pattern matchesscripts/check-control-bytes.mjsas a positive control.So the diff cannot move a lint verdict on a file it does not touch.
NOT MEASURED
check:eager-closure,check:dist-completenessandcheck:published-dist: they need app-shell's owndist. The eager module set is unchanged, but only by reading the code:StudioDesignSurface.tsxalready importednavItemLabel.tseagerly.check:phantom-deps: the new import is@objectstack/spec/ui. app-shell declares that package (^17.5.0), and 76 of its source files already import it.Acceptance notes
labelwithString(), andAppSchema.labelis the sameI18nLabelunion. An uncommitted probe on this PR's harness labelled the app{ en: 'Acme', 'zh-CN': '顶点' }. The rail heading then read[object Object] · Navigationunder en and[object Object] · 导航under zh. The probe was deleted and the tree is clean. The fix is outside this card's file surface, which coversNavTreeand the auto-open caller only.currentholds the Surface as it was resolved when the leaf was opened. After a designer-language switch, the caption and breadcrumb keep the old locale's text until another leaf is picked, while the rail re-resolves on every render. A rename in nav editing leaves the caption stale in the same way, and that part predates this PR. This PR keeps the ruling's "resolves once" design; the note is for whoever next touches the caption.Generated by Claude Code