@object-ui/plugin-aggrid 0.4.1 → 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,603 @@
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 React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
10
+ import { AgGridReact } from 'ag-grid-react';
11
+ import type {
12
+ ColDef,
13
+ GridReadyEvent,
14
+ CellClickedEvent,
15
+ RowClickedEvent,
16
+ SelectionChangedEvent,
17
+ CellValueChangedEvent,
18
+ StatusPanelDef,
19
+ GetContextMenuItemsParams,
20
+ MenuItemDef,
21
+ IServerSideDatasource,
22
+ IServerSideGetRowsParams
23
+ } from 'ag-grid-community';
24
+ import type { DataSource, FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
25
+ import type { ObjectAgGridImplProps } from './object-aggrid.types';
26
+ import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types';
27
+
28
+ /**
29
+ * ObjectAgGridImpl - Metadata-driven AG Grid implementation
30
+ * Fetches object metadata and data from ObjectStack and renders the grid
31
+ */
32
+ export default function ObjectAgGridImpl({
33
+ objectName,
34
+ dataSource,
35
+ fields: providedFields,
36
+ fieldNames,
37
+ filters,
38
+ sort,
39
+ pageSize = 10,
40
+ pagination = true,
41
+ domLayout = 'normal',
42
+ animateRows = true,
43
+ rowSelection,
44
+ theme = 'quartz',
45
+ height = 500,
46
+ className = '',
47
+ editable = false,
48
+ editType,
49
+ singleClickEdit = false,
50
+ stopEditingWhenCellsLoseFocus = true,
51
+ exportConfig,
52
+ statusBar,
53
+ callbacks,
54
+ columnConfig,
55
+ enableRangeSelection = false,
56
+ enableCharts = false,
57
+ contextMenu,
58
+ }: ObjectAgGridImplProps) {
59
+ const gridRef = useRef<any>(null);
60
+ const [loading, setLoading] = useState(true);
61
+ const [error, setError] = useState<Error | null>(null);
62
+ const [objectSchema, setObjectSchema] = useState<ObjectSchemaMetadata | null>(null);
63
+ const [rowData, setRowData] = useState<any[]>([]);
64
+ const [totalCount, setTotalCount] = useState(0);
65
+
66
+ // Fetch object metadata
67
+ useEffect(() => {
68
+ if (!dataSource) {
69
+ setError(new Error('DataSource is required'));
70
+ setLoading(false);
71
+ return;
72
+ }
73
+
74
+ const fetchMetadata = async () => {
75
+ try {
76
+ setLoading(true);
77
+ setError(null);
78
+
79
+ // Fetch object schema/metadata
80
+ const schema = await (dataSource as any).getObjectSchema(objectName);
81
+ setObjectSchema(schema);
82
+ } catch (err) {
83
+ const error = err instanceof Error ? err : new Error(String(err));
84
+ setError(error);
85
+ callbacks?.onDataError?.(error);
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ };
90
+
91
+ fetchMetadata();
92
+ }, [objectName, dataSource, callbacks]);
93
+
94
+ // Fetch data
95
+ useEffect(() => {
96
+ if (!dataSource || !objectSchema) return;
97
+
98
+ const fetchData = async () => {
99
+ try {
100
+ setLoading(true);
101
+ setError(null);
102
+
103
+ const queryParams: any = {
104
+ $top: pageSize,
105
+ $skip: 0,
106
+ };
107
+
108
+ if (filters) {
109
+ queryParams.$filter = filters;
110
+ }
111
+
112
+ if (sort) {
113
+ queryParams.$orderby = sort;
114
+ }
115
+
116
+ const result = await dataSource.find(objectName, queryParams);
117
+ setRowData(result.data || []);
118
+ setTotalCount(result.total || 0);
119
+ callbacks?.onDataLoaded?.(result.data || []);
120
+ } catch (err) {
121
+ const error = err instanceof Error ? err : new Error(String(err));
122
+ setError(error);
123
+ callbacks?.onDataError?.(error);
124
+ } finally {
125
+ setLoading(false);
126
+ }
127
+ };
128
+
129
+ fetchData();
130
+ }, [objectName, dataSource, objectSchema, filters, sort, pageSize, callbacks]);
131
+
132
+ // Generate column definitions from metadata
133
+ const columnDefs = useMemo((): ColDef[] => {
134
+ if (!objectSchema?.fields) return [];
135
+
136
+ // Use provided fields or get from schema
137
+ const fieldMetadata = providedFields || Object.values(objectSchema.fields);
138
+
139
+ // Filter fields if fieldNames is provided
140
+ const fieldsToShow = fieldNames
141
+ ? fieldMetadata.filter(field => fieldNames.includes(field.name))
142
+ : fieldMetadata;
143
+
144
+ return fieldsToShow.map(field => {
145
+ const colDef: ColDef = {
146
+ field: field.name,
147
+ headerName: field.label || field.name,
148
+ sortable: field.sortable !== false,
149
+ filter: getFilterType(field),
150
+ editable: editable && !field.readonly,
151
+ // visible_on will be evaluated by the core renderer
152
+ // For now, we just show all fields. Conditional visibility
153
+ // should be handled at a higher level or via dynamic column updates
154
+ };
155
+
156
+ // Apply column config defaults
157
+ if (columnConfig) {
158
+ if (columnConfig.resizable !== undefined) {
159
+ colDef.resizable = columnConfig.resizable;
160
+ }
161
+ if (columnConfig.sortable !== undefined && colDef.sortable === undefined) {
162
+ colDef.sortable = columnConfig.sortable;
163
+ }
164
+ }
165
+
166
+ // Add custom renderers and formatters based on field type
167
+ applyFieldTypeFormatting(colDef, field);
168
+
169
+ return colDef;
170
+ });
171
+ }, [objectSchema, providedFields, fieldNames, editable, columnConfig]);
172
+
173
+ // Build status bar panels
174
+ const statusPanels = useMemo((): StatusPanelDef[] | undefined => {
175
+ if (!statusBar?.enabled) return undefined;
176
+
177
+ const aggregations = statusBar.aggregations || ['count', 'sum', 'avg'];
178
+ const panels: StatusPanelDef[] = [];
179
+
180
+ if (aggregations.includes('count')) {
181
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['count'] } });
182
+ }
183
+ if (aggregations.includes('sum')) {
184
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['sum'] } });
185
+ }
186
+ if (aggregations.includes('avg')) {
187
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['avg'] } });
188
+ }
189
+ if (aggregations.includes('min')) {
190
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['min'] } });
191
+ }
192
+ if (aggregations.includes('max')) {
193
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['max'] } });
194
+ }
195
+
196
+ return panels;
197
+ }, [statusBar]);
198
+
199
+ // CSV Export handler
200
+ const handleExportCSV = useCallback(() => {
201
+ if (!gridRef.current?.api) return;
202
+
203
+ const params = {
204
+ fileName: exportConfig?.fileName || `${objectName}-export.csv`,
205
+ skipColumnHeaders: exportConfig?.skipColumnHeaders || false,
206
+ allColumns: exportConfig?.allColumns || false,
207
+ onlySelected: exportConfig?.onlySelected || false,
208
+ };
209
+
210
+ gridRef.current.api.exportDataAsCsv(params);
211
+
212
+ if (callbacks?.onExport) {
213
+ const data = exportConfig?.onlySelected
214
+ ? gridRef.current.api.getSelectedRows()
215
+ : rowData;
216
+ callbacks.onExport(data || [], 'csv');
217
+ }
218
+ }, [exportConfig, callbacks, rowData, objectName]);
219
+
220
+ // Context Menu handler
221
+ const getContextMenuItems = useCallback((params: GetContextMenuItemsParams): (string | MenuItemDef)[] => {
222
+ if (!contextMenu?.enabled) return [];
223
+
224
+ const items: (string | MenuItemDef)[] = [];
225
+ const defaultItems = contextMenu.items || ['copy', 'copyWithHeaders', 'separator', 'export'];
226
+
227
+ defaultItems.forEach(item => {
228
+ if (item === 'export') {
229
+ items.push({
230
+ name: 'Export CSV',
231
+ icon: '<span>📥</span>',
232
+ action: () => handleExportCSV(),
233
+ });
234
+ } else if (item === 'autoSizeAll') {
235
+ items.push({
236
+ name: 'Auto-size All Columns',
237
+ action: () => {
238
+ if (gridRef.current?.api) {
239
+ gridRef.current.api.autoSizeAllColumns();
240
+ }
241
+ },
242
+ });
243
+ } else if (item === 'resetColumns') {
244
+ items.push({
245
+ name: 'Reset Columns',
246
+ action: () => {
247
+ if (gridRef.current?.api) {
248
+ gridRef.current.api.resetColumnState();
249
+ }
250
+ },
251
+ });
252
+ } else {
253
+ items.push(item);
254
+ }
255
+ });
256
+
257
+ // Add custom items
258
+ if (contextMenu.customItems) {
259
+ if (items.length > 0) {
260
+ items.push('separator');
261
+ }
262
+ contextMenu.customItems.forEach(customItem => {
263
+ items.push({
264
+ name: customItem.name,
265
+ disabled: customItem.disabled,
266
+ action: () => {
267
+ if (callbacks?.onContextMenuAction) {
268
+ callbacks.onContextMenuAction(customItem.action, params.node?.data);
269
+ }
270
+ },
271
+ });
272
+ });
273
+ }
274
+
275
+ return items;
276
+ }, [contextMenu, handleExportCSV, callbacks]);
277
+
278
+ // Event handlers
279
+ const handleCellClicked = useCallback((event: CellClickedEvent) => {
280
+ callbacks?.onCellClicked?.(event);
281
+ }, [callbacks]);
282
+
283
+ const handleRowClicked = useCallback((event: RowClickedEvent) => {
284
+ callbacks?.onRowClicked?.(event);
285
+ }, [callbacks]);
286
+
287
+ const handleSelectionChanged = useCallback((event: SelectionChangedEvent) => {
288
+ callbacks?.onSelectionChanged?.(event);
289
+ }, [callbacks]);
290
+
291
+ const handleCellValueChanged = useCallback(async (event: CellValueChangedEvent) => {
292
+ callbacks?.onCellValueChanged?.(event);
293
+
294
+ // Save changes to backend if dataSource supports update
295
+ // Note: Assumes records have an 'id' field as primary key
296
+ // TODO: Make the ID field name configurable via schema
297
+ if (dataSource && event.data && event.data.id) {
298
+ try {
299
+ await dataSource.update(objectName, event.data.id, {
300
+ [event.colDef.field!]: event.newValue
301
+ });
302
+ } catch (err) {
303
+ console.error('Failed to update record:', err);
304
+ // Revert the change
305
+ event.node.setDataValue(event.colDef.field!, event.oldValue);
306
+ }
307
+ }
308
+ }, [callbacks, dataSource, objectName]);
309
+
310
+ const onGridReady = useCallback((params: GridReadyEvent) => {
311
+ gridRef.current = params;
312
+ }, []);
313
+
314
+ // Merge grid options with props
315
+ const gridOptions = useMemo(() => ({
316
+ pagination,
317
+ paginationPageSize: pageSize,
318
+ domLayout,
319
+ animateRows,
320
+ rowSelection,
321
+ editType,
322
+ singleClickEdit,
323
+ stopEditingWhenCellsLoseFocus,
324
+ statusBar: statusPanels ? { statusPanels } : undefined,
325
+ enableRangeSelection,
326
+ enableCharts,
327
+ getContextMenuItems: contextMenu?.enabled ? getContextMenuItems : undefined,
328
+ suppressCellFocus: !editable,
329
+ enableCellTextSelection: true,
330
+ ensureDomOrder: true,
331
+ onCellClicked: handleCellClicked,
332
+ onRowClicked: handleRowClicked,
333
+ onSelectionChanged: handleSelectionChanged,
334
+ onCellValueChanged: handleCellValueChanged,
335
+ onGridReady,
336
+ }), [
337
+ pagination,
338
+ pageSize,
339
+ domLayout,
340
+ animateRows,
341
+ rowSelection,
342
+ editType,
343
+ singleClickEdit,
344
+ stopEditingWhenCellsLoseFocus,
345
+ statusPanels,
346
+ enableRangeSelection,
347
+ enableCharts,
348
+ contextMenu,
349
+ getContextMenuItems,
350
+ editable,
351
+ handleCellClicked,
352
+ handleRowClicked,
353
+ handleSelectionChanged,
354
+ handleCellValueChanged,
355
+ onGridReady,
356
+ ]);
357
+
358
+ // Compute container style
359
+ const containerStyle = useMemo(() => ({
360
+ height: typeof height === 'number' ? `${height}px` : height,
361
+ width: '100%',
362
+ }), [height]);
363
+
364
+ // Determine theme class and build complete class list
365
+ const themeClass = `ag-theme-${theme}`;
366
+ const classList = [
367
+ themeClass,
368
+ 'rounded-xl',
369
+ 'border',
370
+ 'border-border',
371
+ 'overflow-hidden',
372
+ 'shadow-lg',
373
+ className
374
+ ].filter(Boolean).join(' ');
375
+
376
+ if (loading) {
377
+ return (
378
+ <div className="flex items-center justify-center" style={containerStyle}>
379
+ <div className="text-muted-foreground">Loading {objectName}...</div>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ if (error) {
385
+ return (
386
+ <div className="flex items-center justify-center" style={containerStyle}>
387
+ <div className="text-destructive">Error loading data: {error.message}</div>
388
+ </div>
389
+ );
390
+ }
391
+
392
+ return (
393
+ <div className="object-aggrid-container">
394
+ {exportConfig?.enabled && (
395
+ <div className="mb-2 flex gap-2">
396
+ <button
397
+ onClick={handleExportCSV}
398
+ className="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
399
+ >
400
+ Export CSV
401
+ </button>
402
+ </div>
403
+ )}
404
+ <div
405
+ className={classList}
406
+ style={containerStyle}
407
+ >
408
+ <AgGridReact
409
+ ref={gridRef}
410
+ rowData={rowData}
411
+ columnDefs={columnDefs}
412
+ gridOptions={gridOptions}
413
+ />
414
+ </div>
415
+ </div>
416
+ );
417
+ }
418
+
419
+ /**
420
+ * Escape HTML to prevent XSS attacks
421
+ */
422
+ function escapeHtml(text: string): string {
423
+ const div = document.createElement('div');
424
+ div.textContent = text;
425
+ return div.innerHTML;
426
+ }
427
+
428
+ /**
429
+ * Get filter type based on field metadata
430
+ */
431
+ function getFilterType(field: FieldMetadata): string | boolean {
432
+ if (field.filterable === false) {
433
+ return false;
434
+ }
435
+
436
+ return FIELD_TYPE_TO_FILTER_TYPE[field.type] || 'agTextColumnFilter';
437
+ }
438
+
439
+ /**
440
+ * Apply field type-specific formatting to column definition
441
+ */
442
+ function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
443
+ switch (field.type) {
444
+ case 'boolean':
445
+ colDef.cellRenderer = (params: any) => {
446
+ if (params.value === true) return '✓ Yes';
447
+ if (params.value === false) return '✗ No';
448
+ return '';
449
+ };
450
+ break;
451
+
452
+ case 'currency':
453
+ colDef.valueFormatter = (params: any) => {
454
+ if (params.value == null) return '';
455
+ const currency = (field as any).currency || 'USD';
456
+ const precision = (field as any).precision || 2;
457
+ return new Intl.NumberFormat('en-US', {
458
+ style: 'currency',
459
+ currency,
460
+ minimumFractionDigits: precision,
461
+ maximumFractionDigits: precision,
462
+ }).format(params.value);
463
+ };
464
+ break;
465
+
466
+ case 'percent':
467
+ colDef.valueFormatter = (params: any) => {
468
+ if (params.value == null) return '';
469
+ const precision = (field as any).precision || 2;
470
+ return `${(params.value * 100).toFixed(precision)}%`;
471
+ };
472
+ break;
473
+
474
+ case 'date':
475
+ colDef.valueFormatter = (params: any) => {
476
+ if (!params.value) return '';
477
+ try {
478
+ const date = new Date(params.value);
479
+ if (isNaN(date.getTime())) return '';
480
+ return date.toLocaleDateString();
481
+ } catch {
482
+ return '';
483
+ }
484
+ };
485
+ break;
486
+
487
+ case 'datetime':
488
+ colDef.valueFormatter = (params: any) => {
489
+ if (!params.value) return '';
490
+ try {
491
+ const date = new Date(params.value);
492
+ if (isNaN(date.getTime())) return '';
493
+ return date.toLocaleString();
494
+ } catch {
495
+ return '';
496
+ }
497
+ };
498
+ break;
499
+
500
+ case 'time':
501
+ colDef.valueFormatter = (params: any) => {
502
+ if (!params.value) return '';
503
+ return params.value;
504
+ };
505
+ break;
506
+
507
+ case 'email':
508
+ colDef.cellRenderer = (params: any) => {
509
+ if (!params.value) return '';
510
+ const escaped = escapeHtml(params.value);
511
+ return `<a href="mailto:${escaped}" class="text-blue-600 hover:underline">${escaped}</a>`;
512
+ };
513
+ break;
514
+
515
+ case 'url':
516
+ colDef.cellRenderer = (params: any) => {
517
+ if (!params.value) return '';
518
+ const escaped = escapeHtml(params.value);
519
+ return `<a href="${escaped}" target="_blank" rel="noopener noreferrer" class="text-blue-600 hover:underline">${escaped}</a>`;
520
+ };
521
+ break;
522
+
523
+ case 'phone':
524
+ colDef.cellRenderer = (params: any) => {
525
+ if (!params.value) return '';
526
+ const escaped = escapeHtml(params.value);
527
+ return `<a href="tel:${escaped}" class="text-blue-600 hover:underline">${escaped}</a>`;
528
+ };
529
+ break;
530
+
531
+ case 'select':
532
+ colDef.valueFormatter = (params: any) => {
533
+ if (!params.value) return '';
534
+ const options = (field as any).options || [];
535
+ const option = options.find((opt: any) => opt.value === params.value);
536
+ return option?.label || params.value;
537
+ };
538
+ break;
539
+
540
+ case 'lookup':
541
+ case 'master_detail':
542
+ colDef.valueFormatter = (params: any) => {
543
+ if (!params.value) return '';
544
+ // Handle lookup values - could be an object or just an ID
545
+ if (typeof params.value === 'object') {
546
+ return params.value.name || params.value.label || params.value.id || '';
547
+ }
548
+ return String(params.value);
549
+ };
550
+ break;
551
+
552
+ case 'number': {
553
+ const precision = (field as any).precision;
554
+ if (precision !== undefined) {
555
+ colDef.valueFormatter = (params: any) => {
556
+ if (params.value == null) return '';
557
+ return Number(params.value).toFixed(precision);
558
+ };
559
+ }
560
+ break;
561
+ }
562
+
563
+ case 'color':
564
+ colDef.cellRenderer = (params: any) => {
565
+ if (!params.value) return '';
566
+ const escaped = escapeHtml(params.value);
567
+ return `<div class="flex items-center gap-2">
568
+ <div style="width: 16px; height: 16px; background-color: ${escaped}; border: 1px solid #ccc; border-radius: 2px;"></div>
569
+ <span>${escaped}</span>
570
+ </div>`;
571
+ };
572
+ break;
573
+
574
+ case 'rating':
575
+ colDef.cellRenderer = (params: any) => {
576
+ if (params.value == null) return '';
577
+ const max = (field as any).max || 5;
578
+ const stars = '⭐'.repeat(Math.min(params.value, max));
579
+ return stars;
580
+ };
581
+ break;
582
+
583
+ case 'image':
584
+ colDef.cellRenderer = (params: any) => {
585
+ if (!params.value) return '';
586
+ const url = typeof params.value === 'string' ? params.value : params.value.url;
587
+ if (!url) return '';
588
+ const escapedUrl = escapeHtml(url);
589
+ return `<img src="${escapedUrl}" alt="" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" />`;
590
+ };
591
+ break;
592
+
593
+ case 'avatar':
594
+ colDef.cellRenderer = (params: any) => {
595
+ if (!params.value) return '';
596
+ const url = typeof params.value === 'string' ? params.value : params.value.url;
597
+ if (!url) return '';
598
+ const escapedUrl = escapeHtml(url);
599
+ return `<img src="${escapedUrl}" alt="" style="width: 32px; height: 32px; object-fit: cover; border-radius: 50%;" />`;
600
+ };
601
+ break;
602
+ }
603
+ }
@@ -0,0 +1,74 @@
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
+ * Virtual Scrolling in AG Grid
11
+ *
12
+ * AG Grid provides built-in virtual scrolling by default, which renders only
13
+ * the visible rows in the viewport. This is a core feature of AG Grid and
14
+ * requires no additional configuration.
15
+ *
16
+ * ## How It Works
17
+ *
18
+ * - AG Grid automatically virtualizes rows by rendering only visible rows
19
+ * - As you scroll, rows are recycled and reused for new data
20
+ * - This provides excellent performance even with datasets of 100,000+ rows
21
+ *
22
+ * ## Performance Tips
23
+ *
24
+ * 1. **Row Buffer**: Adjust `rowBuffer` to control how many extra rows are rendered
25
+ * ```ts
26
+ * gridOptions: { rowBuffer: 10 } // Render 10 extra rows above/below viewport
27
+ * ```
28
+ *
29
+ * 2. **Suppress Animations**: Disable animations for very large datasets
30
+ * ```ts
31
+ * animateRows: false
32
+ * ```
33
+ *
34
+ * 3. **Debounce Vertical Scroll**: Add delay to vertical scroll updates
35
+ * ```ts
36
+ * gridOptions: { debounceVerticalScrollbar: true }
37
+ * ```
38
+ *
39
+ * 4. **Row Height**: Use fixed row heights for better performance
40
+ * ```ts
41
+ * gridOptions: { rowHeight: 40 }
42
+ * ```
43
+ *
44
+ * ## Example Usage
45
+ *
46
+ * ```tsx
47
+ * <AgGrid
48
+ * rowData={largeDataset} // 10,000+ items
49
+ * columnDefs={columns}
50
+ * gridOptions={{
51
+ * rowBuffer: 10,
52
+ * rowHeight: 40,
53
+ * debounceVerticalScrollbar: true,
54
+ * }}
55
+ * animateRows={false}
56
+ * />
57
+ * ```
58
+ *
59
+ * ## References
60
+ *
61
+ * - [AG Grid Row Virtualisation](https://www.ag-grid.com/javascript-data-grid/dom-virtualisation/)
62
+ * - [Performance Best Practices](https://www.ag-grid.com/javascript-data-grid/performance/)
63
+ */
64
+
65
+ export const VIRTUAL_SCROLLING_DOCS = {
66
+ enabled: true,
67
+ automatic: true,
68
+ recommendedSettings: {
69
+ rowBuffer: 10,
70
+ rowHeight: 40,
71
+ debounceVerticalScrollbar: true,
72
+ animateRows: false,
73
+ },
74
+ };
package/src/index.test.ts CHANGED
@@ -13,7 +13,7 @@ describe('Plugin AgGrid', () => {
13
13
  // Import all renderers to register them
14
14
  beforeAll(async () => {
15
15
  await import('./index');
16
- });
16
+ }, 15000); // Increase timeout to 15 seconds for async import
17
17
 
18
18
  describe('aggrid component', () => {
19
19
  it('should be registered in ComponentRegistry', () => {