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/dashboard-inline-chartconfig-4044.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/plugin-dashboard': minor
---

dashboard: honour a widget's declared `chartConfig` on the inline chart relays, not only on the dataset path

`DashboardWidget.chartConfig` is declared as the spec's full `ChartConfigSchema` on **every** dashboard widget, but only the ADR-0021 dataset path (`DatasetWidget`) read it. The two inline relays — `DashboardRenderer` and `DashboardGridLayout`, which compose the chart node for a widget bound to inline rows or to a `provider: 'object'` aggregate — mentioned `chartConfig` zero times, so an author who wrote `chartConfig.title` / `.subtitle` / `.description` / `.colors` / `.height` / `.showLegend` / `.showDataLabels` / `.annotations` / `.interaction` on such a widget parsed clean and got nothing on screen.

Both relays now lower those keys through the same `chartConfigPresentation` whitelist `DatasetWidget` uses (`@object-ui/core`), so one authored chart config means the same thing on every dashboard surface.

**Behaviour change, stated explicitly** — this is why the bump is `minor` and not a patch: a dashboard whose stored metadata ALREADY carries `chartConfig` on an inline-bound chart widget renders differently after this change. It draws the authored titles, accessible description, palette, plot height, data labels, annotations and interaction toggles that were previously dropped. Widgets that declare no `chartConfig` compose exactly what they composed before.

Five of the fourteen declared keys are still not forwarded, each for a measured reason. `type` is refused because the widget's own `type` already picks the chart family on this path. `xAxis` / `yAxis` / `series` are refused because whether an authored axis beats the dataset-derived one is an open protocol question, filed for the spec seat as objectstack-ai/objectstack#17385. `aria` is refused because nothing on this path reads it in EITHER spelling — measured by forwarding it anyway, as the nested object and again flattened onto the node's own `ariaLabel` / `ariaDescribedBy` / `role`: neither changed a single attribute on screen, because `ChartRenderer` drops every prop but `schema` and `onChartClick`. Delivering it needs a reader inside `@object-ui/plugin-charts`, which is a separate decision.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
* produces it is pinned in plugin-dashboard's
* `DatasetWidget.chartConfig.test.tsx`; together the two close the loop from
* dashboard metadata to drawn pixels.
*
* ⭐ What this file is NOT (objectui#4044). Since that card the two INLINE
* dashboard relays (`DashboardRenderer` and `DashboardGridLayout`, for widgets
* bound to inline rows or to a `provider: 'object'` aggregate rather than to an
* ADR-0021 dataset) lower the same keys through the same
* `chartConfigPresentation` whitelist onto a node of the same shape. It is
* tempting to read the assertions below as covering those relays too. They do
* not, and the difference was measured: with the forwarding deleted from BOTH
* relays, every test in this file still passed — because the schema above is
* hand-built here rather than composed by a relay. What this file pins is the
* CHART BLOCK: that a node carrying these keys draws them. The dashboard
* surface pins its own end of the chain, in plugin-dashboard's
* `DashboardChart.chartConfig-4044.test.tsx` (the seam) and its two
* end-to-end siblings `…chartConfigDom-4044` and `…chartConfigMarks-4044`.
*/

import React from 'react';
Expand Down Expand Up @@ -194,6 +208,41 @@ describe('dashboard chartConfig — interaction (objectstack#7016)', () => {
});
});

describe('dashboard chartConfig — showLegend (objectui#4044)', () => {
// #3135 lowered this flag on the dataset path and objectui#4044 lowers it on
// the two inline relays, but it never had a DRAWN pin here — only seam ones.
// A pie is used because it draws one legend entry per CATEGORY, so the
// legend's presence is readable without a second series.
//
// Recharts registers the legend payload from a layout effect and the Legend
// re-renders off that store update, so the legend text arrives a tick after
// the surface does — hence `waitFor` rather than a read straight after
// `plotted`.
const legendText = (c: HTMLElement) => c.querySelector('.recharts-legend-wrapper')?.textContent ?? '';

it('draws the legend when undeclared (the schema default) and when explicitly on', async () => {
const { container: bare } = render(<ChartRenderer schema={dashboardSchema({ chartType: 'pie' }) as any} />);
await plotted(bare);
await waitFor(() => expect(legendText(bare)).toContain('open'));
cleanup();
const { container: on } = render(
<ChartRenderer schema={dashboardSchema({ chartType: 'pie', showLegend: true }) as any} />,
);
await plotted(on);
await waitFor(() => expect(legendText(on)).toContain('open'));
});

it('draws no legend when showLegend is false', async () => {
// `plotted` first: an empty legend has to mean "the plot drew and chose not
// to legend it", never "nothing rendered yet".
const { container } = render(
<ChartRenderer schema={dashboardSchema({ chartType: 'pie', showLegend: false }) as any} />,
);
await plotted(container);
expect(container.querySelector('.recharts-legend-wrapper')).toBeNull();
});
});

