@object-ui/plugin-dashboard 3.3.0 → 3.3.2

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 (48) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +21 -1
  3. package/dist/index.js +876 -797
  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/MetricCard.d.ts.map +1 -1
  8. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts +4 -1
  9. package/dist/packages/plugin-dashboard/src/MetricWidget.d.ts.map +1 -1
  10. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts +2 -0
  11. package/dist/packages/plugin-dashboard/src/ObjectMetricWidget.d.ts.map +1 -1
  12. package/dist/packages/plugin-dashboard/src/index.d.ts +1 -1
  13. package/package.json +40 -7
  14. package/.turbo/turbo-build.log +0 -41
  15. package/src/DashboardConfigPanel.stories.tsx +0 -164
  16. package/src/DashboardConfigPanel.tsx +0 -158
  17. package/src/DashboardGridLayout.tsx +0 -367
  18. package/src/DashboardRenderer.stories.tsx +0 -173
  19. package/src/DashboardRenderer.tsx +0 -479
  20. package/src/DashboardWithConfig.tsx +0 -211
  21. package/src/MetricCard.tsx +0 -102
  22. package/src/MetricWidget.tsx +0 -96
  23. package/src/ObjectDataTable.tsx +0 -226
  24. package/src/ObjectMetricWidget.tsx +0 -159
  25. package/src/ObjectPivotTable.tsx +0 -160
  26. package/src/PivotTable.tsx +0 -262
  27. package/src/WidgetConfigPanel.tsx +0 -540
  28. package/src/__tests__/DashboardConfigPanel.test.tsx +0 -206
  29. package/src/__tests__/DashboardGridLayout.test.tsx +0 -199
  30. package/src/__tests__/DashboardRenderer.autoRefresh.test.tsx +0 -124
  31. package/src/__tests__/DashboardRenderer.designMode.test.tsx +0 -386
  32. package/src/__tests__/DashboardRenderer.header.test.tsx +0 -114
  33. package/src/__tests__/DashboardRenderer.mobile.test.tsx +0 -214
  34. package/src/__tests__/DashboardRenderer.widgetData.test.tsx +0 -1411
  35. package/src/__tests__/DashboardWithConfig.test.tsx +0 -276
  36. package/src/__tests__/MetricCard.test.tsx +0 -107
  37. package/src/__tests__/ObjectDataTable.test.tsx +0 -211
  38. package/src/__tests__/ObjectMetricWidget.test.tsx +0 -196
  39. package/src/__tests__/ObjectPivotTable.test.tsx +0 -192
  40. package/src/__tests__/PivotTable.test.tsx +0 -162
  41. package/src/__tests__/WidgetConfigPanel.test.tsx +0 -492
  42. package/src/__tests__/ensureWidgetIds.test.tsx +0 -103
  43. package/src/index.tsx +0 -236
  44. package/src/utils.ts +0 -17
  45. package/tsconfig.json +0 -19
  46. package/vite.config.ts +0 -64
  47. package/vitest.config.ts +0 -9
  48. 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
- });