@object-ui/plugin-view 0.5.0 → 2.0.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 +184 -6
- package/CHANGELOG.md +16 -0
- package/README.md +58 -0
- package/dist/index.js +1168 -349
- package/dist/index.umd.cjs +2 -2
- package/dist/plugin-view/src/FilterUI.d.ts +16 -0
- package/dist/plugin-view/src/ObjectView.d.ts +85 -5
- package/dist/plugin-view/src/SortUI.d.ts +16 -0
- package/dist/plugin-view/src/ViewSwitcher.d.ts +16 -0
- package/dist/plugin-view/src/__tests__/FilterUI.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/ObjectView.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/SortUI.test.d.ts +8 -0
- package/dist/plugin-view/src/__tests__/registration.test.d.ts +8 -0
- package/dist/plugin-view/src/index.d.ts +7 -1
- package/package.json +8 -7
- package/src/FilterUI.tsx +317 -0
- package/src/ObjectView.tsx +668 -148
- package/src/SortUI.tsx +210 -0
- package/src/ViewSwitcher.tsx +311 -0
- package/src/__tests__/FilterUI.test.tsx +544 -0
- package/src/__tests__/ObjectView.test.tsx +375 -0
- package/src/__tests__/SortUI.test.tsx +380 -0
- package/src/__tests__/registration.test.tsx +32 -0
- package/src/index.tsx +147 -5
- package/vitest.config.ts +12 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,375 @@
|
|
|
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, fireEvent, waitFor } from '@testing-library/react';
|
|
11
|
+
import { ObjectView } from '../ObjectView';
|
|
12
|
+
import type { ObjectViewSchema, DataSource } from '@object-ui/types';
|
|
13
|
+
|
|
14
|
+
// Mock @object-ui/react to avoid circular dependency issues
|
|
15
|
+
vi.mock('@object-ui/react', () => ({
|
|
16
|
+
SchemaRenderer: ({ schema }: any) => (
|
|
17
|
+
<div data-testid="schema-renderer" data-schema-type={schema?.type}>
|
|
18
|
+
{schema?.type}
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
SchemaRendererContext: null,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Mock @object-ui/plugin-grid
|
|
25
|
+
vi.mock('@object-ui/plugin-grid', () => ({
|
|
26
|
+
ObjectGrid: ({ schema, onRowClick }: any) => (
|
|
27
|
+
<div data-testid="object-grid" data-object={schema?.objectName}>
|
|
28
|
+
<button data-testid="grid-row" onClick={() => onRowClick?.({ _id: '1', name: 'Test' })}>
|
|
29
|
+
Row 1
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// Mock @object-ui/plugin-form
|
|
36
|
+
vi.mock('@object-ui/plugin-form', () => ({
|
|
37
|
+
ObjectForm: ({ schema }: any) => (
|
|
38
|
+
<div data-testid="object-form" data-mode={schema?.mode}>
|
|
39
|
+
Form ({schema?.mode})
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const createMockDataSource = (overrides: Partial<DataSource> = {}): DataSource => ({
|
|
45
|
+
find: vi.fn().mockResolvedValue([]),
|
|
46
|
+
findOne: vi.fn().mockResolvedValue(null),
|
|
47
|
+
create: vi.fn().mockResolvedValue({}),
|
|
48
|
+
update: vi.fn().mockResolvedValue({}),
|
|
49
|
+
delete: vi.fn().mockResolvedValue({}),
|
|
50
|
+
getObjectSchema: vi.fn().mockResolvedValue({
|
|
51
|
+
label: 'Contacts',
|
|
52
|
+
fields: {
|
|
53
|
+
name: { label: 'Name', type: 'text' },
|
|
54
|
+
email: { label: 'Email', type: 'text' },
|
|
55
|
+
status: {
|
|
56
|
+
label: 'Status',
|
|
57
|
+
type: 'select',
|
|
58
|
+
options: [
|
|
59
|
+
{ label: 'Active', value: 'active' },
|
|
60
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
created_at: { label: 'Created', type: 'date' },
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
...overrides,
|
|
67
|
+
} as DataSource);
|
|
68
|
+
|
|
69
|
+
describe('ObjectView', () => {
|
|
70
|
+
let mockDataSource: DataSource;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
mockDataSource = createMockDataSource();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ============================
|
|
78
|
+
// Basic Rendering
|
|
79
|
+
// ============================
|
|
80
|
+
describe('Basic Rendering', () => {
|
|
81
|
+
it('should render with minimal schema', () => {
|
|
82
|
+
const schema: ObjectViewSchema = {
|
|
83
|
+
type: 'object-view',
|
|
84
|
+
objectName: 'contacts',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
88
|
+
|
|
89
|
+
// Should render the grid by default
|
|
90
|
+
expect(screen.getByTestId('object-grid')).toBeDefined();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should render title and description', () => {
|
|
94
|
+
const schema: ObjectViewSchema = {
|
|
95
|
+
type: 'object-view',
|
|
96
|
+
objectName: 'contacts',
|
|
97
|
+
title: 'Contact List',
|
|
98
|
+
description: 'Manage your contacts',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
102
|
+
|
|
103
|
+
expect(screen.getByText('Contact List')).toBeDefined();
|
|
104
|
+
expect(screen.getByText('Manage your contacts')).toBeDefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should not render search box (delegated to ListView toolbar)', () => {
|
|
108
|
+
const schema: ObjectViewSchema = {
|
|
109
|
+
type: 'object-view',
|
|
110
|
+
objectName: 'contacts',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
114
|
+
|
|
115
|
+
expect(screen.queryByPlaceholderText(/search/i)).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should not render search box when showSearch is false', () => {
|
|
119
|
+
const schema: ObjectViewSchema = {
|
|
120
|
+
type: 'object-view',
|
|
121
|
+
objectName: 'contacts',
|
|
122
|
+
showSearch: false,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
126
|
+
|
|
127
|
+
expect(screen.queryByPlaceholderText(/search/i)).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should render create button by default', () => {
|
|
131
|
+
const schema: ObjectViewSchema = {
|
|
132
|
+
type: 'object-view',
|
|
133
|
+
objectName: 'contacts',
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByText('Create')).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should hide create button when showCreate is false', () => {
|
|
142
|
+
const schema: ObjectViewSchema = {
|
|
143
|
+
type: 'object-view',
|
|
144
|
+
objectName: 'contacts',
|
|
145
|
+
showCreate: false,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
149
|
+
|
|
150
|
+
expect(screen.queryByText('Create')).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ============================
|
|
155
|
+
// Named List Views
|
|
156
|
+
// ============================
|
|
157
|
+
describe('Named List Views', () => {
|
|
158
|
+
it('should render named view tabs when listViews has multiple entries', () => {
|
|
159
|
+
const schema: ObjectViewSchema = {
|
|
160
|
+
type: 'object-view',
|
|
161
|
+
objectName: 'contacts',
|
|
162
|
+
listViews: {
|
|
163
|
+
all: { label: 'All Contacts', type: 'grid' },
|
|
164
|
+
active: { label: 'Active', type: 'grid', filter: [['status', '=', 'active']] },
|
|
165
|
+
},
|
|
166
|
+
defaultListView: 'all',
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
170
|
+
|
|
171
|
+
expect(screen.getByText('All Contacts')).toBeDefined();
|
|
172
|
+
expect(screen.getByText('Active')).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should not render tabs when only one named view exists', () => {
|
|
176
|
+
const schema: ObjectViewSchema = {
|
|
177
|
+
type: 'object-view',
|
|
178
|
+
objectName: 'contacts',
|
|
179
|
+
listViews: {
|
|
180
|
+
all: { label: 'All Contacts', type: 'grid' },
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
185
|
+
|
|
186
|
+
// Should not show tabs for a single view
|
|
187
|
+
expect(screen.queryByRole('tablist')).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should default to first named view when defaultListView is not set', () => {
|
|
191
|
+
const schema: ObjectViewSchema = {
|
|
192
|
+
type: 'object-view',
|
|
193
|
+
objectName: 'contacts',
|
|
194
|
+
listViews: {
|
|
195
|
+
all: { label: 'All Contacts', type: 'grid' },
|
|
196
|
+
active: { label: 'Active', type: 'grid' },
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
201
|
+
|
|
202
|
+
// The grid should be rendered (first view is grid type)
|
|
203
|
+
expect(screen.getByTestId('object-grid')).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ============================
|
|
208
|
+
// Default View Type
|
|
209
|
+
// ============================
|
|
210
|
+
describe('Default View Type', () => {
|
|
211
|
+
it('should render grid by default when no defaultViewType set', () => {
|
|
212
|
+
const schema: ObjectViewSchema = {
|
|
213
|
+
type: 'object-view',
|
|
214
|
+
objectName: 'contacts',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByTestId('object-grid')).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ============================
|
|
224
|
+
// Navigation Config
|
|
225
|
+
// ============================
|
|
226
|
+
describe('Navigation Config', () => {
|
|
227
|
+
it('should not navigate when mode is none', () => {
|
|
228
|
+
const onRowClick = vi.fn();
|
|
229
|
+
const schema: ObjectViewSchema = {
|
|
230
|
+
type: 'object-view',
|
|
231
|
+
objectName: 'contacts',
|
|
232
|
+
navigation: { mode: 'none' },
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
236
|
+
|
|
237
|
+
// Click a grid row
|
|
238
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
239
|
+
|
|
240
|
+
// onRowClick should not be called (mode is none)
|
|
241
|
+
expect(onRowClick).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should not navigate when preventNavigation is true', () => {
|
|
245
|
+
const onRowClick = vi.fn();
|
|
246
|
+
const schema: ObjectViewSchema = {
|
|
247
|
+
type: 'object-view',
|
|
248
|
+
objectName: 'contacts',
|
|
249
|
+
navigation: { mode: 'page', preventNavigation: true },
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
253
|
+
|
|
254
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
255
|
+
|
|
256
|
+
expect(onRowClick).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should open in new window when mode is new_window', () => {
|
|
260
|
+
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
261
|
+
|
|
262
|
+
const schema: ObjectViewSchema = {
|
|
263
|
+
type: 'object-view',
|
|
264
|
+
objectName: 'contacts',
|
|
265
|
+
navigation: { mode: 'new_window' },
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
269
|
+
|
|
270
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
271
|
+
|
|
272
|
+
expect(openSpy).toHaveBeenCalledWith('/contacts/1', '_blank');
|
|
273
|
+
openSpy.mockRestore();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should call onNavigate for page mode', () => {
|
|
277
|
+
const onNavigate = vi.fn();
|
|
278
|
+
const schema: ObjectViewSchema = {
|
|
279
|
+
type: 'object-view',
|
|
280
|
+
objectName: 'contacts',
|
|
281
|
+
navigation: { mode: 'page' },
|
|
282
|
+
onNavigate,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
286
|
+
|
|
287
|
+
fireEvent.click(screen.getByTestId('grid-row'));
|
|
288
|
+
|
|
289
|
+
expect(onNavigate).toHaveBeenCalledWith('1', 'view');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ============================
|
|
294
|
+
// CRUD Operations
|
|
295
|
+
// ============================
|
|
296
|
+
describe('CRUD Operations', () => {
|
|
297
|
+
it('should hide create button when operations.create is false', () => {
|
|
298
|
+
const schema: ObjectViewSchema = {
|
|
299
|
+
type: 'object-view',
|
|
300
|
+
objectName: 'contacts',
|
|
301
|
+
operations: { create: false, read: true, update: true, delete: true },
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
305
|
+
|
|
306
|
+
expect(screen.queryByText('Create')).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ============================
|
|
311
|
+
// Data Source Integration
|
|
312
|
+
// ============================
|
|
313
|
+
describe('Data Source Integration', () => {
|
|
314
|
+
it('should fetch object schema on mount', async () => {
|
|
315
|
+
const schema: ObjectViewSchema = {
|
|
316
|
+
type: 'object-view',
|
|
317
|
+
objectName: 'contacts',
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
render(<ObjectView schema={schema} dataSource={mockDataSource} />);
|
|
321
|
+
|
|
322
|
+
await waitFor(() => {
|
|
323
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ============================
|
|
329
|
+
// View Switcher (prop-based views)
|
|
330
|
+
// ============================
|
|
331
|
+
describe('View Switcher (prop-based)', () => {
|
|
332
|
+
it('should not render view switcher with single view prop', () => {
|
|
333
|
+
const schema: ObjectViewSchema = {
|
|
334
|
+
type: 'object-view',
|
|
335
|
+
objectName: 'contacts',
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const views = [
|
|
339
|
+
{ id: 'grid', label: 'Grid', type: 'grid' as const },
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
render(
|
|
343
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={views} />,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Only one view, no switcher needed
|
|
347
|
+
expect(screen.getByTestId('object-grid')).toBeDefined();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// ============================
|
|
352
|
+
// showViewSwitcher config
|
|
353
|
+
// ============================
|
|
354
|
+
describe('showViewSwitcher', () => {
|
|
355
|
+
it('should hide view switcher when showViewSwitcher is false', () => {
|
|
356
|
+
const schema: ObjectViewSchema = {
|
|
357
|
+
type: 'object-view',
|
|
358
|
+
objectName: 'contacts',
|
|
359
|
+
showViewSwitcher: false,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const views = [
|
|
363
|
+
{ id: 'grid', label: 'Grid', type: 'grid' as const },
|
|
364
|
+
{ id: 'kanban', label: 'Kanban', type: 'kanban' as const },
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
render(
|
|
368
|
+
<ObjectView schema={schema} dataSource={mockDataSource} views={views} />,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Switcher should be hidden
|
|
372
|
+
expect(screen.queryByText('Kanban')).toBeNull();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|