describe('dashboard chartConfig — the keys that stay out (objectstack#7016)', () => {
// `aria` is declared by ChartConfigSchema and read by NOTHING on this path, so
// DatasetWidget refuses to lower it. This pins the "read by nothing" half: even
Expand Down
20 changes: 17 additions & 3 deletions packages/plugin-dashboard/src/DashboardGridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Edit, GripVertical, Save, X, RefreshCw } from 'lucide-react';
import { SchemaRenderer, useHasDndProvider, useDnd } from '@object-ui/react';
import { useObjectTranslation, pickLocalized } from '@object-ui/i18n';
import type { BaseSchema, DashboardComponentSchema, DashboardWidgetSchema } from '@object-ui/types';
import { chartCategoryKey, chartMeasureKey } from '@object-ui/core';
import { chartCategoryKey, chartConfigPresentation, chartMeasureKey } from '@object-ui/core';
import { isObjectProvider, deriveStaticTableColumns } from './utils';
import { classifyWidgetType } from './widgetDispatch';
import { LEGACY_RETIRED_WIDGET_SCHEMA, isLegacyRetiredWidget } from './legacyRetiredWidget';
Expand Down Expand Up @@ -246,6 +246,18 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
const xAxisKey = options.xField || 'name';
const yField = options.yField || 'value';

// The widget's declared `chartConfig`, lowered onto the chart schema —
// objectui#4044, and the twin of the block in `DashboardRenderer`. This
// surface is the EDITABLE dashboard grid over the same stored widget
// metadata, so an author whose `chartConfig` drew nothing here but drew
// on the read-only renderer would read the difference as a bug in the
// editor. `isLegacyRetiredWidget` above is the settled precedent for the
// pair (objectui#4612): one shared implementation, imported rather than
// restated — here that shared implementation is core's
// `chartConfigPresentation`, the same whitelist `DatasetWidget` lowers
// through.
const chartPresentation = chartConfigPresentation(widget.chartConfig);

// provider: 'object' — delegate to ObjectChart for async data loading.
// Field/aggregate config comes from the nested data provider (the
// pre-ADR-0021 top-level analytics keys were retired in framework#3320).
Expand Down Expand Up @@ -284,7 +296,8 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
colors: CHART_COLORS,
// Deterministic first paint inside the grid (#2756).
isAnimationActive: false,
className: "h-full"
className: "h-full",
...chartPresentation,
};
}

Expand All @@ -299,7 +312,8 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
colors: CHART_COLORS,
// Deterministic first paint inside the grid (#2756).
isAnimationActive: false,
className: "h-full"
className: "h-full",
...chartPresentation,
};
}

Expand Down
29 changes: 27 additions & 2 deletions packages/plugin-dashboard/src/DashboardRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
toDomProps,
chartCategoryKey,
chartMeasureKey,
chartConfigPresentation,
} from '@object-ui/core';
import { cn, Card, CardHeader, CardTitle, CardContent, Button, getLazyIcon } from '@object-ui/components';
import { forwardRef, useState, useEffect, useCallback, useMemo, useRef, Fragment } from 'react';
Expand Down Expand Up @@ -639,6 +640,28 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
const xAxisKey = options.xField || 'name';
const yField = options.yField || 'value';

// The widget's declared `chartConfig`, lowered onto the chart
// schema — objectui#4044. `DashboardWidget.chartConfig` is
// declared as the spec's full `ChartConfigSchema` on EVERY
// dashboard widget, but until this card only the ADR-0021
// dataset path (`DatasetWidget`) read it: this inline path
// mentioned `chartConfig` zero times, so an author who wrote
// `chartConfig.title` / `.colors` / `.height` on a widget bound
// to inline rows or to a `provider: 'object'` aggregate parsed
// clean and got nothing.
//
// `chartConfigPresentation` is the SAME whitelist the dataset
// path lowers through (`@object-ui/core`), not a second copy —
// it admits a key only when the chart block measurably draws it
// (see its docblock for the two criteria and for why `aria` is
// refused). Spread AFTER the derived keys so an authored
// `colors` / `height` overrides the defaults below, and BEFORE
// nothing that would shadow the dataset-derived bindings: the
// whitelist emits no `xAxisKey` and no `series`, which is what
// keeps objectstack#17385's open precedence question (authored
// axes vs derived) out of this change.
const chartPresentation = chartConfigPresentation(widget.chartConfig);

// provider: 'object' — delegate to ObjectChart for async data loading.
// Field/aggregate config comes from the nested data provider.
if (isObjectProvider(widgetData)) {
Expand Down Expand Up @@ -683,7 +706,8 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
// which is what `CompareToConfig` projects — so the cast
// that used to bridge the skew is gone.
compareTo: widget.compareTo,
className: "h-[200px] sm:h-[250px] md:h-[300px]"
className: "h-[200px] sm:h-[250px] md:h-[300px]",
...chartPresentation,
};
}

Expand All @@ -702,7 +726,8 @@ const DashboardRendererInner = forwardRef<HTMLDivElement, DashboardRendererProps
colors: CHART_COLORS,
// Deterministic first paint inside the grid (#2756).
isAnimationActive: false,
className: "h-[200px] sm:h-[250px] md:h-[300px]"
className: "h-[200px] sm:h-[250px] md:h-[300px]",
...chartPresentation,
};
}

Expand Down
Loading
Loading