@object-ui/plugin-grid 0.3.1 → 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.
@@ -0,0 +1,183 @@
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
+ /**
10
+ * VirtualGrid Component
11
+ *
12
+ * Implements virtual scrolling using @tanstack/react-virtual for efficient
13
+ * rendering of large datasets. Only renders visible rows, dramatically improving
14
+ * performance with datasets of 1000+ items.
15
+ *
16
+ * Features:
17
+ * - Virtual scrolling for rows
18
+ * - Configurable row height
19
+ * - Overscan for smooth scrolling
20
+ * - Minimal DOM nodes (only visible items)
21
+ */
22
+
23
+ import React, { useRef } from 'react';
24
+ import { useVirtualizer } from '@tanstack/react-virtual';
25
+
26
+ export interface VirtualGridColumn {
27
+ header: string;
28
+ accessorKey: string;
29
+ cell?: (value: any, row: any) => React.ReactNode;
30
+ width?: number | string;
31
+ align?: 'left' | 'center' | 'right';
32
+ }
33
+
34
+ export interface VirtualGridProps {
35
+ data: any[];
36
+ columns: VirtualGridColumn[];
37
+ rowHeight?: number;
38
+ height?: number | string;
39
+ className?: string;
40
+ headerClassName?: string;
41
+ rowClassName?: string | ((row: any, index: number) => string);
42
+ onRowClick?: (row: any, index: number) => void;
43
+ overscan?: number;
44
+ }
45
+
46
+ /**
47
+ * Virtual scrolling grid component
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * <VirtualGrid
52
+ * data={items}
53
+ * columns={[
54
+ * { header: 'Name', accessorKey: 'name' },
55
+ * { header: 'Age', accessorKey: 'age' },
56
+ * ]}
57
+ * rowHeight={40}
58
+ * />
59
+ * ```
60
+ */
61
+ export const VirtualGrid: React.FC<VirtualGridProps> = ({
62
+ data,
63
+ columns,
64
+ rowHeight = 40,
65
+ height = 600,
66
+ className = '',
67
+ headerClassName = '',
68
+ rowClassName,
69
+ onRowClick,
70
+ overscan = 5,
71
+ }) => {
72
+ const parentRef = useRef<HTMLDivElement>(null);
73
+
74
+ const virtualizer = useVirtualizer({
75
+ count: data.length,
76
+ getScrollElement: () => parentRef.current,
77
+ estimateSize: () => rowHeight,
78
+ overscan,
79
+ });
80
+
81
+ const items = virtualizer.getVirtualItems();
82
+
83
+ return (
84
+ <div className={className}>
85
+ {/* Header */}
86
+ <div
87
+ className={`grid border-b sticky top-0 bg-background z-10 ${headerClassName}`}
88
+ style={{
89
+ gridTemplateColumns: columns
90
+ .map((col) => col.width || '1fr')
91
+ .join(' '),
92
+ }}
93
+ >
94
+ {columns.map((column, index) => (
95
+ <div
96
+ key={index}
97
+ className={`px-4 py-2 font-semibold text-sm ${
98
+ column.align === 'center'
99
+ ? 'text-center'
100
+ : column.align === 'right'
101
+ ? 'text-right'
102
+ : 'text-left'
103
+ }`}
104
+ >
105
+ {column.header}
106
+ </div>
107
+ ))}
108
+ </div>
109
+
110
+ {/* Virtual scrolling container */}
111
+ <div
112
+ ref={parentRef}
113
+ className="overflow-auto"
114
+ style={{
115
+ height: typeof height === 'number' ? `${height}px` : height,
116
+ contain: 'strict'
117
+ }}
118
+ >
119
+ <div
120
+ style={{
121
+ height: `${virtualizer.getTotalSize()}px`,
122
+ width: '100%',
123
+ position: 'relative',
124
+ }}
125
+ >
126
+ {items.map((virtualRow) => {
127
+ const row = data[virtualRow.index];
128
+ const rowClasses =
129
+ typeof rowClassName === 'function'
130
+ ? rowClassName(row, virtualRow.index)
131
+ : rowClassName || '';
132
+
133
+ return (
134
+ <div
135
+ key={virtualRow.key}
136
+ className={`grid border-b hover:bg-muted/50 cursor-pointer ${rowClasses}`}
137
+ style={{
138
+ position: 'absolute',
139
+ top: 0,
140
+ left: 0,
141
+ width: '100%',
142
+ height: `${virtualRow.size}px`,
143
+ transform: `translateY(${virtualRow.start}px)`,
144
+ gridTemplateColumns: columns
145
+ .map((col) => col.width || '1fr')
146
+ .join(' '),
147
+ }}
148
+ onClick={() => onRowClick?.(row, virtualRow.index)}
149
+ >
150
+ {columns.map((column, colIndex) => {
151
+ const value = row[column.accessorKey];
152
+ const cellContent = column.cell
153
+ ? column.cell(value, row)
154
+ : value;
155
+
156
+ return (
157
+ <div
158
+ key={colIndex}
159
+ className={`px-4 py-2 text-sm flex items-center ${
160
+ column.align === 'center'
161
+ ? 'text-center justify-center'
162
+ : column.align === 'right'
163
+ ? 'text-right justify-end'
164
+ : 'text-left justify-start'
165
+ }`}
166
+ >
167
+ {cellContent}
168
+ </div>
169
+ );
170
+ })}
171
+ </div>
172
+ );
173
+ })}
174
+ </div>
175
+ </div>
176
+
177
+ {/* Footer info */}
178
+ <div className="px-4 py-2 text-xs text-muted-foreground border-t">
179
+ Showing {items.length} of {data.length} rows (virtual scrolling enabled)
180
+ </div>
181
+ </div>
182
+ );
183
+ };
@@ -0,0 +1,438 @@
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, cleanup } from '@testing-library/react';
11
+ import '@testing-library/jest-dom';
12
+ import React from 'react';
13
+ import type { VirtualGridColumn, VirtualGridProps } from '../VirtualGrid';
14
+
15
+ // --- Mock @tanstack/react-virtual ---
16
+ // The vitest setup file pre-loads @object-ui/plugin-grid which caches the
17
+ // real virtualizer. We call vi.resetModules() in beforeEach so that the
18
+ // dynamic import of VirtualGrid picks up our mock instead.
19
+ vi.mock('@tanstack/react-virtual', () => ({
20
+ useVirtualizer: (opts: any) => {
21
+ const count: number = opts.count;
22
+ const size: number = opts.estimateSize();
23
+ const items = [];
24
+ for (let i = 0; i < count; i++) {
25
+ items.push({ index: i, key: String(i), start: i * size, size });
26
+ }
27
+ return {
28
+ getVirtualItems: () => items,
29
+ getTotalSize: () => count * size,
30
+ };
31
+ },
32
+ }));
33
+
34
+ // --- Test helpers ---
35
+ const sampleColumns: VirtualGridColumn[] = [
36
+ { header: 'Name', accessorKey: 'name' },
37
+ { header: 'Email', accessorKey: 'email' },
38
+ { header: 'Age', accessorKey: 'age' },
39
+ ];
40
+
41
+ const sampleData = [
42
+ { name: 'Alice', email: 'alice@test.com', age: 30 },
43
+ { name: 'Bob', email: 'bob@test.com', age: 25 },
44
+ { name: 'Charlie', email: 'charlie@test.com', age: 40 },
45
+ ];
46
+
47
+ type VirtualGridComponent = React.FC<VirtualGridProps>;
48
+
49
+ let VirtualGrid: VirtualGridComponent;
50
+
51
+ beforeEach(async () => {
52
+ cleanup();
53
+ vi.resetModules();
54
+ const mod = await import('../VirtualGrid');
55
+ VirtualGrid = mod.VirtualGrid;
56
+ });
57
+
58
+ function renderGrid(overrides: Partial<VirtualGridProps> = {}) {
59
+ const props: VirtualGridProps = {
60
+ data: sampleData,
61
+ columns: sampleColumns,
62
+ ...overrides,
63
+ };
64
+ return render(<VirtualGrid {...props} />);
65
+ }
66
+
67
+ // =========================================================================
68
+ // 1. Basic rendering
69
+ // =========================================================================
70
+ describe('VirtualGrid: basic rendering', () => {
71
+ it('renders column headers', () => {
72
+ renderGrid();
73
+
74
+ expect(screen.getByText('Name')).toBeInTheDocument();
75
+ expect(screen.getByText('Email')).toBeInTheDocument();
76
+ expect(screen.getByText('Age')).toBeInTheDocument();
77
+ });
78
+
79
+ it('renders row cell values', () => {
80
+ renderGrid();
81
+
82
+ expect(screen.getByText('Alice')).toBeInTheDocument();
83
+ expect(screen.getByText('bob@test.com')).toBeInTheDocument();
84
+ expect(screen.getByText('40')).toBeInTheDocument();
85
+ });
86
+
87
+ it('renders footer with row count', () => {
88
+ renderGrid();
89
+
90
+ expect(
91
+ screen.getByText(/Showing 3 of 3 rows/),
92
+ ).toBeInTheDocument();
93
+ });
94
+ });
95
+
96
+ // =========================================================================
97
+ // 2. Empty data
98
+ // =========================================================================
99
+ describe('VirtualGrid: empty data', () => {
100
+ it('renders headers with no rows when data is empty', () => {
101
+ renderGrid({ data: [] });
102
+
103
+ expect(screen.getByText('Name')).toBeInTheDocument();
104
+ expect(screen.getByText('Email')).toBeInTheDocument();
105
+ expect(screen.getByText(/Showing 0 of 0 rows/)).toBeInTheDocument();
106
+ });
107
+
108
+ it('does not render any data cells when data is empty', () => {
109
+ renderGrid({ data: [] });
110
+
111
+ expect(screen.queryByText('Alice')).not.toBeInTheDocument();
112
+ });
113
+ });
114
+
115
+ // =========================================================================
116
+ // 3. Custom className / headerClassName
117
+ // =========================================================================
118
+ describe('VirtualGrid: className support', () => {
119
+ it('applies custom className to the root element', () => {
120
+ const { container } = renderGrid({ className: 'my-custom-grid' });
121
+ const root = container.firstElementChild as HTMLElement;
122
+ expect(root).toHaveClass('my-custom-grid');
123
+ });
124
+
125
+ it('applies headerClassName to the header row', () => {
126
+ const { container } = renderGrid({ headerClassName: 'header-custom' });
127
+ const headerRow = container.querySelector('.header-custom');
128
+ expect(headerRow).toBeInTheDocument();
129
+ });
130
+
131
+ it('uses empty className by default', () => {
132
+ const { container } = renderGrid();
133
+ const root = container.firstElementChild as HTMLElement;
134
+ expect(root.className).toBe('');
135
+ });
136
+ });
137
+
138
+ // =========================================================================
139
+ // 4. Column alignment
140
+ // =========================================================================
141
+ describe('VirtualGrid: column alignment', () => {
142
+ it('defaults to left alignment', () => {
143
+ renderGrid({
144
+ columns: [{ header: 'Name', accessorKey: 'name' }],
145
+ data: [{ name: 'Alice' }],
146
+ });
147
+
148
+ expect(screen.getByText('Name')).toHaveClass('text-left');
149
+ });
150
+
151
+ it('applies center alignment to header and cells', () => {
152
+ renderGrid({
153
+ columns: [{ header: 'Count', accessorKey: 'count', align: 'center' }],
154
+ data: [{ count: 42 }],
155
+ });
156
+
157
+ expect(screen.getByText('Count')).toHaveClass('text-center');
158
+ expect(screen.getByText('42')).toHaveClass('text-center');
159
+ expect(screen.getByText('42')).toHaveClass('justify-center');
160
+ });
161
+
162
+ it('applies right alignment to header and cells', () => {
163
+ renderGrid({
164
+ columns: [{ header: 'Price', accessorKey: 'price', align: 'right' }],
165
+ data: [{ price: 99 }],
166
+ });
167
+
168
+ expect(screen.getByText('Price')).toHaveClass('text-right');
169
+ expect(screen.getByText('99')).toHaveClass('text-right');
170
+ expect(screen.getByText('99')).toHaveClass('justify-end');
171
+ });
172
+ });
173
+
174
+ // =========================================================================
175
+ // 5. Custom cell renderer
176
+ // =========================================================================
177
+ describe('VirtualGrid: custom cell renderer', () => {
178
+ it('uses custom cell function when provided', () => {
179
+ renderGrid({
180
+ columns: [
181
+ {
182
+ header: 'Name',
183
+ accessorKey: 'name',
184
+ cell: (value: string) => <strong data-testid="bold-name">{value.toUpperCase()}</strong>,
185
+ },
186
+ ],
187
+ data: [{ name: 'Alice' }],
188
+ });
189
+
190
+ const cell = screen.getByTestId('bold-name');
191
+ expect(cell).toBeInTheDocument();
192
+ expect(cell.tagName).toBe('STRONG');
193
+ expect(cell).toHaveTextContent('ALICE');
194
+ });
195
+
196
+ it('passes both value and row to custom cell function', () => {
197
+ const cellFn = vi.fn((_value, row) => (
198
+ <span data-testid="composite">{row.name} ({row.age})</span>
199
+ ));
200
+
201
+ renderGrid({
202
+ columns: [{ header: 'Info', accessorKey: 'name', cell: cellFn }],
203
+ data: [{ name: 'Alice', age: 30 }],
204
+ });
205
+
206
+ expect(cellFn).toHaveBeenCalledWith('Alice', { name: 'Alice', age: 30 });
207
+ expect(screen.getByTestId('composite')).toHaveTextContent('Alice (30)');
208
+ });
209
+
210
+ it('renders raw value when no cell function is provided', () => {
211
+ renderGrid({
212
+ columns: [{ header: 'Name', accessorKey: 'name' }],
213
+ data: [{ name: 'Bob' }],
214
+ });
215
+
216
+ expect(screen.getByText('Bob')).toBeInTheDocument();
217
+ });
218
+ });
219
+
220
+ // =========================================================================
221
+ // 6. Column widths
222
+ // =========================================================================
223
+ describe('VirtualGrid: column widths', () => {
224
+ it('uses 1fr default when no width specified', () => {
225
+ const { container } = renderGrid({
226
+ columns: [
227
+ { header: 'A', accessorKey: 'a' },
228
+ { header: 'B', accessorKey: 'b' },
229
+ ],
230
+ data: [{ a: '1', b: '2' }],
231
+ });
232
+
233
+ const headerRow = container.querySelector('.grid.border-b.sticky') as HTMLElement;
234
+ expect(headerRow.style.gridTemplateColumns).toBe('1fr 1fr');
235
+ });
236
+
237
+ it('applies custom column widths', () => {
238
+ const { container } = renderGrid({
239
+ columns: [
240
+ { header: 'A', accessorKey: 'a', width: 200 },
241
+ { header: 'B', accessorKey: 'b', width: '2fr' },
242
+ ],
243
+ data: [{ a: '1', b: '2' }],
244
+ });
245
+
246
+ const headerRow = container.querySelector('.grid.border-b.sticky') as HTMLElement;
247
+ expect(headerRow.style.gridTemplateColumns).toBe('200 2fr');
248
+ });
249
+ });
250
+
251
+ // =========================================================================
252
+ // 7. Row click handler
253
+ // =========================================================================
254
+ describe('VirtualGrid: onRowClick', () => {
255
+ it('calls onRowClick with row data and index when row is clicked', () => {
256
+ const onRowClick = vi.fn();
257
+ renderGrid({ onRowClick });
258
+
259
+ const aliceCell = screen.getByText('Alice');
260
+ const row = aliceCell.closest('[style*="position: absolute"]') as HTMLElement;
261
+ fireEvent.click(row);
262
+
263
+ expect(onRowClick).toHaveBeenCalledTimes(1);
264
+ expect(onRowClick).toHaveBeenCalledWith(
265
+ { name: 'Alice', email: 'alice@test.com', age: 30 },
266
+ 0,
267
+ );
268
+ });
269
+
270
+ it('passes correct index for different rows', () => {
271
+ const onRowClick = vi.fn();
272
+ renderGrid({ onRowClick });
273
+
274
+ const charlieCell = screen.getByText('Charlie');
275
+ const row = charlieCell.closest('[style*="position: absolute"]') as HTMLElement;
276
+ fireEvent.click(row);
277
+
278
+ expect(onRowClick).toHaveBeenCalledWith(
279
+ { name: 'Charlie', email: 'charlie@test.com', age: 40 },
280
+ 2,
281
+ );
282
+ });
283
+
284
+ it('does not error when onRowClick is not provided', () => {
285
+ renderGrid();
286
+ const aliceCell = screen.getByText('Alice');
287
+ const row = aliceCell.closest('[style*="position: absolute"]') as HTMLElement;
288
+
289
+ expect(() => fireEvent.click(row)).not.toThrow();
290
+ });
291
+ });
292
+
293
+ // =========================================================================
294
+ // 8. Row className (static and dynamic)
295
+ // =========================================================================
296
+ describe('VirtualGrid: rowClassName', () => {
297
+ it('applies static rowClassName to all rows', () => {
298
+ renderGrid({ rowClassName: 'row-highlight' });
299
+
300
+ const aliceCell = screen.getByText('Alice');
301
+ const row = aliceCell.closest('[style*="position: absolute"]') as HTMLElement;
302
+ expect(row).toHaveClass('row-highlight');
303
+ });
304
+
305
+ it('applies dynamic rowClassName function', () => {
306
+ renderGrid({
307
+ rowClassName: (_row, index) => (index % 2 === 0 ? 'even-row' : 'odd-row'),
308
+ });
309
+
310
+ const aliceRow = screen.getByText('Alice').closest('[style*="position: absolute"]') as HTMLElement;
311
+ expect(aliceRow).toHaveClass('even-row');
312
+
313
+ const bobRow = screen.getByText('Bob').closest('[style*="position: absolute"]') as HTMLElement;
314
+ expect(bobRow).toHaveClass('odd-row');
315
+ });
316
+
317
+ it('defaults to empty string when rowClassName is not provided', () => {
318
+ renderGrid();
319
+ const row = screen.getByText('Alice').closest('[style*="position: absolute"]') as HTMLElement;
320
+ expect(row.className).toContain('grid');
321
+ expect(row.className).toContain('border-b');
322
+ });
323
+ });
324
+
325
+ // =========================================================================
326
+ // 9. Virtual scrolling props
327
+ // =========================================================================
328
+ describe('VirtualGrid: virtual scrolling configuration', () => {
329
+ it('uses default height of 600px', () => {
330
+ const { container } = renderGrid();
331
+ const scrollContainer = container.querySelector('.overflow-auto') as HTMLElement;
332
+ expect(scrollContainer.style.height).toBe('600px');
333
+ });
334
+
335
+ it('accepts numeric height', () => {
336
+ const { container } = renderGrid({ height: 400 });
337
+ const scrollContainer = container.querySelector('.overflow-auto') as HTMLElement;
338
+ expect(scrollContainer.style.height).toBe('400px');
339
+ });
340
+
341
+ it('accepts string height', () => {
342
+ const { container } = renderGrid({ height: '80vh' });
343
+ const scrollContainer = container.querySelector('.overflow-auto') as HTMLElement;
344
+ expect(scrollContainer.style.height).toBe('80vh');
345
+ });
346
+
347
+ it('renders a relative-positioned inner container for virtual positioning', () => {
348
+ const { container } = renderGrid();
349
+ const innerContainer = container.querySelector(
350
+ '.overflow-auto > div',
351
+ ) as HTMLElement;
352
+ expect(innerContainer.style.position).toBe('relative');
353
+ expect(innerContainer.style.width).toBe('100%');
354
+ });
355
+
356
+ it('positions rows absolutely with translateY', () => {
357
+ renderGrid({ rowHeight: 50 });
358
+
359
+ const aliceRow = screen.getByText('Alice').closest(
360
+ '[style*="position: absolute"]',
361
+ ) as HTMLElement;
362
+ expect(aliceRow.style.position).toBe('absolute');
363
+ expect(aliceRow.style.transform).toBe('translateY(0px)');
364
+
365
+ const bobRow = screen.getByText('Bob').closest(
366
+ '[style*="position: absolute"]',
367
+ ) as HTMLElement;
368
+ expect(bobRow.style.transform).toBe('translateY(50px)');
369
+ });
370
+
371
+ it('sets total height on inner container based on data length and row height', () => {
372
+ const { container } = renderGrid({ rowHeight: 50 });
373
+ const innerContainer = container.querySelector(
374
+ '.overflow-auto > div',
375
+ ) as HTMLElement;
376
+ // 3 rows × 50px = 150px
377
+ expect(innerContainer.style.height).toBe('150px');
378
+ });
379
+ });
380
+
381
+ // =========================================================================
382
+ // 10. Different data types and edge cases
383
+ // =========================================================================
384
+ describe('VirtualGrid: different column types', () => {
385
+ it('renders numeric values correctly', () => {
386
+ renderGrid({
387
+ columns: [{ header: 'Count', accessorKey: 'count' }],
388
+ data: [{ count: 0 }, { count: 100 }, { count: -5 }],
389
+ });
390
+
391
+ expect(screen.getByText('0')).toBeInTheDocument();
392
+ expect(screen.getByText('100')).toBeInTheDocument();
393
+ expect(screen.getByText('-5')).toBeInTheDocument();
394
+ });
395
+
396
+ it('handles null / undefined values gracefully', () => {
397
+ renderGrid({
398
+ columns: [
399
+ { header: 'Name', accessorKey: 'name' },
400
+ { header: 'Email', accessorKey: 'email' },
401
+ ],
402
+ data: [
403
+ { name: 'Alice', email: null },
404
+ { name: undefined, email: 'bob@test.com' },
405
+ ],
406
+ });
407
+
408
+ expect(screen.getByText('bob@test.com')).toBeInTheDocument();
409
+ expect(screen.getByText(/Showing 2 of 2 rows/)).toBeInTheDocument();
410
+ });
411
+
412
+ it('renders many columns without error', () => {
413
+ const cols: VirtualGridColumn[] = Array.from({ length: 10 }, (_, i) => ({
414
+ header: `Col ${i}`,
415
+ accessorKey: `field${i}`,
416
+ }));
417
+ const data = [Object.fromEntries(cols.map((c) => [c.accessorKey, `val-${c.accessorKey}`]))];
418
+
419
+ renderGrid({ columns: cols, data });
420
+
421
+ expect(screen.getByText('Col 0')).toBeInTheDocument();
422
+ expect(screen.getByText('Col 9')).toBeInTheDocument();
423
+ expect(screen.getByText('val-field5')).toBeInTheDocument();
424
+ });
425
+
426
+ it('applies gridTemplateColumns to each data row matching header', () => {
427
+ const { container } = renderGrid({
428
+ columns: [
429
+ { header: 'A', accessorKey: 'a', width: '100px' },
430
+ { header: 'B', accessorKey: 'b', width: '200px' },
431
+ ],
432
+ data: [{ a: '1', b: '2' }],
433
+ });
434
+
435
+ const dataRow = container.querySelector('[style*="position: absolute"]') as HTMLElement;
436
+ expect(dataRow.style.gridTemplateColumns).toBe('100px 200px');
437
+ });
438
+ });
@@ -0,0 +1,29 @@
1
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { SchemaRendererProvider } from '@object-ui/react';
5
+ import * as ObjectGridModule from './ObjectGrid';
6
+ import { ObjectGridRenderer } from './index';
7
+
8
+ describe('Plugin Grid Registration', () => {
9
+ it('renderer passes dataSource from context', async () => {
10
+ // Spy and mock implementation
11
+ vi.spyOn(ObjectGridModule, 'ObjectGrid').mockImplementation(
12
+ (({ dataSource }: any) => (
13
+ <div data-testid="grid-mock">
14
+ {dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
15
+ </div>
16
+ )) as any
17
+ );
18
+
19
+ render(
20
+ <SchemaRendererProvider dataSource={{ type: 'mock-datasource' } as any}>
21
+ <ObjectGridRenderer schema={{ type: 'object-grid' }} />
22
+ </SchemaRendererProvider>
23
+ );
24
+
25
+ // Use findByTestId for async safety
26
+ const element = await screen.findByTestId('grid-mock', {}, { timeout: 5000 });
27
+ expect(element).toHaveTextContent('DataSource: mock-datasource');
28
+ });
29
+ });
package/src/index.tsx CHANGED
@@ -8,15 +8,43 @@
8
8
 
9
9
  import React from 'react';
10
10
  import { ComponentRegistry } from '@object-ui/core';
11
+ import { useSchemaContext } from '@object-ui/react';
11
12
  import { ObjectGrid } from './ObjectGrid';
13
+ import { VirtualGrid } from './VirtualGrid';
12
14
 
13
- export { ObjectGrid };
15
+ export { ObjectGrid, VirtualGrid };
14
16
  export type { ObjectGridProps } from './ObjectGrid';
17
+ export type { VirtualGridProps, VirtualGridColumn } from './VirtualGrid';
15
18
 
16
19
  // Register object-grid component
17
- const ObjectGridRenderer: React.FC<{ schema: any }> = ({ schema }) => {
18
- return <ObjectGrid schema={schema} dataSource={null as any} />;
20
+ export const ObjectGridRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, ...props }) => {
21
+ const { dataSource } = useSchemaContext() || {};
22
+ return <ObjectGrid schema={schema} dataSource={dataSource} {...props} />;
19
23
  };
