@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.
@@ -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
- 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
+ });
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
  });
@@ -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
+ });
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -1,3 +0,0 @@
1
- import { ObjectGrid } from './ObjectGrid';
2
- export { ObjectGrid };
3
- export type { ObjectGridProps } from './ObjectGrid';