Skip to content

fix(app-shell): Studio's app preview resolves locale-map app, nav-item and group labels (objectui#11100) - #11127

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11100-app-preview-labels
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11100-app-preview-labels

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11100

Clause-②: no

Studio's app preview now resolves the app label, each nav item's label and each group's label through the spec's resolveI18nLabel in the designer locale, the way the areas list in the same file already does. A label only decides an entry's text; it no longer decides whether the entry exists.

What was wrong (measured on origin/main 76e9df0, AppPreview.tsx)

  • The header ran String() over the app label, so a locale-map label printed [object Object].
  • normalizeNav kept a label only when typeof it.label === 'string', so a childless nav item with a map label was dropped together with its target.
  • The same test made a map-labelled group fall to (unnamed).

The spec types all three as I18nLabel (a plain string or a per-locale map): the label of every navigation branch except separator, and the app's own label.

What changed

  • AppPreview.tsx: the app label is resolveI18nLabel(draft.label, locale) ?? appName; normalizeNav takes the designer locale (threaded the way unnamed is) and resolves each item's and group's label with it. The header no longer calls String() on a label.
  • An entry is dropped only when it has no label, no children and no target. (unnamed) now reads only for a label that is truly absent, or a map that resolves to nothing.
  • New pin AppPreview.labelMaps-11100.test.tsx (7 tests) and one patch changeset for @object-ui/app-shell.

Behavior change to review

An entry that names a record (has a target) but carries no label used to vanish from the preview. It is now kept and reads (unnamed), which is what the direction on the card rules ("a nav item is kept whenever it has a target"). An entry with no label, no children and no target is still dropped, as before. Nothing else about a plain-string label moves; the plain-string control is pinned.

Evidence

  • Before, pins against the unmodified file: Tests 5 failed | 2 passed (7).
  • Reverse verification from the committed state: git checkout of AppPreview.tsx from the pinned base commit 76e9df0 (the resolveI18nLabel(draft.label anchor count 1 to 0, the String(label) anchor 0 to 1), then the pin file again: Tests 5 failed | 2 passed (7) (direction: red, the two controls stay green). Restored with git checkout HEAD -- FILE; git diff HEAD empty and the HEAD blob hash equals the working-tree hash (5557011d1 at that step). The subject is imported by relative path from src, so no rebuild sits between the mutation and the run.
  • After, at head cbe37f8:
    • pnpm exec vitest run packages/app-shell/src/views/metadata-admin/previews/: Test Files 79 passed (79), Tests 1057 passed (1057), 7 of them the new pin.
    • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json), after building its dependency closure with pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (the upstream direction): exit 0. tsc -p tsconfig.test.json --listFilesOnly names the new test file, so the test project covers it.
    • pnpm exec eslint on both changed files: 0 errors, 7 warnings, all the no-explicit-any casts that were already in the file (the count is unchanged by this diff).
    • node scripts/check-changeset-presence.mjs, node scripts/check-control-bytes.mjs, pnpm run check:new-line-citations (0 new citations), check:changeset-claims, check:pending-changeset-literals, check:test-path-roots: all exit 0.

Declared narrowing (CI owns the rest)

The repo-wide lint scan and the full test and check farm are CI's. The public surface is unchanged (AppPreview's props, no new export; normalizeNav is module-private), so no downstream sweep is owed. The pins are the evidence; the browser gallery was not driven.

Acceptance notes

  • Out of scope, measured, not fixed here: in design mode the nav is drawn by AppNavCanvas, whose own navLabel also tests typeof label === 'string'. Probe at this head: a draft with a nav item labelled as a locale map, rendered with editing in zh-CN, prints Item 1 for that row while the landing line above it (this PR) prints the resolved 客户. The canvas is a different file and an inline-rename editor, so it is reported for a separate card, not widened into this one.
  • objectui#10862 (designer chrome strings in this file) stays serial behind this card and is not addressed here.

Generated by Claude Code