20
24
 
21
- ComponentRegistry.register('object-grid', ObjectGridRenderer);
22
- ComponentRegistry.register('grid', ObjectGridRenderer); // Alias
25
+ ComponentRegistry.register('object-grid', ObjectGridRenderer, {
26
+ namespace: 'plugin-grid',
27
+ label: 'Object Grid',
28
+ category: 'plugin',
29
+ inputs: [
30
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
31
+ { name: 'columns', type: 'array', label: 'Columns' },
32
+ { name: 'filters', type: 'array', label: 'Filters' },
33
+ ]
34
+ });
35
+
36
+ // Alias for view namespace - this allows using { type: 'view:grid' } in schemas
37
+ // which is semantically meaningful for data display components
38
+ ComponentRegistry.register('grid', ObjectGridRenderer, {
39
+ namespace: 'view',
40
+ label: 'Data Grid',
41
+ category: 'view',
42
+ inputs: [
43
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
44
+ { name: 'columns', type: 'array', label: 'Columns' },
45
+ { name: 'filters', type: 'array', label: 'Filters' },
46
+ ]
47
+ });
48
+
49
+ // Note: 'grid' type is handled by @object-ui/components Grid layout component
50
+ // This plugin only handles 'object-grid' which integrates with ObjectQL/ObjectStack