Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/objectchart-label-net-third-copy-4405.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/plugin-charts': patch
---

Analytics: `ObjectChart` consumes the shared label-net helpers instead of a third copy

objectui#4389 (PR #4404) named two copies of the analytics label-net glue — the dashboard's `DatasetWidget` and plugin-report's dataset block — and retired both into `@object-ui/core` + `@object-ui/react`. There was a THIRD, which that card did not name and its PR deliberately left out of scope: `packages/plugin-charts/src/ObjectChart.tsx` carried its own `translatorFor` closure, its own `buildDimensionLabelMap` loop, and its own base-object-read-then-walk composition. The `translatorFor` copy was logically identical to the two that were deleted, down to the comment explaining the binding.

`ObjectChart` now calls core's `dimensionOptionTranslator`, `deriveDimensionLabelMaps` and `loadDimensionFieldMeta` directly. Nothing about what a label IS changes — those helpers are the same code the two retired copies were rewritten onto, so the part that was genuinely duplicated three times is now written once.

Behaviour is unchanged by construction: same two metadata reads in the same order on the dataset path, same one read on the aggregate path, same best-effort fallback (an unresolvable path yields no entry and the raw value survives), same locale-applying memo boundary. `plugin-charts`' 22 test files / 170 assertions pass unchanged and their files are byte-identical to before, which is the acceptance evidence for a pure swap.

