@object-ui/plugin-dashboard 3.3.0 → 3.3.1

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +21 -1
  3. package/dist/index.js +869 -787
  4. package/dist/index.umd.cjs +4 -4
  5. package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts +5 -0
  6. package/dist/packages/plugin-dashboard/src/DashboardRenderer.d.ts.map +1 -1
  7. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
  8. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
  9. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
  10. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
  11. package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
  12. package/package.json +40 -7
  13. package/.turbo/turbo-build.log +0 -41
  14. package/src/DashboardConfigPanel.stories.tsx +0 -164
  15. package/src/DashboardConfigPanel.tsx +0 -158
  16. package/src/DashboardGridLayout.tsx +0 -367
  17. package/src/DashboardRenderer.stories.tsx +0 -173
  18. package/src/DashboardRenderer.tsx +0 -479
  19. package/src/DashboardWithConfig.tsx +0 -211
  20. package/src/MetricCard.tsx +0 -102
  21. package/src/MetricWidget.tsx +0 -96
  22. package/src/ObjectDataTable.tsx +0 -226
  23. package/src/ObjectMetricWidget.tsx +0 -159
  24. package/src/ObjectPivotTable.tsx +0 -160
  25. package/src/PivotTable.tsx +0 -262
  26. package/src/WidgetConfigPanel.tsx +0 -540
  27. package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
  28. package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
  29. package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
  30. package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
  31. package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
  32. package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
  33. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
  34. package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
  35. package/src/__tests__/MetricCard.test.tsx +0 -107
  36. package/src/__tests__/ObjectDataTable.test.tsx +0 -211
  37. package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
  38. package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
  39. package/src/__tests__/PivotTable.test.tsx +0 -162
  40. package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
  41. package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
  42. package/src/index.tsx +0 -236
  43. package/src/utils.ts +0 -17
  44. package/tsconfig.json +0 -19
  45. package/vite.config.ts +0 -64
  46. package/vitest.config.ts +0 -9
  47. package/vitest.setup.tsx +0 -18
