Skip to content

Commit 45a10eb

Browse files
authored
Merge pull request #850 from objectstack-ai/copilot/fix-widget-refresh-issue
2 parents 6a57677 + b5b9e64 commit 45a10eb

File tree

6 files changed

+362
-15
lines changed

6 files changed

+362
-15
lines changed

ROADMAP.md

Lines changed: 14 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)
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).
2323
5. **Console Advanced Polish** — Remaining upgrades for forms, import/export, automation, comments
2424
6. **PWA Sync** — Background sync is simulated only
2525

@@ -401,6 +401,18 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
401401
- [x] Updated SchemaRenderer mock to forward `className` and include interactive child button for more realistic testing
402402
- [x] Add 9 new Vitest tests: pointer-events-none presence/absence, overlay presence/absence, relative positioning, click-to-select on Card-based widgets
403403

404+
**Phase 10 — Widget Config Live Preview Sync & Type Switch Fix:**
405+
- [x] Fix: `DashboardWithConfig` did not pass `onFieldChange` to `WidgetConfigPanel`, preventing live preview of widget config changes
406+
- [x] Add internal `liveSchema` state to `DashboardWithConfig` for real-time widget preview during editing
407+
- [x] Add `configVersion` counter to stabilize `selectedWidgetConfig` and prevent `useConfigDraft` draft reset loops
408+
- [x] Fix: `scatter` chart type was not handled in `DashboardRenderer` and `DashboardGridLayout` — switching to scatter caused errors
409+
- [x] Add `scatter` to chart type conditions in both `DashboardRenderer.getComponentSchema()` and `DashboardGridLayout.getComponentSchema()`
410+
- [x] Fix: Data binding fields (`categoryField`, `valueField`, `object`, `aggregate`) from config panel did not affect rendering — `getComponentSchema()` only read from `options.xField`/`options.yField`/`options.data`
411+
- [x] Add widget-level field fallbacks: `widget.categoryField || options.xField`, `widget.valueField || options.yField` in both `DashboardRenderer` and `DashboardGridLayout`
412+
- [x] Support object-chart construction from widget-level fields when no explicit data provider exists (e.g. newly created widgets via config panel)
413+
- [x] Support data-table construction from `widget.object` when no data provider exists (table widgets created via config panel)
414+
- [x] Add 7 new Vitest tests: scatter chart (2), widget-level field fallbacks (2), object-chart from widget fields, data-table from widget.object, DashboardWithConfig live preview
415+
404416
### P1.11 Console — Schema-Driven View Config Panel Migration
405417

406418
> 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.
@@ -969,6 +981,6 @@ The `FlowDesigner` is a canvas-based flow editor that bridges the gap between th
969981

970982
---
971983

972-
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
984+
**Roadmap Status:** 🎯 Active — AppShell · Designer Interaction · View Config Live Preview Sync (P1.8.1) · Dashboard Config Panel (widget live preview & scatter type switch ✅ fixed) · Schema-Driven View Config Panel ✅ · Right-Side Visual Editor Drawer ✅ · Airtable UX Parity
973985
**Next Review:** March 15, 2026
974986
**Contact:** hello@objectui.org | https://github.com/objectstack-ai/objectui

packages/plugin-dashboard/src/DashboardGridLayout.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,11 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
129129

130130
const widgetType = widget.type;
131131
const options = (widget.options || {}) as Record<string, any>;
132-
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
132+
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut' || widgetType === 'scatter') {
133133
const widgetData = (widget as any).data || options.data;
134-
const xAxisKey = options.xField || 'name';
135-
const yField = options.yField || 'value';
134+
// Widget-level fields (from config panel) override options-level fields
135+
const xAxisKey = widget.categoryField || options.xField || 'name';
136+
const yField = widget.valueField || options.yField || 'value';
136137

137138
// provider: 'object' — delegate to ObjectChart for async data loading
138139
if (isObjectProvider(widgetData)) {
@@ -149,6 +150,26 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
149150
};
150151
}
151152

