@object-ui/plugin-charts 3.1.5 → 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.
- package/CHANGELOG.md +28 -0
- package/README.md +24 -0
- package/dist/{AdvancedChartImpl-DmHTUUVD.js → AdvancedChartImpl-DxaZtNlE.js} +1214 -1206
- package/dist/{BarChart-XZkfLmcU.js → BarChart-BQS4sYHd.js} +3859 -3766
- package/dist/{ChartImpl-0VlpsMWG.js → ChartImpl-BaXisyXJ.js} +6 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +207 -63
- package/dist/index.umd.cjs +19 -19
- package/dist/{jsx-runtime-C8d0IhUE.js → jsx-runtime-Caia9pQX.js} +1 -1
- package/dist/packages/plugin-charts/src/ObjectChart.d.ts +31 -0
- package/package.json +34 -10
- package/.turbo/turbo-build.log +0 -26
- package/dist/src/ObjectChart.d.ts +0 -12
- package/examples/chart-examples.ts +0 -54
- package/src/AdvancedChartImpl.tsx +0 -309
- package/src/ChartContainerImpl.tsx +0 -353
- package/src/ChartImpl.tsx +0 -91
- package/src/ChartRenderer.tsx +0 -112
- package/src/ObjectChart.stories.tsx +0 -104
- package/src/ObjectChart.tsx +0 -145
- package/src/__tests__/ObjectChart.aggregation.test.ts +0 -166
- package/src/__tests__/ObjectChart.dataFetch.test.tsx +0 -303
- package/src/index.test.ts +0 -136
- package/src/index.tsx +0 -172
- package/src/types.ts +0 -68
- package/tsconfig.json +0 -17
- package/vite.config.ts +0 -61
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-charts/src}/AdvancedChartImpl.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/ChartContainerImpl.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/ChartImpl.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/ChartRenderer.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/ObjectChart.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/index.d.ts +0 -0
- /package/dist/{src → packages/plugin-charts/src}/types.d.ts +0 -0
package/src/ObjectChart.tsx
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import React, { useState, useEffect, useContext } from 'react';
|
|
3
|
-
import { useDataScope, SchemaRendererContext } from '@object-ui/react';
|
|
4
|
-
import { ChartRenderer } from './ChartRenderer';
|
|
5
|
-
import { ComponentRegistry, extractRecords } from '@object-ui/core';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Client-side aggregation for fetched records.
|
|
9
|
-
* Groups records by `groupBy` field and applies the aggregation function
|
|
10
|
-
* to the `field` values in each group.
|
|
11
|
-
*/
|
|
12
|
-
export function aggregateRecords(
|
|
13
|
-
records: any[],
|
|
14
|
-
aggregate: { field: string; function: string; groupBy: string }
|
|
15
|
-
): any[] {
|
|
16
|
-
const { field, function: aggFn, groupBy } = aggregate;
|
|
17
|
-
const groups: Record<string, any[]> = {};
|
|
18
|
-
|
|
19
|
-
for (const record of records) {
|
|
20
|
-
const key = String(record[groupBy] ?? 'Unknown');
|
|
21
|
-
if (!groups[key]) groups[key] = [];
|
|
22
|
-
groups[key].push(record);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return Object.entries(groups).map(([key, group]) => {
|
|
26
|
-
const values = group.map(r => Number(r[field]) || 0);
|
|
27
|
-
let result: number;
|
|
28
|
-
|
|
29
|
-
switch (aggFn) {
|
|
30
|
-
case 'count':
|
|
31
|
-
result = group.length;
|
|
32
|
-
break;
|
|
33
|
-
case 'avg':
|
|
34
|
-
result = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
35
|
-
break;
|
|
36
|
-
case 'min':
|
|
37
|
-
result = values.length > 0 ? Math.min(...values) : 0;
|
|
38
|
-
break;
|
|
39
|
-
case 'max':
|
|
40
|
-
result = values.length > 0 ? Math.max(...values) : 0;
|
|
41
|
-
break;
|
|
42
|
-
case 'sum':
|
|
43
|
-
default:
|
|
44
|
-
result = values.reduce((a, b) => a + b, 0);
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { [groupBy]: key, [field]: result };
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Re-export extractRecords from @object-ui/core for backward compatibility
|
|
53
|
-
export { extractRecords } from '@object-ui/core';
|
|
54
|
-
|
|
55
|
-
export const ObjectChart = (props: any) => {
|
|
56
|
-
const { schema } = props;
|
|
57
|
-
const context = useContext(SchemaRendererContext);
|
|
58
|
-
const dataSource = props.dataSource || context?.dataSource;
|
|
59
|
-
const boundData = useDataScope(schema.bind);
|
|
60
|
-
|
|
61
|
-
const [fetchedData, setFetchedData] = useState<any[]>([]);
|
|
62
|
-
const [loading, setLoading] = useState(false);
|
|
63
|
-
|
|
64
|
-
useEffect(() => {
|
|
65
|
-
let isMounted = true;
|
|
66
|
-
const fetchData = async () => {
|
|
67
|
-
if (!dataSource || !schema.objectName) return;
|
|
68
|
-
if (isMounted) setLoading(true);
|
|
69
|
-
try {
|
|
70
|
-
let data: any[];
|
|
71
|
-
|
|
72
|
-
// Prefer server-side aggregation when aggregate config is provided
|
|
73
|
-
// and dataSource supports the aggregate() method.
|
|
74
|
-
if (schema.aggregate && typeof dataSource.aggregate === 'function') {
|
|
75
|
-
const results = await dataSource.aggregate(schema.objectName, {
|
|
76
|
-
field: schema.aggregate.field,
|
|
77
|
-
function: schema.aggregate.function,
|
|
78
|
-
groupBy: schema.aggregate.groupBy,
|
|
79
|
-
filter: schema.filter,
|
|
80
|
-
});
|
|
81
|
-
data = Array.isArray(results) ? results : [];
|
|
82
|
-
} else if (typeof dataSource.find === 'function') {
|
|
83
|
-
// Fallback: fetch all records and aggregate client-side
|
|
84
|
-
const results = await dataSource.find(schema.objectName, {
|
|
85
|
-
$filter: schema.filter
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
data = extractRecords(results);
|
|
89
|
-
|
|
90
|
-
// Apply client-side aggregation when aggregate config is provided
|
|
91
|
-
if (schema.aggregate && data.length > 0) {
|
|
92
|
-
data = aggregateRecords(data, schema.aggregate);
|
|
93
|
-
}
|
|
94
|
-
} else {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (isMounted) {
|
|
99
|
-
setFetchedData(data);
|
|
100
|
-
}
|
|
101
|
-
} catch (e) {
|
|
102
|
-
console.error('[ObjectChart] Fetch error:', e);
|
|
103
|
-
} finally {
|
|
104
|
-
if (isMounted) setLoading(false);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
if (schema.objectName && !boundData && !schema.data) {
|
|
109
|
-
fetchData();
|
|
110
|
-
}
|
|
111
|
-
return () => { isMounted = false; };
|
|
112
|
-
}, [schema.objectName, dataSource, boundData, schema.data, schema.filter, schema.aggregate]);
|
|
113
|
-
|
|
114
|
-
const rawData = boundData || schema.data || fetchedData;
|
|
115
|
-
const finalData = Array.isArray(rawData) ? rawData : [];
|
|
116
|
-
|
|
117
|
-
// Merge data if not provided in schema
|
|
118
|
-
const finalSchema = {
|
|
119
|
-
...schema,
|
|
120
|
-
data: finalData
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
if (loading && finalData.length === 0) {
|
|
124
|
-
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>Loading chart data…</div>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!dataSource && schema.objectName && finalData.length === 0) {
|
|
128
|
-
return <div className={"flex items-center justify-center text-muted-foreground text-sm p-4 " + (schema.className || '')}>No data source available for "{schema.objectName}"</div>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return <ChartRenderer {...props} schema={finalSchema} />;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Register it
|
|
135
|
-
ComponentRegistry.register('object-chart', ObjectChart, {
|
|
136
|
-
namespace: 'plugin-charts',
|
|
137
|
-
label: 'Object Chart',
|
|
138
|
-
category: 'view',
|
|
139
|
-
inputs: [
|
|
140
|
-
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
141
|
-
{ name: 'data', type: 'array', label: 'Data', description: 'Optional static data' },
|
|
142
|
-
{ name: 'filter', type: 'array', label: 'Filter' },
|
|
143
|
-
{ name: 'aggregate', type: 'object', label: 'Aggregate', description: 'Aggregation config: { field, function, groupBy }' },
|
|
144
|
-
]
|
|
145
|
-
});
|
|
@@ -1,166 +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 { aggregateRecords, extractRecords } from '../ObjectChart';
|
|
11
|
-
|
|
12
|
-
describe('aggregateRecords', () => {
|
|
13
|
-
const records = [
|
|
14
|
-
{ account: 'Acme Corp', amount: 100 },
|
|
15
|
-
{ account: 'Acme Corp', amount: 200 },
|
|
16
|
-
{ account: 'Globex', amount: 150 },
|
|
17
|
-
{ account: 'Globex', amount: 50 },
|
|
18
|
-
{ account: 'Initech', amount: 300 },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
it('should aggregate using sum', () => {
|
|
22
|
-
const result = aggregateRecords(records, {
|
|
23
|
-
field: 'amount',
|
|
24
|
-
function: 'sum',
|
|
25
|
-
groupBy: 'account',
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
expect(result).toHaveLength(3);
|
|
29
|
-
expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(300);
|
|
30
|
-
expect(result.find(r => r.account === 'Globex')?.amount).toBe(200);
|
|
31
|
-
expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should aggregate using count', () => {
|
|
35
|
-
const result = aggregateRecords(records, {
|
|
36
|
-
field: 'amount',
|
|
37
|
-
function: 'count',
|
|
38
|
-
groupBy: 'account',
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(result).toHaveLength(3);
|
|
42
|
-
expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(2);
|
|
43
|
-
expect(result.find(r => r.account === 'Globex')?.amount).toBe(2);
|
|
44
|
-
expect(result.find(r => r.account === 'Initech')?.amount).toBe(1);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should aggregate using avg', () => {
|
|
48
|
-
const result = aggregateRecords(records, {
|
|
49
|
-
field: 'amount',
|
|
50
|
-
function: 'avg',
|
|
51
|
-
groupBy: 'account',
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(result).toHaveLength(3);
|
|
55
|
-
expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(150);
|
|
56
|
-
expect(result.find(r => r.account === 'Globex')?.amount).toBe(100);
|
|
57
|
-
expect(result.find(r => r.account === 'Initech')?.amount).toBe(300);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should aggregate using min', () => {
|
|
61
|
-
const result = aggregateRecords(records, {
|
|
62
|
-
field: 'amount',
|
|
63
|
-
function: 'min',
|
|
64
|
-
groupBy: 'account',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
expect(result).toHaveLength(3);
|
|
68
|
-
expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(100);
|
|
69
|
-
expect(result.find(r => r.account === 'Globex')?.amount).toBe(50);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should aggregate using max', () => {
|
|
73
|
-
const result = aggregateRecords(records, {
|
|
74
|
-
field: 'amount',
|
|
75
|
-
function: 'max',
|
|
76
|
-
groupBy: 'account',
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result).toHaveLength(3);
|
|
80
|
-
expect(result.find(r => r.account === 'Acme Corp')?.amount).toBe(200);
|
|
81
|
-
expect(result.find(r => r.account === 'Globex')?.amount).toBe(150);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should handle records with missing groupBy field', () => {
|
|
85
|
-
const input = [
|
|
86
|
-
{ account: 'Acme', amount: 100 },
|
|
87
|
-
{ amount: 200 }, // missing account
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const result = aggregateRecords(input, {
|
|
91
|
-
field: 'amount',
|
|
92
|
-
function: 'sum',
|
|
93
|
-
groupBy: 'account',
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(result).toHaveLength(2);
|
|
97
|
-
expect(result.find(r => r.account === 'Acme')?.amount).toBe(100);
|
|
98
|
-
expect(result.find(r => r.account === 'Unknown')?.amount).toBe(200);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should handle empty records', () => {
|
|
102
|
-
const result = aggregateRecords([], {
|
|
103
|
-
field: 'amount',
|
|
104
|
-
function: 'sum',
|
|
105
|
-
groupBy: 'account',
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(result).toEqual([]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('should handle non-numeric values gracefully', () => {
|
|
112
|
-
const input = [
|
|
113
|
-
{ account: 'Acme', amount: 'not-a-number' },
|
|
114
|
-
{ account: 'Acme', amount: 100 },
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
const result = aggregateRecords(input, {
|
|
118
|
-
field: 'amount',
|
|
119
|
-
function: 'sum',
|
|
120
|
-
groupBy: 'account',
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
expect(result).toHaveLength(1);
|
|
124
|
-
expect(result[0].amount).toBe(100); // non-numeric value coerced to 0, sum is 0 + 100
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('extractRecords', () => {
|
|
129
|
-
const sampleData = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
|
|
130
|
-
|
|
131
|
-
it('should return the array directly when results is an array', () => {
|
|
132
|
-
expect(extractRecords(sampleData)).toEqual(sampleData);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should extract from results.records', () => {
|
|
136
|
-
expect(extractRecords({ records: sampleData })).toEqual(sampleData);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should extract from results.data', () => {
|
|
140
|
-
expect(extractRecords({ data: sampleData })).toEqual(sampleData);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should extract from results.value', () => {
|
|
144
|
-
expect(extractRecords({ value: sampleData })).toEqual(sampleData);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should return empty array for null/undefined', () => {
|
|
148
|
-
expect(extractRecords(null)).toEqual([]);
|
|
149
|
-
expect(extractRecords(undefined)).toEqual([]);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should return empty array for non-array/non-object', () => {
|
|
153
|
-
expect(extractRecords('string')).toEqual([]);
|
|
154
|
-
expect(extractRecords(42)).toEqual([]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('should return empty array for object without recognized keys', () => {
|
|
158
|
-
expect(extractRecords({ total: 100 })).toEqual([]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should prefer records over data and value', () => {
|
|
162
|
-
const records = [{ id: 1 }];
|
|
163
|
-
const data = [{ id: 2 }];
|
|
164
|
-
expect(extractRecords({ records, data })).toEqual(records);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for ObjectChart data fetching & fault tolerance.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that ObjectChart:
|
|
5
|
-
* - Calls dataSource.find() when objectName is set and no bound data
|
|
6
|
-
* - Handles missing/invalid dataSource gracefully
|
|
7
|
-
* - Works without a SchemaRendererProvider
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
-
import { render, waitFor } from '@testing-library/react';
|
|
12
|
-
import React from 'react';
|
|
13
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
14
|
-
import { ObjectChart } from '../ObjectChart';
|
|
15
|
-
|
|
16
|
-
// Suppress console.error from React error boundary / fetch errors
|
|
17
|
-
const originalConsoleError = console.error;
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
console.error = vi.fn();
|
|
20
|
-
});
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
console.error = originalConsoleError;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe('ObjectChart data fetching', () => {
|
|
26
|
-
it('should call dataSource.find when objectName is set and no bind path', async () => {
|
|
27
|
-
const mockFind = vi.fn().mockResolvedValue([
|
|
28
|
-
{ stage: 'Prospect', amount: 100 },
|
|
29
|
-
{ stage: 'Proposal', amount: 200 },
|
|
30
|
-
]);
|
|
31
|
-
const dataSource = { find: mockFind };
|
|
32
|
-
|
|
33
|
-
render(
|
|
34
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
35
|
-
<ObjectChart
|
|
36
|
-
schema={{
|
|
37
|
-
type: 'object-chart',
|
|
38
|
-
objectName: 'opportunity',
|
|
39
|
-
chartType: 'bar',
|
|
40
|
-
xAxisKey: 'stage',
|
|
41
|
-
series: [{ dataKey: 'amount' }],
|
|
42
|
-
}}
|
|
43
|
-
/>
|
|
44
|
-
</SchemaRendererProvider>
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
await waitFor(() => {
|
|
48
|
-
expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should NOT call dataSource.find when schema.data is provided', () => {
|
|
53
|
-
const mockFind = vi.fn();
|
|
54
|
-
const dataSource = { find: mockFind };
|
|
55
|
-
|
|
56
|
-
render(
|
|
57
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
58
|
-
<ObjectChart
|
|
59
|
-
schema={{
|
|
60
|
-
type: 'object-chart',
|
|
61
|
-
objectName: 'opportunity',
|
|
62
|
-
chartType: 'bar',
|
|
63
|
-
data: [{ stage: 'A', amount: 100 }],
|
|
64
|
-
xAxisKey: 'stage',
|
|
65
|
-
series: [{ dataKey: 'amount' }],
|
|
66
|
-
}}
|
|
67
|
-
/>
|
|
68
|
-
</SchemaRendererProvider>
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
expect(mockFind).not.toHaveBeenCalled();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should apply aggregation to fetched data', async () => {
|
|
75
|
-
const mockFind = vi.fn().mockResolvedValue([
|
|
76
|
-
{ stage: 'Prospect', amount: 100 },
|
|
77
|
-
{ stage: 'Prospect', amount: 200 },
|
|
78
|
-
{ stage: 'Proposal', amount: 300 },
|
|
79
|
-
]);
|
|
80
|
-
const dataSource = { find: mockFind };
|
|
81
|
-
|
|
82
|
-
const { container } = render(
|
|
83
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
84
|
-
<ObjectChart
|
|
85
|
-
schema={{
|
|
86
|
-
type: 'object-chart',
|
|
87
|
-
objectName: 'opportunity',
|
|
88
|
-
chartType: 'bar',
|
|
89
|
-
xAxisKey: 'stage',
|
|
90
|
-
series: [{ dataKey: 'amount' }],
|
|
91
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
92
|
-
}}
|
|
93
|
-
/>
|
|
94
|
-
</SchemaRendererProvider>
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
await waitFor(() => {
|
|
98
|
-
expect(mockFind).toHaveBeenCalled();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should prefer dataSource.aggregate() over find() when aggregate config is set', async () => {
|
|
103
|
-
const mockFind = vi.fn().mockResolvedValue([]);
|
|
104
|
-
const mockAggregate = vi.fn().mockResolvedValue([
|
|
105
|
-
{ stage: 'Prospect', amount: 300 },
|
|
106
|
-
{ stage: 'Proposal', amount: 300 },
|
|
107
|
-
]);
|
|
108
|
-
const dataSource = { find: mockFind, aggregate: mockAggregate };
|
|
109
|
-
|
|
110
|
-
render(
|
|
111
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
112
|
-
<ObjectChart
|
|
113
|
-
schema={{
|
|
114
|
-
type: 'object-chart',
|
|
115
|
-
objectName: 'opportunity',
|
|
116
|
-
chartType: 'bar',
|
|
117
|
-
xAxisKey: 'stage',
|
|
118
|
-
series: [{ dataKey: 'amount' }],
|
|
119
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
120
|
-
}}
|
|
121
|
-
/>
|
|
122
|
-
</SchemaRendererProvider>
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
await waitFor(() => {
|
|
126
|
-
expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
|
|
127
|
-
field: 'amount',
|
|
128
|
-
function: 'sum',
|
|
129
|
-
groupBy: 'stage',
|
|
130
|
-
filter: undefined,
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
// find() should NOT be called when aggregate() is available
|
|
134
|
-
expect(mockFind).not.toHaveBeenCalled();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('should fall back to find() when aggregate() is not available', async () => {
|
|
138
|
-
const mockFind = vi.fn().mockResolvedValue([
|
|
139
|
-
{ stage: 'Prospect', amount: 100 },
|
|
140
|
-
{ stage: 'Prospect', amount: 200 },
|
|
141
|
-
]);
|
|
142
|
-
// dataSource without aggregate method
|
|
143
|
-
const dataSource = { find: mockFind };
|
|
144
|
-
|
|
145
|
-
render(
|
|
146
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
147
|
-
<ObjectChart
|
|
148
|
-
schema={{
|
|
149
|
-
type: 'object-chart',
|
|
150
|
-
objectName: 'opportunity',
|
|
151
|
-
chartType: 'bar',
|
|
152
|
-
xAxisKey: 'stage',
|
|
153
|
-
series: [{ dataKey: 'amount' }],
|
|
154
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
155
|
-
}}
|
|
156
|
-
/>
|
|
157
|
-
</SchemaRendererProvider>
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
await waitFor(() => {
|
|
161
|
-
expect(mockFind).toHaveBeenCalledWith('opportunity', { $filter: undefined });
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should NOT use aggregate() when no aggregate config is set', async () => {
|
|
166
|
-
const mockFind = vi.fn().mockResolvedValue([
|
|
167
|
-
{ stage: 'Prospect', amount: 100 },
|
|
168
|
-
]);
|
|
169
|
-
const mockAggregate = vi.fn().mockResolvedValue([]);
|
|
170
|
-
const dataSource = { find: mockFind, aggregate: mockAggregate };
|
|
171
|
-
|
|
172
|
-
render(
|
|
173
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
174
|
-
<ObjectChart
|
|
175
|
-
schema={{
|
|
176
|
-
type: 'object-chart',
|
|
177
|
-
objectName: 'opportunity',
|
|
178
|
-
chartType: 'bar',
|
|
179
|
-
xAxisKey: 'stage',
|
|
180
|
-
series: [{ dataKey: 'amount' }],
|
|
181
|
-
}}
|
|
182
|
-
/>
|
|
183
|
-
</SchemaRendererProvider>
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
await waitFor(() => {
|
|
187
|
-
expect(mockFind).toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
// aggregate() should NOT be called when no aggregate config
|
|
190
|
-
expect(mockAggregate).not.toHaveBeenCalled();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should pass filter to aggregate() when both aggregate and filter are set', async () => {
|
|
194
|
-
const mockAggregate = vi.fn().mockResolvedValue([
|
|
195
|
-
{ stage: 'Won', amount: 500 },
|
|
196
|
-
]);
|
|
197
|
-
const dataSource = { find: vi.fn(), aggregate: mockAggregate };
|
|
198
|
-
const filter = { status: 'active' };
|
|
199
|
-
|
|
200
|
-
render(
|
|
201
|
-
<SchemaRendererProvider dataSource={dataSource}>
|
|
202
|
-
<ObjectChart
|
|
203
|
-
schema={{
|
|
204
|
-
type: 'object-chart',
|
|
205
|
-
objectName: 'opportunity',
|
|
206
|
-
chartType: 'bar',
|
|
207
|
-
xAxisKey: 'stage',
|
|
208
|
-
series: [{ dataKey: 'amount' }],
|
|
209
|
-
filter,
|
|
210
|
-
aggregate: { field: 'amount', function: 'sum', groupBy: 'stage' },
|
|
211
|
-
}}
|
|
212
|
-
/>
|
|
213
|
-
</SchemaRendererProvider>
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
await waitFor(() => {
|
|
217
|
-
expect(mockAggregate).toHaveBeenCalledWith('opportunity', {
|
|
218
|
-
field: 'amount',
|
|
219
|
-
function: 'sum',
|
|
220
|
-
groupBy: 'stage',
|
|
221
|
-
filter: { status: 'active' },
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
describe('ObjectChart fault tolerance', () => {
|
|
228
|
-
it('should not crash when dataSource has no find method', () => {
|
|
229
|
-
const { container } = render(
|
|
230
|
-
<SchemaRendererProvider dataSource={{}}>
|
|
231
|
-
<ObjectChart
|
|
232
|
-
schema={{
|
|
233
|
-
type: 'object-chart',
|
|
234
|
-
objectName: 'opportunity',
|
|
235
|
-
chartType: 'bar',
|
|
236
|
-
xAxisKey: 'stage',
|
|
237
|
-
series: [{ dataKey: 'amount' }],
|
|
238
|
-
}}
|
|
239
|
-
/>
|
|
240
|
-
</SchemaRendererProvider>
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Should render without crashing
|
|
244
|
-
expect(container).toBeDefined();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('should not crash when rendered outside SchemaRendererProvider', () => {
|
|
248
|
-
const { container } = render(
|
|
249
|
-
<ObjectChart
|
|
250
|
-
schema={{
|
|
251
|
-
type: 'object-chart',
|
|
252
|
-
chartType: 'bar',
|
|
253
|
-
xAxisKey: 'stage',
|
|
254
|
-
series: [{ dataKey: 'amount' }],
|
|
255
|
-
}}
|
|
256
|
-
/>
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
// Should render without crashing
|
|
260
|
-
expect(container).toBeDefined();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('should show "No data source available" when no dataSource and objectName set', () => {
|
|
264
|
-
const { container } = render(
|
|
265
|
-
<ObjectChart
|
|
266
|
-
schema={{
|
|
267
|
-
type: 'object-chart',
|
|
268
|
-
objectName: 'opportunity',
|
|
269
|
-
chartType: 'bar',
|
|
270
|
-
xAxisKey: 'stage',
|
|
271
|
-
series: [{ dataKey: 'amount' }],
|
|
272
|
-
}}
|
|
273
|
-
/>
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
expect(container.textContent).toContain('No data source available');
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('should use dataSource prop over context when both are present', async () => {
|
|
280
|
-
const contextFind = vi.fn().mockResolvedValue([]);
|
|
281
|
-
const propFind = vi.fn().mockResolvedValue([{ stage: 'A', amount: 1 }]);
|
|
282
|
-
|
|
283
|
-
render(
|
|
284
|
-
<SchemaRendererProvider dataSource={{ find: contextFind }}>
|
|
285
|
-
<ObjectChart
|
|
286
|
-
dataSource={{ find: propFind }}
|
|
287
|
-
schema={{
|
|
288
|
-
type: 'object-chart',
|
|
289
|
-
objectName: 'opportunity',
|
|
290
|
-
chartType: 'bar',
|
|
291
|
-
xAxisKey: 'stage',
|
|
292
|
-
series: [{ dataKey: 'amount' }],
|
|
293
|
-
}}
|
|
294
|
-
/>
|
|
295
|
-
</SchemaRendererProvider>
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
await waitFor(() => {
|
|
299
|
-
expect(propFind).toHaveBeenCalled();
|
|
300
|
-
});
|
|
301
|
-
expect(contextFind).not.toHaveBeenCalled();
|
|
302
|
-
});
|
|
303
|
-
});
|