@object-ui/plugin-list 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 +34 -0
- package/README.md +21 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30492 -38346
- package/dist/index.umd.cjs +30 -38
- package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
- package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
- package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
- package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
- package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
- package/dist/plugin-list.css +1 -2
- package/package.json +35 -13
- package/.turbo/turbo-build.log +0 -24
- package/dist/src/ListView.d.ts.map +0 -1
- package/dist/src/ListView.stories.d.ts.map +0 -1
- package/dist/src/ObjectGallery.d.ts.map +0 -1
- package/dist/src/UserFilters.d.ts.map +0 -1
- package/dist/src/ViewSwitcher.d.ts.map +0 -1
- package/dist/src/components/TabBar.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/src/ListView.stories.tsx +0 -64
- package/src/ListView.tsx +0 -1688
- package/src/ObjectGallery.tsx +0 -308
- package/src/UserFilters.tsx +0 -453
- package/src/ViewSwitcher.tsx +0 -113
- package/src/__tests__/ConditionalFormatting.test.ts +0 -285
- package/src/__tests__/DataFetch.test.tsx +0 -253
- package/src/__tests__/Export.test.tsx +0 -175
- package/src/__tests__/FilterNormalization.test.ts +0 -162
- package/src/__tests__/GalleryGrouping.test.tsx +0 -237
- package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
- package/src/__tests__/ListView.test.tsx +0 -2151
- package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
- package/src/__tests__/ListViewPersistence.test.tsx +0 -129
- package/src/__tests__/ObjectGallery.test.tsx +0 -208
- package/src/__tests__/TabBar.test.tsx +0 -199
- package/src/__tests__/UserFilters.test.tsx +0 -486
- package/src/components/TabBar.tsx +0 -120
- package/src/index.tsx +0 -78
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
- /package/dist/{src → packages/plugin-list/src}/components/TabBar.d.ts +0 -0
|
@@ -1,285 +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 { evaluateConditionalFormatting } from '../ListView';
|
|
11
|
-
|
|
12
|
-
describe('evaluateConditionalFormatting', () => {
|
|
13
|
-
// =========================================================================
|
|
14
|
-
// Standard operator-based rules
|
|
15
|
-
// =========================================================================
|
|
16
|
-
describe('operator-based rules', () => {
|
|
17
|
-
it('matches "equals" operator', () => {
|
|
18
|
-
const result = evaluateConditionalFormatting(
|
|
19
|
-
{ status: 'active' },
|
|
20
|
-
[{ field: 'status', operator: 'equals', value: 'active', backgroundColor: '#e0ffe0' }],
|
|
21
|
-
);
|
|
22
|
-
expect(result).toEqual({ backgroundColor: '#e0ffe0' });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('matches "not_equals" operator', () => {
|
|
26
|
-
const result = evaluateConditionalFormatting(
|
|
27
|
-
{ status: 'inactive' },
|
|
28
|
-
[{ field: 'status', operator: 'not_equals', value: 'active', textColor: '#f00' }],
|
|
29
|
-
);
|
|
30
|
-
expect(result).toEqual({ color: '#f00' });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('matches "contains" operator', () => {
|
|
34
|
-
const result = evaluateConditionalFormatting(
|
|
35
|
-
{ name: 'John Doe' },
|
|
36
|
-
[{ field: 'name', operator: 'contains', value: 'Doe', borderColor: '#00f' }],
|
|
37
|
-
);
|
|
38
|
-
expect(result).toEqual({ borderColor: '#00f' });
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('matches "greater_than" operator', () => {
|
|
42
|
-
const result = evaluateConditionalFormatting(
|
|
43
|
-
{ amount: 500 },
|
|
44
|
-
[{ field: 'amount', operator: 'greater_than', value: 100, backgroundColor: '#ff0' }],
|
|
45
|
-
);
|
|
46
|
-
expect(result).toEqual({ backgroundColor: '#ff0' });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('does not match "greater_than" when equal', () => {
|
|
50
|
-
const result = evaluateConditionalFormatting(
|
|
51
|
-
{ amount: 100 },
|
|
52
|
-
[{ field: 'amount', operator: 'greater_than', value: 100, backgroundColor: '#ff0' }],
|
|
53
|
-
);
|
|
54
|
-
expect(result).toEqual({});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('matches "less_than" operator', () => {
|
|
58
|
-
const result = evaluateConditionalFormatting(
|
|
59
|
-
{ score: 3 },
|
|
60
|
-
[{ field: 'score', operator: 'less_than', value: 5, textColor: '#aaa' }],
|
|
61
|
-
);
|
|
62
|
-
expect(result).toEqual({ color: '#aaa' });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('matches "in" operator', () => {
|
|
66
|
-
const result = evaluateConditionalFormatting(
|
|
67
|
-
{ priority: 'high' },
|
|
68
|
-
[{ field: 'priority', operator: 'in', value: ['high', 'critical'], backgroundColor: '#fee' }],
|
|
69
|
-
);
|
|
70
|
-
expect(result).toEqual({ backgroundColor: '#fee' });
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('does not match "in" when value is absent from array', () => {
|
|
74
|
-
const result = evaluateConditionalFormatting(
|
|
75
|
-
{ priority: 'low' },
|
|
76
|
-
[{ field: 'priority', operator: 'in', value: ['high', 'critical'], backgroundColor: '#fee' }],
|
|
77
|
-
);
|
|
78
|
-
expect(result).toEqual({});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// =========================================================================
|
|
83
|
-
// Expression-based rules (L2 feature)
|
|
84
|
-
// =========================================================================
|
|
85
|
-
describe('expression-based rules', () => {
|
|
86
|
-
it('evaluates a simple expression', () => {
|
|
87
|
-
const result = evaluateConditionalFormatting(
|
|
88
|
-
{ amount: 2000, status: 'urgent' },
|
|
89
|
-
[{
|
|
90
|
-
field: '',
|
|
91
|
-
operator: 'equals',
|
|
92
|
-
value: '',
|
|
93
|
-
expression: '${data.amount > 1000}',
|
|
94
|
-
backgroundColor: '#f0f0f0',
|
|
95
|
-
}],
|
|
96
|
-
);
|
|
97
|
-
expect(result).toEqual({ backgroundColor: '#f0f0f0' });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('evaluates a complex expression with && operator', () => {
|
|
101
|
-
const result = evaluateConditionalFormatting(
|
|
102
|
-
{ amount: 2000, status: 'urgent' },
|
|
103
|
-
[{
|
|
104
|
-
field: '',
|
|
105
|
-
operator: 'equals',
|
|
106
|
-
value: '',
|
|
107
|
-
expression: '${data.amount > 1000 && data.status === "urgent"}',
|
|
108
|
-
backgroundColor: '#fee2e2',
|
|
109
|
-
textColor: '#dc2626',
|
|
110
|
-
}],
|
|
111
|
-
);
|
|
112
|
-
expect(result).toEqual({ backgroundColor: '#fee2e2', color: '#dc2626' });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('returns empty object when expression evaluates to false', () => {
|
|
116
|
-
const result = evaluateConditionalFormatting(
|
|
117
|
-
{ amount: 50, status: 'normal' },
|
|
118
|
-
[{
|
|
119
|
-
field: '',
|
|
120
|
-
operator: 'equals',
|
|
121
|
-
value: '',
|
|
122
|
-
expression: '${data.amount > 1000 && data.status === "urgent"}',
|
|
123
|
-
backgroundColor: '#fee2e2',
|
|
124
|
-
}],
|
|
125
|
-
);
|
|
126
|
-
expect(result).toEqual({});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('does not throw on invalid expression and returns empty', () => {
|
|
130
|
-
const result = evaluateConditionalFormatting(
|
|
131
|
-
{ amount: 100 },
|
|
132
|
-
[{
|
|
133
|
-
field: '',
|
|
134
|
-
operator: 'equals',
|
|
135
|
-
value: '',
|
|
136
|
-
expression: '${data.!!!invalidSyntax}',
|
|
137
|
-
backgroundColor: '#f00',
|
|
138
|
-
}],
|
|
139
|
-
);
|
|
140
|
-
expect(result).toEqual({});
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// =========================================================================
|
|
145
|
-
// Spec expression format (plain condition + style object)
|
|
146
|
-
// =========================================================================
|
|
147
|
-
describe('spec expression format', () => {
|
|
148
|
-
it('evaluates a plain condition string via "condition" field', () => {
|
|
149
|
-
const result = evaluateConditionalFormatting(
|
|
150
|
-
{ status: 'overdue' },
|
|
151
|
-
[{ condition: "status == 'overdue'", style: { backgroundColor: 'red' } }],
|
|
152
|
-
);
|
|
153
|
-
expect(result).toEqual({ backgroundColor: 'red' });
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('evaluates a numeric comparison in plain condition', () => {
|
|
157
|
-
const result = evaluateConditionalFormatting(
|
|
158
|
-
{ amount: 2500 },
|
|
159
|
-
[{ condition: 'amount > 1000', style: { backgroundColor: '#fee2e2', color: '#991b1b' } }],
|
|
160
|
-
);
|
|
161
|
-
expect(result).toEqual({ backgroundColor: '#fee2e2', color: '#991b1b' });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('returns empty when plain condition does not match', () => {
|
|
165
|
-
const result = evaluateConditionalFormatting(
|
|
166
|
-
{ status: 'active' },
|
|
167
|
-
[{ condition: "status == 'overdue'", style: { backgroundColor: 'red' } }],
|
|
168
|
-
);
|
|
169
|
-
expect(result).toEqual({});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('supports compound conditions with && operator', () => {
|
|
173
|
-
const result = evaluateConditionalFormatting(
|
|
174
|
-
{ amount: 2000, status: 'urgent' },
|
|
175
|
-
[{ condition: "amount > 1000 && status === 'urgent'", style: { backgroundColor: '#fee' } }],
|
|
176
|
-
);
|
|
177
|
-
expect(result).toEqual({ backgroundColor: '#fee' });
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('merges style with individual color properties', () => {
|
|
181
|
-
const result = evaluateConditionalFormatting(
|
|
182
|
-
{ status: 'overdue' },
|
|
183
|
-
[{
|
|
184
|
-
condition: "status == 'overdue'",
|
|
185
|
-
style: { fontWeight: 'bold' },
|
|
186
|
-
backgroundColor: '#f00',
|
|
187
|
-
}],
|
|
188
|
-
);
|
|
189
|
-
expect(result).toEqual({ fontWeight: 'bold', backgroundColor: '#f00' });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('does not throw on invalid plain condition', () => {
|
|
193
|
-
const result = evaluateConditionalFormatting(
|
|
194
|
-
{ status: 'ok' },
|
|
195
|
-
[{ condition: '!!!invalidSyntax', style: { backgroundColor: 'red' } }],
|
|
196
|
-
);
|
|
197
|
-
expect(result).toEqual({});
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// =========================================================================
|
|
202
|
-
// Mixed rules (expression + standard) – first match wins
|
|
203
|
-
// =========================================================================
|
|
204
|
-
describe('mixed rules', () => {
|
|
205
|
-
it('returns the first matching rule (expression first)', () => {
|
|
206
|
-
const result = evaluateConditionalFormatting(
|
|
207
|
-
{ amount: 5000, status: 'active' },
|
|
208
|
-
[
|
|
209
|
-
{
|
|
210
|
-
field: '',
|
|
211
|
-
operator: 'equals',
|
|
212
|
-
value: '',
|
|
213
|
-
expression: '${data.amount > 1000}',
|
|
214
|
-
backgroundColor: '#expr_match',
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
field: 'status',
|
|
218
|
-
operator: 'equals',
|
|
219
|
-
value: 'active',
|
|
220
|
-
backgroundColor: '#operator_match',
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
);
|
|
224
|
-
expect(result).toEqual({ backgroundColor: '#expr_match' });
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('falls through non-matching expression to matching operator rule', () => {
|
|
228
|
-
const result = evaluateConditionalFormatting(
|
|
229
|
-
{ amount: 50, status: 'active' },
|
|
230
|
-
[
|
|
231
|
-
{
|
|
232
|
-
field: '',
|
|
233
|
-
operator: 'equals',
|
|
234
|
-
value: '',
|
|
235
|
-
expression: '${data.amount > 1000}',
|
|
236
|
-
backgroundColor: '#expr_match',
|
|
237
|
-
},
|
|
238
|
-
{
|
|
239
|
-
field: 'status',
|
|
240
|
-
operator: 'equals',
|
|
241
|
-
value: 'active',
|
|
242
|
-
backgroundColor: '#operator_match',
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
);
|
|
246
|
-
expect(result).toEqual({ backgroundColor: '#operator_match' });
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('handles mixed spec condition + ObjectUI field rules', () => {
|
|
250
|
-
const result = evaluateConditionalFormatting(
|
|
251
|
-
{ status: 'overdue', priority: 'low' },
|
|
252
|
-
[
|
|
253
|
-
{ condition: "status == 'overdue'", style: { backgroundColor: 'red' } },
|
|
254
|
-
{ field: 'priority', operator: 'equals', value: 'low', backgroundColor: '#ccc' },
|
|
255
|
-
],
|
|
256
|
-
);
|
|
257
|
-
// First matching rule wins
|
|
258
|
-
expect(result).toEqual({ backgroundColor: 'red' });
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('falls through non-matching spec condition to ObjectUI field rule', () => {
|
|
262
|
-
const result = evaluateConditionalFormatting(
|
|
263
|
-
{ status: 'active', priority: 'low' },
|
|
264
|
-
[
|
|
265
|
-
{ condition: "status == 'overdue'", style: { backgroundColor: 'red' } },
|
|
266
|
-
{ field: 'priority', operator: 'equals', value: 'low', backgroundColor: '#ccc' },
|
|
267
|
-
],
|
|
268
|
-
);
|
|
269
|
-
expect(result).toEqual({ backgroundColor: '#ccc' });
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// =========================================================================
|
|
274
|
-
// Edge cases
|
|
275
|
-
// =========================================================================
|
|
276
|
-
describe('edge cases', () => {
|
|
277
|
-
it('returns empty object for undefined rules', () => {
|
|
278
|
-
expect(evaluateConditionalFormatting({ a: 1 }, undefined)).toEqual({});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('returns empty object for empty rules array', () => {
|
|
282
|
-
expect(evaluateConditionalFormatting({ a: 1 }, [])).toEqual({});
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
});
|
|
@@ -1,253 +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, beforeEach } from 'vitest';
|
|
10
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import { ListView } from '../ListView';
|
|
12
|
-
import type { ListViewSchema } from '@object-ui/types';
|
|
13
|
-
import { SchemaRendererProvider } from '@object-ui/react';
|
|
14
|
-
|
|
15
|
-
const renderWithProvider = (component: React.ReactNode, dataSource?: any) => {
|
|
16
|
-
return render(
|
|
17
|
-
<SchemaRendererProvider dataSource={dataSource || mockDataSource}>
|
|
18
|
-
{component}
|
|
19
|
-
</SchemaRendererProvider>
|
|
20
|
-
);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
let mockDataSource: any;
|
|
24
|
-
|
|
25
|
-
describe('ListView Data Fetch', () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
mockDataSource = {
|
|
28
|
-
find: vi.fn().mockResolvedValue([]),
|
|
29
|
-
findOne: vi.fn(),
|
|
30
|
-
create: vi.fn(),
|
|
31
|
-
update: vi.fn(),
|
|
32
|
-
delete: vi.fn(),
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// =========================================================================
|
|
37
|
-
// Data Limit Warning (Issue #7)
|
|
38
|
-
// =========================================================================
|
|
39
|
-
describe('data limit warning', () => {
|
|
40
|
-
it('shows data limit warning when items reach the page size', async () => {
|
|
41
|
-
// Generate exactly 100 items (default page size)
|
|
42
|
-
const items = Array.from({ length: 100 }, (_, i) => ({
|
|
43
|
-
id: String(i),
|
|
44
|
-
name: `Item ${i}`,
|
|
45
|
-
}));
|
|
46
|
-
mockDataSource.find.mockResolvedValue(items);
|
|
47
|
-
|
|
48
|
-
const schema: ListViewSchema = {
|
|
49
|
-
type: 'list-view',
|
|
50
|
-
objectName: 'contacts',
|
|
51
|
-
viewType: 'grid',
|
|
52
|
-
fields: ['name'],
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
56
|
-
|
|
57
|
-
await vi.waitFor(() => {
|
|
58
|
-
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
expect(screen.getByTestId('data-limit-warning')).toBeInTheDocument();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('does not show data limit warning when items are below page size', async () => {
|
|
64
|
-
const items = [
|
|
65
|
-
{ id: '1', name: 'Alice' },
|
|
66
|
-
{ id: '2', name: 'Bob' },
|
|
67
|
-
];
|
|
68
|
-
mockDataSource.find.mockResolvedValue(items);
|
|
69
|
-
|
|
70
|
-
const schema: ListViewSchema = {
|
|
71
|
-
type: 'list-view',
|
|
72
|
-
objectName: 'contacts',
|
|
73
|
-
viewType: 'grid',
|
|
74
|
-
fields: ['name'],
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
78
|
-
|
|
79
|
-
await vi.waitFor(() => {
|
|
80
|
-
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
expect(screen.queryByTestId('data-limit-warning')).not.toBeInTheDocument();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('uses custom page size from schema.pagination', async () => {
|
|
86
|
-
const items = Array.from({ length: 50 }, (_, i) => ({
|
|
87
|
-
id: String(i),
|
|
88
|
-
name: `Item ${i}`,
|
|
89
|
-
}));
|
|
90
|
-
mockDataSource.find.mockResolvedValue(items);
|
|
91
|
-
|
|
92
|
-
const schema: ListViewSchema = {
|
|
93
|
-
type: 'list-view',
|
|
94
|
-
objectName: 'contacts',
|
|
95
|
-
viewType: 'grid',
|
|
96
|
-
fields: ['name'],
|
|
97
|
-
pagination: { pageSize: 50 },
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
101
|
-
|
|
102
|
-
await vi.waitFor(() => {
|
|
103
|
-
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Should show warning since exactly 50 items = pageSize
|
|
107
|
-
expect(screen.getByTestId('data-limit-warning')).toBeInTheDocument();
|
|
108
|
-
|
|
109
|
-
// Verify that $top was set to custom page size
|
|
110
|
-
expect(mockDataSource.find).toHaveBeenCalledWith('contacts', expect.objectContaining({
|
|
111
|
-
$top: 50,
|
|
112
|
-
}));
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// =========================================================================
|
|
117
|
-
// OData { value: [] } response format handling
|
|
118
|
-
// =========================================================================
|
|
119
|
-
describe('OData value response format', () => {
|
|
120
|
-
it('should extract records from { value: [] } OData response', async () => {
|
|
121
|
-
const items = [
|
|
122
|
-
{ id: '1', name: 'Alice' },
|
|
123
|
-
{ id: '2', name: 'Bob' },
|
|
124
|
-
];
|
|
125
|
-
mockDataSource.find.mockResolvedValue({ value: items, '@odata.count': 2 });
|
|
126
|
-
|
|
127
|
-
const schema: ListViewSchema = {
|
|
128
|
-
type: 'list-view',
|
|
129
|
-
objectName: 'contacts',
|
|
130
|
-
viewType: 'grid',
|
|
131
|
-
fields: ['name'],
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
135
|
-
|
|
136
|
-
await vi.waitFor(() => {
|
|
137
|
-
expect(screen.getByTestId('record-count-bar')).toBeInTheDocument();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Records should be extracted and rendered (not empty)
|
|
141
|
-
expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// =========================================================================
|
|
146
|
-
// $expand race condition fix (Issue #939 Bug 1)
|
|
147
|
-
// =========================================================================
|
|
148
|
-
describe('$expand with objectSchema', () => {
|
|
149
|
-
it('should include $expand in find() when objectSchema has lookup fields', async () => {
|
|
150
|
-
mockDataSource.getObjectSchema = vi.fn().mockResolvedValue({
|
|
151
|
-
fields: {
|
|
152
|
-
name: { type: 'text' },
|
|
153
|
-
customer: { type: 'lookup', reference_to: 'contact' },
|
|
154
|
-
account: { type: 'master_detail', reference_to: 'account' },
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
mockDataSource.find.mockResolvedValue([
|
|
158
|
-
{ id: '1', name: 'Order 1', customer: { name: 'Alice' }, account: { name: 'Acme' } },
|
|
159
|
-
]);
|
|
160
|
-
|
|
161
|
-
const schema: ListViewSchema = {
|
|
162
|
-
type: 'list-view',
|
|
163
|
-
objectName: 'orders',
|
|
164
|
-
viewType: 'grid',
|
|
165
|
-
fields: ['name', 'customer', 'account'],
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
169
|
-
|
|
170
|
-
await vi.waitFor(() => {
|
|
171
|
-
expect(mockDataSource.find).toHaveBeenCalledWith(
|
|
172
|
-
'orders',
|
|
173
|
-
expect.objectContaining({
|
|
174
|
-
$expand: expect.arrayContaining(['customer', 'account']),
|
|
175
|
-
}),
|
|
176
|
-
);
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should wait for objectSchema before fetching data', async () => {
|
|
181
|
-
let resolveSchema: (value: any) => void;
|
|
182
|
-
const schemaPromise = new Promise(resolve => { resolveSchema = resolve; });
|
|
183
|
-
mockDataSource.getObjectSchema = vi.fn().mockReturnValue(schemaPromise);
|
|
184
|
-
mockDataSource.find.mockResolvedValue([{ id: '1', name: 'Item 1' }]);
|
|
185
|
-
|
|
186
|
-
const schema: ListViewSchema = {
|
|
187
|
-
type: 'list-view',
|
|
188
|
-
objectName: 'orders',
|
|
189
|
-
viewType: 'grid',
|
|
190
|
-
fields: ['name'],
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
renderWithProvider(<ListView schema={schema} dataSource={mockDataSource} />);
|
|
194
|
-
|
|
195
|
-
// find() should NOT be called yet because objectSchema hasn't resolved
|
|
196
|
-
expect(mockDataSource.find).not.toHaveBeenCalled();
|
|
197
|
-
|
|
198
|
-
// Resolve objectSchema
|
|
199
|
-
resolveSchema!({ fields: { name: { type: 'text' } } });
|
|
200
|
-
|
|
201
|
-
// Now find() should be called
|
|
202
|
-
await vi.waitFor(() => {
|
|
203
|
-
expect(mockDataSource.find).toHaveBeenCalled();
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// =========================================================================
|
|
209
|
-
// Request Debounce (Issue #5)
|
|
210
|
-
// =========================================================================
|
|
211
|
-
describe('request debounce', () => {
|
|
212
|
-
it('only uses data from the latest request when multiple fetches occur', async () => {
|
|
213
|
-
let resolveFirst: (value: any) => void;
|
|
214
|
-
let resolveSecond: (value: any) => void;
|
|
215
|
-
|
|
216
|
-
const firstPromise = new Promise(resolve => { resolveFirst = resolve; });
|
|
217
|
-
const secondPromise = new Promise(resolve => { resolveSecond = resolve; });
|
|
218
|
-
|
|
219
|
-
mockDataSource.find
|
|
220
|
-
.mockReturnValueOnce(firstPromise)
|
|
221
|
-
.mockReturnValueOnce(secondPromise);
|
|
222
|
-
|
|
223
|
-
const schema: ListViewSchema = {
|
|
224
|
-
type: 'list-view',
|
|
225
|
-
objectName: 'contacts',
|
|
226
|
-
viewType: 'grid',
|
|
227
|
-
fields: ['name'],
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const { rerender } = renderWithProvider(
|
|
231
|
-
<ListView schema={schema} dataSource={mockDataSource} />,
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
// Resolve second (newer) request first
|
|
235
|
-
resolveSecond!([{ id: '2', name: 'Second' }]);
|
|
236
|
-
|
|
237
|
-
await vi.waitFor(() => {
|
|
238
|
-
expect(mockDataSource.find).toHaveBeenCalled();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
// Resolve first (stale) request later
|
|
242
|
-
resolveFirst!([{ id: '1', name: 'First (stale)' }]);
|
|
243
|
-
|
|
244
|
-
// Wait for state to settle — second request data should win
|
|
245
|
-
await vi.waitFor(() => {
|
|
246
|
-
// Data should eventually render from latest successful request
|
|
247
|
-
expect(screen.queryByTestId('empty-state')).not.toBeNull();
|
|
248
|
-
}, { timeout: 2000 }).catch(() => {
|
|
249
|
-
// This is fine — the key point is stale data doesn't overwrite new data
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|