153+
// No explicit data provider but widget has object binding
154+
// (e.g. newly created widget via config panel) — build object-chart
155+
if (!widgetData && widget.object) {
156+
const aggregate = widget.aggregate ? {
157+
field: widget.valueField || 'value',
158+
function: widget.aggregate,
159+
groupBy: widget.categoryField || 'name',
160+
} : undefined;
161+
return {
162+
type: 'object-chart',
163+
chartType: widgetType,
164+
objectName: widget.object,
165+
aggregate,
166+
xAxisKey: xAxisKey,
167+
series: [{ dataKey: widget.valueField || 'value' }],
168+
colors: CHART_COLORS,
169+
className: "h-full"
170+
};
171+
}
172+
152173
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
153174

154175
return {
@@ -180,6 +201,19 @@ export const DashboardGridLayout: React.FC<DashboardGridLayoutProps> = ({
180201
};
181202
}
182203

204+
// No explicit data provider but widget has object binding
205+
if (!widgetData && widget.object) {
206+
return {
207+
type: 'data-table',
208+
...options,
209+
objectName: widget.object,
210+
data: [],
211+
searchable: false,
212+
pagination: false,
213+
className: "border-0"
214+
};
215+
}
216+
183217
return {
184218
type: 'data-table',
185219
...options,

packages/plugin-dashboard/src/DashboardRenderer.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,12 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
112112
// Handle Shorthand Registry Mappings
113113
const widgetType = widget.type;
114114
const options = (widget.options || {}) as Record<string, any>;
115-
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
115+
if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut' || widgetType === 'scatter') {
116116
// Support data at widget level or nested inside options
117117
const widgetData = (widget as any).data || options.data;
118-
const xAxisKey = options.xField || 'name';
119-
const yField = options.yField || 'value';
118+
// Widget-level fields (from config panel) override options-level fields
119+
const xAxisKey = widget.categoryField || options.xField || 'name';
120+
const yField = widget.valueField || options.yField || 'value';
120121

121122
// provider: 'object' — delegate to ObjectChart for async data loading
122123
if (isObjectProvider(widgetData)) {
@@ -136,6 +137,26 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
136137
};
137138
}
138139

140+
// No explicit data provider but widget has object binding
141+
// (e.g. newly created widget via config panel) — build object-chart
142+
if (!widgetData && widget.object) {
143+
const aggregate = widget.aggregate ? {
144+
field: widget.valueField || 'value',
145+
function: widget.aggregate,
146+
groupBy: widget.categoryField || 'name',
147+
} : undefined;
148+
return {
149+
type: 'object-chart',
150+
chartType: widgetType,
151+
objectName: widget.object,
152+
aggregate,
153+
xAxisKey: xAxisKey,
154+
series: [{ dataKey: widget.valueField || 'value' }],
155+
colors: CHART_COLORS,
156+
className: "h-[200px] sm:h-[250px] md:h-[300px]"
157+
};
158+
}
159+
139160
const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
140161

141162
return {
@@ -168,6 +189,19 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
168189
};
169190
}
170191

