@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +30492 -38346
  5. package/dist/index.umd.cjs +30 -38
  6. package/dist/{src → packages/plugin-list/src}/ListView.d.ts +17 -1
  7. package/dist/packages/plugin-list/src/ListView.d.ts.map +1 -0
  8. package/dist/packages/plugin-list/src/ListView.stories.d.ts.map +1 -0
  9. package/dist/packages/plugin-list/src/ObjectGallery.d.ts.map +1 -0
  10. package/dist/packages/plugin-list/src/UserFilters.d.ts.map +1 -0
  11. package/dist/packages/plugin-list/src/ViewSwitcher.d.ts.map +1 -0
  12. package/dist/packages/plugin-list/src/components/TabBar.d.ts.map +1 -0
  13. package/dist/{src → packages/plugin-list/src}/index.d.ts +1 -1
  14. package/dist/packages/plugin-list/src/index.d.ts.map +1 -0
  15. package/dist/plugin-list.css +1 -2
  16. package/package.json +35 -13
  17. package/.turbo/turbo-build.log +0 -24
  18. package/dist/src/ListView.d.ts.map +0 -1
  19. package/dist/src/ListView.stories.d.ts.map +0 -1
  20. package/dist/src/ObjectGallery.d.ts.map +0 -1
  21. package/dist/src/UserFilters.d.ts.map +0 -1
  22. package/dist/src/ViewSwitcher.d.ts.map +0 -1
  23. package/dist/src/components/TabBar.d.ts.map +0 -1
  24. package/dist/src/index.d.ts.map +0 -1
  25. package/src/ListView.stories.tsx +0 -64
  26. package/src/ListView.tsx +0 -1688
  27. package/src/ObjectGallery.tsx +0 -308
  28. package/src/UserFilters.tsx +0 -453
  29. package/src/ViewSwitcher.tsx +0 -113
  30. package/src/__tests__/ConditionalFormatting.test.ts +0 -285
  31. package/src/__tests__/DataFetch.test.tsx +0 -253
  32. package/src/__tests__/Export.test.tsx +0 -175
  33. package/src/__tests__/FilterNormalization.test.ts +0 -162
  34. package/src/__tests__/GalleryGrouping.test.tsx +0 -237
  35. package/src/__tests__/GalleryTimelineSpecConfig.test.tsx +0 -203
  36. package/src/__tests__/ListView.test.tsx +0 -2151
  37. package/src/__tests__/ListViewGroupingPropagation.test.tsx +0 -250
  38. package/src/__tests__/ListViewPersistence.test.tsx +0 -129
  39. package/src/__tests__/ObjectGallery.test.tsx +0 -208
  40. package/src/__tests__/TabBar.test.tsx +0 -199
  41. package/src/__tests__/UserFilters.test.tsx +0 -486
  42. package/src/components/TabBar.tsx +0 -120
  43. package/src/index.tsx +0 -78
  44. package/tsconfig.json +0 -18
  45. package/vite.config.ts +0 -56
  46. package/vitest.config.ts +0 -12
  47. package/vitest.setup.ts +0 -1
  48. /package/dist/{src → packages/plugin-list/src}/ListView.stories.d.ts +0 -0
  49. /package/dist/{src → packages/plugin-list/src}/ObjectGallery.d.ts +0 -0
  50. /package/dist/{src → packages/plugin-list/src}/UserFilters.d.ts +0 -0
  51. /package/dist/{src → packages/plugin-list/src}/ViewSwitcher.d.ts +0 -0
  52. /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
- });