@visns-studio/visns-components 5.6.10 → 5.6.12

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/README.md CHANGED
@@ -191,12 +191,28 @@ A powerful data table component with sorting, filtering, and pagination.
191
191
  params: {},
192
192
  }}
193
193
  columns={[
194
- { field: 'id', headerName: 'ID', width: 90 },
195
- { field: 'name', headerName: 'Name', width: 150 },
194
+ { name: 'id', headerName: 'ID', width: 90 },
195
+ { name: 'name', headerName: 'Name', width: 150 },
196
+ {
197
+ name: 'department',
198
+ headerName: 'Department',
199
+ width: 180,
200
+ groupSummaryReducer: 'count',
201
+ groupSummaryRenderer: (count) => `${count} employees`,
202
+ },
203
+ {
204
+ name: 'salary',
205
+ headerName: 'Salary',
206
+ width: 120,
207
+ type: 'number',
208
+ groupSummaryReducer: 'sum',
209
+ groupSummaryRenderer: (sum) => `$${sum.toLocaleString()}`,
210
+ },
196
211
  // More columns...
197
212
  ]}
198
213
  form={formConfig}
199
214
  gridHeight={500}
215
+ defaultGroupBy={[{ name: 'department' }]}
200
216
  settings={{
201
217
  pagination: true,
202
218
  pageSize: 25,
@@ -214,16 +230,26 @@ A powerful data table component with sorting, filtering, and pagination.
214
230
 
215
231
  #### DataGrid Props
216
232
 
217
- | Prop | Description | Type | Default |
218
- | ------------- | ---------------------------------- | -------- | ------- |
219
- | ajaxSetting | Configuration for data fetching | Object | - |
220
- | columns | Column definitions | Array | - |
221
- | form | Form configuration for editing | Object | - |
222
- | gridHeight | Height of the grid | Number | 500 |
223
- | settings | Grid settings (pagination, etc.) | Object | - |
224
- | setFilterData | Callback for filter changes | Function | - |
225
- | setConfig | Callback for configuration changes | Function | - |
226
- | style | Custom styles | Object | - |
233
+ | Prop | Description | Type | Default |
234
+ | --------------------- | ------------------------------------------ | -------- | ------- |
235
+ | ajaxSetting | Configuration for data fetching | Object | - |
236
+ | columns | Column definitions | Array | - |
237
+ | defaultExpandedNodes | Initial expanded nodes for TreeGrid | Object | - |
238
+ | defaultGroupBy | Initial grouping configuration | Array | - |
239
+ | expandedNodes | Current expanded nodes for TreeGrid | Object | - |
240
+ | form | Form configuration for editing | Object | - |
241
+ | gridHeight | Height of the grid | Number | 500 |
242
+ | groupBy | Current grouping configuration | Array | - |
243
+ | loadNode | Function to load TreeGrid nodes on demand | Function | - |
244
+ | onExpandedNodesChange | Callback for TreeGrid node expand/collapse | Function | - |
245
+ | onGroupByChange | Callback for grouping changes | Function | - |
246
+ | renderGroupTitle | Custom renderer for group headers | Function | - |
247
+ | settings | Grid settings (pagination, etc.) | Object | - |
248
+ | setFilterData | Callback for filter changes | Function | - |
249
+ | setConfig | Callback for configuration changes | Function | - |
250
+ | style | Custom styles | Object | - |
251
+ | treeColumn | Column name to display TreeGrid hierarchy | String | - |
252
+ | treeNestingSize | Indentation size for TreeGrid levels | Number | 20 |
227
253
 
228
254
  #### Auto-Refresh Functionality
229
255
 
@@ -253,6 +279,444 @@ When auto-refresh is configured:
253
279
  - Setting `autoRefresh: true` uses the default 30-second interval
254
280
  - The component silently refreshes data without showing notifications
255
281
 
282
+ #### Group By Functionality
283
+
284
+ The DataGrid component supports grouping data by one or more columns, allowing you to organize and visualize data in hierarchical structures.
285
+
286
+ ##### Using defaultGroupBy
287
+
288
+ The `defaultGroupBy` property allows you to specify initial grouping when the DataGrid loads:
289
+
290
+ ```jsx
291
+ <DataGrid
292
+ // Other props...
293
+ defaultGroupBy={[
294
+ { name: 'category' }, // Group by a single column
295
+ ]}
296
+ />
297
+ ```
298
+
299
+ For more complex grouping with multiple levels:
300
+
301
+ ```jsx
302
+ <DataGrid
303
+ // Other props...
304
+ defaultGroupBy={[
305
+ { name: 'department' }, // First level grouping
306
+ { name: 'role' }, // Second level grouping
307
+ ]}
308
+ />
309
+ ```
310
+
311
+ ##### Dynamic Grouping
312
+
313
+ You can also control grouping programmatically:
314
+
315
+ ```jsx
316
+ const [groupBy, setGroupBy] = useState([{ name: 'category' }]);
317
+
318
+ // Later in your component
319
+ <DataGrid
320
+ // Other props...
321
+ groupBy={groupBy}
322
+ onGroupByChange={setGroupBy}
323
+ />;
324
+ ```
325
+
326
+ ##### Server-Side Grouping
327
+
328
+ For server-side grouping, the DataGrid sends the groupBy information to your API endpoint:
329
+
330
+ ```jsx
331
+ <DataGrid
332
+ ajaxSetting={{
333
+ url: '/api/data',
334
+ method: 'POST',
335
+ // Other settings...
336
+ }}
337
+ defaultGroupBy={[{ name: 'category' }]}
338
+ // Other props...
339
+ />
340
+ ```
341
+
342
+ Your server will receive a request with groupBy information:
343
+
344
+ ```json
345
+ {
346
+ "page": 1,
347
+ "take": 25,
348
+ "groupBy": [{ "name": "category" }]
349
+ // Other parameters...
350
+ }
351
+ ```
352
+
353
+ ##### Group Summaries
354
+
355
+ You can add summary calculations to grouped data:
356
+
357
+ ```jsx
358
+ <DataGrid
359
+ // Other props...
360
+ columns={[
361
+ {
362
+ name: 'amount',
363
+ headerName: 'Amount',
364
+ type: 'number',
365
+ groupSummaryReducer: 'sum', // Options: 'sum', 'avg', 'min', 'max', 'count'
366
+ groupSummaryRenderer: (summary) => `Total: $${summary.toFixed(2)}`,
367
+ },
368
+ // Other columns...
369
+ ]}
370
+ defaultGroupBy={[{ name: 'category' }]}
371
+ />
372
+ ```
373
+
374
+ ##### Customizing Group Rendering
375
+
376
+ You can customize how groups are displayed:
377
+
378
+ ```jsx
379
+ <DataGrid
380
+ // Other props...
381
+ renderGroupTitle={(groupData) => (
382
+ <div style={{ fontWeight: 'bold', color: 'var(--primary-color)' }}>
383
+ {groupData.name}: {groupData.value} ({groupData.count} items)
384
+ </div>
385
+ )}
386
+ defaultGroupBy={[{ name: 'category' }]}
387
+ />
388
+ ```
389
+
390
+ ##### Complete Example with Dynamic Grouping
391
+
392
+ Here's a complete example of a component that uses DataGrid with dynamic grouping controls:
393
+
394
+ ```jsx
395
+ import React, { useState, useEffect } from 'react';
396
+ import { DataGrid } from 'visns-components';
397
+
398
+ const EmployeeDataGrid = () => {
399
+ // State for grouping
400
+ const [groupBy, setGroupBy] = useState([]);
401
+
402
+ // Sample data for the grid
403
+ const [employees, setEmployees] = useState([]);
404
+
405
+ // Fetch data on component mount
406
+ useEffect(() => {
407
+ const fetchEmployees = async () => {
408
+ try {
409
+ const response = await fetch('/api/employees');
410
+ const data = await response.json();
411
+ setEmployees(data);
412
+ } catch (error) {
413
+ console.error('Error fetching employees:', error);
414
+ }
415
+ };
416
+
417
+ fetchEmployees();
418
+ }, []);
419
+
420
+ // Column definitions
421
+ const columns = [
422
+ {
423
+ name: 'id',
424
+ headerName: 'ID',
425
+ width: 80,
426
+ },
427
+ {
428
+ name: 'name',
429
+ headerName: 'Name',
430
+ width: 180,
431
+ },
432
+ {
433
+ name: 'department',
434
+ headerName: 'Department',
435
+ width: 150,
436
+ groupSummaryReducer: 'count',
437
+ groupSummaryRenderer: (count) => `${count} employees`,
438
+ },
439
+ {
440
+ name: 'role',
441
+ headerName: 'Role',
442
+ width: 150,
443
+ },
444
+ {
445
+ name: 'salary',
446
+ headerName: 'Salary',
447
+ width: 120,
448
+ type: 'number',
449
+ groupSummaryReducer: 'sum',
450
+ groupSummaryRenderer: (sum) => `$${sum.toLocaleString()}`,
451
+ },
452
+ {
453
+ name: 'hireDate',
454
+ headerName: 'Hire Date',
455
+ width: 120,
456
+ type: 'date',
457
+ },
458
+ ];
459
+
460
+ // Group by options for the dropdown
461
+ const groupByOptions = [
462
+ { label: 'No Grouping', value: 'none' },
463
+ { label: 'Department', value: 'department' },
464
+ { label: 'Role', value: 'role' },
465
+ { label: 'Department & Role', value: 'department-role' },
466
+ ];
467
+
468
+ // Handle group by change
469
+ const handleGroupByChange = (e) => {
470
+ const value = e.target.value;
471
+
472
+ switch (value) {
473
+ case 'department':
474
+ setGroupBy([{ name: 'department' }]);
475
+ break;
476
+ case 'role':
477
+ setGroupBy([{ name: 'role' }]);
478
+ break;
479
+ case 'department-role':
480
+ setGroupBy([{ name: 'department' }, { name: 'role' }]);
481
+ break;
482
+ default:
483
+ setGroupBy([]);
484
+ break;
485
+ }
486
+ };
487
+
488
+ return (
489
+ <div>
490
+ <div style={{ marginBottom: '1rem' }}>
491
+ <label htmlFor="groupBy">Group By: </label>
492
+ <select
493
+ id="groupBy"
494
+ onChange={handleGroupByChange}
495
+ style={{ padding: '0.5rem', borderRadius: '4px' }}
496
+ >
497
+ {groupByOptions.map((option) => (
498
+ <option key={option.value} value={option.value}>
499
+ {option.label}
500
+ </option>
501
+ ))}
502
+ </select>
503
+ </div>
504
+
505
+ <DataGrid
506
+ columns={columns}
507
+ dataSource={employees}
508
+ groupBy={groupBy}
509
+ onGroupByChange={setGroupBy}
510
+ gridHeight={600}
511
+ settings={{
512
+ pagination: true,
513
+ pageSize: 25,
514
+ }}
515
+ renderGroupTitle={(groupData) => (
516
+ <div
517
+ style={{
518
+ fontWeight: 'bold',
519
+ color: 'var(--primary-color)',
520
+ padding: '0.25rem 0',
521
+ }}
522
+ >
523
+ {groupData.name === 'department'
524
+ ? 'Department'
525
+ : 'Role'}
526
+ :<span style={{ marginLeft: '0.5rem' }}>
527
+ {groupData.value} ({groupData.count} employees)
528
+ </span>
529
+ </div>
530
+ )}
531
+ />
532
+ </div>
533
+ );
534
+ };
535
+
536
+ export default EmployeeDataGrid;
537
+ ```
538
+
539
+ #### TreeGrid Functionality
540
+
541
+ The DataGrid component also supports hierarchical data display through its TreeGrid functionality. This allows you to represent parent-child relationships in your data with expandable/collapsible nodes.
542
+
543
+ ##### Basic TreeGrid Configuration
544
+
545
+ To use the TreeGrid functionality:
546
+
547
+ ```jsx
548
+ <DataGrid
549
+ // Other props...
550
+ treeColumn="name" // Specify which column will display the expand/collapse icons
551
+ dataSource={hierarchicalData} // Data with parent-child relationships
552
+ />
553
+ ```
554
+
555
+ Your data should have a structure where parent nodes contain child nodes:
556
+
557
+ ```jsx
558
+ const hierarchicalData = [
559
+ {
560
+ id: 1,
561
+ name: 'Parent 1',
562
+ nodes: [
563
+ // Child nodes
564
+ { id: 11, name: 'Child 1.1' },
565
+ {
566
+ id: 12,
567
+ name: 'Child 1.2',
568
+ nodes: [
569
+ // Nested children
570
+ { id: 121, name: 'Grandchild 1.2.1' },
571
+ ],
572
+ },
573
+ ],
574
+ },
575
+ {
576
+ id: 2,
577
+ name: 'Parent 2',
578
+ nodes: [{ id: 21, name: 'Child 2.1' }],
579
+ },
580
+ ];
581
+ ```
582
+
583
+ ##### Controlling Expanded Nodes
584
+
585
+ You can control which nodes are expanded:
586
+
587
+ ```jsx
588
+ const [expandedNodes, setExpandedNodes] = useState({
589
+ 1: true, // Node with ID 1 is expanded
590
+ '1/12': true, // Node with path 1/12 is expanded
591
+ });
592
+
593
+ <DataGrid
594
+ // Other props...
595
+ treeColumn="name"
596
+ dataSource={hierarchicalData}
597
+ expandedNodes={expandedNodes}
598
+ onExpandedNodesChange={setExpandedNodes}
599
+ />;
600
+ ```
601
+
602
+ ##### Asynchronous Node Loading
603
+
604
+ For large datasets, you can load child nodes on demand:
605
+
606
+ ```jsx
607
+ <DataGrid
608
+ // Other props...
609
+ treeColumn="name"
610
+ dataSource={asyncHierarchicalData}
611
+ loadNode={({ node }) => {
612
+ // Return a Promise that resolves to the child nodes
613
+ return fetchChildNodes(node.id);
614
+ }}
615
+ />
616
+ ```
617
+
618
+ For asynchronous loading, parent nodes should have `nodes: null` to indicate they have children that need to be loaded.
619
+
620
+ ##### Complete TreeGrid Example
621
+
622
+ Here's a complete example of a component that uses DataGrid with TreeGrid functionality:
623
+
624
+ ```jsx
625
+ import React, { useState } from 'react';
626
+ import { DataGrid } from 'visns-components';
627
+
628
+ const OrganizationTreeGrid = () => {
629
+ // Sample hierarchical data
630
+ const departments = [
631
+ {
632
+ id: 1,
633
+ name: 'Executive',
634
+ budget: 1500000,
635
+ nodes: [
636
+ {
637
+ id: 11,
638
+ name: 'Finance',
639
+ budget: 850000,
640
+ nodes: [
641
+ { id: 111, name: 'Accounting', budget: 350000 },
642
+ { id: 112, name: 'Investment', budget: 500000 },
643
+ ],
644
+ },
645
+ {
646
+ id: 12,
647
+ name: 'Operations',
648
+ budget: 650000,
649
+ nodes: [
650
+ { id: 121, name: 'HR', budget: 200000 },
651
+ { id: 122, name: 'Facilities', budget: 450000 },
652
+ ],
653
+ },
654
+ ],
655
+ },
656
+ {
657
+ id: 2,
658
+ name: 'Sales & Marketing',
659
+ budget: 1200000,
660
+ nodes: [
661
+ { id: 21, name: 'Sales', budget: 700000 },
662
+ { id: 22, name: 'Marketing', budget: 500000 },
663
+ ],
664
+ },
665
+ {
666
+ id: 3,
667
+ name: 'Technology',
668
+ budget: 2000000,
669
+ nodes: [
670
+ { id: 31, name: 'Development', budget: 1200000 },
671
+ { id: 32, name: 'IT Support', budget: 800000 },
672
+ ],
673
+ },
674
+ ];
675
+
676
+ // State for expanded nodes
677
+ const [expandedNodes, setExpandedNodes] = useState({
678
+ 1: true, // Executive department is expanded by default
679
+ 3: true, // Technology department is expanded by default
680
+ });
681
+
682
+ // Column definitions
683
+ const columns = [
684
+ {
685
+ name: 'name',
686
+ headerName: 'Department',
687
+ width: 250,
688
+ // This column will display the tree expand/collapse icons
689
+ },
690
+ {
691
+ name: 'budget',
692
+ headerName: 'Budget',
693
+ width: 150,
694
+ type: 'number',
695
+ render: ({ value }) => `$${value.toLocaleString()}`,
696
+ },
697
+ ];
698
+
699
+ return (
700
+ <div>
701
+ <h2>Organization Structure</h2>
702
+
703
+ <DataGrid
704
+ columns={columns}
705
+ dataSource={departments}
706
+ treeColumn="name"
707
+ expandedNodes={expandedNodes}
708
+ onExpandedNodesChange={setExpandedNodes}
709
+ treeNestingSize={30} // Indentation size for nested levels
710
+ gridHeight={500}
711
+ idProperty="id"
712
+ />
713
+ </div>
714
+ );
715
+ };
716
+
717
+ export default OrganizationTreeGrid;
718
+ ```
719
+
256
720
  ### Form Components
257
721
 
258
722
  #### Form
package/package.json CHANGED
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^17.0.0 || ^18.0.0"
85
85
  },
86
86
  "name": "@visns-studio/visns-components",
87
- "version": "5.6.10",
87
+ "version": "5.6.12",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -935,6 +935,12 @@ const DataGrid = forwardRef(
935
935
  const cellElement = event.target.closest(
936
936
  '.InovuaReactDataGrid__cell'
937
937
  );
938
+
939
+ // Add null check to prevent errors when cellElement is null
940
+ if (!cellElement) {
941
+ return; // Exit early if no cell element was found
942
+ }
943
+
938
944
  const columnIndex = Array.from(
939
945
  cellElement.parentNode.children
940
946
  ).indexOf(cellElement);
@@ -1042,6 +1048,12 @@ const DataGrid = forwardRef(
1042
1048
  const cellElement = event.target.closest(
1043
1049
  '.InovuaReactDataGrid__cell'
1044
1050
  );
1051
+
1052
+ // Add null check to prevent errors when cellElement is null
1053
+ if (!cellElement) {
1054
+ return; // Exit early if no cell element was found
1055
+ }
1056
+
1045
1057
  const columnIndex = Array.from(
1046
1058
  cellElement.parentNode.children
1047
1059
  ).indexOf(cellElement);
@@ -2773,13 +2785,68 @@ const DataGrid = forwardRef(
2773
2785
  e.preventDefault();
2774
2786
 
2775
2787
  // Check if every required field in column.validate exists and is truthy in data
2776
- const isValid =
2777
- column.validate?.every(
2778
- (v) =>
2779
- data[v]
2780
- ) ?? true;
2788
+ if (
2789
+ column.validate &&
2790
+ column.validate
2791
+ .length > 0
2792
+ ) {
2793
+ const missingFields =
2794
+ column.validate.filter(
2795
+ (v) =>
2796
+ !data[
2797
+ v
2798
+ ]
2799
+ );
2781
2800
 
2782
- if (isValid) {
2801
+ if (
2802
+ missingFields.length ===
2803
+ 0
2804
+ ) {
2805
+ handleTimer(
2806
+ data[
2807
+ form
2808
+ .primaryKey
2809
+ ],
2810
+ column.id
2811
+ );
2812
+ } else {
2813
+ // Format field names to be more readable
2814
+ const formattedFields =
2815
+ missingFields.map(
2816
+ (
2817
+ field
2818
+ ) => {
2819
+ // Try to find the column with this field ID to get its label
2820
+ const fieldColumn =
2821
+ columns.find(
2822
+ (
2823
+ col
2824
+ ) =>
2825
+ col.id ===
2826
+ field
2827
+ );
2828
+ // Use the label if found, otherwise use the field ID
2829
+ return (
2830
+ fieldColumn?.label ||
2831
+ field
2832
+ );
2833
+ }
2834
+ );
2835
+
2836
+ const fieldList =
2837
+ formattedFields.join(
2838
+ ', '
2839
+ );
2840
+ toast.error(
2841
+ `Please complete the following required field${
2842
+ missingFields.length >
2843
+ 1
2844
+ ? 's'
2845
+ : ''
2846
+ } before starting the timer: ${fieldList}`
2847
+ );
2848
+ }
2849
+ } else {
2783
2850
  handleTimer(
2784
2851
  data[
2785
2852
  form
@@ -2787,10 +2854,6 @@ const DataGrid = forwardRef(
2787
2854
  ],
2788
2855
  column.id
2789
2856
  );
2790
- } else {
2791
- toast.error(
2792
- 'Please complete the required fields before starting the timer.'
2793
- );
2794
2857
  }
2795
2858
  }}
2796
2859
  >
@@ -3206,6 +3269,22 @@ const DataGrid = forwardRef(
3206
3269
  };
3207
3270
  }, []);
3208
3271
 
3272
+ // Reset state when ajaxSetting changes (e.g., when navigating to a new page)
3273
+ useEffect(() => {
3274
+ // Reset selected state
3275
+ setSelected({});
3276
+
3277
+ // Reset filter value if needed
3278
+ if (setFilterData) {
3279
+ setFilterData([]);
3280
+ }
3281
+
3282
+ // Force reload of data
3283
+ if (gridRef.current && gridRef.current.paginationProps) {
3284
+ gridRef.current.paginationProps.reload();
3285
+ }
3286
+ }, [ajaxSetting?.url]);
3287
+
3209
3288
  // Auto-refresh effect
3210
3289
  useEffect(() => {
3211
3290
  // Clear any existing timer
@@ -3258,9 +3337,15 @@ const DataGrid = forwardRef(
3258
3337
  <div className={styles.dataGridContainer}>
3259
3338
  {limit && limit > 0 ? (
3260
3339
  <ReactDataGrid
3340
+ key={`datagrid-${ajaxSetting?.url || ''}-${
3341
+ ajaxSetting?.groupBy ? 'grouped' : 'ungrouped'
3342
+ }`}
3261
3343
  {...tableSetting}
3262
3344
  columns={gridColumns}
3263
3345
  dataSource={dataSource}
3346
+ {...(ajaxSetting && ajaxSetting.groupBy
3347
+ ? { defaultGroupBy: ajaxSetting.groupBy }
3348
+ : {})}
3264
3349
  filterValue={filterValue}
3265
3350
  limit={limit}
3266
3351
  enableColumnAutosize={false}