Skip to content

Commit 5d811e0

Browse files
Copilothotlong
andcommitted
fix: widget-level fields override data provider config for live preview
- DashboardRenderer/DashboardGridLayout getComponentSchema() now merges widget-level fields (categoryField, valueField, aggregate, object) with data provider config, with widget-level fields taking precedence - Fixes objectName precedence for table/pivot widget types - DashboardWithConfig now passes designMode/selectedWidgetId/onWidgetClick to DashboardRenderer to enable widget selection and live preview - Added 8 new tests for live preview field override behavior Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent d692ef7 commit 5d811e0

5 files changed

Lines changed: 283 additions & 13 deletions

File tree

packages/plugin-dashboard/src/DashboardGridLayout.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,21 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
137137

138138
// provider: 'object' — delegate to ObjectChart for async data loading
139139
if (isObjectProvider(widgetData)) {
140-
const effectiveYField = widgetData.aggregate?.field || yField;
140+
// Merge widget-level fields with data provider config.
141+
// Widget-level fields take precedence so that config panel
142+
// edits are immediately reflected in the live preview.
143+
const providerAgg = widgetData.aggregate;
144+
const effectiveAggregate = providerAgg ? {
145+
field: widget.valueField || providerAgg.field,
146+
function: widget.aggregate || providerAgg.function,
147+
groupBy: widget.categoryField || providerAgg.groupBy,
148+
} : undefined;
149+
const effectiveYField = effectiveAggregate?.field || yField;
141150
return {
142151
type: 'object-chart',
143152
chartType: widgetType,
144-
objectName: widgetData.object || widget.object,
145-
aggregate: widgetData.aggregate,
153+
objectName: widget.object || widgetData.object,
154+
aggregate: effectiveAggregate,
146155
xAxisKey: xAxisKey,
147156
series: [{ dataKey: effectiveYField }],
148157
colors: CHART_COLORS,
@@ -192,7 +201,7 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
192201
return {
193202
type: 'data-table',
194203
...restOptions,
195-
objectName: widgetData.object || widget.object,
204+
objectName: widget.object || widgetData.object,
196205
dataProvider: widgetData,
197206
data: [],
198207
searchable: false,
@@ -233,7 +242,7 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
233242
return {
234243
type: 'pivot',
235244
...restOptions,
236-
objectName: widgetData.object || widget.object,
245+
objectName: widget.object || widgetData.object,
237246
dataProvider: widgetData,
238247
data: [],
239248
};

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,21 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
121121

122122
// provider: 'object' — delegate to ObjectChart for async data loading
123123
if (isObjectProvider(widgetData)) {
124-
// When aggregate is configured, use the aggregate field as the
125-
// series dataKey so it matches the output of aggregateRecords()
126-
// which produces { [groupBy]: key, [field]: result }.
127-
const effectiveYField = widgetData.aggregate?.field || yField;
124+
// Merge widget-level fields with data provider config.
125+
// Widget-level fields take precedence so that config panel
126+
// edits are immediately reflected in the live preview.
127+
const providerAgg = widgetData.aggregate;
128+
const effectiveAggregate = providerAgg ? {
129+
field: widget.valueField || providerAgg.field,
130+
function: widget.aggregate || providerAgg.function,
131+
groupBy: widget.categoryField || providerAgg.groupBy,
132+
} : undefined;
133+
const effectiveYField = effectiveAggregate?.field || yField;
128134
return {
129135
type: 'object-chart',
130136
chartType: widgetType,
131-
objectName: widgetData.object || widget.object,
132-
aggregate: widgetData.aggregate,
137+
objectName: widget.object || widgetData.object,
138+
aggregate: effectiveAggregate,
133139
xAxisKey: xAxisKey,
134140
series: [{ dataKey: effectiveYField }],
135141
colors: CHART_COLORS,
@@ -180,7 +186,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
180186
return {
181187
type: 'data-table',
182188
...restOptions,
183-
objectName: widgetData.object || widget.object,
189+
objectName: widget.object || widgetData.object,
184190
dataProvider: widgetData,
185191
data: [],
186192
searchable: false,
@@ -221,7 +227,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
221227
return {
222228
type: 'pivot',
223229
...restOptions,
224-
objectName: widgetData.object || widget.object,
230+
objectName: widget.object || widgetData.object,
225231
dataProvider: widgetData,
226232
data: [],
227233
};

packages/plugin-dashboard/src/DashboardWithConfig.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ export function DashboardWithConfig({
179179
schema={liveSchema}
180180
onRefresh={onRefresh}
181181
recordCount={recordCount}
182+
designMode={configOpen}
183+
selectedWidgetId={selectedWidgetId}
184+
onWidgetClick={handleWidgetSelect}
182185
/>
183186
</div>
184187

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

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,4 +925,217 @@ describe('DashboardRenderer widget data extraction', () => {
925925
// Either way, it should not crash
926926
expect(container).toBeDefined();
927927
});
928+
929+
// ---- Live preview: widget-level fields override data provider config ------
930+
931+
it('should override data provider aggregate.groupBy with widget.categoryField', () => {
932+
const schema = {
933+
type: 'dashboard' as const,
934+
name: 'test',
935+
title: 'Test',
936+
widgets: [
937+
{
938+
type: 'bar',
939+
title: 'Live Preview',
940+
object: 'opportunity',
941+
categoryField: 'region',
942+
layout: { x: 0, y: 0, w: 2, h: 2 },
943+
options: {
944+
xField: 'stage',
945+
yField: 'amount',
946+
data: {
947+
provider: 'object',
948+
object: 'opportunity',
949+
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
950+
},
951+
},
952+
},
953+
],
954+
} as any;
955+
956+
const { container } = render(<DashboardRenderer schema={schema} />);
957+
const schemas = getRenderedSchemas(container);
958+
const chartSchema = schemas.find(s => s.type === 'object-chart');
959+
960+
expect(chartSchema).toBeDefined();
961+
// widget.categoryField ('region') should override aggregate.groupBy ('stage')
962+
expect(chartSchema.aggregate.groupBy).toBe('region');
963+
expect(chartSchema.xAxisKey).toBe('region');
964+
});
965+
966+
it('should override data provider aggregate.field with widget.valueField', () => {
967+
const schema = {
968+
type: 'dashboard' as const,
969+
name: 'test',
970+
title: 'Test',
971+
widgets: [
972+
{
973+
type: 'area',
974+
title: 'Live Preview',
975+
object: 'opportunity',
976+
valueField: 'expected_revenue',
977+
layout: { x: 0, y: 0, w: 3, h: 2 },
978+
options: {
979+
xField: 'stage',
980+
yField: 'amount',
981+
data: {
982+
provider: 'object',
983+
object: 'opportunity',
984+
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
985+
},
986+
},
987+
},
988+
],
989+
} as any;
990+
991+
const { container } = render(<DashboardRenderer schema={schema} />);
992+
const schemas = getRenderedSchemas(container);
993+
const chartSchema = schemas.find(s => s.type === 'object-chart');
994+
995+
expect(chartSchema).toBeDefined();
996+
// widget.valueField ('expected_revenue') should override aggregate.field ('amount')
997+
expect(chartSchema.aggregate.field).toBe('expected_revenue');
998+
expect(chartSchema.series).toEqual([{ dataKey: 'expected_revenue' }]);
999+
});
1000+
1001+
it('should override data provider aggregate.function with widget.aggregate', () => {
1002+
const schema = {
1003+
type: 'dashboard' as const,
1004+
name: 'test',
1005+
title: 'Test',
1006+
widgets: [
1007+
{
1008+
type: 'bar',
1009+
title: 'Live Preview',
1010+
object: 'opportunity',
1011+
aggregate: 'count',
1012+
layout: { x: 0, y: 0, w: 2, h: 2 },
1013+
options: {
1014+
xField: 'stage',
1015+
yField: 'amount',
1016+
data: {
1017+
provider: 'object',
1018+
object: 'opportunity',
1019+
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
1020+
},
1021+
},
1022+
},
1023+
],
1024+
} as any;
1025+
1026+
const { container } = render(<DashboardRenderer schema={schema} />);
1027+
const schemas = getRenderedSchemas(container);
1028+
const chartSchema = schemas.find(s => s.type === 'object-chart');
1029+
1030+
expect(chartSchema).toBeDefined();
1031+
// widget.aggregate ('count') should override aggregate.function ('sum')
1032+
expect(chartSchema.aggregate.function).toBe('count');
1033+
});
1034+
1035+
it('should prefer widget.object over data provider object for objectName', () => {
1036+
const schema = {
1037+
type: 'dashboard' as const,
1038+
name: 'test',
1039+
title: 'Test',
1040+
widgets: [
1041+
{
1042+
type: 'line',
1043+
title: 'Live Preview',
1044+
object: 'contact',
1045+
layout: { x: 0, y: 0, w: 3, h: 2 },
1046+
options: {
1047+
xField: 'month',
1048+
yField: 'count',
1049+
data: {
1050+
provider: 'object',
1051+
object: 'opportunity',
1052+
aggregate: { field: 'count', function: 'count', groupBy: 'month' },
1053+
},
1054+
},
1055+
},
1056+
],
1057+
} as any;
1058+
1059+
const { container } = render(<DashboardRenderer schema={schema} />);
1060+
const schemas = getRenderedSchemas(container);
1061+
const chartSchema = schemas.find(s => s.type === 'object-chart');
1062+
1063+
expect(chartSchema).toBeDefined();
1064+
// widget.object ('contact') should override data.object ('opportunity')
1065+
expect(chartSchema.objectName).toBe('contact');
1066+
});
1067+
1068+
it('should prefer widget.object for table widgets with data provider', () => {
1069+
const schema = {
1070+
type: 'dashboard' as const,
1071+
name: 'test',
1072+
title: 'Test',
1073+
widgets: [
1074+
{
1075+
type: 'table',
1076+
title: 'Live Preview Table',
1077+
object: 'contact',
1078+
layout: { x: 0, y: 0, w: 4, h: 2 },
1079+
options: {
1080+
data: {
1081+
provider: 'object',
1082+
object: 'opportunity',
1083+
},
1084+
},
1085+
},
1086+
],
1087+
} as any;
1088+
1089+
const { container } = render(<DashboardRenderer schema={schema} />);
1090+
const schemas = getRenderedSchemas(container);
1091+
const tableSchema = schemas.find(s => s.type === 'data-table');
1092+
1093+
if (tableSchema) {
1094+
// widget.object ('contact') should override data.object ('opportunity')
1095+
expect(tableSchema.objectName).toBe('contact');
1096+
}
1097+
});
1098+
1099+
it('should apply all widget-level field overrides simultaneously for live preview', () => {
1100+
const schema = {
1101+
type: 'dashboard' as const,
1102+
name: 'test',
1103+
title: 'Test',
1104+
widgets: [
1105+
{
1106+
type: 'pie',
1107+
title: 'Full Override',
1108+
object: 'account',
1109+
categoryField: 'industry',
1110+
valueField: 'revenue',
1111+
aggregate: 'avg',
1112+
layout: { x: 0, y: 0, w: 2, h: 2 },
1113+
options: {
1114+
xField: 'stage',
1115+
yField: 'amount',
1116+
data: {
1117+
provider: 'object',
1118+
object: 'opportunity',
1119+
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
1120+
},
1121+
},
1122+
},
1123+
],
1124+
} as any;
1125+
1126+
const { container } = render(<DashboardRenderer schema={schema} />);
1127+
const schemas = getRenderedSchemas(container);
1128+
const chartSchema = schemas.find(s => s.type === 'object-chart');
1129+
1130+
expect(chartSchema).toBeDefined();
1131+
expect(chartSchema.chartType).toBe('pie');
1132+
expect(chartSchema.objectName).toBe('account');
1133+
expect(chartSchema.xAxisKey).toBe('industry');
1134+
expect(chartSchema.aggregate).toEqual({
1135+
field: 'revenue',
1136+
function: 'avg',
1137+
groupBy: 'industry',
1138+
});
1139+
expect(chartSchema.series).toEqual([{ dataKey: 'revenue' }]);
1140+
});
9281141
});

packages/plugin-dashboard/src/__tests__/DashboardWithConfig.test.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,43 @@ describe('DashboardWithConfig', () => {
234234
expect(container).toBeDefined();
235235
expect(screen.getByTestId('dashboard-with-config')).toBeDefined();
236236
});
237+
238+
it('should enable design mode and pass widget selection props to DashboardRenderer when config is open', () => {
239+
render(
240+
<DashboardWithConfig
241+
schema={sampleSchema}
242+
config={sampleConfig}
243+
onConfigSave={vi.fn()}
244+
onWidgetSave={vi.fn()}
245+
defaultConfigOpen={true}
246+
/>,
247+
);
248+
// When config panel is open, widget click overlays should be present
249+
// (design mode enabled) for interactive widget selection
250+
const overlays = screen.queryAllByTestId('widget-click-overlay');
251+
expect(overlays.length).toBeGreaterThan(0);
252+
});
253+
254+
it('should switch to WidgetConfigPanel when a widget is clicked in design mode', () => {
255+
render(
256+
<DashboardWithConfig
257+
schema={sampleSchema}
258+
config={sampleConfig}
259+
onConfigSave={vi.fn()}
260+
onWidgetSave={vi.fn()}
261+
defaultConfigOpen={true}
262+
/>,
263+
);
264+
// Initially shows Dashboard > Configuration
265+
expect(screen.getByText('Dashboard')).toBeDefined();
266+
expect(screen.getByText('Configuration')).toBeDefined();
267+
expect(screen.queryByText('Widget')).toBeNull();
268+
269+
// Click on a widget preview to select it
270+
const widgetOverlay = screen.getByTestId('dashboard-preview-widget-widget-1');
271+
fireEvent.click(widgetOverlay);
272+
273+
// Should now show Widget breadcrumb (WidgetConfigPanel)
274+
expect(screen.getByText('Widget')).toBeDefined();
275+
});
237276
});

0 commit comments

Comments
 (0)