@object-ui/plugin-dashboard 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,15 +61,17 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
61
61
  };
62
62
  }, [schema.refreshInterval, onRefresh, handleRefresh]);
63
63
 
64
- const renderWidget = (widget: DashboardWidgetSchema) => {
64
+ const renderWidget = (widget: DashboardWidgetSchema, index: number) => {
65
65
  const getComponentSchema = () => {
66
66
  if (widget.component) return widget.component;
67
67
 
68
68
  // Handle Shorthand Registry Mappings
69
- const widgetType = (widget as any).type;
69
+ const widgetType = widget.type;
70
+ const options = (widget.options || {}) as Record<string, any>;
70
71
  if (widgetType === 'bar' || widgetType === 'line' || widgetType === 'area' || widgetType === 'pie' || widgetType === 'donut') {
71
- const dataItems = Array.isArray((widget as any).data) ? (widget as any).data : (widget as any).data?.items || [];
72
- const options = (widget as any).options || {};
72
+ // Support data at widget level or nested inside options
73
+ const widgetData = (widget as any).data || options.data;
74
+ const dataItems = Array.isArray(widgetData) ? widgetData : widgetData?.items || [];
73
75
  const xAxisKey = options.xField || 'name';
74
76
  const yField = options.yField || 'value';
75
77
 
@@ -85,10 +87,12 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
85
87
  }
86
88
 
87
89
  if (widgetType === 'table') {
90
+ // Support data at widget level or nested inside options
91
+ const widgetData = (widget as any).data || options.data;
88
92
  return {
89
93
  type: 'data-table',
90
- ...(widget as any).options,
91
- data: (widget as any).data?.items || [],
94
+ ...options,
95
+ data: widgetData?.items || [],
92
96
  searchable: false,
93
97
  pagination: false,
94
98
  className: "border-0"
@@ -97,17 +101,18 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
97
101
 
98
102
  return {
99
103
  ...widget,
100
- ...((widget as any).options || {})
104
+ ...options
101
105
  };
102
106
  };
103
107
 
104
108
  const componentSchema = getComponentSchema();
105
- const isSelfContained = (widget as any).type === 'metric';
109
+ const isSelfContained = widget.type === 'metric';
110
+ const widgetKey = widget.id || widget.title || `widget-${index}`;
106
111
 
107
112
  if (isSelfContained) {
108
113
  return (
109
114
  <div
110
- key={widget.id || widget.title}
115
+ key={widgetKey}
111
116
  className={cn("h-full w-full", isMobile && "w-[85vw] shrink-0 snap-center")}
112
117
  style={!isMobile && widget.layout ? {
113
118
  gridColumn: `span ${widget.layout.w}`,
@@ -121,7 +126,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
121
126
 
122
127
  return (
123
128
  <Card
124
- key={widget.id || widget.title}
129
+ key={widgetKey}
125
130
  className={cn(
126
131
  "overflow-hidden border-border/50 shadow-sm transition-all hover:shadow-md",
127
132
  "bg-card/50 backdrop-blur-sm",
@@ -171,7 +176,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
171
176
  className="flex overflow-x-auto snap-x snap-mandatory gap-3 pb-4 [-webkit-overflow-scrolling:touch]"
172
177
  style={{ scrollPaddingLeft: '0.75rem' }}
173
178
  >
174
- {schema.widgets?.map((widget: DashboardWidgetSchema) => renderWidget(widget))}
179
+ {schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
175
180
  </div>
176
181
  </div>
177
182
  );
@@ -192,7 +197,7 @@ export const DashboardRenderer = forwardRef<HTMLDivElement, DashboardRendererPro
192
197
  {...props}
193
198
  >
194
199
  {refreshButton}
195
- {schema.widgets?.map((widget: DashboardWidgetSchema) => renderWidget(widget))}
200
+ {schema.widgets?.map((widget: DashboardWidgetSchema, index: number) => renderWidget(widget, index))}
196
201
  </div>
197
202
  );
198
203
  }
@@ -0,0 +1,261 @@
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
+ import { describe, it, expect } from 'vitest';
10
+ import { render } from '@testing-library/react';
11
+ import { DashboardRenderer } from '../DashboardRenderer';
12
+
13
+ /**
14
+ * Extract component schemas rendered by SchemaRenderer from the DOM.
15
+ * When a component type is not registered, SchemaRenderer renders
16
+ * an error block containing a JSON <pre> element with the schema.
17
+ * We parse those to verify the schema shape produced by DashboardRenderer.
18
+ */
19
+ function getRenderedSchemas(container: HTMLElement): any[] {
20
+ const pres = container.querySelectorAll('pre');
21
+ return Array.from(pres).map(el => JSON.parse(el.textContent!));
22
+ }
23
+
24
+ describe('DashboardRenderer widget data extraction', () => {
25
+ it('should extract chart data from options.data.items', () => {
26
+ const schema = {
27
+ type: 'dashboard' as const,
28
+ name: 'test',
29
+ title: 'Test',
30
+ widgets: [
31
+ {
32
+ type: 'bar',
33
+ title: 'Test Bar',
34
+ layout: { x: 0, y: 0, w: 2, h: 2 },
35
+ options: {
36
+ xField: 'name',
37
+ yField: 'value',
38
+ data: {
39
+ provider: 'value',
40
+ items: [
41
+ { name: 'A', value: 100 },
42
+ { name: 'B', value: 200 },
43
+ ],
44
+ },
45
+ },
46
+ },
47
+ ],
48
+ } as any;
49
+
50
+ const { container } = render(<DashboardRenderer schema={schema} />);
51
+ const schemas = getRenderedSchemas(container);
52
+ const chartSchema = schemas.find(s => s.type === 'chart');
53
+
54
+ expect(chartSchema).toBeDefined();
55
+ expect(chartSchema.chartType).toBe('bar');
56
+ expect(chartSchema.data).toHaveLength(2);
57
+ expect(chartSchema.data[0]).toEqual({ name: 'A', value: 100 });
58
+ expect(chartSchema.xAxisKey).toBe('name');
59
+ expect(chartSchema.series).toEqual([{ dataKey: 'value' }]);
60
+ });
61
+
62
+ it('should extract chart data from widget.data.items (backward compat)', () => {
63
+ const schema = {
64
+ type: 'dashboard' as const,
65
+ name: 'test',
66
+ title: 'Test',
67
+ widgets: [
68
+ {
69
+ type: 'area',
70
+ title: 'Test Area',
71
+ layout: { x: 0, y: 0, w: 3, h: 2 },
72
+ options: { xField: 'month', yField: 'revenue' },
73
+ data: {
74
+ provider: 'value',
75
+ items: [
76
+ { month: 'Jan', revenue: 155000 },
77
+ { month: 'Feb', revenue: 87000 },
78
+ ],
79
+ },
80
+ },
81
+ ],
82
+ } as any;
83
+
84
+ const { container } = render(<DashboardRenderer schema={schema} />);
85
+ const schemas = getRenderedSchemas(container);
86
+ const chartSchema = schemas.find(s => s.type === 'chart');
87
+
88
+ expect(chartSchema).toBeDefined();
89
+ expect(chartSchema.chartType).toBe('area');
90
+ expect(chartSchema.data).toHaveLength(2);
91
+ expect(chartSchema.data[0].month).toBe('Jan');
92
+ });
93
+
94
+ it('should extract table data from options.data.items', () => {
95
+ const schema = {
96
+ type: 'dashboard' as const,
97
+ name: 'test',
98
+ title: 'Test',
99
+ widgets: [
100
+ {
101
+ type: 'table',
102
+ title: 'Test Table',
103
+ layout: { x: 0, y: 0, w: 4, h: 2 },
104
+ options: {
105
+ columns: [
106
+ { header: 'Name', accessorKey: 'name' },
107
+ { header: 'Amount', accessorKey: 'amount' },
108
+ ],
109
+ data: {
110
+ provider: 'value',
111
+ items: [
112
+ { name: 'Item A', amount: '$100' },
113
+ { name: 'Item B', amount: '$200' },
114
+ { name: 'Item C', amount: '$300' },
115
+ ],
116
+ },
117
+ },
118
+ },
119
+ ],
120
+ } as any;
121
+
122
+ const { container } = render(<DashboardRenderer schema={schema} />);
123
+ // data-table is a registered component that renders a real table,
124
+ // so we verify the data reaches it by checking for rendered cell content
125
+ expect(container.textContent).toContain('Item A');
126
+ expect(container.textContent).toContain('$200');
127
+ expect(container.textContent).toContain('Item C');
128
+ });
129
+
130
+ it('should handle donut chart data from options', () => {
131
+ const schema = {
132
+ type: 'dashboard' as const,
133
+ name: 'test',
134
+ title: 'Test',
135
+ widgets: [
136
+ {
137
+ type: 'donut',
138
+ title: 'Test Donut',
139
+ layout: { x: 0, y: 0, w: 1, h: 2 },
140
+ options: {
141
+ xField: 'source',
142
+ yField: 'value',
143
+ data: {
144
+ provider: 'value',
145
+ items: [
146
+ { source: 'Web', value: 2 },
147
+ { source: 'Referral', value: 1 },
148
+ ],
149
+ },
150
+ },
151
+ },
152
+ ],
153
+ } as any;
154
+
155
+ const { container } = render(<DashboardRenderer schema={schema} />);
156
+ const schemas = getRenderedSchemas(container);
157
+ const chartSchema = schemas.find(s => s.type === 'chart');
158
+
159
+ expect(chartSchema).toBeDefined();
160
+ expect(chartSchema.chartType).toBe('donut');
161
+ expect(chartSchema.data).toHaveLength(2);
162
+ expect(chartSchema.xAxisKey).toBe('source');
163
+ });
164
+
165
+ it('should default to empty array when no data is provided', () => {
166
+ const schema = {
167
+ type: 'dashboard' as const,
168
+ name: 'test',
169
+ title: 'Test',
170
+ widgets: [
171
+ {
172
+ type: 'bar',
173
+ title: 'No Data Bar',
174
+ layout: { x: 0, y: 0, w: 2, h: 2 },
175
+ options: { xField: 'x', yField: 'y' },
176
+ },
177
+ ],
178
+ } as any;
179
+
180
+ const { container } = render(<DashboardRenderer schema={schema} />);
181
+ const schemas = getRenderedSchemas(container);
182
+ const chartSchema = schemas.find(s => s.type === 'chart');
183
+
184
+ expect(chartSchema).toBeDefined();
185
+ expect(chartSchema.data).toEqual([]);
186
+ });
187
+
188
+ it('should render metric widgets using spec shorthand format', () => {
189
+ const schema = {
190
+ type: 'dashboard' as const,
191
+ name: 'test',
192
+ title: 'Test',
193
+ widgets: [
194
+ {
195
+ type: 'metric',
196
+ layout: { x: 0, y: 0, w: 1, h: 1 },
197
+ options: {
198
+ label: 'Total Revenue',
199
+ value: '$652,000',
200
+ trend: { value: 12.5, direction: 'up', label: 'vs last month' },
201
+ icon: 'DollarSign',
202
+ },
203
+ },
204
+ {
205
+ type: 'metric',
206
+ layout: { x: 1, y: 0, w: 1, h: 1 },
207
+ options: {
208
+ label: 'Active Deals',
209
+ value: '5',
210
+ trend: { value: 2.1, direction: 'down', label: 'vs last month' },
211
+ icon: 'Briefcase',
212
+ },
213
+ },
214
+ ],
215
+ } as any;
216
+
217
+ const { container } = render(<DashboardRenderer schema={schema} />);
218
+
219
+ // MetricWidget is registered in the ComponentRegistry, so it should render
220
+ // the label and value from the merged options
221
+ expect(container.textContent).toContain('Total Revenue');
222
+ expect(container.textContent).toContain('$652,000');
223
+ expect(container.textContent).toContain('Active Deals');
224
+ expect(container.textContent).toContain('5');
225
+ });
226
+
227
+ it('should assign unique keys to widgets without id or title', () => {
228
+ const schema = {
229
+ type: 'dashboard' as const,
230
+ name: 'test',
231
+ title: 'Test',
232
+ widgets: [
233
+ {
234
+ type: 'metric',
235
+ layout: { x: 0, y: 0, w: 1, h: 1 },
236
+ options: { label: 'Metric A', value: '100' },
237
+ },
238
+ {
239
+ type: 'metric',
240
+ layout: { x: 1, y: 0, w: 1, h: 1 },
241
+ options: { label: 'Metric B', value: '200' },
242
+ },
243
+ {
244
+ type: 'metric',
245
+ layout: { x: 2, y: 0, w: 1, h: 1 },
246
+ options: { label: 'Metric C', value: '300' },
247
+ },
248
+ ],
249
+ } as any;
250
+
251
+ const { container } = render(<DashboardRenderer schema={schema} />);
252
+
253
+ // All three metrics should render without React key warnings
254
+ expect(container.textContent).toContain('Metric A');
255
+ expect(container.textContent).toContain('Metric B');
256
+ expect(container.textContent).toContain('Metric C');
257
+ expect(container.textContent).toContain('100');
258
+ expect(container.textContent).toContain('200');
259
+ expect(container.textContent).toContain('300');
260
+ });
261
+ });