Skip to content

Commit a27c3c7

Browse files
authored
Merge pull request #840 from objectstack-ai/copilot/fix-dashboard-chart-rendering
2 parents b313e6c + 37c4b31 commit a27c3c7

File tree

8 files changed

+161
-18
lines changed

8 files changed

+161
-18
lines changed

ROADMAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,7 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
864864
- [x] **P2: i18n (10 locales)** — Full CRM metadata translations for en, zh, ja, ko, de, fr, es, pt, ru, ar — objects, fields, fieldOptions, navigation, actions, views, formSections, dashboard, reports, pages (24 tests)
865865
- [x] **P2: Full Examples Metadata Audit** — Systematic spec compliance audit across all 4 examples: added `type: 'dashboard'` + `description` to todo/kitchen-sink dashboards, refactored msw-todo to use `ObjectSchema.create` + `Field.*` with snake_case field names, added explicit views to kitchen-sink and msw-todo, added missing `successMessage` on CRM opportunity action, 21 automated compliance tests
866866
- [x] **P2: CRM Dashboard Full provider:'object' Adaptation** — Converted all chart and table widgets in CRM dashboard from static `provider: 'value'` to dynamic `provider: 'object'` with aggregation configs. 12 widgets total: 4 KPI metrics (static), 7 charts (sum/count/avg/max aggregation across opportunity, product, order objects), 1 table (dynamic fetch). Cross-object coverage (order), diverse aggregate functions (sum, count, avg, max). Fixed table `close_date` field alignment. Added i18n for 2 new widgets (10 locales). 9 new CRM metadata tests, 6 new DashboardRenderer rendering tests (area/donut/line/cross-object + edge cases). All provider:'object' paths covered.
867+
- [x] **P1: Dashboard provider:'object' Crash & Blank Rendering Fixes** — Fixed 3 critical bugs causing all charts to be blank and tables to crash on provider:'object' dashboards: (1) DashboardRenderer `...options` spread was leaking provider config objects as `data` in data-table and pivot schemas — fixed by destructuring `data` out before spread, (2) DataTableRenderer and PivotTable now guard with `Array.isArray()` for graceful degradation when non-array data arrives, (3) ObjectChart now shows visible loading/warning messages instead of silently rendering blank when `dataSource` is missing. Also added provider:'object' support to DashboardGridLayout (charts, tables, pivots). 2 new regression tests.
867868

868869
### Ecosystem & Marketplace
869870
- Plugin marketplace website with search, ratings, and install count

packages/components/src/renderers/complex/data-table.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
9191
const {
9292
caption,
9393
columns: rawColumns = [],
94-
data = [],
94+
data: rawData = [],
9595
pagination = true,
9696
pageSize: initialPageSize = 10,
9797
searchable = true,
@@ -109,6 +109,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
109109
showAddRow = false,
110110
} = schema;
111111

