@object-ui/plugin-charts 3.1.4 → 3.3.0
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +27 -0
- package/dist/{AdvancedChartImpl-DmHTUUVD.js → AdvancedChartImpl-JDjuxIZW.js} +1201 -1198
- package/dist/{BarChart-XZkfLmcU.js → BarChart-Bvt5Se8Q.js} +3858 -3765
- package/dist/{ChartImpl-0VlpsMWG.js → ChartImpl-CQj8Kris.js} +5 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +206 -62
- package/dist/index.umd.cjs +19 -19
- package/dist/packages/plugin-charts/src/ObjectChart.d.ts +31 -0
- package/package.json +9 -8
- package/src/AdvancedChartImpl.tsx +17 -3
- package/src/ObjectChart.tsx +201 -48
- package/src/__tests__/ObjectChart.labelResolution.test.ts +329 -0
- package/vite.config.ts +1 -0
- package/dist/src/ObjectChart.d.ts +0 -12
- /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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for ObjectChart groupBy value→label resolution.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that resolveGroupByLabels():
|
|
5
|
+
* - Maps select field values to their option labels
|
|
6
|
+
* - Handles string-only options (value === label)
|
|
7
|
+
* - Falls back to humanizeLabel() when no options match
|
|
8
|
+
* - Resolves lookup field IDs to referenced record names
|
|
9
|
+
* - Gracefully handles missing metadata or dataSource errors
|
|
10
|
+
* - Falls back to humanizeLabel() for unknown field types
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
14
|
+
import { resolveGroupByLabels, humanizeLabel } from '../ObjectChart';
|
|
15
|
+
|
|
16
|
+
describe('humanizeLabel', () => {
|
|
17
|
+
it('should convert snake_case to Title Case', () => {
|
|
18
|
+
expect(humanizeLabel('closed_won')).toBe('Closed Won');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should convert kebab-case to Title Case', () => {
|
|
22
|
+
expect(humanizeLabel('high-priority')).toBe('High Priority');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should handle single word', () => {
|
|
26
|
+
expect(humanizeLabel('active')).toBe('Active');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should handle empty string', () => {
|
|
30
|
+
expect(humanizeLabel('')).toBe('');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('resolveGroupByLabels', () => {
|
|
35
|
+
// ---- select fields ----
|
|
36
|
+
|
|
37
|
+
it('should map select field values to option labels', async () => {
|
|
38
|
+
const data = [
|
|
39
|
+
{ stage: 'closed_won', amount: 500 },
|
|
40
|
+
{ stage: 'prospecting', amount: 200 },
|
|
41
|
+
{ stage: 'closed_lost', amount: 100 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const objectSchema = {
|
|
45
|
+
fields: {
|
|
46
|
+
stage: {
|
|
47
|
+
type: 'select',
|
|
48
|
+
options: [
|
|
49
|
+
{ value: 'prospecting', label: 'Prospecting' },
|
|
50
|
+
{ value: 'closed_won', label: 'Closed Won' },
|
|
51
|
+
{ value: 'closed_lost', label: 'Closed Lost' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = await resolveGroupByLabels(data, 'stage', objectSchema);
|
|
58
|
+
|
|
59
|
+
expect(result).toEqual([
|
|
60
|
+
{ stage: 'Closed Won', amount: 500 },
|
|
61
|
+
{ stage: 'Prospecting', amount: 200 },
|
|
62
|
+
{ stage: 'Closed Lost', amount: 100 },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should handle string-only options where value equals label', async () => {
|
|
67
|
+
const data = [
|
|
68
|
+
{ lead_source: 'Web', count: 10 },
|
|
69
|
+
{ lead_source: 'Phone', count: 5 },
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const objectSchema = {
|
|
73
|
+
fields: {
|
|
74
|
+
lead_source: {
|
|
75
|
+
type: 'select',
|
|
76
|
+
options: ['Web', 'Phone', 'Partner'],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = await resolveGroupByLabels(data, 'lead_source', objectSchema);
|
|
82
|
+
|
|
83
|
+
expect(result).toEqual([
|
|
84
|
+
{ lead_source: 'Web', count: 10 },
|
|
85
|
+
{ lead_source: 'Phone', count: 5 },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should humanize unmatched select values', async () => {
|
|
90
|
+
const data = [
|
|
91
|
+
{ status: 'not_yet_started', count: 3 },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const objectSchema = {
|
|
95
|
+
fields: {
|
|
96
|
+
status: {
|
|
97
|
+
type: 'select',
|
|
98
|
+
options: [
|
|
99
|
+
{ value: 'active', label: 'Active' },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await resolveGroupByLabels(data, 'status', objectSchema);
|
|
106
|
+
|
|
107
|
+
expect(result[0].status).toBe('Not Yet Started');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should humanize values when select field has empty options', async () => {
|
|
111
|
+
const data = [
|
|
112
|
+
{ status: 'in_progress', count: 1 },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const objectSchema = {
|
|
116
|
+
fields: {
|
|
117
|
+
status: {
|
|
118
|
+
type: 'select',
|
|
119
|
+
options: [],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await resolveGroupByLabels(data, 'status', objectSchema);
|
|
125
|
+
expect(result[0].status).toBe('In Progress');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ---- lookup fields ----
|
|
129
|
+
|
|
130
|
+
it('should resolve lookup field IDs to record names', async () => {
|
|
131
|
+
const data = [
|
|
132
|
+
{ account: '1', amount: 500 },
|
|
133
|
+
{ account: '2', amount: 300 },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const objectSchema = {
|
|
137
|
+
fields: {
|
|
138
|
+
account: {
|
|
139
|
+
type: 'lookup',
|
|
140
|
+
reference_to: 'accounts',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const mockFind = vi.fn().mockResolvedValue([
|
|
146
|
+
{ id: '1', name: 'Acme Corp' },
|
|
147
|
+
{ id: '2', name: 'Globex Inc' },
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const dataSource = { find: mockFind };
|
|
151
|
+
|
|
152
|
+
const result = await resolveGroupByLabels(data, 'account', objectSchema, dataSource);
|
|
153
|
+
|
|
154
|
+
expect(result).toEqual([
|
|
155
|
+
{ account: 'Acme Corp', amount: 500 },
|
|
156
|
+
{ account: 'Globex Inc', amount: 300 },
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
expect(mockFind).toHaveBeenCalledWith('accounts', {
|
|
160
|
+
$filter: { id: { $in: ['1', '2'] } },
|
|
161
|
+
$top: 2,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should use display_field from metadata for lookup label', async () => {
|
|
166
|
+
const data = [
|
|
167
|
+
{ project: 'p1', hours: 40 },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const objectSchema = {
|
|
171
|
+
fields: {
|
|
172
|
+
project: {
|
|
173
|
+
type: 'lookup',
|
|
174
|
+
reference_to: 'projects',
|
|
175
|
+
display_field: 'title',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const mockFind = vi.fn().mockResolvedValue([
|
|
181
|
+
{ id: 'p1', title: 'Website Redesign', name: 'PRJ-001' },
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
const result = await resolveGroupByLabels(data, 'project', objectSchema, { find: mockFind });
|
|
185
|
+
|
|
186
|
+
expect(result[0].project).toBe('Website Redesign');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should use id_field from metadata for lookup query', async () => {
|
|
190
|
+
const data = [
|
|
191
|
+
{ owner: 'uid_42', count: 5 },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const objectSchema = {
|
|
195
|
+
fields: {
|
|
196
|
+
owner: {
|
|
197
|
+
type: 'lookup',
|
|
198
|
+
reference_to: 'users',
|
|
199
|
+
id_field: 'uid',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const mockFind = vi.fn().mockResolvedValue([
|
|
205
|
+
{ uid: 'uid_42', name: 'Jane Doe' },
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const result = await resolveGroupByLabels(data, 'owner', objectSchema, { find: mockFind });
|
|
209
|
+
|
|
210
|
+
expect(result[0].owner).toBe('Jane Doe');
|
|
211
|
+
expect(mockFind).toHaveBeenCalledWith('users', {
|
|
212
|
+
$filter: { uid: { $in: ['uid_42'] } },
|
|
213
|
+
$top: 1,
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should handle lookup with reference property (ObjectStack convention)', async () => {
|
|
218
|
+
const data = [
|
|
219
|
+
{ customer: '10', total: 100 },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
const objectSchema = {
|
|
223
|
+
fields: {
|
|
224
|
+
customer: {
|
|
225
|
+
type: 'master_detail',
|
|
226
|
+
reference: 'customers',
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const mockFind = vi.fn().mockResolvedValue([
|
|
232
|
+
{ id: '10', name: 'Big Client' },
|
|
233
|
+
]);
|
|
234
|
+
|
|
235
|
+
const result = await resolveGroupByLabels(data, 'customer', objectSchema, { find: mockFind });
|
|
236
|
+
|
|
237
|
+
expect(result[0].customer).toBe('Big Client');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should keep raw values when lookup fetch fails', async () => {
|
|
241
|
+
const data = [
|
|
242
|
+
{ account: '1', amount: 500 },
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
const objectSchema = {
|
|
246
|
+
fields: {
|
|
247
|
+
account: {
|
|
248
|
+
type: 'lookup',
|
|
249
|
+
reference_to: 'accounts',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const dataSource = {
|
|
255
|
+
find: vi.fn().mockRejectedValue(new Error('Network error')),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = await resolveGroupByLabels(data, 'account', objectSchema, dataSource);
|
|
259
|
+
|
|
260
|
+
// Should gracefully return original data
|
|
261
|
+
expect(result[0].account).toBe('1');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should keep raw values when no dataSource is available for lookup', async () => {
|
|
265
|
+
const data = [
|
|
266
|
+
{ account: '1', amount: 500 },
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
const objectSchema = {
|
|
270
|
+
fields: {
|
|
271
|
+
account: {
|
|
272
|
+
type: 'lookup',
|
|
273
|
+
reference_to: 'accounts',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const result = await resolveGroupByLabels(data, 'account', objectSchema);
|
|
279
|
+
|
|
280
|
+
expect(result[0].account).toBe('1');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ---- fallback / edge cases ----
|
|
284
|
+
|
|
285
|
+
it('should humanize values when no field metadata exists', async () => {
|
|
286
|
+
const data = [
|
|
287
|
+
{ category: 'high_priority', count: 5 },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
const objectSchema = { fields: {} };
|
|
291
|
+
|
|
292
|
+
const result = await resolveGroupByLabels(data, 'category', objectSchema);
|
|
293
|
+
|
|
294
|
+
expect(result[0].category).toBe('High Priority');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should humanize values for unknown field types', async () => {
|
|
298
|
+
const data = [
|
|
299
|
+
{ region: 'north_america', revenue: 1000 },
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const objectSchema = {
|
|
303
|
+
fields: {
|
|
304
|
+
region: { type: 'text' },
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await resolveGroupByLabels(data, 'region', objectSchema);
|
|
309
|
+
|
|
310
|
+
expect(result[0].region).toBe('North America');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return empty array for empty data', async () => {
|
|
314
|
+
const result = await resolveGroupByLabels([], 'stage', { fields: {} });
|
|
315
|
+
expect(result).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should return data as-is when groupByField is empty', async () => {
|
|
319
|
+
const data = [{ stage: 'a', count: 1 }];
|
|
320
|
+
const result = await resolveGroupByLabels(data, '', { fields: {} });
|
|
321
|
+
expect(result).toEqual(data);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should handle null objectSchema gracefully', async () => {
|
|
325
|
+
const data = [{ stage: 'closed_won', count: 1 }];
|
|
326
|
+
const result = await resolveGroupByLabels(data, 'stage', null);
|
|
327
|
+
expect(result[0].stage).toBe('Closed Won');
|
|
328
|
+
});
|
|
329
|
+
});
|
package/vite.config.ts
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side aggregation for fetched records.
|
|
3
|
-
* Groups records by `groupBy` field and applies the aggregation function
|
|
4
|
-
* to the `field` values in each group.
|
|
5
|
-
*/
|
|
6
|
-
export declare function aggregateRecords(records: any[], aggregate: {
|
|
7
|
-
field: string;
|
|
8
|
-
function: string;
|
|
9
|
-
groupBy: string;
|
|
10
|
-
}): any[];
|
|
11
|
-
export { extractRecords } from '../../core/src';
|
|
12
|
-
export declare const ObjectChart: (props: any) => import("react/jsx-runtime").JSX.Element;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|