@object-ui/plugin-aggrid 0.4.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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +99 -0
  2. package/CHANGELOG.md +16 -0
  3. package/OBJECT_AGGRID_CN.md +483 -0
  4. package/QUICKSTART.md +186 -0
  5. package/README.md +221 -1
  6. package/dist/AddressField-Bntpynvd.js +95 -0
  7. package/dist/AgGridImpl-3Mmf2qrR.js +229 -0
  8. package/dist/AutoNumberField-C1kBJaxh.js +8 -0
  9. package/dist/FileField-BDwbJvor.js +101 -0
  10. package/dist/FormulaField-BXNiyGoh.js +9 -0
  11. package/dist/GeolocationField-Df3yYcM9.js +141 -0
  12. package/dist/GridField-CcjQp4WM.js +29 -0
  13. package/dist/LocationField-BIfN5QIq.js +33 -0
  14. package/dist/MasterDetailField-CAEmxbIT.js +117 -0
  15. package/dist/ObjectAgGridImpl-EjifM4aY.js +28727 -0
  16. package/dist/ObjectField-BpkQpIF-.js +51 -0
  17. package/dist/QRCodeField-VCBewTDG.js +96 -0
  18. package/dist/RichTextField-CyQwSi2C.js +37 -0
  19. package/dist/SignatureField-Cr4tsEbj.js +96 -0
  20. package/dist/SummaryField-CnEJ_GZI.js +9 -0
  21. package/dist/UserField-DJjaVyrV.js +49 -0
  22. package/dist/VectorField-cPYmcKnV.js +25 -0
  23. package/dist/{index-B6NPAFZx.js → index-B87wd1E0.js} +301 -143
  24. package/dist/index.css +1 -1
  25. package/dist/index.js +4 -3
  26. package/dist/index.umd.cjs +225 -2
  27. package/dist/src/AgGridImpl.d.ts +5 -2
  28. package/dist/src/ObjectAgGridImpl.d.ts +6 -0
  29. package/dist/src/VirtualScrolling.d.ts +72 -0
  30. package/dist/src/field-renderers.d.ts +67 -0
  31. package/dist/src/index.d.ts +47 -2
  32. package/dist/src/object-aggrid.types.d.ts +74 -0
  33. package/dist/src/types.d.ts +48 -1
  34. package/package.json +11 -9
  35. package/src/AgGridImpl.tsx +100 -11
  36. package/src/ObjectAgGridImpl.tsx +501 -0
  37. package/src/VirtualScrolling.ts +74 -0
  38. package/src/field-renderers.test.tsx +383 -0
  39. package/src/field-renderers.tsx +224 -0
  40. package/src/index.test.ts +1 -1
  41. package/src/index.tsx +211 -2
  42. package/src/object-aggrid.test.ts +99 -0
  43. package/src/object-aggrid.types.ts +123 -0
  44. package/src/types.ts +57 -1
  45. package/vite.config.ts +13 -0
  46. package/dist/AgGridImpl-DKkq6v1B.js +0 -171
@@ -0,0 +1,501 @@
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
+ } from 'ag-grid-community';
22
+ import type { FieldMetadata, ObjectSchemaMetadata } from '@object-ui/types';
23
+ import type { ObjectAgGridImplProps } from './object-aggrid.types';
24
+ import { FIELD_TYPE_TO_FILTER_TYPE } from './object-aggrid.types';
25
+ import { createFieldCellRenderer, createFieldCellEditor } from './field-renderers';
26
+
27
+ /**
28
+ * ObjectAgGridImpl - Metadata-driven AG Grid implementation
29
+ * Fetches object metadata and data from ObjectStack and renders the grid
30
+ */
31
+ export default function ObjectAgGridImpl({
32
+ objectName,
33
+ dataSource,
34
+ fields: providedFields,
35
+ fieldNames,
36
+ filters,
37
+ sort,
38
+ pageSize = 10,
39
+ pagination = true,
40
+ domLayout = 'normal',
41
+ animateRows = true,
42
+ rowSelection,
43
+ theme = 'quartz',
44
+ height = 500,
45
+ className = '',
46
+ editable = false,
47
+ editType,
48
+ singleClickEdit = false,
49
+ stopEditingWhenCellsLoseFocus = true,
50
+ exportConfig,
51
+ statusBar,
52
+ callbacks,
53
+ columnConfig,
54
+ enableRangeSelection = false,
55
+ enableCharts = false,
56
+ contextMenu,
57
+ }: ObjectAgGridImplProps) {
58
+ const gridRef = useRef<any>(null);
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState<Error | null>(null);
61
+ const [objectSchema, setObjectSchema] = useState<ObjectSchemaMetadata | null>(null);
62
+ const [rowData, setRowData] = useState<any[]>([]);
63
+
64
+ // Fetch object metadata
65
+ useEffect(() => {
66
+ if (!dataSource) {
67
+ setError(new Error('DataSource is required'));
68
+ setLoading(false);
69
+ return;
70
+ }
71
+
72
+ const fetchMetadata = async () => {
73
+ try {
74
+ setLoading(true);
75
+ setError(null);
76
+
77
+ // Fetch object schema/metadata
78
+ const schema = await (dataSource as any).getObjectSchema(objectName);
79
+ setObjectSchema(schema);
80
+ } catch (err) {
81
+ const error = err instanceof Error ? err : new Error(String(err));
82
+ setError(error);
83
+ callbacks?.onDataError?.(error);
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ };
88
+
89
+ fetchMetadata();
90
+ }, [objectName, dataSource, callbacks]);
91
+
92
+ // Fetch data
93
+ useEffect(() => {
94
+ if (!dataSource || !objectSchema) return;
95
+
96
+ const fetchData = async () => {
97
+ try {
98
+ setLoading(true);
99
+ setError(null);
100
+
101
+ const queryParams: any = {
102
+ $top: pageSize,
103
+ $skip: 0,
104
+ };
105
+
106
+ if (filters) {
107
+ queryParams.$filter = filters;
108
+ }
109
+
110
+ if (sort) {
111
+ queryParams.$orderby = sort;
112
+ }
113
+
114
+ const result = await dataSource.find(objectName, queryParams);
115
+ setRowData(result.data || []);
116
+ callbacks?.onDataLoaded?.(result.data || []);
117
+ } catch (err) {
118
+ const error = err instanceof Error ? err : new Error(String(err));
119
+ setError(error);
120
+ callbacks?.onDataError?.(error);
121
+ } finally {
122
+ setLoading(false);
123
+ }
124
+ };
125
+
126
+ fetchData();
127
+ }, [objectName, dataSource, objectSchema, filters, sort, pageSize, callbacks]);
128
+
129
+ // Generate column definitions from metadata
130
+ const columnDefs = useMemo((): ColDef[] => {
131
+ if (!objectSchema?.fields) return [];
132
+
133
+ // Use provided fields or get from schema
134
+ const fieldMetadata = providedFields || Object.values(objectSchema.fields);
135
+
136
+ // Filter fields if fieldNames is provided
137
+ const fieldsToShow = fieldNames
138
+ ? fieldMetadata.filter(field => fieldNames.includes(field.name))
139
+ : fieldMetadata;
140
+
141
+ return fieldsToShow.map(field => {
142
+ const colDef: ColDef = {
143
+ field: field.name,
144
+ headerName: field.label || field.name,
145
+ sortable: field.sortable !== false,
146
+ filter: getFilterType(field),
147
+ editable: editable && !field.readonly,
148
+ // visible_on will be evaluated by the core renderer
149
+ // For now, we just show all fields. Conditional visibility
150
+ // should be handled at a higher level or via dynamic column updates
151
+ };
152
+
153
+ // Apply column config defaults
154
+ if (columnConfig) {
155
+ if (columnConfig.resizable !== undefined) {
156
+ colDef.resizable = columnConfig.resizable;
157
+ }
158
+ if (columnConfig.sortable !== undefined && colDef.sortable === undefined) {
159
+ colDef.sortable = columnConfig.sortable;
160
+ }
161
+ }
162
+
163
+ // Add custom renderers and formatters based on field type
164
+ applyFieldTypeFormatting(colDef, field);
165
+
166
+ return colDef;
167
+ });
168
+ }, [objectSchema, providedFields, fieldNames, editable, columnConfig]);
169
+
170
+ // Build status bar panels
171
+ const statusPanels = useMemo((): StatusPanelDef[] | undefined => {
172
+ if (!statusBar?.enabled) return undefined;
173
+
174
+ const aggregations = statusBar.aggregations || ['count', 'sum', 'avg'];
175
+ const panels: StatusPanelDef[] = [];
176
+
177
+ if (aggregations.includes('count')) {
178
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['count'] } });
179
+ }
180
+ if (aggregations.includes('sum')) {
181
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['sum'] } });
182
+ }
183
+ if (aggregations.includes('avg')) {
184
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['avg'] } });
185
+ }
186
+ if (aggregations.includes('min')) {
187
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['min'] } });
188
+ }
189
+ if (aggregations.includes('max')) {
190
+ panels.push({ statusPanel: 'agAggregationComponent', statusPanelParams: { aggFuncs: ['max'] } });
191
+ }
192
+
193
+ return panels;
194
+ }, [statusBar]);
195
+
196
+ // CSV Export handler
197
+ const handleExportCSV = useCallback(() => {
198
+ if (!gridRef.current?.api) return;
199
+
200
+ const params = {
201
+ fileName: exportConfig?.fileName || `${objectName}-export.csv`,
202
+ skipColumnHeaders: exportConfig?.skipColumnHeaders || false,
203
+ allColumns: exportConfig?.allColumns || false,
204
+ onlySelected: exportConfig?.onlySelected || false,
205
+ };
206
+
207
+ gridRef.current.api.exportDataAsCsv(params);
208
+
209
+ if (callbacks?.onExport) {
210
+ const data = exportConfig?.onlySelected
211
+ ? gridRef.current.api.getSelectedRows()
212
+ : rowData;
213
+ callbacks.onExport(data || [], 'csv');
214
+ }
215
+ }, [exportConfig, callbacks, rowData, objectName]);
216
+
217
+ // Context Menu handler
218
+ const getContextMenuItems = useCallback((params: GetContextMenuItemsParams): (string | MenuItemDef)[] => {
219
+ if (!contextMenu?.enabled) return [];
220
+
221
+ const items: (string | MenuItemDef)[] = [];
222
+ const defaultItems = contextMenu.items || ['copy', 'copyWithHeaders', 'separator', 'export'];
223
+
224
+ defaultItems.forEach(item => {
225
+ if (item === 'export') {
226
+ items.push({
227
+ name: 'Export CSV',
228
+ icon: '<span>📥</span>',
229
+ action: () => handleExportCSV(),
230
+ });
231
+ } else if (item === 'autoSizeAll') {
232
+ items.push({
233
+ name: 'Auto-size All Columns',
234
+ action: () => {
235
+ if (gridRef.current?.api) {
236
+ gridRef.current.api.autoSizeAllColumns();
237
+ }
238
+ },
239
+ });
240
+ } else if (item === 'resetColumns') {
241
+ items.push({
242
+ name: 'Reset Columns',
243
+ action: () => {
244
+ if (gridRef.current?.api) {
245
+ gridRef.current.api.resetColumnState();
246
+ }
247
+ },
248
+ });
249
+ } else {
250
+ items.push(item);
251
+ }
252
+ });
253
+
254
+ // Add custom items
255
+ if (contextMenu.customItems) {
256
+ if (items.length > 0) {
257
+ items.push('separator');
258
+ }
259
+ contextMenu.customItems.forEach(customItem => {
260
+ items.push({
261
+ name: customItem.name,
262
+ disabled: customItem.disabled,
263
+ action: () => {
264
+ if (callbacks?.onContextMenuAction) {
265
+ callbacks.onContextMenuAction(customItem.action, params.node?.data);
266
+ }
267
+ },
268
+ });
269
+ });
270
+ }
271
+
272
+ return items;
273
+ }, [contextMenu, handleExportCSV, callbacks]);
274
+
275
+ // Event handlers
276
+ const handleCellClicked = useCallback((event: CellClickedEvent) => {
277
+ callbacks?.onCellClicked?.(event);
278
+ }, [callbacks]);
279
+
280
+ const handleRowClicked = useCallback((event: RowClickedEvent) => {
281
+ callbacks?.onRowClicked?.(event);
282
+ }, [callbacks]);
283
+
284
+ const handleSelectionChanged = useCallback((event: SelectionChangedEvent) => {
285
+ callbacks?.onSelectionChanged?.(event);
286
+ }, [callbacks]);
287
+
288
+ const handleCellValueChanged = useCallback(async (event: CellValueChangedEvent) => {
289
+ callbacks?.onCellValueChanged?.(event);
290
+
291
+ // Save changes to backend if dataSource supports update
292
+ // Note: Assumes records have an 'id' field as primary key
293
+ // TODO: Make the ID field name configurable via schema
294
+ if (dataSource && event.data && event.data.id) {
295
+ try {
296
+ await dataSource.update(objectName, event.data.id, {
297
+ [event.colDef.field!]: event.newValue
298
+ });
299
+ } catch (err) {
300
+ console.error('Failed to update record:', err);
301
+ // Revert the change
302
+ event.node.setDataValue(event.colDef.field!, event.oldValue);
303
+ }
304
+ }
305
+ }, [callbacks, dataSource, objectName]);
306
+
307
+ const onGridReady = useCallback((params: GridReadyEvent) => {
308
+ gridRef.current = params;
309
+ }, []);
310
+
311
+ // Merge grid options with props
312
+ const gridOptions = useMemo(() => ({
313
+ pagination,
314
+ paginationPageSize: pageSize,
315
+ domLayout,
316
+ animateRows,
317
+ rowSelection,
318
+ editType,
319
+ singleClickEdit,
320
+ stopEditingWhenCellsLoseFocus,
321
+ statusBar: statusPanels ? { statusPanels } : undefined,
322
+ enableRangeSelection,
323
+ enableCharts,
324
+ getContextMenuItems: contextMenu?.enabled ? getContextMenuItems : undefined,
325
+ suppressCellFocus: !editable,
326
+ enableCellTextSelection: true,
327
+ ensureDomOrder: true,
328
+ onCellClicked: handleCellClicked,
329
+ onRowClicked: handleRowClicked,
330
+ onSelectionChanged: handleSelectionChanged,
331
+ onCellValueChanged: handleCellValueChanged,
332
+ onGridReady,
333
+ }), [
334
+ pagination,
335
+ pageSize,
336
+ domLayout,
337
+ animateRows,
338
+ rowSelection,
339
+ editType,
340
+ singleClickEdit,
341
+ stopEditingWhenCellsLoseFocus,
342
+ statusPanels,
343
+ enableRangeSelection,
344
+ enableCharts,
345
+ contextMenu,
346
+ getContextMenuItems,
347
+ editable,
348
+ handleCellClicked,
349
+ handleRowClicked,
350
+ handleSelectionChanged,
351
+ handleCellValueChanged,
352
+ onGridReady,
353
+ ]);
354
+
355
+ // Compute container style
356
+ const containerStyle = useMemo(() => ({
357
+ height: typeof height === 'number' ? `${height}px` : height,
358
+ width: '100%',
359
+ }), [height]);
360
+
361
+ // Determine theme class and build complete class list
362
+ const themeClass = `ag-theme-${theme}`;
363
+ const classList = [
364
+ themeClass,
365
+ 'rounded-xl',
366
+ 'border',
367
+ 'border-border',
368
+ 'overflow-hidden',
369
+ 'shadow-lg',
370
+ className
371
+ ].filter(Boolean).join(' ');
372
+
373
+ if (loading) {
374
+ return (
375
+ <div className="flex items-center justify-center" style={containerStyle}>
376
+ <div className="text-muted-foreground">Loading {objectName}...</div>
377
+ </div>
378
+ );
379
+ }
380
+
381
+ if (error) {
382
+ return (
383
+ <div className="flex items-center justify-center" style={containerStyle}>
384
+ <div className="text-destructive">Error loading data: {error.message}</div>
385
+ </div>
386
+ );
387
+ }
388
+
389
+ return (
390
+ <div className="object-aggrid-container">
391
+ {exportConfig?.enabled && (
392
+ <div className="mb-2 flex gap-2">
393
+ <button
394
+ onClick={handleExportCSV}
395
+ className="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
396
+ >
397
+ Export CSV
398
+ </button>
399
+ </div>
400
+ )}
401
+ <div
402
+ className={classList}
403
+ style={containerStyle}
404
+ >
405
+ <AgGridReact
406
+ ref={gridRef}
407
+ rowData={rowData}
408
+ columnDefs={columnDefs}
409
+ gridOptions={gridOptions}
410
+ />
411
+ </div>
412
+ </div>
413
+ );
414
+ }
415
+
416
+ /**
417
+ * Get filter type based on field metadata
418
+ */
419
+ function getFilterType(field: FieldMetadata): string | boolean {
420
+ if (field.filterable === false) {
421
+ return false;
422
+ }
423
+
424
+ return FIELD_TYPE_TO_FILTER_TYPE[field.type] || 'agTextColumnFilter';
425
+ }
426
+
427
+ /**
428
+ * Apply field type-specific formatting to column definition
429
+ * Uses field widgets from @object-ui/fields for consistent rendering
430
+ */
431
+ function applyFieldTypeFormatting(colDef: ColDef, field: FieldMetadata): void {
432
+ // Define field types that should use field widgets for rendering
433
+ const fieldWidgetTypes = [
434
+ 'text', 'textarea', 'number', 'currency', 'percent',
435
+ 'boolean', 'select', 'date', 'datetime', 'time',
436
+ 'email', 'phone', 'url', 'password', 'color',
437
+ 'rating', 'image', 'avatar', 'lookup', 'slider', 'code'
438
+ ];
439
+
440
+ // Use field widget renderer if the type is supported
441
+ if (fieldWidgetTypes.includes(field.type)) {
442
+ colDef.cellRenderer = createFieldCellRenderer(field);
443
+
444
+ // Add cell editor for editable fields
445
+ if (colDef.editable) {
446
+ colDef.cellEditor = createFieldCellEditor(field);
447
+
448
+ // Configure editor based on field type
449
+ if (['date', 'datetime', 'select', 'lookup', 'color'].includes(field.type)) {
450
+ colDef.cellEditorPopup = true;
451
+ }
452
+ }
453
+ } else {
454
+ // Fallback to simple rendering for unsupported types
455
+ switch (field.type) {
456
+ case 'master_detail':
457
+ colDef.valueFormatter = (params: any) => {
458
+ if (!params.value) return '';
459
+ // Handle lookup values - could be an object or just an ID
460
+ if (typeof params.value === 'object') {
461
+ return params.value.name || params.value.label || params.value.id || '';
462
+ }
463
+ return String(params.value);
464
+ };
465
+ break;
466
+
467
+ case 'object':
468
+ colDef.cellRenderer = () => {
469
+ const span = document.createElement('span');
470
+ span.className = 'text-gray-500 italic';
471
+ span.textContent = '[Object]';
472
+ return span;
473
+ };
474
+ break;
475
+
476
+ case 'vector':
477
+ colDef.cellRenderer = () => {
478
+ const span = document.createElement('span');
479
+ span.className = 'text-gray-500 italic';
480
+ span.textContent = '[Vector]';
481
+ return span;
482
+ };
483
+ break;
484
+
485
+ case 'grid':
486
+ colDef.cellRenderer = () => {
487
+ const span = document.createElement('span');
488
+ span.className = 'text-gray-500 italic';
489
+ span.textContent = '[Grid]';
490
+ return span;
491
+ };
492
+ break;
493
+
494
+ default:
495
+ // Default text rendering
496
+ colDef.valueFormatter = (params: any) => {
497
+ return params.value != null ? String(params.value) : '';
498
+ };
499
+ }
500
+ }
501
+ }
@@ -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
+ };