Skip to content

fix(analytics): resolve a dotted dimension's select options against the relationship target - #4261

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4053-dotted-dimension-labels
Aug 11, 2026
Merged

fix(analytics): resolve a dotted dimension's select options against the relationship target#4261
yinlianghui merged 1 commit into
mainfrom
claude/issue-4053-dotted-dimension-labels

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4053

A DatasetDimension whose field is a dotted relationship path (crm_account.industry) got no select-option resolution at all, so the chart plotted the raw stored enum (education, finance) — while the same underlying field reached as a local dimension rendered Education, Finance beside it on the same dashboard. Nothing errored; end users just read database enum values.

Source thread objectstack#5144 (required reading per the migration header) was reviewed: its triage ruling is "先按纯前端解析修" — fix as pure frontend resolution, and only split cross-domain if the spec surface must change. It did not: no DatasetDimension key was added, so this stays entirely in the renderer.

Where the resolver lives

Two sites carried the identical lookup, both reading options off the dataset's base object:

sitepre-fix expression
packages/plugin-dashboard/src/DatasetWidget.tsxobjSchema?.fields?.[fieldOf(dim)]?.options
packages/plugin-charts/src/ObjectChart.tsx (dataset path)objSchema?.fields?.[f]?.options

objSchema is the dataset's base object, and fieldOf(dim) comes from the server's dimensionFields map — for the issue's widget A that is the literal string crm_account.industry. Indexing a base-object field map with a dotted path only ever matches the local spelling, so the lookup missed silently and the renderer fell through to the stored value.

How the target object is reached

resolveDimensionFieldOptions (new, in packages/core/src/utils/chart-series.ts beside buildDimensionLabelMap / buildOptionColorMap) is the object-resolution step of that same lookup, not a dotted-path variant beside it: a single-segment path never enters the walk and resolves exactly as before, so the local and joined paths cannot drift.

Each segment before the last must be a declared relationship — type in lookup / master_detail / masterdetail / master-detail, target read from reference / reference_to / referenceTo / reference_to_object, accepting a bare name, a one-element array, or { object }. Same canonicalization the dataset designer's resolveReferenceTo already uses. The type gate is deliberate: a segment naming a plain field can never be turned into an object name and fetched speculatively.

No new fetch layer. The related object's metadata was already reachable at this point — the resolver's own GET /api/v1/meta/object/:name read, which rides the host apiFetch when a provider supplies one (objectui#4121). Hops go through that same channel via a loadObjectSchema callback the call site passes in, so authentication, base-URL rewriting and draft-preview params are inherited rather than re-implemented. The loader is memoized per resolution and seeded with the base schema under its own name, so sibling dimensions sharing a prefix (crm_account.industry alongside crm_account.tier) fetch crm_account once and the base is never re-read.

Multi-hop verdict: handled, and pinned

Multi-hop is not hypothetical here — the dataset designer emits it. useDatasetFields.ts builds relationship.relationship.field paths under ADR-0071 (capped at 3 hops in the picker), so 2-hop paths genuinely reach this resolver. The walk is per-segment rather than single-hop special-casing, so N hops work for free; crm_account.owner.department is pinned end-to-end through the widget, asserting both the resolved label and that every hop was fetched in order. No artificial cap is imposed — each hop must be a real declared relationship, so a garbage path stops on its first segment.

Evidence

Red-first, before any fix, with one fixture carrying both dimensions so the issue's side-by-side split is a single assertion pair:

× resolves a dotted dimension against the relationship target, matching the local dimension
expected [ 'education', 'finance' ] to deeply equal [ 'Education', 'Finance' ]
× walks MULTI-HOP paths (a.b.field), the shape the dataset designer emits
expected [ 'rnd' ] to deeply equal [ 'Research & Development' ]
Tests 2 failed | 3 passed (5)

The local dimension in that same fixture rendered Education / Finance pre-fix — the split reproduced exactly as reported. The 3 passing controls are the fall-through cases, green before and after.

After the fix: Test Files 132 passed (132), Tests 2108 passed (2108) across packages/core/, packages/plugin-dashboard/, packages/plugin-charts/ — including the existing DatasetWidget.relabel.test.tsx suite and the objectui#4121 apiFetch-routing pins, so local dimensions and probe routing are unchanged.

Reverse verification (walk removed, fix otherwise intact — the direction was predicted before running): both dotted pins went RED with the raw values returning, and the three fall-through controls stayed GREEN. One core unit test I had predicted green also went red — yields NO entry for a terminal field with no options… — because besides asserting the empty result it asserts the walk reachedcrm_account once; with the walk gone the fetch trace is empty. Its result assertion still held; only the trace moved. Reported rather than smoothed over.

Controls pinned (all green before and after, i.e. today's behavior preserved):

  • local dimensions unchanged;
  • a dotted path to a non-select target field (crm_account.website) — raw value survives, nothing coerced;
  • a dotted path whose target object lacks the field — graceful fall-through, no crash;
  • a segment that is not a relationship (industry.nested) — no target invented, only the base object ever fetched.

Gates

pnpm exec vitest run for the three packages (repo root, per AGENTS.md) · turbo run type-check lint for the three packages: 17/17 · node scripts/check-control-bytes.mjs: OK · check-changeset-presence: OK · check-changeset-no-major: OK. Changeset added (patch × 3; never major, per the version-alignment rule — and no skip-changeset, per objectui#3724). No copy was touched, so the i18n gates do not apply.

The core change is purely additive — two new exports, no existing signature narrowed — so no downstream consumer can break on it; the two consumers of the changed lookup are both edited and green here.

Scope

Ends at "the label is in hand", per the card's own not-a-duplicate reasoning. Whether that label then passes through the i18n bundle is objectstack#5076 and is deliberately neither fixed nor asserted here.


Generated by Claude Code

…tionship target (#4053)
A DatasetDimension whose `field` is a relationship path (`crm_account.industry`)
got no select-option resolution: the label lookup read options as
`baseObject.fields[<path>]`, which only matches the local spelling. For a dotted
path the options live on the RELATED object, so the lookup missed silently and
the chart plotted the raw stored enum — while the same field as a LOCAL
dimension rendered its option labels beside it on the same dashboard.
Extend the object-resolution step of that one lookup rather than forking a
dotted-path variant: `resolveDimensionFieldOptions` walks each segment before
the last as a declared relationship and reads the terminal field's options off
the object that owns it. A single-segment path never enters the walk, so the
local path resolves exactly as before and the two cannot drift. Multi-hop paths
resolve too (ADR-0071, the shape the dataset designer emits).
Hops ride the caller's existing `GET /meta/object/:name` channel, memoized per
resolution, so no new fetch layer is added. Unresolvable paths yield no mapping
and the raw value survives, as today.
Wired at both sites that carried the lookup: DatasetWidget and ObjectChart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 11, 2026 9:32am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-BILQzm9S.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)488.62KB108.26KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)150.04KB39.79KB
fields (index.js)228.45KB56.62KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.98KB10.85KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)45.23KB12.45KB
plugin-charts (index.js)61.73KB17.54KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.67KB30.75KB
plugin-designer (index.js)210.91KB42.67KB
plugin-detail (index.js)238.88KB59.71KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)187.97KB49.90KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.31KB26.76KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)23.71KB7.96KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude