@object-ui/plugin-grid 0.3.0 → 0.5.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 +21 -0
- package/CHANGELOG.md +13 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1018 -295
- package/dist/index.umd.cjs +5 -2
- package/dist/packages/plugin-grid/src/ObjectGrid.msw.test.d.ts +0 -0
- package/dist/packages/plugin-grid/src/VirtualGrid.d.ts +35 -0
- package/dist/packages/plugin-grid/src/VirtualGrid.test.d.ts +8 -0
- package/dist/packages/plugin-grid/src/index.d.ts +10 -0
- package/dist/packages/plugin-grid/src/index.test.d.ts +1 -0
- package/package.json +11 -8
- package/src/ObjectGrid.msw.test.tsx +107 -0
- package/src/ObjectGrid.tsx +81 -21
- package/src/VirtualGrid.test.tsx +23 -0
- package/src/VirtualGrid.tsx +183 -0
- package/src/index.test.tsx +29 -0
- package/src/index.tsx +23 -5
- package/vite.config.ts +18 -0
- package/vitest.config.ts +13 -0
- package/vitest.setup.ts +1 -0
- package/dist/plugin-grid/src/index.d.ts +0 -3
- /package/dist/{plugin-grid → packages/plugin-grid}/src/ObjectGrid.d.ts +0 -0
|
@@ -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,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,33 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
ComponentRegistry.register('object-grid', ObjectGridRenderer, {
|
|
26
|
+
namespace: 'plugin-grid',
|
|
27
|
+
label: 'Object Grid',
|
|
28
|
+
category: 'plugin'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Alias for view namespace - this allows using { type: 'view:grid' } in schemas
|
|
32
|
+
// which is semantically meaningful for data display components
|
|
33
|
+
ComponentRegistry.register('grid', ObjectGridRenderer, {
|
|
34
|
+
namespace: 'view',
|
|
35
|
+
label: 'Data Grid',
|
|
36
|
+
category: 'view'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Note: 'grid' type is handled by @object-ui/components Grid layout component
|
|
40
|
+
// This plugin only handles 'object-grid' which integrates with ObjectQL/ObjectStack
|
package/vite.config.ts
CHANGED
|
@@ -11,6 +11,18 @@ export default defineConfig({
|
|
|
11
11
|
include: ['src'],
|
|
12
12
|
}),
|
|
13
13
|
],
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
'@object-ui/core': resolve(__dirname, '../core/src'),
|
|
17
|
+
'@object-ui/types': resolve(__dirname, '../types/src'),
|
|
18
|
+
'@object-ui/data-objectstack': resolve(__dirname, '../data-objectstack/src'),
|
|
19
|
+
'@object-ui/react': resolve(__dirname, '../react/src'),
|
|
20
|
+
'@object-ui/components': resolve(__dirname, '../components/src'),
|
|
21
|
+
'@object-ui/fields': resolve(__dirname, '../fields/src'),
|
|
22
|
+
'@object-ui/plugin-dashboard': resolve(__dirname, '../plugin-dashboard/src'),
|
|
23
|
+
'@object-ui/plugin-grid': resolve(__dirname, '../plugin-grid/src'),
|
|
24
|
+
}
|
|
25
|
+
},
|
|
14
26
|
build: {
|
|
15
27
|
lib: {
|
|
16
28
|
entry: resolve(__dirname, 'src/index.tsx'),
|
|
@@ -36,4 +48,10 @@ export default defineConfig({
|
|
|
36
48
|
},
|
|
37
49
|
},
|
|
38
50
|
},
|
|
51
|
+
test: {
|
|
52
|
+
globals: true,
|
|
53
|
+
environment: 'happy-dom',
|
|
54
|
+
setupFiles: ['../../vitest.setup.tsx'],
|
|
55
|
+
passWithNoTests: true,
|
|
56
|
+
},
|
|
39
57
|
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
test: {
|
|
9
|
+
environment: 'happy-dom',
|
|
10
|
+
globals: true,
|
|
11
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
12
|
+
},
|
|
13
|
+
});
|
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
File without changes
|