112+
// Ensure data is always an array – provider config objects or null/undefined
113+
// must not reach array operations like .filter() / .some()
114+
const data = Array.isArray(rawData) ? rawData : [];
115+
112116
// Normalize columns to support legacy keys (label/name) from existing JSONs
113117
const initialColumns = useMemo(() => {
114118
return rawColumns.map((col: any) => ({

packages/plugin-charts/src/ObjectChart.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,11 @@ export const ObjectChart = (props: any) => {
104104
};
105105

106106
if (loading && finalData.length === 0) {
107-
// Return skeleton or loading state?
108-
// ChartRenderer has suspense/skeleton handling but needs to be triggered.
109-
// We pass empty data but it might render empty chart.
107+
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>Loading chart data…</div>;
108+
}
109+
110+
if (!dataSource && schema.objectName && finalData.length === 0) {
111+
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>No data source available for "{schema.objectName}"</div>;
110112
}
111113

112114
return <ChartRenderer {...props} schema={finalSchema} />;

packages/plugin-dashboard/src/DashboardGridLayout.tsx

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui
55
import { Edit, GripVertical, Save, X, RefreshCw } from 'lucide-react';
66
import { SchemaRenderer, useHasDndProvider, useDnd } from '@object-ui/react';
77
import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types';
8+
import { isObjectProvider } from './utils';
89

910
/** Bridges editMode transitions to the ObjectUI DnD system when a DndProvider is present. */
1011
function DndEditModeBridge({ editMode }: { editMode: boolean }) {
@@ -130,9 +131,25 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
130131
const options = (widget.options || {}) as Record<string, any>;
131132
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
132133
const widgetData = (widget as any).data || options.data;
133-
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
134134
const xAxisKey = options.xField || 'name';
135135
const yField = options.yField || 'value';
136+
137+
// provider: 'object' — delegate to ObjectChart for async data loading
138+
if (isObjectProvider(widgetData)) {
139+
const effectiveYField = widgetData.aggregate?.field || yField;
140+
return {
141+
type: 'object-chart',
142+
chartType: widgetType,
143+
objectName: widgetData.object || widget.object,
144+
aggregate: widgetData.aggregate,
145+
xAxisKey: xAxisKey,
146+
series: [{ dataKey: effectiveYField }],
147+
colors: CHART_COLORS,
148+
className: "h-full"
149+
};
150+
}
151+
152+
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
136153

137154
return {
138155
type: 'chart',
@@ -147,6 +164,22 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
147164

148165
if (widgetType === 'table') {
149166
const widgetData = (widget as any).data || options.data;
167+
168+
// provider: 'object' — pass through object config for async data loading
169+
if (isObjectProvider(widgetData)) {
170+
const { data: _data, ...restOptions } = options;
171+
return {
172+
type: 'data-table',
173+
...restOptions,
174+
objectName: widgetData.object || widget.object,
175+
dataProvider: widgetData,
176+
data: [],
177+
searchable: false,
178+
pagination: false,
179+
className: "border-0"
180+
};
181+
}
182+
150183
return {
151184
type: 'data-table',
152185
...options,
@@ -157,6 +190,28 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
157190
};
158191
}
159192

193+
if (widgetType === 'pivot') {
194+
const widgetData = (widget as any).data || options.data;
195+
196+
// provider: 'object' — pass through object config for async data loading
197+
if (isObjectProvider(widgetData)) {
198+
const { data: _data, ...restOptions } = options;
199+
return {
200+
type: 'pivot',
201+
...restOptions,
202+
objectName: widgetData.object || widget.object,
203+
dataProvider: widgetData,
204+
data: [],
205+
};
206+
}
207+
208+
return {
209+
type: 'pivot',
210+
...options,
211+
data: Array.isArray(widgetData) ? widgetData : widgetData?.items || [],
212+
};
213+
}
214+
160215
return {
161216
...widget,
162217
...options

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { SchemaRenderer } from '@object-ui/react';
1111
import { cn, Card, CardHeader, CardTitle, CardContent, Button } from '@object-ui/components';
1212
import { forwardRef, useState, useEffect, useCallback, useRef } from 'react';
1313
import { RefreshCw } from 'lucide-react';
14+
import { isObjectProvider } from './utils';
1415

1516
// Color palette for charts
1617
const CHART_COLORS = [
@@ -21,16 +22,6 @@ const CHART_COLORS = [
2122
'hsl(var(--chart-5))',
2223
];
2324

24-
/** Returns true when the widget data config uses provider: 'object' (async data source). */
25-
function isObjectProvider(widgetData: unknown): widgetData is { provider: 'object'; object?: string; aggregate?: any } {
26-
return (
27-
widgetData != null &&
28-
typeof widgetData === 'object' &&
29-
!Array.isArray(widgetData) &&
30-
(widgetData as any).provider === 'object'
31-
);
32-
}
33-
3425
export interface DashboardRendererProps {
3526
schema: DashboardSchema;
3627
className?: string;
@@ -164,11 +155,13 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
164155

165156
// provider: 'object' — pass through object config for async data loading
166157
if (isObjectProvider(widgetData)) {
158+
const { data: _data, ...restOptions } = options;
167159
return {
168160
type: 'data-table',
169-
...options,
161+
...restOptions,
170162
objectName: widgetData.object || widget.object,
171163
dataProvider: widgetData,
164+
data: [],
172165
searchable: false,
173166
pagination: false,
174167
className: "border-0"
@@ -190,11 +183,13 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
190183

191184
// provider: 'object' — pass through object config for async data loading
192185
if (isObjectProvider(widgetData)) {
186+
const { data: _data, ...restOptions } = options;
193187
return {
194188
type: 'pivot',
195-
...options,
189+
...restOptions,
196190
objectName: widgetData.object || widget.object,
197191
dataProvider: widgetData,
192+
data: [],
198193
};
199194
}
200195

packages/plugin-dashboard/src/PivotTable.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,16 @@ export const PivotTable: React.FC<PivotTableProps> = ({ schema, className }) =>
9595
columnField,
9696
valueField,
9797
aggregation = 'sum',
98-
data = [],
98+
data: rawData = [],
9999
showRowTotals = false,
100100
showColumnTotals = false,
101101
format,
102102
columnColors,
103103
} = schema;
104104

105+
// Ensure data is always an array – provider config objects must not reach iteration
106+
const data = Array.isArray(rawData) ? rawData : [];
107+
105108
const { rowKeys, colKeys, matrix, rowTotals, colTotals, grandTotal } = useMemo(() => {
106109
// Collect unique row/column values preserving insertion order
107110
const rowSet = new Map<string, true>();

packages/plugin-dashboard/src/__tests__/DashboardRenderer.widgetData.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,4 +659,70 @@ describe('DashboardRenderer widget data extraction', () => {
659659
expect(chartSchema).toBeDefined();
660660
expect(chartSchema.data).toEqual([]);
661661
});
662+
663+
it('should not crash data-table when provider:object leaks data config via options spread', () => {
664+
const schema = {
665+
type: 'dashboard' as const,
666+
name: 'test',
667+
title: 'Test',
668+
widgets: [
669+
{
670+
type: 'table',
671+
title: 'Provider Object Table',
672+
object: 'opportunity',
673+
layout: { x: 0, y: 0, w: 4, h: 2 },
674+
options: {
675+
columns: [
676+
{ header: 'Name', accessorKey: 'name' },
677+
{ header: 'Amount', accessorKey: 'amount' },
678+
],
679+
data: {
680+
provider: 'object',
681+
object: 'opportunity',
682+
},
683+
},
684+
},
685+
],
686+
} as any;
687+
688+
// Must render without throwing. Previously this crashed with
689+
// "paginatedData.some is not a function" because the provider
690+
// config object leaked through as data.
691+
const { container } = render(<DashboardRenderer schema={schema} />);
692+
expect(container).toBeDefined();
693+
// The component should not show a crash error
694+
expect(container.textContent).not.toContain('is not a function');
695+
});
696+
697+
it('should not crash pivot table when provider:object leaks data config via options spread', () => {
698+
const schema = {
699+
type: 'dashboard' as const,
700+
name: 'test',
701+
title: 'Test',
702+
widgets: [
703+
{
704+
type: 'pivot',
705+
title: 'Provider Object Pivot',
706+
object: 'sales',
707+
layout: { x: 0, y: 0, w: 4, h: 2 },
708+
options: {
709+
rowField: 'region',
710+
columnField: 'quarter',
711+
valueField: 'revenue',
712+
data: {
713+
provider: 'object',
714+
object: 'sales',
715+
},
716+
},
717+
},
718+
],
719+
} as any;
720+
721+
// Must render without throwing. Previously this crashed with
722+
// "data is not iterable" because the provider config object
723+
// leaked through as data.
724+
const { container } = render(<DashboardRenderer schema={schema} />);
725+
expect(container).toBeDefined();
726+
expect(container.textContent).not.toContain('is not iterable');
727+
});
662728
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/** Returns true when the widget data config uses provider: 'object' (async data source). */
10+
export function isObjectProvider(widgetData: unknown): widgetData is { provider: 'object'; object?: string; aggregate?: any } {
11+
return (
12+
widgetData != null &&
13+
typeof widgetData === 'object' &&
14+
!Array.isArray(widgetData) &&
15+
(widgetData as any).provider === 'object'
16+
);
17+
}

0 commit comments

Comments
 (0)