192+
// No explicit data provider but widget has object binding
193+
if (!widgetData && widget.object) {
194+
return {
195+
type: 'data-table',
196+
...options,
197+
objectName: widget.object,
198+
data: [],
199+
searchable: false,
200+
pagination: false,
201+
className: "border-0"
202+
};
203+
}
204+
171205
return {
172206
type: 'data-table',
173207
...options,

packages/plugin-dashboard/src/DashboardWithConfig.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
*/
88

99
import * as React from 'react';
10-
import { useState, useCallback } from 'react';
10+
import { useState, useCallback, useEffect } from 'react';
1111
import { Settings } from 'lucide-react';
1212
import { cn, Button } from '@object-ui/components';
13-
import type { DashboardSchema } from '@object-ui/types';
13+
import type { DashboardSchema, DashboardWidgetSchema } from '@object-ui/types';
1414

1515
import { DashboardRenderer } from './DashboardRenderer';
1616
import { DashboardConfigPanel } from './DashboardConfigPanel';
@@ -52,6 +52,7 @@ export interface DashboardWithConfigProps {
5252
* - Dashboard-level configuration editing
5353
* - Click-to-select a widget → sidebar switches to WidgetConfigPanel
5454
* - Back navigation from widget config to dashboard config
55+
* - Live preview: widget config changes are reflected in real time
5556
*/
5657
export function DashboardWithConfig({
5758
schema,
@@ -66,10 +67,22 @@ export function DashboardWithConfig({
6667
const [configOpen, setConfigOpen] = useState(defaultConfigOpen);
6768
const [selectedWidgetId, setSelectedWidgetId] = useState<string | null>(null);
6869

69-
// Find the selected widget's config (flattened)
70+
// Internal schema state for live preview during widget editing.
71+
// Updated on every field change; reset when external schema prop changes.
72+
const [liveSchema, setLiveSchema] = useState<DashboardSchema>(schema);
73+
const [configVersion, setConfigVersion] = useState(0);
74+
75+
useEffect(() => {
76+
setLiveSchema(schema);
77+
setConfigVersion((v) => v + 1);
78+
}, [schema]);
79+
80+
// Stable widget config for the config panel — only recomputed on
81+
// widget selection change or save (configVersion), NOT on every live
82+
// field change. This prevents useConfigDraft from resetting the draft.
7083
const selectedWidgetConfig = React.useMemo(() => {
71-
if (!selectedWidgetId || !schema.widgets) return null;
72-
const widget = schema.widgets.find(
84+
if (!selectedWidgetId || !liveSchema.widgets) return null;
85+
const widget = liveSchema.widgets.find(
7386
(w) => (w.id || w.title) === selectedWidgetId,
7487
);
7588
if (!widget) return null;
@@ -87,7 +100,8 @@ export function DashboardWithConfig({
87100
layoutW: widget.layout?.w ?? 1,
88101
layoutH: widget.layout?.h ?? 1,
89102
};
90-
}, [selectedWidgetId, schema.widgets]);
103+
// eslint-disable-next-line react-hooks/exhaustive-deps
104+
}, [selectedWidgetId, configVersion]);
91105

92106
const handleWidgetSelect = useCallback(
93107
(widgetId: string) => {
@@ -101,12 +115,37 @@ export function DashboardWithConfig({
101115
setSelectedWidgetId(null);
102116
}, []);
103117

118+
// Live-update handler: updates liveSchema so DashboardRenderer re-renders.
119+
const handleWidgetFieldChange = useCallback(
120+
(field: string, value: any) => {
121+
if (!selectedWidgetId) return;
122+
setLiveSchema((prev) => {
123+
if (!prev.widgets) return prev;
124+
return {
125+
...prev,
126+
widgets: prev.widgets.map((w) => {
127+
if ((w.id || w.title) !== selectedWidgetId) return w;
128+
if (field === 'layoutW') {
129+
return { ...w, layout: { ...(w.layout || {}), w: value } as DashboardWidgetSchema['layout'] };
130+
}
131+
if (field === 'layoutH') {
132+
return { ...w, layout: { ...(w.layout || {}), h: value } as DashboardWidgetSchema['layout'] };
133+
}
134+
return { ...w, [field]: value };
135+
}),
136+
};
137+
});
138+
},
139+
[selectedWidgetId],
140+
);
141+
104142
const handleWidgetSave = useCallback(
105143
(widgetConfig: Record<string, any>) => {
106144
if (selectedWidgetId && onWidgetSave) {
107145
onWidgetSave(selectedWidgetId, widgetConfig);
108146
}
109147
setSelectedWidgetId(null);
148+
setConfigVersion((v) => v + 1);
110149
},
111150
[selectedWidgetId, onWidgetSave],
112151
);
@@ -137,7 +176,7 @@ export function DashboardWithConfig({
137176
</div>
138177

139178
<DashboardRenderer
140-
schema={schema}
179+
schema={liveSchema}
141180
onRefresh={onRefresh}
142181
recordCount={recordCount}
143182
/>
@@ -152,6 +191,7 @@ export function DashboardWithConfig({
152191
onClose={handleWidgetClose}
153192
config={selectedWidgetConfig}
154193
onSave={handleWidgetSave}
194+
onFieldChange={handleWidgetFieldChange}
155195
/>
156196
) : (
157197
<DashboardConfigPanel

0 commit comments

Comments
 (0)