The card's second, optional step — moving the DATASET path's metadata read onto `@object-ui/react`'s `useDatasetDimensionMeta` — was attempted and declined on measurement; the shape blocker is recorded on objectui#4405 and in the PR. The two bug-fix properties the family exists to state (the read rides the host's authenticated `apiFetch`, objectui#4121; the fetched metadata stays locale-free, objectui#4030 / PR #4324) therefore remain stated locally in this file, exactly as before, and are undisturbed by this change.
61 changes: 33 additions & 28 deletions packages/plugin-charts/src/ObjectChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import React, { useState, useEffect, useContext, useCallback, useMemo, useRef } from 'react';
import { useDataScope, SchemaRendererContext, SchemaRenderer, useDrillNavigation, useFilterScope, ElementDataSourceGate, type ElementDataSourceMapping } from '@object-ui/react';
import { ChartRenderer } from './ChartRenderer';
import { ComponentRegistry, extractRecords, computeDrillFilter, isDrillEnabled, resolveDrillTitle, resolveFilterPlaceholders, resolveContextTokens, shiftFilterByCompareTo, compareToTrendLabelKey, buildChartSeries, buildOptionColorMap, buildDimensionLabelMap, relabelDimensions, localizeFieldOptions, resolveDimensionFieldMeta, type DimensionFieldMeta, type OptionLabelTranslator, type CompareToConfig, type DrillEvent, type ChartResultField } from '@object-ui/core';
import { ComponentRegistry, extractRecords, computeDrillFilter, isDrillEnabled, resolveDrillTitle, resolveFilterPlaceholders, resolveContextTokens, shiftFilterByCompareTo, compareToTrendLabelKey, buildChartSeries, buildOptionColorMap, deriveDimensionLabelMaps, dimensionOptionTranslator, loadDimensionFieldMeta, relabelDimensions, localizeFieldOptions, type DimensionFieldMeta, type CompareToConfig, type DrillEvent, type ChartResultField } from '@object-ui/core';
import { Sheet, SheetContent, SheetHeader, SheetTitle, Dialog, DialogContent, DialogHeader, DialogTitle, RefreshIndicator, Button, ChartSkeleton } from '@object-ui/components';
import { AlertCircle, ArrowUpRight } from 'lucide-react';
import { useSafeFieldLabel, useSafeTranslate } from '@object-ui/i18n';
Expand Down Expand Up @@ -377,7 +377,6 @@ export const ObjectChart = (props: any) => {
const doc = await r.json().catch(() => null);
return doc?.item ?? doc?.data ?? doc;
};
const objSchema = await loadObjectSchema(objectName);
// Each dimension's underlying field, which for a dataset dimension may be
// a DOTTED relationship path (`crm_account.industry`) — objectui#4053.
const fieldByDim: Record<string, string> = {};
Expand All @@ -387,15 +386,20 @@ export const ObjectChart = (props: any) => {
fieldByDim[dimName] = dimDef?.field ?? dimName;
}
}
// One resolution for every path: a local field name reads straight off
// `objSchema` as before, a dotted one walks to the object that owns the
// terminal field. Unresolvable paths yield no entry → raw value survives.
// `…Meta` keeps the OWNING object + terminal field beside the options —
// the key the locale bundle is written under (objectui#4030).
const metaByPath = await resolveDimensionFieldMeta(
objSchema,
[fieldName, ...Object.values(fieldByDim)],
// Read the base object's schema, then resolve every path against it.
// Core's `loadDimensionFieldMeta` (objectui#4389 / PR #4404) IS that
// two-step composition, written once: a local field name reads straight
// off the base schema as before, a dotted one walks to the object that
// owns the terminal field. Unresolvable paths yield no entry → raw
// value survives. `…Meta` keeps the OWNING object + terminal field
// beside the options — the key the locale bundle is written under
// (objectui#4030). The resolution is memoized per call and seeded with
// the base schema, so the base object is read ONCE however many dotted
// dimensions walk back through it — the read count the pins assert.
const metaByPath = await loadDimensionFieldMeta(
loadObjectSchema,
objectName,
[fieldName, ...Object.values(fieldByDim)],
);
if (!cancelled) {
setOptionMeta({
Expand All @@ -422,28 +426,29 @@ export const ObjectChart = (props: any) => {
const { fieldOptionColors, dimensionLabels } = useMemo(() => {
if (!optionMeta) return { fieldOptionColors: null, dimensionLabels: null };
const { metaByPath, colorPath, fieldByDim } = optionMeta;
// Bound to the object that OWNS the terminal field — `crm_account` for
// `crm_account.industry`, not the dataset's base object.
const translatorFor = (path: string | undefined): OptionLabelTranslator | undefined => {
const meta = path ? metaByPath[path] : undefined;
const owner = meta?.object;
if (!owner) return undefined;
return (value, authored) => fieldOptionLabel(owner, meta.field, value, authored);
};
// Colours read `option.label`, so they are fed the LOCALIZED options and
// stay keyed by the string the rendered category actually carries — on the
// aggregate path `resolveGroupByLabels` already translates it, so an
// untranslated colour map missed every category in a localized app.
const colorOptions = localizeFieldOptions(metaByPath[colorPath]?.options, translatorFor(colorPath));
let labels: Record<string, Record<string, string>> | null = null;
if (fieldByDim) {
const acc: Record<string, Record<string, string>> = {};
for (const [dimName, path] of Object.entries(fieldByDim)) {
const m = buildDimensionLabelMap(metaByPath[path]?.options, translatorFor(path));
if (m) acc[dimName] = m;
}
if (Object.keys(acc).length > 0) labels = acc;
}
//
// `dimensionOptionTranslator` is core's binding of the resolver to ONE
// resolved dimension field (objectui#4389 / PR #4404). It states the part
// that is easy to get wrong: the translator is bound to the object that
// OWNS the terminal field — `crm_account` for `crm_account.industry`, not
// the dataset's base object — because that owner is the key the locale
// bundle is written under.
const colorOptions = localizeFieldOptions(
metaByPath[colorPath]?.options,
dimensionOptionTranslator(metaByPath[colorPath], fieldOptionLabel),
);
// Dataset path only: the same derivation the dashboard and report surfaces
// take, written once in core. A null `fieldByDim` (the aggregate path)
// yields null labels, exactly as the longhand loop did.
const labels = deriveDimensionLabelMaps(
metaByPath,
fieldByDim ? Object.entries(fieldByDim).map(([dim, path]) => ({ dim, path })) : null,
fieldOptionLabel,
);
return { fieldOptionColors: buildOptionColorMap(colorOptions), dimensionLabels: labels };
}, [optionMeta, fieldOptionLabel]);

Expand Down
Loading