Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 3ff3fc6

Browse files
os-steveclaude
andauthored
fix(app-shell): route the dataset preview chart through the dimension-label net (objectui#8187) (#8966)
`DatasetPreview` handed `state.rows` to `ChartRenderer` exactly as they arrived, so the preview's x axis plotted whatever the analytics layer grouped by: the raw stored enum for a dotted dimension (the server resolves nothing for a dotted path), and the object's authored English label for a local select on any locale. It is the fourth analytics chart on the platform and the only one outside `@object-ui/core`'s dimension-label net — it fell outside the producer census because it never calls `buildChartSeries`. Both inputs the net needs were already in hand, so this is that net's fourth call site: `useDatasetDimensionLabels` (the shared React half, objectui#4389) plus `relabelDimensions`. No new vocabulary, config key or public surface. The rows are relabelled once and fed to both the chart and the table below it, as `DatasetWidget` and `DatasetReportRenderer` do — relabelling only the axis would put `Manufacturing` on a bar and `MFG` in the row directly beneath it. Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB Co-authored-by: Claude <noreply@anthropic.com>
1 parent fb82877 commit 3ff3fc6

5 files changed

Lines changed: 460 additions & 4 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'@object-ui/app-shell': minor
3+
---
4+
5+
The metadata-admin dataset preview resolves its dimension values to labels, like every other analytics chart on the platform.
6+
7+
`@object-ui/core` exports one dimension-label net (`loadDimensionFieldMeta` →
8+
`resolveDimensionFieldMeta` → `deriveDimensionLabelMaps` / `localizeFieldOptions`
9+
→ `relabelDimensions`, objectui#4030 / PR #4324, widened by objectui#4330 /
10+
PR #4388), with the React half — the authenticated read and its locale-free memo
11+
— stated once in `@object-ui/react`'s `useDatasetDimensionLabels` (objectui#4389).
12+
Enumerating the analytics chart producers by their every non-test call site of
13+
`buildChartSeries` gives exactly three, and all three relabel immediately before
14+
charting: `plugin-dashboard`'s `DatasetWidget`, `plugin-report`'s
15+
`DatasetReportRenderer`, and `plugin-charts`' `ObjectChart`.
16+
17+
`DatasetPreview` is a fourth analytics chart on a different construction, and it
18+
fell outside that enumeration because it never calls `buildChartSeries` — it
19+
handed `state.rows` to `ChartRenderer` exactly as they arrived. Nothing was
20+
missing structurally: the draft carries the base object and each dimension's
21+
field path, and the preview already resolved its measure captions through
22+
`headerLabel`. The call simply was not made, and nothing in the file declared
23+
the omission deliberate.
24+
25+
Two user-visible readings change, and they are two different defects:
26+
27+
- a **dotted** dimension (`crm_account.industry`) plotted the **raw stored
28+
value**. The server resolves nothing for a dotted path — the options live on
29+
the relationship's target — so without the client net there was nothing on the
30+
axis but the enum (`MFG`). This is objectui#4053 / #4263's shape, one surface
31+
over;
32+
- a **local** select plotted the object's **authored English** label on every
33+
locale, objectui#4330's shape.
34+
35+
Neither threw and neither was red: the axis rendered a plausible string where a
36+
label belonged. That matters most on this particular surface, because it is the
37+
screen an author uses to decide whether their dataset is right — a raw enum here
38+
reads as "my dataset is wrong" and invites editing a configuration that was
39+
correct.
40+
41+
The rows are relabelled ONCE and fed to both the chart and the result table
42+
below it, which is what `DatasetWidget` and `DatasetReportRenderer` do for the
43+
same reason: relabelling only the axis would put `Manufacturing` on a bar and
44+
`MFG` in the row directly beneath it.
45+
46+
Behaviour is unchanged wherever the net resolves nothing. The read is
47+
best-effort by construction — an unreachable relationship, a field with no
48+
`options`, or a failed metadata read leaves the rows exactly as the server sent
49+
them — and `relabelDimensions` is idempotent and never touches measure columns,
50+
so a server-resolved value passes straight through. No new vocabulary, config
51+
key or public surface: this is the existing net's fourth call site.

‎packages/app-shell/src/views/metadata-admin/previews/DatasetPreview.measureLocale.test.tsx‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929

3030
import * as React from 'react';
31-
import { describe, it, expect, vi, afterEach } from 'vitest';
31+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3232
import { render, screen, cleanup } from '@testing-library/react';
3333
// Same pre-warm as the sibling `DatasetPreview.test.tsx`, for the same reason:
3434
// the preview renders its chart behind `React.lazy(() => import(...))`, and
@@ -46,9 +46,31 @@ vi.mock('../../../providers/AdapterProvider', () => ({
4646
useAdapter: () => ({ queryDataset }),
4747
}));
4848

49+
/**
50+
* objectui#8187 — the preview now resolves its dimensions' option labels
51+
* through the shared label net, which issues `GET /api/v1/meta/object/<name>`.
52+
* happy-dom resolves that relative URL against `localhost:3000`, so without a
53+
* double every test here reaches a real socket and the network-escape guard
54+
* (objectui#6640) fails the file.
55+
*
56+
* The double serves the base object with NO select options, which is what these
57+
* cases have always assumed: nothing resolves, `relabelDimensions` returns the
58+
* server's rows by identity, and every assertion below is byte-identical to
59+
* what it pinned before that card. Label resolution itself is pinned in
60+
* `__tests__/DatasetPreview.dimensionLabels-8187.test.tsx`, not here.
61+
*/
62+
const realFetch = global.fetch;
63+
beforeEach(() => {
64+
global.fetch = vi.fn(async () => ({
65+
ok: true,
66+
json: async () => ({ item: { name: 'opportunity', fields: {} } }),
67+
})) as any;
68+
});
69+
4970
afterEach(() => {
5071
cleanup();
5172
queryDataset.mockReset();
73+
global.fetch = realFetch;
5274
});
5375

5476
/**

‎packages/app-shell/src/views/metadata-admin/previews/DatasetPreview.test.tsx‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

3-
import { describe, it, expect, vi, afterEach } from 'vitest';
3+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
44
import { render, screen, cleanup, waitFor } from '@testing-library/react';
55
// DatasetPreview renders its chart behind
66
// `React.lazy(() => import('@object-ui/plugin-charts'))`, and the "use the right
@@ -25,9 +25,31 @@ vi.mock('../../../providers/AdapterProvider', () => ({
2525
useAdapter: () => ({ queryDataset }),
2626
}));
2727

28+
/**
29+
* objectui#8187 — the preview now resolves its dimensions' option labels
30+
* through the shared label net, which issues `GET /api/v1/meta/object/<name>`.
31+
* happy-dom resolves that relative URL against `localhost:3000`, so without a
32+
* double every test here reaches a real socket and the network-escape guard
33+
* (objectui#6640) fails the file.
34+
*
35+
* The double serves the base object with NO select options, which is what these
36+
* cases have always assumed: nothing resolves, `relabelDimensions` returns the
37+
* server's rows by identity, and every assertion below is byte-identical to
38+
* what it pinned before that card. Label resolution itself is pinned in
39+
* `__tests__/DatasetPreview.dimensionLabels-8187.test.tsx`, not here.
40+
*/
41+
const realFetch = global.fetch;
42+
beforeEach(() => {
43+
global.fetch = vi.fn(async () => ({
44+
ok: true,
45+
json: async () => ({ item: { name: 'opportunity', fields: {} } }),
46+
})) as any;
47+
});
48+
2849
afterEach(() => {
2950
cleanup();
3051
queryDataset.mockReset();
52+
global.fetch = realFetch;
3153
});
3254

3355
const baseProps = { type: 'dataset', name: 'sales', locale: 'en-US' as const };

‎packages/app-shell/src/views/metadata-admin/previews/DatasetPreview.tsx‎

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import {
2525
formatMeasure,
2626
formatDimensionValue,
2727
buildDatasetFieldHelpers,
28+
relabelDimensions,
2829
type DatasetResultField,
2930
} from '@object-ui/core';
31+
import { useDatasetDimensionLabels } from '@object-ui/react';
3032
import { builtinAggregateLabels, useSafeFieldLabel, useSafeTranslate, useDisplayLocale } from '@object-ui/i18n';
3133

3234
// Lazy-loaded so the (recharts-backed) chart bundle only loads when a dataset
@@ -71,6 +73,43 @@ export function DatasetPreview({ draft }: MetadataPreviewProps) {
7173
return d.map((x) => String(x?.name ?? '')).filter(Boolean);
7274
}, [draft]);
7375

76+
/**
77+
* The dataset's `dimension name -> field path` map, read off the DRAFT.
78+
*
79+
* This is the same map the analytics result reports as `dimensionFields` and
80+
* that the three other producers feed the net from — but the draft is the
81+
* authority HERE, because this surface previews a possibly-unsaved dataset:
82+
* the draft's `dimensions[].field` is present before any server round-trip,
83+
* and no result echo is needed to know it. A dotted path (`crm_account
84+
* .industry`) resolves against the relationship TARGET, which is the whole
85+
* reason the path — not the dimension name — is what the net walks.
86+
*/
87+
const dimensionFields = React.useMemo(() => {
88+
const d = Array.isArray((draft as any).dimensions) ? ((draft as any).dimensions as Array<Record<string, unknown>>) : [];
89+
const out: Record<string, string> = {};
90+
for (const x of d) {
91+
const name = String(x?.name ?? '');
92+
const field = String(x?.field ?? '');
93+
if (name && field) out[name] = field;
94+
}
95+
return out;
96+
}, [draft]);
97+
98+
// objectui#8187 — the dimension-label net (objectui#4030 / PR #4324, widened
99+
// by objectui#4330 / PR #4388), at its FOURTH call site. Analytics groups by
100+
// a select's STORED value and resolves nothing at all for a dotted path, so
101+
// without this the axis below plots `MFG` where `Manufacturing` belongs — and
102+
// a local select plots the object's authored English on every locale. This is
103+
// the screen an author uses to judge "is my dataset right?", so a raw enum
104+
// here reads as "my dataset is wrong" and invites editing a correct one.
105+
//
106+
// The hook is the shared React half of that one net (`@object-ui/react`, the
107+
// glue objectui#4389 stated once) — it holds the authenticated read and keeps
108+
// the fetched metadata LOCALE-FREE, so switching language re-labels in place
109+
// instead of re-fetching. Called above the early returns below, as the rules
110+
// of hooks require; it issues no read when there is no object or no dimension.
111+
const dimensionLabels = useDatasetDimensionLabels(objectName, dimensionFields, dimensionNames);
112+
74113
const canRun = !!objectName && measureNames.length > 0;
75114

76115
const [state, setState] = React.useState<PreviewState>({ status: 'idle', rows: [] });
@@ -147,6 +186,18 @@ export function DatasetPreview({ draft }: MetadataPreviewProps) {
147186
builtinAggregateLabels(tt),
148187
);
149188
const columns = [...dimensionNames, ...measureNames];
189+
/**
190+
* The rows as they are DISPLAYED — dimension values resolved to their option
191+
* labels. Derived once and fed to BOTH the chart and the table, exactly as
192+
* `DatasetWidget` and `DatasetReportRenderer` do: relabelling only the axis
193+
* would put `Manufacturing` on a bar and `MFG` in the row directly beneath it.
194+
*
195+
* Idempotent and best-effort by construction — a value with no mapping (the
196+
* server already resolved it, a lookup id, free text) passes through, and a
197+
* failed metadata read leaves `dimensionLabels` null and returns `state.rows`
198+
* by identity. Measure columns are never touched.
199+
*/
200+
const displayRows = relabelDimensions(state.rows, dimensionLabels);
150201

151202
// A ratio/percent measure (format like `0.0%`) on the same axis as a
152203
// magnitude measure (currency in the hundred-thousands) renders as an
@@ -209,7 +260,7 @@ export function DatasetPreview({ draft }: MetadataPreviewProps) {
209260
<div className="rounded-md border p-2">
210261
<ChartRenderer
211262
schema={{
212-
data: state.rows as Array<Record<string, unknown>>,
263+
data: displayRows,
213264
xAxisKey: dimensionNames[0],
214265
chartType: mixedScale ? 'combo' : 'bar',
215266
series: measureNames.map((m) => ({
@@ -240,7 +291,7 @@ export function DatasetPreview({ draft }: MetadataPreviewProps) {
240291
</tr>
241292
</thead>
242293
<tbody>
243-
{state.rows.map((row, i) => (
294+
{displayRows.map((row, i) => (
244295
<tr key={i} className="border-t">
245296
{columns.map((c) => (
246297
<td key={c} className="px-2 py-1 tabular-nums whitespace-nowrap">

0 commit comments

Comments
 (0)