@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,380 @@
|
|
|
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 } from 'vitest';
|
|
10
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
+
import { SortUI } from '../SortUI';
|
|
12
|
+
import type { SortUISchema } from '@object-ui/types';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Mock @object-ui/components – provide lightweight stand-ins for Shadcn
|
|
16
|
+
// primitives so tests render without a full component tree.
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
vi.mock('@object-ui/components', () => {
|
|
19
|
+
const cn = (...args: any[]) => args.filter(Boolean).join(' ');
|
|
20
|
+
|
|
21
|
+
const Button = ({ children, onClick, variant, ...rest }: any) => (
|
|
22
|
+
<button onClick={onClick} data-variant={variant} {...rest}>
|
|
23
|
+
{children}
|
|
24
|
+
</button>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const Select = ({ children, value, onValueChange }: any) => (
|
|
28
|
+
<div data-testid="select-root" data-value={value}>
|
|
29
|
+
{typeof children === 'function'
|
|
30
|
+
? children({ value, onValueChange })
|
|
31
|
+
: children}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const SelectTrigger = ({ children, className }: any) => (
|
|
36
|
+
<button data-testid="select-trigger" className={className}>
|
|
37
|
+
{children}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const SelectValue = ({ placeholder }: any) => (
|
|
42
|
+
<span data-testid="select-value">{placeholder}</span>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const SelectContent = ({ children }: any) => (
|
|
46
|
+
<div data-testid="select-content">{children}</div>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const SelectItem = ({ children, value }: any) => (
|
|
50
|
+
<div data-testid="select-item" data-value={value}>
|
|
51
|
+
{children}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const SortBuilder = ({ fields, value, onChange }: any) => (
|
|
56
|
+
<div data-testid="sort-builder" data-fields={JSON.stringify(fields)} data-value={JSON.stringify(value)}>
|
|
57
|
+
<button
|
|
58
|
+
data-testid="sort-builder-change"
|
|
59
|
+
onClick={() =>
|
|
60
|
+
onChange?.([
|
|
61
|
+
{ id: 'date-desc', field: 'date', order: 'desc' },
|
|
62
|
+
])
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
Change Sort
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return { cn, Button, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SortBuilder };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Helpers
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
const baseFields: SortUISchema['fields'] = [
|
|
77
|
+
{ field: 'name', label: 'Name' },
|
|
78
|
+
{ field: 'date', label: 'Date' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const makeSchema = (overrides: Partial<SortUISchema> = {}): SortUISchema => ({
|
|
82
|
+
type: 'sort-ui',
|
|
83
|
+
fields: baseFields,
|
|
84
|
+
...overrides,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Tests
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
describe('SortUI', () => {
|
|
91
|
+
// -------------------------------------------------------------------------
|
|
92
|
+
// 1. Renders with default (buttons) variant
|
|
93
|
+
// -------------------------------------------------------------------------
|
|
94
|
+
describe('buttons variant', () => {
|
|
95
|
+
it('renders sort buttons for each field', () => {
|
|
96
|
+
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
97
|
+
|
|
98
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText('Date')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('renders all fields as outline buttons when no sort is active', () => {
|
|
103
|
+
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
104
|
+
|
|
105
|
+
const buttons = screen.getAllByRole('button');
|
|
106
|
+
expect(buttons).toHaveLength(2);
|
|
107
|
+
buttons.forEach(btn => {
|
|
108
|
+
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('highlights active sort field with secondary variant', () => {
|
|
113
|
+
render(
|
|
114
|
+
<SortUI
|
|
115
|
+
schema={makeSchema({
|
|
116
|
+
variant: 'buttons',
|
|
117
|
+
sort: [{ field: 'name', direction: 'asc' }],
|
|
118
|
+
})}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
123
|
+
expect(nameBtn).toHaveAttribute('data-variant', 'secondary');
|
|
124
|
+
|
|
125
|
+
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
126
|
+
expect(dateBtn).toHaveAttribute('data-variant', 'outline');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('cycles through asc → desc → removed on repeated clicks', () => {
|
|
130
|
+
const onChange = vi.fn();
|
|
131
|
+
render(
|
|
132
|
+
<SortUI schema={makeSchema({ variant: 'buttons' })} onChange={onChange} />,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
136
|
+
|
|
137
|
+
// First click: activate asc
|
|
138
|
+
fireEvent.click(nameBtn);
|
|
139
|
+
expect(onChange).toHaveBeenCalledWith([{ field: 'name', direction: 'asc' }]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
// 2. Renders with dropdown variant
|
|
145
|
+
// -------------------------------------------------------------------------
|
|
146
|
+
describe('dropdown variant', () => {
|
|
147
|
+
it('renders select elements for field and direction', () => {
|
|
148
|
+
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
149
|
+
|
|
150
|
+
const selectRoots = screen.getAllByTestId('select-root');
|
|
151
|
+
expect(selectRoots.length).toBe(2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('renders field options inside select', () => {
|
|
155
|
+
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
156
|
+
|
|
157
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText('Date')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('renders direction options (Ascending / Descending)', () => {
|
|
162
|
+
render(<SortUI schema={makeSchema({ variant: 'dropdown' })} />);
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText('Ascending')).toBeInTheDocument();
|
|
165
|
+
expect(screen.getByText('Descending')).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('defaults to dropdown when variant is omitted', () => {
|
|
169
|
+
render(<SortUI schema={makeSchema()} />);
|
|
170
|
+
|
|
171
|
+
// dropdown renders select-root elements, not buttons
|
|
172
|
+
const selectRoots = screen.getAllByTestId('select-root');
|
|
173
|
+
expect(selectRoots.length).toBe(2);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
// 3. Renders with builder variant (multiple = true)
|
|
179
|
+
// -------------------------------------------------------------------------
|
|
180
|
+
describe('builder variant (multiple)', () => {
|
|
181
|
+
it('renders SortBuilder when multiple is true', () => {
|
|
182
|
+
render(
|
|
183
|
+
<SortUI
|
|
184
|
+
schema={makeSchema({ multiple: true })}
|
|
185
|
+
/>,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(screen.getByTestId('sort-builder')).toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('passes fields and value to SortBuilder', () => {
|
|
192
|
+
render(
|
|
193
|
+
<SortUI
|
|
194
|
+
schema={makeSchema({
|
|
195
|
+
multiple: true,
|
|
196
|
+
sort: [{ field: 'name', direction: 'asc' }],
|
|
197
|
+
})}
|
|
198
|
+
/>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const builder = screen.getByTestId('sort-builder');
|
|
202
|
+
const fields = JSON.parse(builder.getAttribute('data-fields')!);
|
|
203
|
+
expect(fields).toEqual([
|
|
204
|
+
{ value: 'name', label: 'Name' },
|
|
205
|
+
{ value: 'date', label: 'Date' },
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
const value = JSON.parse(builder.getAttribute('data-value')!);
|
|
209
|
+
expect(value).toEqual([
|
|
210
|
+
{ id: 'name-asc', field: 'name', order: 'asc' },
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('calls onChange when SortBuilder triggers a change', () => {
|
|
215
|
+
const onChange = vi.fn();
|
|
216
|
+
render(
|
|
217
|
+
<SortUI
|
|
218
|
+
schema={makeSchema({ multiple: true })}
|
|
219
|
+
onChange={onChange}
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
fireEvent.click(screen.getByTestId('sort-builder-change'));
|
|
224
|
+
expect(onChange).toHaveBeenCalledWith([{ field: 'date', direction: 'desc' }]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// -------------------------------------------------------------------------
|
|
229
|
+
// 4. Initial sort configuration from schema
|
|
230
|
+
// -------------------------------------------------------------------------
|
|
231
|
+
describe('initial sort from schema', () => {
|
|
232
|
+
it('initialises state from schema.sort in buttons variant', () => {
|
|
233
|
+
render(
|
|
234
|
+
<SortUI
|
|
235
|
+
schema={makeSchema({
|
|
236
|
+
variant: 'buttons',
|
|
237
|
+
sort: [{ field: 'date', direction: 'desc' }],
|
|
238
|
+
})}
|
|
239
|
+
/>,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
243
|
+
expect(dateBtn).toHaveAttribute('data-variant', 'secondary');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('renders without error when schema.sort is undefined', () => {
|
|
247
|
+
render(<SortUI schema={makeSchema({ variant: 'buttons' })} />);
|
|
248
|
+
|
|
249
|
+
const buttons = screen.getAllByRole('button');
|
|
250
|
+
expect(buttons).toHaveLength(2);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('renders without error when schema.sort is empty', () => {
|
|
254
|
+
render(
|
|
255
|
+
<SortUI schema={makeSchema({ variant: 'buttons', sort: [] })} />,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const buttons = screen.getAllByRole('button');
|
|
259
|
+
buttons.forEach(btn => {
|
|
260
|
+
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// -------------------------------------------------------------------------
|
|
266
|
+
// 5. onChange callback
|
|
267
|
+
// -------------------------------------------------------------------------
|
|
268
|
+
describe('onChange callback', () => {
|
|
269
|
+
it('fires onChange when a button sort is toggled', () => {
|
|
270
|
+
const onChange = vi.fn();
|
|
271
|
+
render(
|
|
272
|
+
<SortUI
|
|
273
|
+
schema={makeSchema({ variant: 'buttons' })}
|
|
274
|
+
onChange={onChange}
|
|
275
|
+
/>,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
279
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(onChange).toHaveBeenCalledWith([{ field: 'name', direction: 'asc' }]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('dispatches custom window event when schema.onChange is set', () => {
|
|
284
|
+
const spy = vi.fn();
|
|
285
|
+
window.addEventListener('sort:changed', spy);
|
|
286
|
+
|
|
287
|
+
render(
|
|
288
|
+
<SortUI
|
|
289
|
+
schema={makeSchema({ variant: 'buttons', onChange: 'sort:changed' })}
|
|
290
|
+
/>,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
fireEvent.click(screen.getByText('Name').closest('button')!);
|
|
294
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
295
|
+
|
|
296
|
+
const detail = (spy.mock.calls[0][0] as CustomEvent).detail;
|
|
297
|
+
expect(detail).toEqual({ sort: [{ field: 'name', direction: 'asc' }] });
|
|
298
|
+
|
|
299
|
+
window.removeEventListener('sort:changed', spy);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('replaces active sort when multiple is false (buttons)', () => {
|
|
303
|
+
const onChange = vi.fn();
|
|
304
|
+
render(
|
|
305
|
+
<SortUI
|
|
306
|
+
schema={makeSchema({
|
|
307
|
+
variant: 'buttons',
|
|
308
|
+
sort: [{ field: 'name', direction: 'asc' }],
|
|
309
|
+
})}
|
|
310
|
+
onChange={onChange}
|
|
311
|
+
/>,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Click a different field — should replace, not append
|
|
315
|
+
fireEvent.click(screen.getByText('Date').closest('button')!);
|
|
316
|
+
expect(onChange).toHaveBeenCalledWith([{ field: 'date', direction: 'asc' }]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
// 6. Helper functions (toSortEntries / toSortItems) – tested indirectly
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
describe('helper functions (toSortEntries / toSortItems)', () => {
|
|
324
|
+
it('toSortEntries: maps schema.sort to internal state shown via button variant', () => {
|
|
325
|
+
render(
|
|
326
|
+
<SortUI
|
|
327
|
+
schema={makeSchema({
|
|
328
|
+
variant: 'buttons',
|
|
329
|
+
sort: [
|
|
330
|
+
{ field: 'name', direction: 'asc' },
|
|
331
|
+
{ field: 'date', direction: 'desc' },
|
|
332
|
+
],
|
|
333
|
+
multiple: true,
|
|
334
|
+
})}
|
|
335
|
+
/>,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// Both fields should be highlighted since both are in the sort config
|
|
339
|
+
const nameBtn = screen.getByText('Name').closest('button')!;
|
|
340
|
+
const dateBtn = screen.getByText('Date').closest('button')!;
|
|
341
|
+
expect(nameBtn).toHaveAttribute('data-variant', 'secondary');
|
|
342
|
+
expect(dateBtn).toHaveAttribute('data-variant', 'secondary');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('toSortItems: maps sort entries to SortBuilder items', () => {
|
|
346
|
+
render(
|
|
347
|
+
<SortUI
|
|
348
|
+
schema={makeSchema({
|
|
349
|
+
multiple: true,
|
|
350
|
+
sort: [
|
|
351
|
+
{ field: 'name', direction: 'asc' },
|
|
352
|
+
{ field: 'date', direction: 'desc' },
|
|
353
|
+
],
|
|
354
|
+
})}
|
|
355
|
+
/>,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const builder = screen.getByTestId('sort-builder');
|
|
359
|
+
const value = JSON.parse(builder.getAttribute('data-value')!);
|
|
360
|
+
expect(value).toEqual([
|
|
361
|
+
{ id: 'name-asc', field: 'name', order: 'asc' },
|
|
362
|
+
{ id: 'date-desc', field: 'date', order: 'desc' },
|
|
363
|
+
]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('toSortEntries: returns empty array when sort is undefined', () => {
|
|
367
|
+
render(
|
|
368
|
+
<SortUI
|
|
369
|
+
schema={makeSchema({ variant: 'buttons' })}
|
|
370
|
+
/>,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
// No button should have secondary variant
|
|
374
|
+
const buttons = screen.getAllByRole('button');
|
|
375
|
+
buttons.forEach(btn => {
|
|
376
|
+
expect(btn).toHaveAttribute('data-variant', 'outline');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
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 { ObjectView, ViewSwitcher, FilterUI, SortUI } from '../index';
|
|
11
|
+
|
|
12
|
+
describe('Plugin View Registration', () => {
|
|
13
|
+
it('exports ObjectView component', () => {
|
|
14
|
+
expect(ObjectView).toBeDefined();
|
|
15
|
+
expect(typeof ObjectView).toBe('function');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('exports ViewSwitcher component', () => {
|
|
19
|
+
expect(ViewSwitcher).toBeDefined();
|
|
20
|
+
expect(typeof ViewSwitcher).toBe('function');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('exports FilterUI component', () => {
|
|
24
|
+
expect(FilterUI).toBeDefined();
|
|
25
|
+
expect(typeof FilterUI).toBe('function');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('exports SortUI component', () => {
|
|
29
|
+
expect(SortUI).toBeDefined();
|
|
30
|
+
expect(typeof SortUI).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -6,24 +6,166 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React from 'react';
|
|
9
|
+
import React, { useContext } from 'react';
|
|
10
10
|
import { ComponentRegistry } from '@object-ui/core';
|
|
11
11
|
import { ObjectView } from './ObjectView';
|
|
12
|
+
import { ViewSwitcher } from './ViewSwitcher';
|
|
13
|
+
import { FilterUI } from './FilterUI';
|
|
14
|
+
import { SortUI } from './SortUI';
|
|
12
15
|
|
|
13
|
-
export { ObjectView };
|
|
16
|
+
export { ObjectView, ViewSwitcher, FilterUI, SortUI };
|
|
14
17
|
export type { ObjectViewProps } from './ObjectView';
|
|
18
|
+
export type { ViewSwitcherProps } from './ViewSwitcher';
|
|
19
|
+
export type { FilterUIProps } from './FilterUI';
|
|
20
|
+
export type { SortUIProps } from './SortUI';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SchemaRendererContext is created by @object-ui/react.
|
|
24
|
+
* We import it dynamically to avoid a circular dependency.
|
|
25
|
+
* The context value provides { dataSource }.
|
|
26
|
+
* A fallback context is created so hooks are never called conditionally.
|
|
27
|
+
*/
|
|
28
|
+
const FallbackContext = React.createContext<any>(null);
|
|
29
|
+
let SchemaRendererContext: React.Context<any> = FallbackContext;
|
|
30
|
+
try {
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
32
|
+
const mod = require('@object-ui/react');
|
|
33
|
+
// The context is re-exported from @object-ui/react
|
|
34
|
+
if (mod.SchemaRendererContext) {
|
|
35
|
+
SchemaRendererContext = mod.SchemaRendererContext;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// @object-ui/react not available — registry-based dataSource only
|
|
39
|
+
}
|
|
15
40
|
|
|
16
41
|
// Register object-view component
|
|
17
42
|
const ObjectViewRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
18
|
-
|
|
43
|
+
// Resolve dataSource from SchemaRendererProvider context
|
|
44
|
+
const ctx = useContext(SchemaRendererContext);
|
|
45
|
+
const dataSource = ctx?.dataSource ?? null;
|
|
46
|
+
|
|
47
|
+
return <ObjectView schema={schema} dataSource={dataSource} />;
|
|
19
48
|
};
|
|
20
49
|
|
|
21
50
|
ComponentRegistry.register('object-view', ObjectViewRenderer, {
|
|
22
|
-
namespace: 'plugin-view'
|
|
51
|
+
namespace: 'plugin-view',
|
|
52
|
+
label: 'Object View',
|
|
53
|
+
category: 'view',
|
|
54
|
+
icon: 'LayoutDashboard',
|
|
55
|
+
inputs: [
|
|
56
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
57
|
+
{ name: 'title', type: 'string', label: 'Title' },
|
|
58
|
+
{ name: 'description', type: 'string', label: 'Description' },
|
|
59
|
+
{ name: 'layout', type: 'enum', label: 'Form Layout', enum: ['drawer', 'modal', 'page'] },
|
|
60
|
+
{ name: 'defaultViewType', type: 'enum', label: 'Default View Type', enum: ['grid', 'kanban', 'gallery', 'calendar', 'timeline', 'gantt', 'map'] },
|
|
61
|
+
{ name: 'defaultListView', type: 'string', label: 'Default Named View' },
|
|
62
|
+
{ name: 'showSearch', type: 'boolean', label: 'Show Search' },
|
|
63
|
+
{ name: 'showFilters', type: 'boolean', label: 'Show Filters' },
|
|
64
|
+
{ name: 'showCreate', type: 'boolean', label: 'Show Create Button' },
|
|
65
|
+
{ name: 'showRefresh', type: 'boolean', label: 'Show Refresh Button' },
|
|
66
|
+
{ name: 'showViewSwitcher', type: 'boolean', label: 'Show View Switcher' },
|
|
67
|
+
{ name: 'listViews', type: 'object', label: 'Named List Views' },
|
|
68
|
+
{ name: 'navigation', type: 'object', label: 'Navigation Config' },
|
|
69
|
+
{ name: 'searchableFields', type: 'array', label: 'Searchable Fields' },
|
|
70
|
+
{ name: 'filterableFields', type: 'array', label: 'Filterable Fields' },
|
|
71
|
+
],
|
|
72
|
+
defaultProps: {
|
|
73
|
+
layout: 'drawer',
|
|
74
|
+
defaultViewType: 'grid',
|
|
75
|
+
showSearch: true,
|
|
76
|
+
showFilters: true,
|
|
77
|
+
showCreate: true,
|
|
78
|
+
showRefresh: true,
|
|
79
|
+
showViewSwitcher: true,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Register alias 'view' → same renderer
|
|
84
|
+
ComponentRegistry.register('view', ObjectViewRenderer, {
|
|
85
|
+
namespace: 'plugin-view',
|
|
86
|
+
label: 'View',
|
|
87
|
+
category: 'view',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
ComponentRegistry.register('view-switcher', ViewSwitcher, {
|
|
91
|
+
namespace: 'view',
|
|
92
|
+
label: 'View Switcher',
|
|
93
|
+
category: 'view',
|
|
94
|
+
icon: 'LayoutGrid',
|
|
95
|
+
inputs: [
|
|
96
|
+
{ name: 'views', type: 'array', label: 'Views', required: true },
|
|
97
|
+
{ name: 'defaultView', type: 'string', label: 'Default View' },
|
|
98
|
+
{ name: 'activeView', type: 'string', label: 'Active View' },
|
|
99
|
+
{ name: 'variant', type: 'enum', label: 'Variant', enum: ['tabs', 'buttons', 'dropdown'] },
|
|
100
|
+
{ name: 'position', type: 'enum', label: 'Position', enum: ['top', 'bottom', 'left', 'right'] },
|
|
101
|
+
{ name: 'persistPreference', type: 'boolean', label: 'Persist Preference' },
|
|
102
|
+
{ name: 'storageKey', type: 'string', label: 'Storage Key' },
|
|
103
|
+
{ name: 'onViewChange', type: 'string', label: 'On View Change Event' },
|
|
104
|
+
],
|
|
105
|
+
defaultProps: {
|
|
106
|
+
variant: 'tabs',
|
|
107
|
+
position: 'top',
|
|
108
|
+
defaultView: 'grid',
|
|
109
|
+
views: [
|
|
110
|
+
{ type: 'grid', label: 'Grid', schema: { type: 'text', content: 'Grid view' } },
|
|
111
|
+
{ type: 'list', label: 'List', schema: { type: 'text', content: 'List view' } },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
ComponentRegistry.register('filter-ui', FilterUI, {
|
|
117
|
+
namespace: 'view',
|
|
118
|
+
label: 'Filter UI',
|
|
119
|
+
category: 'view',
|
|
120
|
+
icon: 'SlidersHorizontal',
|
|
121
|
+
inputs: [
|
|
122
|
+
{ name: 'filters', type: 'array', label: 'Filters', required: true },
|
|
123
|
+
{ name: 'values', type: 'object', label: 'Values' },
|
|
124
|
+
{ name: 'onChange', type: 'string', label: 'On Change Event' },
|
|
125
|
+
{ name: 'showClear', type: 'boolean', label: 'Show Clear Button' },
|
|
126
|
+
{ name: 'showApply', type: 'boolean', label: 'Show Apply Button' },
|
|
127
|
+
{ name: 'layout', type: 'enum', label: 'Layout', enum: ['inline', 'popover', 'drawer'] },
|
|
128
|
+
],
|
|
129
|
+
defaultProps: {
|
|
130
|
+
layout: 'inline',
|
|
131
|
+
showApply: false,
|
|
132
|
+
showClear: true,
|
|
133
|
+
filters: [
|
|
134
|
+
{ field: 'name', label: 'Name', type: 'text', placeholder: 'Search name' },
|
|
135
|
+
{ field: 'status', label: 'Status', type: 'select', options: [
|
|
136
|
+
{ label: 'Open', value: 'open' },
|
|
137
|
+
{ label: 'Closed', value: 'closed' },
|
|
138
|
+
] },
|
|
139
|
+
{ field: 'created_at', label: 'Created', type: 'date' },
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
ComponentRegistry.register('sort-ui', SortUI, {
|
|
145
|
+
namespace: 'view',
|
|
146
|
+
label: 'Sort UI',
|
|
147
|
+
category: 'view',
|
|
148
|
+
icon: 'ArrowUpDown',
|
|
149
|
+
inputs: [
|
|
150
|
+
{ name: 'fields', type: 'array', label: 'Fields', required: true },
|
|
151
|
+
{ name: 'sort', type: 'array', label: 'Sort' },
|
|
152
|
+
{ name: 'onChange', type: 'string', label: 'On Change Event' },
|
|
153
|
+
{ name: 'multiple', type: 'boolean', label: 'Allow Multiple' },
|
|
154
|
+
{ name: 'variant', type: 'enum', label: 'Variant', enum: ['dropdown', 'buttons'] },
|
|
155
|
+
],
|
|
156
|
+
defaultProps: {
|
|
157
|
+
variant: 'dropdown',
|
|
158
|
+
multiple: true,
|
|
159
|
+
fields: [
|
|
160
|
+
{ field: 'name', label: 'Name' },
|
|
161
|
+
{ field: 'created_at', label: 'Created At' },
|
|
162
|
+
],
|
|
163
|
+
sort: [{ field: 'name', direction: 'asc' }],
|
|
164
|
+
},
|
|
23
165
|
});
|
|
24
166
|
|
|
25
167
|
// Simple View Renderer (Container)
|
|
26
|
-
const SimpleViewRenderer: React.FC<any> = ({ schema, className, children, ...props }) => {
|
|
168
|
+
const SimpleViewRenderer: React.FC<any> = ({ schema, className, children, dataSource, ...props }) => {
|
|
27
169
|
// If columns prop is present, use grid layout
|
|
28
170
|
const style = schema.props?.columns
|
|
29
171
|
? { display: 'grid', gridTemplateColumns: `repeat(${schema.props.columns}, 1fr)`, gap: '1rem' }
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
test: {
|
|
8
|
+
environment: 'happy-dom',
|
|
9
|
+
globals: true,
|
|
10
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
11
|
+
},
|
|
12
|
+
});
|
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|