…m and group labels (objectui#11100)

The app label, each nav item's label and each group's label are the spec's
I18nLabel (a plain string or a locale map). AppPreview read them as strings:
the header ran String() over the app label ([object Object]), normalizeNav
kept a label only when typeof was string (a map-labelled childless item was
dropped with its target) and a map-labelled group fell to (unnamed).

All three now resolve through resolveI18nLabel in the designer locale, as the
areas list in the same file does. A nav item is kept whenever it has a target
or children; its label only decides the text, and (unnamed) stays for a label
that is truly absent.

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
…ad, no new any-cast (objectui#11100)

Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3110.2 KB 3149.4 KB
Main entry chunk (gzip) 149.3 KB 350 KB
Entry file index-Cnb2xAQi.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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.17KB 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.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 558.84KB 133.94KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.99KB 63.22KB
fields (index.js) 261.11KB 66.33KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.32KB 11.09KB
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.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 234.38KB 62.21KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 169.69KB 43.37KB
plugin-gantt (index.js) 171.88KB 42.61KB
plugin-grid (index.js) 229.01KB 62.89KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 31.93KB 9.33KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 90.43KB 22.76KB
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) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
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) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.26KB
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 (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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.30KB 6.99KB
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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: cbe37f8b208ddb584e856b60c066a3e4e769cfc6
Local-runs: none

Inputs: card #11100 (body; triage 5890841243; claim 5894398404; os-dev-report 5894787139), PR #11127 (body, its 3 files, the net diff against main at 76e9df06c), and the head's check-runs read with filter=latest. For context, AppPreview.tsx, AppNavCanvas.tsx, preview-registry.ts and i18n.ts were read at the head and at the base through contents/…?ref=, and the spec's resolveI18nLabel source (packages/spec/src/ui/i18n-label-resolver.ts) was read for its miss semantics. Nothing built, run or re-run.

① Derived judgments

Diff: AppPreview.tsx (+17/−9), new pin AppPreview.labelMaps-11100.test.tsx (7 tests), one changeset. Public surface: AppPreview's props are unchanged (MetadataPreviewProps), normalizeNav and I18nText stay module-private, no new export, no new dependency (resolveI18nLabel was already imported from @objectstack/spec/ui for readAreas). Right: nothing public moves.

  1. App label (site 1). Base :239 (draft as any).label ?? appName, rendered at :304 as {String(label)}. Head :247 resolveI18nLabel(draft.label as I18nText, locale) ?? appName, rendered at :312 as {label}. The String() call is gone; the read follows the typed-draft pattern readAreas already uses in this file (draft is typed as a record of unknown values in MetadataPreviewProps, so no new any-cast). A map resolves in the designer locale; appName fires only when the resolver answers undefined (label absent, or a map with no string entry). A plain string, '' included, comes back unchanged, so the control holds. Right.

  2. Nav-item label (site 2). Base :137 typeof it.label === 'string' ? it.label.trim() : ''. Head :143 (resolveI18nLabel(it.label, locale) ?? '').trim(). locale is threaded as a required positional string | undefined (the spec's own rule for this resolver, and the shape readAreas takes), passed into the recursion at :162, and added to the useMemo deps at :268 so a locale switch re-resolves the nav. Under en-US the map {en, zh-CN} resolves through the resolver's base-language limb to Accounts; under zh-CN through the exact limb to 客户. Right.

  3. Group label (site 3). Same path: a group's map label resolves, and label || unnamed at :163 now fires only on an empty resolved text. Right.

  4. Keep/drop rule. Base dropped an entry on !label && !it.children, before the target was even read. Head reads navTarget(it, kind) first and drops only on !label && !it.children && !target (:144–145). Judged against the triage ("a nav item is kept whenever it has a target; its label only decides the text, never whether the item exists"): an entry with a target is now unconditionally kept, whatever its label. Entries with a label or with children were never dropped before and are not now; only the entry with nothing to show (no label, no children, no target) is dropped, as before. Right.
    The behaviour change the dev names, an entry that names a record but carries no label used to vanish and now reads (unnamed), IS the triage's rule rather than a departure from it: "kept whenever it has a target" is stated with no condition on the label, and "(unnamed) stays only for a label that is truly absent" names the text such an entry gets. The two rulings compose to exactly this outcome, and the changeset tells the author. Right.

  5. A map resolving to nothing. resolveI18nLabel({}, locale) walks all six limbs, finds no string entry and answers undefined (never '', never a stringified object); the head folds that to '', so the row reads (unnamed) and keeps its target. Pinned by the deals case. Right: that is "truly absent" as far as the designer can show anything, and no [object Object] path remains, since neither site calls String() on a label and the resolver refuses a non-map object rather than stringifying it.

  6. The pins. Seven cases: map app / nav-item / group labels under en-US and zh-CN (resolved text present, the item keeps its account target, the group keeps its child, the landing line reads the resolved label, neither locale's (unnamed) string appears, no [object Object] in the body); plain strings as the control; an app with no label falls to its name; (unnamed) for a truly absent label and for an empty map under both locales, with lead and deal kept and the empty ghost entry dropped; and the app label in design mode. The navRow helper reads the row through the label span's parentElement, which is the NavRow div carrying the kind and target spans (:415–425), so the target assertions address the right element. The pin file follows the directory's Name.topic-NNNN.test.tsx convention and imports ../i18n the way AppPreview.areas-11027.test.tsx does. Right; the pins are the card's pins line.

Nothing else in the diff. No accept-set moves on the spec side: the preview now honours what I18nLabelSchema already accepted.

② Semver level

.changeset/11100-app-preview-label-maps.md: '@object-ui/app-shell': patch. Right: a rendering fix to the Studio preview with no prop, type, export or schema change; the one visible behaviour change (a targeted, label-less entry shown as (unnamed) instead of hidden) is a fix in the direction the card ruled, and the changeset text states it. The changeset prose matches the code (kept when it names a record or holds children; (unnamed) for an absent or unresolvable label; the app name when no label is authored). Gates Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Changeset Overwrite Report: success.

Clause-②: no
Confirmed. The PR body and the claim both carry it; no schema, prop, type or accepted set moves, and normalizeNav's new parameter is module-private.

③ Boundary flags

Dev deviations (three), open_questions (none), one out-of-scope finding.

a. PR assignee write refused. label-write --assign os-support-ai was denied by the dev's own permission layer before any request left; PR #11127 has no assignee (read: empty assignees), and its labels are the path labeler's tests and package: app-shell only. Observation only, as the brief frames it: the card is assigned to os-support-ai; the seat sets the PR assignee. Not a contract breach.

b. Attribution trailer. The two commits (2f2a7933d, cbe37f8b2) carry Claude-Session: and a model-free Co-authored-by: Claude pair; no model identifier appears in any commit, the PR title or body, the changeset or the report. Conforms to the repo rule. Answered.

c. Base moved. The order measured cff8641e2; the PR's base is 76e9df06c. compare/cff8641e2...76e9df06c is one commit (#11121, objectui#11082) touching 7 files, none under metadata-admin/previews/; the three sites read at 76e9df06c are the card's, line for line (:137, :157, :304). main is still 76e9df06c at review time. Answered; no rebase owed.

d. Out-of-scope finding: AppNavCanvas.tsx navLabel in design mode. Confirmed by reading at the head: navLabel (:164–169) tests typeof l === 'string' && l.trim() and otherwise returns the positional Item N; RawNav.label is typed string only (:50). So a map-labelled nav item in design mode (editing) shows Item 1 in the canvas row while the header's landing line (this PR) shows the resolved text; the dev's probe reads right. Same family as this card: the spec's AppSchema.navigation[].label (I18nLabel) read through a string test, exactly site 2's defect, in the design-mode renderer of the same nav. Pre-existing: the file is byte-identical at base and head, last touched 2026-08-20 (#5383), and untouched by this PR. Not widened here, rightly: the card names three sites in AppPreview.tsx, the dispatch surface was that file alone, and the canvas is an inline-rename editor whose write path (renameItem writes a plain string over label, :263–264; the dev's read-only inference, not measured) needs its own ruling before a fix. Escalated to the seat: file a sibling card for AppNavCanvas.navLabel (dedupe words: AppNavCanvas, navLabel, Item 1) and rule the rename-over-map write path there; objectui#10862 is queued on this file family and is the natural serial neighbour. This PR's design-mode pin covers the header only, as it should.

Gates on the head. Check-runs on cbe37f8b2 (filter=latest), polled until every run had completed: 43 runs, 40 success, 3 skipped by condition (the two coverage matrix placeholders and dependabot), 0 failure, 0 in progress. The derived families are all green: Type Check, Lint, Test shards 1–8, Test (dist pins), Build & E2E, Live E2E (informational), Spec Main Shape Gate, Doc Snippet Type Check, Doc Component Type Check, the five changeset gates, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check, Governed Surface Queue Guard, Bundle Analysis, Build Docs. Those conclusions are the gate verdicts; the dev's own runs (pins red before and on reverse verification, green at the head; type-check, eslint and the check:* farm exit 0) are consistent with them and were not re-run here.

Implemented-by: claude/issue-11100-app-preview-labels
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 17:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 8a5ae3d Sep 29, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11100-app-preview-labels branch September 29, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant