@object-ui/plugin-list 3.3.0 → 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.
@@ -1,203 +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 type { ListViewSchema } from '@object-ui/types';
11
-
12
- /**
13
- * Tests for Gallery/Timeline spec config propagation through ListView's
14
- * buildViewSchema. We test the internal logic by checking that the
15
- * ListViewSchema types accept spec config and that the config values are correct.
16
- */
17
-
18
- describe('Gallery/Timeline Spec Config Types', () => {
19
- describe('GalleryConfig on ListViewSchema', () => {
20
- it('accepts spec gallery config with coverField', () => {
21
- const schema: ListViewSchema = {
22
- type: 'list-view',
23
- objectName: 'products',
24
- viewType: 'gallery',
25
- fields: ['name', 'photo'],
26
- gallery: {
27
- coverField: 'photo',
28
- coverFit: 'contain',
29
- cardSize: 'large',
30
- titleField: 'name',
31
- visibleFields: ['status', 'price'],
32
- },
33
- };
34
-
35
- expect(schema.gallery?.coverField).toBe('photo');
36
- expect(schema.gallery?.coverFit).toBe('contain');
37
- expect(schema.gallery?.cardSize).toBe('large');
38
- expect(schema.gallery?.titleField).toBe('name');
39
- expect(schema.gallery?.visibleFields).toEqual(['status', 'price']);
40
- });
41
-
42
- it('accepts all cardSize values', () => {
43
- const sizes = ['small', 'medium', 'large'] as const;
44
- sizes.forEach((cardSize) => {
45
- const schema: ListViewSchema = {
46
- type: 'list-view',
47
- objectName: 'products',
48
- viewType: 'gallery',
49
- fields: ['name'],
50
- gallery: { cardSize },
51
- };
52
- expect(schema.gallery?.cardSize).toBe(cardSize);
53
- });
54
- });
55
-
56
- it('accepts all coverFit values', () => {
57
- const fits = ['cover', 'contain', 'fill'] as const;
58
- fits.forEach((coverFit) => {
59
- const schema: ListViewSchema = {
60
- type: 'list-view',
61
- objectName: 'products',
62
- viewType: 'gallery',
63
- fields: ['name'],
64
- gallery: { coverFit },
65
- };
66
- expect(schema.gallery?.coverFit).toBe(coverFit);
67
- });
68
- });
69
-
70
- it('accepts legacy imageField and subtitleField alongside spec fields', () => {
71
- const schema: ListViewSchema = {
72
- type: 'list-view',
73
- objectName: 'products',
74
- viewType: 'gallery',
75
- fields: ['name'],
76
- gallery: {
77
- coverField: 'photo',
78
- imageField: 'legacyImg',
79
- subtitleField: 'description',
80
- },
81
- };
82
-
83
- expect(schema.gallery?.coverField).toBe('photo');
84
- expect(schema.gallery?.imageField).toBe('legacyImg');
85
- expect(schema.gallery?.subtitleField).toBe('description');
86
- });
87
-
88
- it('accepts gallery config from legacy options as fallback', () => {
89
- const schema: ListViewSchema = {
90
- type: 'list-view',
91
- objectName: 'products',
92
- viewType: 'gallery',
93
- fields: ['name'],
94
- options: {
95
- gallery: { imageField: 'oldImg', titleField: 'label' },
96
- },
97
- };
98
-
99
- expect(schema.options?.gallery?.imageField).toBe('oldImg');
100
- expect(schema.options?.gallery?.titleField).toBe('label');
101
- });
102
- });
103
-
104
- describe('TimelineConfig on ListViewSchema', () => {
105
- it('accepts spec timeline config with all fields', () => {
106
- const schema: ListViewSchema = {
107
- type: 'list-view',
108
- objectName: 'events',
109
- viewType: 'timeline',
110
- fields: ['name', 'date'],
111
- timeline: {
112
- startDateField: 'start_date',
113
- endDateField: 'end_date',
114
- titleField: 'event_name',
115
- groupByField: 'category',
116
- colorField: 'priority_color',
117
- scale: 'month',
118
- },
119
- };
120
-
121
- expect(schema.timeline?.startDateField).toBe('start_date');
122
- expect(schema.timeline?.endDateField).toBe('end_date');
123
- expect(schema.timeline?.titleField).toBe('event_name');
124
- expect(schema.timeline?.groupByField).toBe('category');
125
- expect(schema.timeline?.colorField).toBe('priority_color');
126
- expect(schema.timeline?.scale).toBe('month');
127
- });
128
-
129
- it('accepts legacy dateField for backward compatibility', () => {
130
- const schema: ListViewSchema = {
131
- type: 'list-view',
132
- objectName: 'events',
133
- viewType: 'timeline',
134
- fields: ['name'],
135
- timeline: {
136
- startDateField: 'created_at',
137
- titleField: 'name',
138
- dateField: 'legacy_date',
139
- },
140
- };
141
-
142
- expect(schema.timeline?.startDateField).toBe('created_at');
143
- expect(schema.timeline?.dateField).toBe('legacy_date');
144
- });
145
-
146
- it('supports all scale values', () => {
147
- const scales = ['hour', 'day', 'week', 'month', 'quarter', 'year'] as const;
148
- scales.forEach((scale) => {
149
- const schema: ListViewSchema = {
150
- type: 'list-view',
151
- objectName: 'events',
152
- viewType: 'timeline',
153
- fields: ['name'],
154
- timeline: { startDateField: 'date', titleField: 'name', scale },
155
- };
156
- expect(schema.timeline?.scale).toBe(scale);
157
- });
158
- });
159
-
160
- it('accepts timeline config from legacy options as fallback', () => {
161
- const schema: ListViewSchema = {
162
- type: 'list-view',
163
- objectName: 'events',
164
- viewType: 'timeline',
165
- fields: ['name'],
166
- options: {
167
- timeline: { dateField: 'created_at', titleField: 'name' },
168
- },
169
- };
170
-
171
- expect(schema.options?.timeline?.dateField).toBe('created_at');
172
- });
173
- });
174
-
175
- describe('spec config co-existence', () => {
176
- it('gallery and timeline configs can coexist on the same ListViewSchema', () => {
177
- const schema: ListViewSchema = {
178
- type: 'list-view',
179
- objectName: 'projects',
180
- viewType: 'grid',
181
- fields: ['name', 'date', 'photo'],
182
- gallery: {
183
- coverField: 'photo',
184
- cardSize: 'medium',
185
- titleField: 'name',
186
- visibleFields: ['status'],
187
- },
188
- timeline: {
189
- startDateField: 'start_date',
190
- titleField: 'name',
191
- scale: 'quarter',
192
- groupByField: 'team',
193
- },
194
- };
195
-
196
- expect(schema.gallery?.coverField).toBe('photo');
197
- expect(schema.gallery?.cardSize).toBe('medium');
198
- expect(schema.timeline?.startDateField).toBe('start_date');
199
- expect(schema.timeline?.scale).toBe('quarter');
200
- expect(schema.timeline?.groupByField).toBe('team');
201
- });
202
- });
203
- });
@@ -1,276 +0,0 @@
1
- /**
2
- * ObjectUI — List Refresh Tests
3
- * Tests for the standardized list refresh after mutation mechanism.
4
- *
5
- * Covers:
6
- * - P0: refreshTrigger prop triggers data re-fetch
7
- * - P1: Imperative refresh() via forwardRef
8
- * - P2: Auto-refresh via DataSource.onMutation()
9
- */
10
-
11
- import { describe, it, expect, vi, beforeEach } from 'vitest';
12
- import * as React from 'react';
13
- import { render, act } from '@testing-library/react';
14
- import { ListView } from '../ListView';
15
- import type { ListViewHandle } from '../ListView';
16
- import type { ListViewSchema } from '@object-ui/types';
17
- import { SchemaRendererProvider } from '@object-ui/react';
18
-
19
- let mockDataSource: any;
20
-
21
- const renderWithProvider = (component: React.ReactNode, ds?: any) =>
22
- render(
23
- <SchemaRendererProvider dataSource={ds || mockDataSource}>
24
- {component}
25
- </SchemaRendererProvider>,
26
- );
27
-
28
- describe('ListView Refresh Mechanisms', () => {
29
- beforeEach(() => {
30
- mockDataSource = {
31
- find: vi.fn().mockResolvedValue([]),
32
- findOne: vi.fn(),
33
- create: vi.fn(),
34
- update: vi.fn(),
35
- delete: vi.fn(),
36
- getObjectSchema: vi.fn().mockResolvedValue({ name: 'contacts', fields: {} }),
37
- };
38
- });
39
-
40
- // =========================================================================
41
- // P0: refreshTrigger schema prop
42
- // =========================================================================
43
- describe('P0 — refreshTrigger prop', () => {
44
- it('should re-fetch data when refreshTrigger changes', async () => {
45
- const schema: ListViewSchema = {
46
- type: 'list-view',
47
- objectName: 'contacts',
48
- fields: ['name'],
49
- refreshTrigger: 0,
50
- };
51
-
52
- const { rerender } = renderWithProvider(
53
- <ListView schema={schema} dataSource={mockDataSource} />,
54
- );
55
-
56
- // Wait for initial data fetch
57
- await vi.waitFor(() => {
58
- expect(mockDataSource.find).toHaveBeenCalled();
59
- });
60
-
61
- const initialCallCount = mockDataSource.find.mock.calls.length;
62
-
63
- // Increment refreshTrigger → should trigger a new data fetch
64
- const updatedSchema: ListViewSchema = { ...schema, refreshTrigger: 1 };
65
- rerender(
66
- <SchemaRendererProvider dataSource={mockDataSource}>
67
- <ListView schema={updatedSchema} dataSource={mockDataSource} />
68
- </SchemaRendererProvider>,
69
- );
70
-
71
- await vi.waitFor(() => {
72
- expect(mockDataSource.find.mock.calls.length).toBeGreaterThan(initialCallCount);
73
- });
74
- });
75
-
76
- it('should NOT re-fetch when refreshTrigger stays the same', async () => {
77
- const schema: ListViewSchema = {
78
- type: 'list-view',
79
- objectName: 'contacts',
80
- fields: ['name'],
81
- refreshTrigger: 5,
82
- };
83
-
84
- const { rerender } = renderWithProvider(
85
- <ListView schema={schema} dataSource={mockDataSource} />,
86
- );
87
-
88
- await vi.waitFor(() => {
89
- expect(mockDataSource.find).toHaveBeenCalled();
90
- });
91
-
92
- const callCount = mockDataSource.find.mock.calls.length;
93
-
94
- // Re-render with the same refreshTrigger → no extra fetch
95
- rerender(
96
- <SchemaRendererProvider dataSource={mockDataSource}>
97
- <ListView schema={{ ...schema }} dataSource={mockDataSource} />
98
- </SchemaRendererProvider>,
99
- );
100
-
101
- // Wait a tick and verify no extra call
102
- await new Promise(r => setTimeout(r, 100));
103
- expect(mockDataSource.find.mock.calls.length).toBe(callCount);
104
- });
105
- });
106
-
107
- // =========================================================================
108
- // P1: Imperative refresh() via forwardRef
109
- // =========================================================================
110
- describe('P1 — imperative refresh() API', () => {
111
- it('should expose a refresh() method via ref', () => {
112
- const ref = React.createRef<ListViewHandle>();
113
- const schema: ListViewSchema = {
114
- type: 'list-view',
115
- objectName: 'contacts',
116
- fields: ['name'],
117
- };
118
-
119
- renderWithProvider(<ListView ref={ref} schema={schema} dataSource={mockDataSource} />);
120
-
121
- expect(ref.current).toBeDefined();
122
- expect(typeof ref.current?.refresh).toBe('function');
123
- });
124
-
125
- it('calling refresh() should trigger a data re-fetch', async () => {
126
- const ref = React.createRef<ListViewHandle>();
127
- const schema: ListViewSchema = {
128
- type: 'list-view',
129
- objectName: 'contacts',
130
- fields: ['name'],
131
- };
132
-
133
- renderWithProvider(<ListView ref={ref} schema={schema} dataSource={mockDataSource} />);
134
-
135
- await vi.waitFor(() => {
136
- expect(mockDataSource.find).toHaveBeenCalled();
137
- });
138
-
139
- const callCount = mockDataSource.find.mock.calls.length;
140
-
141
- // Call imperative refresh
142
- act(() => {
143
- ref.current?.refresh();
144
- });
145
-
146
- await vi.waitFor(() => {
147
- expect(mockDataSource.find.mock.calls.length).toBeGreaterThan(callCount);
148
- });
149
- });
150
- });
151
-
152
- // =========================================================================
153
- // P2: Auto-refresh via DataSource.onMutation()
154
- // =========================================================================
155
- describe('P2 — DataSource.onMutation() auto-refresh', () => {
156
- it('should auto-refresh when a mutation event fires for the same resource', async () => {
157
- let mutationCallback: ((event: any) => void) | null = null;
158
- const unsub = vi.fn();
159
-
160
- const dsWithMutation = {
161
- ...mockDataSource,
162
- onMutation: vi.fn((cb: any) => {
163
- mutationCallback = cb;
164
- return unsub;
165
- }),
166
- };
167
-
168
- const schema: ListViewSchema = {
169
- type: 'list-view',
170
- objectName: 'contacts',
171
- fields: ['name'],
172
- };
173
-
174
- renderWithProvider(
175
- <ListView schema={schema} dataSource={dsWithMutation} />,
176
- dsWithMutation,
177
- );
178
-
179
- await vi.waitFor(() => {
180
- expect(dsWithMutation.find).toHaveBeenCalled();
181
- });
182
-
183
- const callCount = dsWithMutation.find.mock.calls.length;
184
-
185
- // Simulate a mutation on the same resource
186
- act(() => {
187
- mutationCallback?.({ type: 'create', resource: 'contacts', record: { id: '1' } });
188
- });
189
-
190
- await vi.waitFor(() => {
191
- expect(dsWithMutation.find.mock.calls.length).toBeGreaterThan(callCount);
192
- });
193
- });
194
-
195
- it('should NOT refresh when a mutation fires for a different resource', async () => {
196
- let mutationCallback: ((event: any) => void) | null = null;
197
-
198
- const dsWithMutation = {
199
- ...mockDataSource,
200
- onMutation: vi.fn((cb: any) => {
201
- mutationCallback = cb;
202
- return vi.fn();
203
- }),
204
- };
205
-
206
- const schema: ListViewSchema = {
207
- type: 'list-view',
208
- objectName: 'contacts',
209
- fields: ['name'],
210
- };
211
-
212
- renderWithProvider(
213
- <ListView schema={schema} dataSource={dsWithMutation} />,
214
- dsWithMutation,
215
- );
216
-
217
- await vi.waitFor(() => {
218
- expect(dsWithMutation.find).toHaveBeenCalled();
219
- });
220
-
221
- const callCount = dsWithMutation.find.mock.calls.length;
222
-
223
- // Fire a mutation for a different resource
224
- act(() => {
225
- mutationCallback?.({ type: 'create', resource: 'accounts', record: { id: '2' } });
226
- });
227
-
228
- // Should NOT trigger a refresh
229
- await new Promise(r => setTimeout(r, 100));
230
- expect(dsWithMutation.find.mock.calls.length).toBe(callCount);
231
- });
232
-
233
- it('should unsubscribe when unmounted', async () => {
234
- const unsub = vi.fn();
235
- const dsWithMutation = {
236
- ...mockDataSource,
237
- onMutation: vi.fn(() => unsub),
238
- };
239
-
240
- const schema: ListViewSchema = {
241
- type: 'list-view',
242
- objectName: 'contacts',
243
- fields: ['name'],
244
- };
245
-
246
- const { unmount } = renderWithProvider(
247
- <ListView schema={schema} dataSource={dsWithMutation} />,
248
- dsWithMutation,
249
- );
250
-
251
- await vi.waitFor(() => {
252
- expect(dsWithMutation.onMutation).toHaveBeenCalled();
253
- });
254
-
255
- unmount();
256
- expect(unsub).toHaveBeenCalled();
257
- });
258
-
259
- it('should work without onMutation (backward compatible)', async () => {
260
- // DataSource without onMutation — should not throw
261
- const schema: ListViewSchema = {
262
- type: 'list-view',
263
- objectName: 'contacts',
264
- fields: ['name'],
265
- };
266
-
267
- renderWithProvider(
268
- <ListView schema={schema} dataSource={mockDataSource} />,
269
- );
270
-
271
- await vi.waitFor(() => {
272
- expect(mockDataSource.find).toHaveBeenCalled();
273
- });
274
- });
275
- });
276
- });