@object-ui/plugin-aggrid 0.4.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,139 @@
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, beforeAll } from 'vitest';
10
+ import { ComponentRegistry } from '@object-ui/core';
11
+
12
+ describe('Plugin AgGrid', () => {
13
+ // Import all renderers to register them
14
+ beforeAll(async () => {
15
+ await import('./index');
16
+ });
17
+
18
+ describe('aggrid component', () => {
19
+ it('should be registered in ComponentRegistry', () => {
20
+ const aggridRenderer = ComponentRegistry.get('aggrid');
21
+ expect(aggridRenderer).toBeDefined();
22
+ });
23
+
24
+ it('should have proper metadata', () => {
25
+ const config = ComponentRegistry.getConfig('aggrid');
26
+ expect(config).toBeDefined();
27
+ expect(config?.label).toBe('AG Grid');
28
+ expect(config?.icon).toBe('Table');
29
+ expect(config?.category).toBe('plugin');
30
+ expect(config?.inputs).toBeDefined();
31
+ expect(config?.defaultProps).toBeDefined();
32
+ });
33
+
34
+ it('should have expected inputs', () => {
35
+ const config = ComponentRegistry.getConfig('aggrid');
36
+ const inputNames = config?.inputs?.map((input: any) => input.name) || [];
37
+
38
+ // Original inputs
39
+ expect(inputNames).toContain('rowData');
40
+ expect(inputNames).toContain('columnDefs');
41
+ expect(inputNames).toContain('pagination');
42
+ expect(inputNames).toContain('paginationPageSize');
43
+ expect(inputNames).toContain('theme');
44
+ expect(inputNames).toContain('height');
45
+ expect(inputNames).toContain('rowSelection');
46
+ expect(inputNames).toContain('domLayout');
47
+ expect(inputNames).toContain('animateRows');
48
+ expect(inputNames).toContain('gridOptions');
49
+ expect(inputNames).toContain('className');
50
+
51
+ // New inputs for enterprise features
52
+ expect(inputNames).toContain('editable');
53
+ expect(inputNames).toContain('singleClickEdit');
54
+ expect(inputNames).toContain('exportConfig');
55
+ expect(inputNames).toContain('statusBar');
56
+ expect(inputNames).toContain('callbacks');
57
+ expect(inputNames).toContain('columnConfig');
58
+ expect(inputNames).toContain('enableRangeSelection');
59
+ expect(inputNames).toContain('enableCharts');
60
+ expect(inputNames).toContain('contextMenu');
61
+ });
62
+
63
+ it('should have rowData and columnDefs as required inputs', () => {
64
+ const config = ComponentRegistry.getConfig('aggrid');
65
+ const rowDataInput = config?.inputs?.find((input: any) => input.name === 'rowData');
66
+ const columnDefsInput = config?.inputs?.find((input: any) => input.name === 'columnDefs');
67
+
68
+ expect(rowDataInput).toBeDefined();
69
+ expect(rowDataInput?.required).toBe(true);
70
+ expect(rowDataInput?.type).toBe('array');
71
+
72
+ expect(columnDefsInput).toBeDefined();
73
+ expect(columnDefsInput?.required).toBe(true);
74
+ expect(columnDefsInput?.type).toBe('array');
75
+ });
76
+
77
+ it('should have theme as enum input', () => {
78
+ const config = ComponentRegistry.getConfig('aggrid');
79
+ const themeInput = config?.inputs?.find((input: any) => input.name === 'theme');
80
+
81
+ expect(themeInput).toBeDefined();
82
+ expect(themeInput?.type).toBe('enum');
83
+ expect(themeInput?.enum).toBeDefined();
84
+ expect(Array.isArray(themeInput?.enum)).toBe(true);
85
+
86
+ const enumValues = themeInput?.enum?.map((e: any) => e.value) || [];
87
+ expect(enumValues).toContain('quartz');
88
+ expect(enumValues).toContain('alpine');
89
+ expect(enumValues).toContain('balham');
90
+ expect(enumValues).toContain('material');
91
+ });
92
+
93
+ it('should have sensible default props', () => {
94
+ const config = ComponentRegistry.getConfig('aggrid');
95
+ const defaults = config?.defaultProps;
96
+
97
+ expect(defaults).toBeDefined();
98
+ expect(defaults?.pagination).toBe(true);
99
+ expect(defaults?.paginationPageSize).toBe(10);
100
+ expect(defaults?.theme).toBe('quartz');
101
+ expect(defaults?.height).toBe(500);
102
+ expect(defaults?.animateRows).toBe(true);
103
+ expect(defaults?.domLayout).toBe('normal');
104
+ expect(defaults?.rowData).toBeDefined();
105
+ expect(Array.isArray(defaults?.rowData)).toBe(true);
106
+ expect(defaults?.rowData.length).toBeGreaterThan(0);
107
+ expect(defaults?.columnDefs).toBeDefined();
108
+ expect(Array.isArray(defaults?.columnDefs)).toBe(true);
109
+ expect(defaults?.columnDefs.length).toBeGreaterThan(0);
110
+ });
111
+
112
+ it('should have default columnDefs with proper structure', () => {
113
+ const config = ComponentRegistry.getConfig('aggrid');
114
+ const defaults = config?.defaultProps;
115
+ const columnDefs = defaults?.columnDefs || [];
116
+
117
+ expect(columnDefs.length).toBeGreaterThan(0);
118
+
119
+ // Verify each column has required properties
120
+ columnDefs.forEach((col: any) => {
121
+ expect(col.field).toBeDefined();
122
+ expect(typeof col.field).toBe('string');
123
+ });
124
+ });
125
+
126
+ it('should have default rowData with proper structure', () => {
127
+ const config = ComponentRegistry.getConfig('aggrid');
128
+ const defaults = config?.defaultProps;
129
+ const rowData = defaults?.rowData || [];
130
+
131
+ expect(rowData.length).toBeGreaterThan(0);
132
+
133
+ // Verify first row has expected properties
134
+ const firstRow = rowData[0];
135
+ expect(firstRow).toBeDefined();
136
+ expect(typeof firstRow).toBe('object');
137
+ });
138
+ });
139
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,305 @@
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, { Suspense } from 'react';
10
+ import { ComponentRegistry } from '@object-ui/core';
11
+ import { Skeleton } from '@object-ui/components';
12
+
13
+ // Import AG Grid CSS - These must be imported at the entry point for Next.js
14
+ // Importing them in the lazy-loaded component doesn't work properly
15
+ import 'ag-grid-community/styles/ag-grid.css';
16
+ import 'ag-grid-community/styles/ag-theme-quartz.css';
17
+ import 'ag-grid-community/styles/ag-theme-alpine.css';
18
+ import 'ag-grid-community/styles/ag-theme-balham.css';
19
+ import 'ag-grid-community/styles/ag-theme-material.css';
20
+
21
+ // Export types for external use
22
+ export type { AgGridSchema, SimpleColumnDef, AgGridCallbacks, ExportConfig, StatusBarConfig, ColumnConfig, ContextMenuConfig } from './types';
23
+
24
+ import type { AgGridCallbacks, ExportConfig, StatusBarConfig, ColumnConfig, ContextMenuConfig } from './types';
25
+
26
+ // 🚀 Lazy load the implementation file
27
+ // This ensures AG Grid is only loaded when the component is actually rendered
28
+ const LazyAgGrid = React.lazy(() => import('./AgGridImpl'));
29
+
30
+ export interface AgGridRendererProps {
31
+ schema: {
32
+ type: string;
33
+ id?: string;
34
+ className?: string;
35
+ rowData?: any[];
36
+ columnDefs?: any[];
37
+ gridOptions?: any;
38
+ pagination?: boolean;
39
+ paginationPageSize?: number;
40
+ domLayout?: 'normal' | 'autoHeight' | 'print';
41
+ animateRows?: boolean;
42
+ rowSelection?: 'single' | 'multiple';
43
+ theme?: 'alpine' | 'balham' | 'material' | 'quartz';
44
+ height?: number | string;
45
+ editable?: boolean;
46
+ editType?: 'fullRow';
47
+ singleClickEdit?: boolean;
48
+ stopEditingWhenCellsLoseFocus?: boolean;
49
+ exportConfig?: ExportConfig;
50
+ statusBar?: StatusBarConfig;
51
+ callbacks?: AgGridCallbacks;
52
+ columnConfig?: ColumnConfig;
53
+ enableRangeSelection?: boolean;
54
+ enableCharts?: boolean;
55
+ contextMenu?: ContextMenuConfig;
56
+ };
57
+ }
58
+
59
+ /**
60
+ * AgGridRenderer - The public API for the AG Grid component
61
+ * This wrapper handles lazy loading internally using React.Suspense
62
+ */
63
+ export const AgGridRenderer: React.FC<AgGridRendererProps> = ({ schema }) => {
64
+ return (
65
+ <Suspense fallback={<Skeleton className="w-full h-[500px]" />}>
66
+ <LazyAgGrid
67
+ rowData={schema.rowData}
68
+ columnDefs={schema.columnDefs}
69
+ gridOptions={schema.gridOptions}
70
+ pagination={schema.pagination}
71
+ paginationPageSize={schema.paginationPageSize}
72
+ domLayout={schema.domLayout}
73
+ animateRows={schema.animateRows}
74
+ rowSelection={schema.rowSelection}
75
+ theme={schema.theme}
76
+ height={schema.height}
77
+ className={schema.className}
78
+ editable={schema.editable}
79
+ editType={schema.editType}
80
+ singleClickEdit={schema.singleClickEdit}
81
+ stopEditingWhenCellsLoseFocus={schema.stopEditingWhenCellsLoseFocus}
82
+ exportConfig={schema.exportConfig}
83
+ statusBar={schema.statusBar}
84
+ callbacks={schema.callbacks}
85
+ columnConfig={schema.columnConfig}
86
+ enableRangeSelection={schema.enableRangeSelection}
87
+ enableCharts={schema.enableCharts}
88
+ contextMenu={schema.contextMenu}
89
+ />
90
+ </Suspense>
91
+ );
92
+ };
93
+
94
+ // Register the component with the ComponentRegistry
95
+ ComponentRegistry.register(
96
+ 'aggrid',
97
+ AgGridRenderer,
98
+ {
99
+ label: 'AG Grid',
100
+ icon: 'Table',
101
+ category: 'plugin',
102
+ inputs: [
103
+ {
104
+ name: 'rowData',
105
+ type: 'array',
106
+ label: 'Row Data',
107
+ description: 'Array of objects to display in the grid',
108
+ required: true
109
+ },
110
+ {
111
+ name: 'columnDefs',
112
+ type: 'array',
113
+ label: 'Column Definitions',
114
+ description: 'Array of column definitions',
115
+ required: true
116
+ },
117
+ {
118
+ name: 'pagination',
119
+ type: 'boolean',
120
+ label: 'Enable Pagination',
121
+ defaultValue: false
122
+ },
123
+ {
124
+ name: 'paginationPageSize',
125
+ type: 'number',
126
+ label: 'Page Size',
127
+ defaultValue: 10,
128
+ description: 'Number of rows per page when pagination is enabled'
129
+ },
130
+ {
131
+ name: 'theme',
132
+ type: 'enum',
133
+ label: 'Theme',
134
+ enum: [
135
+ { label: 'Quartz', value: 'quartz' },
136
+ { label: 'Alpine', value: 'alpine' },
137
+ { label: 'Balham', value: 'balham' },
138
+ { label: 'Material', value: 'material' }
139
+ ],
140
+ defaultValue: 'quartz'
141
+ },
142
+ {
143
+ name: 'rowSelection',
144
+ type: 'enum',
145
+ label: 'Row Selection',
146
+ enum: [
147
+ { label: 'None', value: undefined },
148
+ { label: 'Single', value: 'single' },
149
+ { label: 'Multiple', value: 'multiple' }
150
+ ],
151
+ defaultValue: undefined,
152
+ advanced: true
153
+ },
154
+ {
155
+ name: 'domLayout',
156
+ type: 'enum',
157
+ label: 'DOM Layout',
158
+ enum: [
159
+ { label: 'Normal', value: 'normal' },
160
+ { label: 'Auto Height', value: 'autoHeight' },
161
+ { label: 'Print', value: 'print' }
162
+ ],
163
+ defaultValue: 'normal',
164
+ advanced: true
165
+ },
166
+ {
167
+ name: 'animateRows',
168
+ type: 'boolean',
169
+ label: 'Animate Rows',
170
+ defaultValue: true,
171
+ advanced: true
172
+ },
173
+ {
174
+ name: 'height',
175
+ type: 'number',
176
+ label: 'Height (px)',
177
+ defaultValue: 500
178
+ },
179
+ {
180
+ name: 'editable',
181
+ type: 'boolean',
182
+ label: 'Enable Editing',
183
+ defaultValue: false,
184
+ description: 'Allow cells to be edited inline',
185
+ advanced: true
186
+ },
187
+ {
188
+ name: 'singleClickEdit',
189
+ type: 'boolean',
190
+ label: 'Single Click Edit',
191
+ defaultValue: false,
192
+ description: 'Start editing on single click instead of double click',
193
+ advanced: true
194
+ },
195
+ {
196
+ name: 'exportConfig',
197
+ type: 'code',
198
+ label: 'Export Config (JSON)',
199
+ description: 'Configure CSV export: { enabled: true, fileName: "data.csv" }',
200
+ advanced: true
201
+ },
202
+ {
203
+ name: 'statusBar',
204
+ type: 'code',
205
+ label: 'Status Bar Config (JSON)',
206
+ description: 'Configure status bar: { enabled: true, aggregations: ["count", "sum", "avg"] }',
207
+ advanced: true
208
+ },
209
+ {
210
+ name: 'callbacks',
211
+ type: 'code',
212
+ label: 'Event Callbacks (JSON)',
213
+ description: 'Event handlers for cell clicks, selection changes, etc.',
214
+ advanced: true
215
+ },
216
+ {
217
+ name: 'columnConfig',
218
+ type: 'code',
219
+ label: 'Column Config (JSON)',
220
+ description: 'Global column settings: { resizable: true, sortable: true, filterable: true }',
221
+ advanced: true
222
+ },
223
+ {
224
+ name: 'enableRangeSelection',
225
+ type: 'boolean',
226
+ label: 'Enable Range Selection',
227
+ defaultValue: false,
228
+ description: 'Allow selecting ranges of cells (like Excel)',
229
+ advanced: true
230
+ },
231
+ {
232
+ name: 'enableCharts',
233
+ type: 'boolean',
234
+ label: 'Enable Charts',
235
+ defaultValue: false,
236
+ description: 'Enable integrated charting (requires AG Grid Enterprise)',
237
+ advanced: true
238
+ },
239
+ {
240
+ name: 'contextMenu',
241
+ type: 'code',
242
+ label: 'Context Menu Config (JSON)',
243
+ description: 'Configure right-click menu: { enabled: true, items: ["copy", "export"] }',
244
+ advanced: true
245
+ },
246
+ {
247
+ name: 'gridOptions',
248
+ type: 'code',
249
+ label: 'Grid Options (JSON)',
250
+ description: 'Advanced AG Grid options',
251
+ advanced: true
252
+ },
253
+ {
254
+ name: 'className',
255
+ type: 'string',
256
+ label: 'CSS Class'
257
+ }
258
+ ],
259
+ defaultProps: {
260
+ rowData: [
261
+ { make: 'Tesla', model: 'Model Y', price: 64950, electric: true },
262
+ { make: 'Ford', model: 'F-Series', price: 33850, electric: false },
263
+ { make: 'Toyota', model: 'Corolla', price: 29600, electric: false },
264
+ { make: 'Mercedes', model: 'EQA', price: 48890, electric: true },
265
+ { make: 'Fiat', model: '500', price: 15774, electric: false },
266
+ { make: 'Nissan', model: 'Juke', price: 20675, electric: false },
267
+ { make: 'Vauxhall', model: 'Corsa', price: 18460, electric: false },
268
+ { make: 'Volvo', model: 'XC90', price: 72835, electric: false },
269
+ { make: 'Mercedes', model: 'GLA', price: 47825, electric: false },
270
+ { make: 'Ford', model: 'Puma', price: 27420, electric: false },
271
+ { make: 'Volkswagen', model: 'Golf', price: 28850, electric: false },
272
+ { make: 'Kia', model: 'Sportage', price: 31095, electric: false }
273
+ ],
274
+ columnDefs: [
275
+ { field: 'make', headerName: 'Make', sortable: true, filter: true },
276
+ { field: 'model', headerName: 'Model', sortable: true, filter: true },
277
+ {
278
+ field: 'price',
279
+ headerName: 'Price',
280
+ sortable: true,
281
+ filter: 'number',
282
+ valueFormatter: (params: any) => params.value != null ? '$' + params.value.toLocaleString() : ''
283
+ },
284
+ {
285
+ field: 'electric',
286
+ headerName: 'Electric',
287
+ sortable: true,
288
+ filter: true,
289
+ cellRenderer: (params: any) => params.value === true ? '⚡ Yes' : 'No'
290
+ }
291
+ ],
292
+ pagination: true,
293
+ paginationPageSize: 10,
294
+ theme: 'quartz',
295
+ height: 500,
296
+ animateRows: true,
297
+ domLayout: 'normal'
298
+ }
299
+ }
300
+ );
301
+
302
+ // Standard Export Protocol - for manual integration
303
+ export const aggridComponents = {
304
+ 'aggrid': AgGridRenderer,
305
+ };
package/src/types.ts ADDED
@@ -0,0 +1,128 @@
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 type { ColDef, GridOptions, CellClickedEvent, RowClickedEvent, SelectionChangedEvent, CellValueChangedEvent } from 'ag-grid-community';
10
+
11
+ /**
12
+ * Event callback types
13
+ */
14
+ export interface AgGridCallbacks {
15
+ onCellClicked?: (event: CellClickedEvent) => void;
16
+ onRowClicked?: (event: RowClickedEvent) => void;
17
+ onSelectionChanged?: (event: SelectionChangedEvent) => void;
18
+ onCellValueChanged?: (event: CellValueChangedEvent) => void;
19
+ onExport?: (data: any[], format: 'csv') => void;
20
+ onContextMenuAction?: (action: string, rowData: any) => void;
21
+ }
22
+
23
+ /**
24
+ * Export configuration
25
+ */
26
+ export interface ExportConfig {
27
+ enabled?: boolean;
28
+ fileName?: string;
29
+ skipColumnHeaders?: boolean;
30
+ allColumns?: boolean;
31
+ onlySelected?: boolean;
32
+ }
33
+
34
+ /**
35
+ * Status bar configuration
36
+ */
37
+ export interface StatusBarConfig {
38
+ enabled?: boolean;
39
+ aggregations?: ('sum' | 'avg' | 'count' | 'min' | 'max')[];
40
+ }
41
+
42
+ /**
43
+ * Column configuration enhancements
44
+ */
45
+ export interface ColumnConfig {
46
+ resizable?: boolean;
47
+ sortable?: boolean;
48
+ filterable?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Context menu configuration
53
+ */
54
+ export interface ContextMenuConfig {
55
+ enabled?: boolean;
56
+ items?: ('copy' | 'copyWithHeaders' | 'paste' | 'separator' | 'export' | 'autoSizeAll' | 'resetColumns' | string)[];
57
+ customItems?: Array<{
58
+ name: string;
59
+ action: string;
60
+ disabled?: boolean;
61
+ }>;
62
+ }
63
+
64
+ /**
65
+ * AG Grid schema for ObjectUI
66
+ */
67
+ export interface AgGridSchema {
68
+ type: 'aggrid';
69
+ id?: string;
70
+ className?: string;
71
+
72
+ // Data
73
+ rowData?: any[];
74
+
75
+ // Column definitions
76
+ columnDefs?: ColDef[];
77
+
78
+ // Grid configuration
79
+ gridOptions?: GridOptions;
80
+
81
+ // Common options
82
+ pagination?: boolean;
83
+ paginationPageSize?: number;
84
+ domLayout?: 'normal' | 'autoHeight' | 'print';
85
+ animateRows?: boolean;
86
+ rowSelection?: 'single' | 'multiple';
87
+
88
+ // Editing
89
+ editable?: boolean;
90
+ editType?: 'fullRow';
91
+ singleClickEdit?: boolean;
92
+ stopEditingWhenCellsLoseFocus?: boolean;
93
+
94
+ // Export
95
+ exportConfig?: ExportConfig;
96
+
97
+ // Status bar
98
+ statusBar?: StatusBarConfig;
99
+
100
+ // Column features
101
+ columnConfig?: ColumnConfig;
102
+ enableRangeSelection?: boolean;
103
+ enableCharts?: boolean;
104
+
105
+ // Context menu
106
+ contextMenu?: ContextMenuConfig;
107
+
108
+ // Event callbacks
109
+ callbacks?: AgGridCallbacks;
110
+
111
+ // Styling
112
+ theme?: 'alpine' | 'balham' | 'material' | 'quartz';
113
+ height?: number | string;
114
+ }
115
+
116
+ /**
117
+ * Column definition with simplified schema
118
+ */
119
+ export interface SimpleColumnDef {
120
+ field: string;
121
+ headerName?: string;
122
+ width?: number;
123
+ sortable?: boolean;
124
+ filter?: boolean | 'text' | 'number' | 'date';
125
+ editable?: boolean;
126
+ cellRenderer?: string;
127
+ valueFormatter?: (params: any) => string;
128
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "jsx": "react-jsx",
6
+ "baseUrl": ".",
7
+ "paths": {
8
+ "@/*": ["src/*"]
9
+ },
10
+ "noImplicitAny": true,
11
+ "noEmit": false,
12
+ "declaration": true,
13
+ "composite": true,
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["src"]
17
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,50 @@
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 { defineConfig } from 'vite';
10
+ import react from '@vitejs/plugin-react';
11
+ import dts from 'vite-plugin-dts';
12
+ import { resolve } from 'path';
13
+
14
+ export default defineConfig({
15
+ plugins: [
16
+ react(),
17
+ dts({
18
+ insertTypesEntry: true,
19
+ include: ['src'],
20
+ exclude: ['**/*.test.ts', '**/*.test.tsx', 'node_modules'],
21
+ skipDiagnostics: true,
22
+ }),
23
+ ],
24
+ resolve: {
25
+ alias: {
26
+ '@': resolve(__dirname, './src'),
27
+ },
28
+ },
29
+ build: {
30
+ lib: {
31
+ entry: resolve(__dirname, 'src/index.tsx'),
32
+ name: 'ObjectUIPluginAgGrid',
33
+ fileName: 'index',
34
+ },
35
+ rollupOptions: {
36
+ external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react', 'ag-grid-community', 'ag-grid-react'],
37
+ output: {
38
+ globals: {
39
+ react: 'React',
40
+ 'react-dom': 'ReactDOM',
41
+ '@object-ui/components': 'ObjectUIComponents',
42
+ '@object-ui/core': 'ObjectUICore',
43
+ '@object-ui/react': 'ObjectUIReact',
44
+ 'ag-grid-community': 'AgGridCommunity',
45
+ 'ag-grid-react': 'AgGridReact',
46
+ },
47
+ },
48
+ },
49
+ },
50
+ });