@@ -1,276 +0,0 @@
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, vi } from 'vitest';
10
- import { render, screen, fireEvent } from '@testing-library/react';
11
- import { DashboardWithConfig } from '../DashboardWithConfig';
12
- import type { DashboardSchema } from '@object-ui/types';
13
-
14
- // Mock SchemaRenderer to avoid deep component tree
15
- vi.mock('@object-ui/react', () => ({
16
- SchemaRenderer: ({ schema }: any) => (
17
- <div data-testid="schema-renderer">{schema?.type ?? 'unknown'}</div>
18
- ),
19
- }));
20
-
21
- const sampleSchema: DashboardSchema = {
22
- type: 'dashboard',
23
- columns: 3,
24
- gap: 4,
25
- widgets: [
26
- {
27
- id: 'widget-1',
28
- title: 'Revenue',
29
- type: 'metric',
30
- component: { type: 'metric', label: 'Revenue', value: '$100K' },
31
- },
32
- {
33
- id: 'widget-2',
34
- title: 'Users',
35
- type: 'bar',
36
- object: 'users',
37
- categoryField: 'role',
38
- valueField: 'count',
39
- layout: { x: 0, y: 1, w: 2, h: 1 },
40
- },
41
- ],
42
- };
43
-
44
- const sampleConfig = {
45
- columns: 3,
46
- gap: 4,
47
- rowHeight: '120',
48
- refreshInterval: '0',
49
- title: 'My Dashboard',
50
- showDescription: true,
51
- theme: 'auto',
52
- };
53
-
54
- describe('DashboardWithConfig', () => {
55
- it('should render the dashboard container', () => {
56
- render(
57
- <DashboardWithConfig
58
- schema={sampleSchema}
59
- config={sampleConfig}
60
- onConfigSave={vi.fn()}
61
- />,
62
- );
63
- expect(screen.getByTestId('dashboard-with-config')).toBeDefined();
64
- });
65
-
66
- it('should render the settings toggle button', () => {
67
- render(
68
- <DashboardWithConfig
69
- schema={sampleSchema}
70
- config={sampleConfig}
71
- onConfigSave={vi.fn()}
72
- />,
73
- );
74
- expect(screen.getByTestId('dashboard-config-toggle')).toBeDefined();
75
- expect(screen.getByText('Settings')).toBeDefined();
76
- });
77
-
78
- it('should not show config panel by default', () => {
79
- render(
80
- <DashboardWithConfig
81
- schema={sampleSchema}
82
- config={sampleConfig}
83
- onConfigSave={vi.fn()}
84
- />,
85
- );
86
- expect(screen.queryByTestId('config-panel')).toBeNull();
87
- });
88
-
89
- it('should show config panel when defaultConfigOpen is true', () => {
90
- render(
91
- <DashboardWithConfig
92
- schema={sampleSchema}
93
- config={sampleConfig}
94
- onConfigSave={vi.fn()}
95
- defaultConfigOpen={true}
96
- />,
97
- );
98
- expect(screen.getByTestId('config-panel')).toBeDefined();
99
- });
100
-
101
- it('should toggle config panel when settings button is clicked', () => {
102
- render(
103
- <DashboardWithConfig
104
- schema={sampleSchema}
105
- config={sampleConfig}
106
- onConfigSave={vi.fn()}
107
- />,
108
- );
109
- // Initially closed
110
- expect(screen.queryByTestId('config-panel')).toBeNull();
111
-
112
- // Open
113
- fireEvent.click(screen.getByTestId('dashboard-config-toggle'));
114
- expect(screen.getByTestId('config-panel')).toBeDefined();
115
-
116
- // Close
117
- fireEvent.click(screen.getByTestId('dashboard-config-toggle'));
118
- expect(screen.queryByTestId('config-panel')).toBeNull();
119
- });
120
-
121
- it('should show dashboard breadcrumb in config panel', () => {
122
- render(
123
- <DashboardWithConfig
124
- schema={sampleSchema}
125
- config={sampleConfig}
126
- onConfigSave={vi.fn()}
127
- defaultConfigOpen={true}
128
- />,
129
- );
130
- expect(screen.getByText('Dashboard')).toBeDefined();
131
- expect(screen.getByText('Configuration')).toBeDefined();
132
- });
133
-
134
- it('should close config panel via close button', () => {
135
- render(
136
- <DashboardWithConfig
137
- schema={sampleSchema}
138
- config={sampleConfig}
139
- onConfigSave={vi.fn()}
140
- defaultConfigOpen={true}
141
- />,
142
- );
143
- fireEvent.click(screen.getByTestId('config-panel-close'));
144
- expect(screen.queryByTestId('config-panel')).toBeNull();
145
- });
146
-
147
- it('should call onConfigSave when saving dashboard config', () => {
148
- const onConfigSave = vi.fn();
149
- render(
150
- <DashboardWithConfig
151
- schema={sampleSchema}
152
- config={sampleConfig}
153
- onConfigSave={onConfigSave}
154
- defaultConfigOpen={true}
155
- />,
156
- );
157
- // Trigger a change to make the panel dirty
158
- const rowHeightInput = screen.getByTestId('config-field-rowHeight');
159
- fireEvent.change(rowHeightInput, { target: { value: '200' } });
160
- // Save
161
- fireEvent.click(screen.getByTestId('config-panel-save'));
162
- expect(onConfigSave).toHaveBeenCalledTimes(1);
163
- expect(onConfigSave.mock.calls[0][0].rowHeight).toBe('200');
164
- });
165
-
166
- it('should apply className to container', () => {
167
- render(
168
- <DashboardWithConfig
169
- schema={sampleSchema}
170
- config={sampleConfig}
171
- onConfigSave={vi.fn()}
172
- className="custom-class"
173
- />,
174
- );
175
- const container = screen.getByTestId('dashboard-with-config');
176
- expect(container.className).toContain('custom-class');
177
- });
178
-
179
- it('should show Dashboard > Configuration breadcrumb when no widget is selected', () => {
180
- render(
181
- <DashboardWithConfig
182
- schema={sampleSchema}
183
- config={sampleConfig}
184
- onConfigSave={vi.fn()}
185
- defaultConfigOpen={true}
186
- />,
187
- );
188
- expect(screen.getByText('Dashboard')).toBeDefined();
189
- expect(screen.getByText('Configuration')).toBeDefined();
190
- // Widget breadcrumb should NOT be present
191
- expect(screen.queryByText('Widget')).toBeNull();
192
- });
193
-
194
- it('should accept onWidgetSave prop without errors', () => {
195
- const onWidgetSave = vi.fn();
196
- render(
197
- <DashboardWithConfig
198
- schema={sampleSchema}
199
- config={sampleConfig}
200
- onConfigSave={vi.fn()}
201
- onWidgetSave={onWidgetSave}
202
- defaultConfigOpen={true}
203
- />,
204
- );
205
- // Config panel should open showing Dashboard config by default (no widget selected)
206
- expect(screen.getByText('Dashboard')).toBeDefined();
207
- expect(screen.getByText('Configuration')).toBeDefined();
208
- expect(screen.queryByText('Widget')).toBeNull();
209
- });
210
-
211
- it('should pass onFieldChange to WidgetConfigPanel for live preview', () => {
212
- // Schema with a widget that will be selected via mocking selection state
213
- const schemaWithWidget: DashboardSchema = {
214
- ...sampleSchema,
215
- widgets: [
216
- {
217
- id: 'w1',
218
- title: 'Test Widget',
219
- type: 'bar',
220
- layout: { x: 0, y: 0, w: 2, h: 1 },
221
- },
222
- ],
223
- };
224
- const { container } = render(
225
- <DashboardWithConfig
226
- schema={schemaWithWidget}
227
- config={sampleConfig}
228
- onConfigSave={vi.fn()}
229
- onWidgetSave={vi.fn()}
230
- defaultConfigOpen={true}
231
- />,
232
- );
233
- // Container should render the dashboard
234
- expect(container).toBeDefined();
235
- expect(screen.getByTestId('dashboard-with-config')).toBeDefined();
236
- });
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
- });
276
- });
@@ -1,107 +0,0 @@
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, screen } from '@testing-library/react';
11
- import '@testing-library/jest-dom';
12
- import { MetricCard } from '../MetricCard';
13
-
14
- describe('MetricCard', () => {
15
- it('should render metric card with title and value', () => {
16
- render(<MetricCard title="Total Users" value="1,234" />);
17
-
18
- expect(screen.getByText('Total Users')).toBeInTheDocument();
19
- expect(screen.getByText('1,234')).toBeInTheDocument();
20
- });
21
-
22
- it('should render trend indicator when trend is provided', () => {
23
- render(
24
- <MetricCard
25
- title="Revenue"
26
- value="$45,231"
27
- trend="up"
28
- trendValue="+12%"
29
- description="vs last month"
30
- />
31
- );
32
-
33
- expect(screen.getByText('Revenue')).toBeInTheDocument();
34
- expect(screen.getByText('$45,231')).toBeInTheDocument();
35
- expect(screen.getByText('+12%')).toBeInTheDocument();
36
- expect(screen.getByText('vs last month')).toBeInTheDocument();
37
- });
38
-
39
- it('should render description without trend', () => {
40
- render(
41
- <MetricCard
42
- title="Active Sessions"
43
- value="432"
44
- description="current users online"
45
- />
46
- );
47
-
48
- expect(screen.getByText('Active Sessions')).toBeInTheDocument();
49
- expect(screen.getByText('432')).toBeInTheDocument();
50
- expect(screen.getByText('current users online')).toBeInTheDocument();
51
- });
52
-
53
- it('should handle numeric values', () => {
54
- render(<MetricCard title="Count" value={1234} />);
55
-
56
- expect(screen.getByText('Count')).toBeInTheDocument();
57
- expect(screen.getByText('1234')).toBeInTheDocument();
58
- });
59
-
60
- it('should resolve I18nLabel objects for title', () => {
61
- render(
62
- <MetricCard
63
- title={{ key: 'crm.dashboard.widgets.totalRevenue', defaultValue: 'Total Revenue' }}
64
- value="$45,231"
65
- />
66
- );
67
-
68
- expect(screen.getByText('Total Revenue')).toBeInTheDocument();
69
- });
70
-
71
- it('should resolve I18nLabel objects for description', () => {
72
- render(
73
- <MetricCard
74
- title="Revenue"
75
- value="$45,231"
76
- description={{ key: 'crm.dashboard.trendLabel', defaultValue: 'vs last month' }}
77
- />
78
- );
79
-
80
- expect(screen.getByText('vs last month')).toBeInTheDocument();
81
- });
82
-
83
- it('should show loading state when loading prop is true', () => {
84
- const { container } = render(
85
- <MetricCard title="Revenue" value="$45,231" loading={true} />
86
- );
87
-
88
- const loadingEl = container.querySelector('[data-testid="metric-card-loading"]');
89
- expect(loadingEl).toBeTruthy();
90
- // Value should not be rendered during loading
91
- expect(container.textContent).not.toContain('$45,231');
92
- });
93
-
94
- it('should show error state when error prop is set', () => {
95
- const { container } = render(
96
- <MetricCard title="Revenue" value="$45,231" error="Connection refused" />
97
- );
98
-
99
- const errorEl = container.querySelector('[data-testid="metric-card-error"]');
100
- expect(errorEl).toBeTruthy();
101
- expect(screen.getByText('Connection refused')).toBeInTheDocument();
102
- // Value should not be rendered during error
103
- expect(container.textContent).not.toContain('$45,231');
104
- // Title should still be visible
105
- expect(screen.getByText('Revenue')).toBeInTheDocument();
106
- });
107
- });
@@ -1,211 +0,0 @@
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, vi } from 'vitest';
10
- import { render, screen, waitFor } from '@testing-library/react';
11
- import React from 'react';
12
- import { ObjectDataTable, normalizeColumns } from '../ObjectDataTable';
13
- import { SchemaRendererProvider } from '@object-ui/react';
14
-
15
- describe('ObjectDataTable', () => {
16
- const baseSchema = {
17
- type: 'object-data-table',
18
- objectName: 'contacts',
19
- };
20
-
21
- it('should show loading skeleton when fetching data', async () => {
22
- const dataSource = {
23
- find: vi.fn(() => new Promise(() => {})), // Never resolves
24
- };
25
-
26
- const { container } = render(
27
- <SchemaRendererProvider dataSource={dataSource}>
28
- <ObjectDataTable schema={baseSchema} />
29
- </SchemaRendererProvider>,
30
- );
31
-
32
- await waitFor(() => {
33
- const loadingEl = container.querySelector('[data-testid="table-loading"]');
34
- expect(loadingEl).toBeDefined();
35
- });
36
- });
37
-
38
- it('should show error state on fetch failure', async () => {
39
- const dataSource = {
40
- find: vi.fn().mockRejectedValue(new Error('Connection refused')),
41
- };
42
-
43
- const { container } = render(
44
- <SchemaRendererProvider dataSource={dataSource}>
45
- <ObjectDataTable schema={baseSchema} />
46
- </SchemaRendererProvider>,
47
- );
48
-
49
- await waitFor(() => {
50
- const errorEl = container.querySelector('[data-testid="table-error"]');
51
- expect(errorEl).toBeTruthy();
52
- });
53
-
54
- expect(screen.getByText('Connection refused')).toBeTruthy();
55
- });
56
-
57
- it('should show empty state when no data returned', async () => {
58
- const dataSource = {
59
- find: vi.fn().mockResolvedValue({ records: [] }),
60
- };
61
-
62
- const { container } = render(
63
- <SchemaRendererProvider dataSource={dataSource}>
64
- <ObjectDataTable schema={baseSchema} />
65
- </SchemaRendererProvider>,
66
- );
67
-
68
- await waitFor(() => {
69
- const emptyState = container.querySelector('[data-testid="table-empty-state"]');
70
- expect(emptyState).toBeDefined();
71
- });
72
- });
73
-
74
- it('should show no-data-source message when objectName is set but no dataSource', () => {
75
- render(<ObjectDataTable schema={baseSchema} />);
76
- expect(screen.getByText(/No data source available/)).toBeDefined();
77
- });
78
-
79
- it('should auto-derive columns from fetched data keys', async () => {
80
- const dataSource = {
81
- find: vi.fn().mockResolvedValue({
82
- records: [
83
- { firstName: 'Alice', lastName: 'Smith', email: 'alice@test.com' },
84
- { firstName: 'Bob', lastName: 'Jones', email: 'bob@test.com' },
85
- ],
86
- }),
87
- };
88
-
89
- const schema = { ...baseSchema, objectName: 'contacts' };
90
-
91
- const { container } = render(
92
- <SchemaRendererProvider dataSource={dataSource}>
93
- <ObjectDataTable schema={schema} />
94
- </SchemaRendererProvider>,
95
- );
96
-
97
- // Wait for data to be fetched and rendered
98
- await waitFor(() => {
99
- // data-table renders via SchemaRenderer, so look for content
100
- expect(container.textContent).toBeDefined();
101
- });
102
-
103
- expect(dataSource.find).toHaveBeenCalledWith('contacts', { $filter: undefined });
104
- });
105
-
106
- it('should prefer static data over fetched data', () => {
107
- const dataSource = { find: vi.fn() };
108
-
109
- const schema = {
110
- ...baseSchema,
111
- data: [{ name: 'Static Row', value: 42 }],
112
- };
113
-
114
- render(
115
- <SchemaRendererProvider dataSource={dataSource}>
116
- <ObjectDataTable schema={schema} />
117
- </SchemaRendererProvider>,
118
- );
119
-
120
- expect(dataSource.find).not.toHaveBeenCalled();
121
- });
122
-
123
- it('should normalize string[] columns without crashing', () => {
124
- const schema = {
125
- ...baseSchema,
126
- columns: ['name', 'amount', 'close_date'],
127
- data: [
128
- { name: 'Deal A', amount: 1000, close_date: '2025-01-01' },
129
- { name: 'Deal B', amount: 2000, close_date: '2025-06-01' },
130
- ],
131
- };
132
-
133
- // Should not crash when columns are strings
134
- const { container } = render(
135
- <SchemaRendererProvider>
136
- <ObjectDataTable schema={schema} />
137
- </SchemaRendererProvider>,
138
- );
139
-
140
- expect(container).toBeDefined();
141
- });
142
-
143
- it('should pass through object[] columns unchanged', () => {
144
- const schema = {
145
- ...baseSchema,
146
- columns: [
147
- { header: 'Name', accessorKey: 'name' },
148
- { header: 'Amount', accessorKey: 'amount' },
149
- ],
150
- data: [
151
- { name: 'Deal A', amount: 1000 },
152
- ],
153
- };
154
-
155
- const { container } = render(
156
- <SchemaRendererProvider>
157
- <ObjectDataTable schema={schema} />
158
- </SchemaRendererProvider>,
159
- );
160
-
161
- expect(container).toBeDefined();
162
- });
163
- });
164
-
165
- describe('normalizeColumns', () => {
166
- it('should convert snake_case string to title-cased header', () => {
167
- const result = normalizeColumns(['close_date']);
168
- expect(result).toEqual([{ header: 'Close Date', accessorKey: 'close_date' }]);
169
- });
170
-
171
- it('should convert camelCase string to title-cased header', () => {
172
- const result = normalizeColumns(['firstName']);
173
- expect(result).toEqual([{ header: 'First Name', accessorKey: 'firstName' }]);
174
- });
175
-
176
- it('should convert simple string to capitalized header', () => {
177
- const result = normalizeColumns(['name']);
178
- expect(result).toEqual([{ header: 'Name', accessorKey: 'name' }]);
179
- });
180
-
181
- it('should handle multiple string columns', () => {
182
- const result = normalizeColumns(['name', 'total_amount', 'createdAt']);
183
- expect(result).toEqual([
184
- { header: 'Name', accessorKey: 'name' },
185
- { header: 'Total Amount', accessorKey: 'total_amount' },
186
- { header: 'Created At', accessorKey: 'createdAt' },
187
- ]);
188
- });
189
-
190
- it('should pass through object columns unchanged', () => {
191
- const cols = [
192
- { header: 'Custom Name', accessorKey: 'name' },
193
- { header: 'Amount ($)', accessorKey: 'amount' },
194
- ];
195
- const result = normalizeColumns(cols);
196
- expect(result).toEqual(cols);
197
- });
198
-
199
- it('should handle mixed string and object columns', () => {
200
- const result = normalizeColumns([
201
- 'name',
202
- { header: 'Custom Amount', accessorKey: 'amount' },
203
- 'close_date',
204
- ]);
205
- expect(result).toEqual([
206
- { header: 'Name', accessorKey: 'name' },
207
- { header: 'Custom Amount', accessorKey: 'amount' },
208
- { header: 'Close Date', accessorKey: 'close_date' },
209
- ]);
210
- });
211
- });