@visns-studio/visns-components 5.6.9 → 5.6.11

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.9",
87
+ "version": "5.6.11",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -2,6 +2,8 @@ import React from 'react';
2
2
  import { SortableElement, sortableHandle } from 'react-sortable-hoc';
3
3
  import { ChevronVertical, Pencil, TrashCan } from 'akar-icons';
4
4
 
5
+ import styles from './styles/Item.module.scss';
6
+
5
7
  const DragHandle = sortableHandle(() => (
6
8
  <ChevronVertical className="drag-handle" strokeWidth={1} size={26} />
7
9
  ));
@@ -63,9 +65,9 @@ const SortableItem = ({
63
65
 
64
66
  const EditButton = () => (
65
67
  <Pencil
66
- className="delete pencil-edit"
67
- strokeWidth={1}
68
- size={26}
68
+ className="pencil-edit"
69
+ strokeWidth={2}
70
+ size={24}
69
71
  onClick={(e) => {
70
72
  e.preventDefault();
71
73
  e.stopPropagation();
@@ -91,22 +93,26 @@ const SortableItem = ({
91
93
 
92
94
  {renderImage(value)}
93
95
 
94
- {renderEditButton()}
96
+ {(handleEdit || handleDelete) && (
97
+ <div className={styles.actionIcons}>
98
+ {renderEditButton()}
95
99
 
96
- {handleDelete && (
97
- <TrashCan
98
- className="delete"
99
- strokeWidth={1}
100
- size={26}
101
- onClick={(e) => {
102
- e.preventDefault();
103
- e.stopPropagation();
104
- handleDelete(
105
- value.id || containerIndex,
106
- value.label || value.file_name
107
- );
108
- }}
109
- />
100
+ {handleDelete && (
101
+ <TrashCan
102
+ className="delete"
103
+ strokeWidth={2}
104
+ size={24}
105
+ onClick={(e) => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ handleDelete(
109
+ value.id || containerIndex,
110
+ value.label || value.file_name
111
+ );
112
+ }}
113
+ />
114
+ )}
115
+ </div>
110
116
  )}
111
117
  </li>
112
118
  );
@@ -2,6 +2,8 @@ import React from 'react';
2
2
  import VisnsSortableItem from './Item';
3
3
  import { SortableContainer } from 'react-sortable-hoc';
4
4
 
5
+ import styles from './styles/List.module.scss';
6
+
5
7
  const SortableList = ({
6
8
  handleClick,
7
9
  handleDelete,
@@ -10,7 +12,7 @@ const SortableList = ({
10
12
  showImage,
11
13
  }) => {
12
14
  return (
13
- <ul className="sortlist">
15
+ <ul className={styles.sortlist}>
14
16
  {items.map((value, index) => (
15
17
  <VisnsSortableItem
16
18
  key={`item-${index}`}
@@ -0,0 +1,38 @@
1
+ .actionIcons {
2
+ position: absolute;
3
+ right: 10px;
4
+ top: 50%;
5
+ transform: translateY(-50%);
6
+ display: flex;
7
+ gap: 12px;
8
+ align-items: center;
9
+ z-index: 10;
10
+ }
11
+
12
+ :global {
13
+ .delete {
14
+ width: 22px;
15
+ height: 22px;
16
+ color: var(--primary-color);
17
+ transition: color 0.2s ease;
18
+ cursor: pointer;
19
+ display: block;
20
+
21
+ &:hover {
22
+ color: #d32f2f;
23
+ }
24
+ }
25
+
26
+ .pencil-edit {
27
+ width: 22px;
28
+ height: 22px;
29
+ color: var(--primary-color);
30
+ transition: color 0.2s ease;
31
+ cursor: pointer;
32
+ display: block;
33
+
34
+ &:hover {
35
+ color: var(--secondary-color);
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,74 @@
1
+ .sortlist {
2
+ width: 100%;
3
+ display: grid;
4
+ grid-gap: 0.4rem;
5
+ grid-template-columns: repeat(1, 1fr);
6
+ box-sizing: border-box;
7
+ padding: 0;
8
+ margin: 0;
9
+ font-size: 0.8rem;
10
+ list-style: none;
11
+
12
+ @media (min-width: 576px) {
13
+ grid-template-columns: repeat(1, 1fr);
14
+ }
15
+
16
+ @media (min-width: 992px) {
17
+ grid-template-columns: repeat(1, 1fr);
18
+ }
19
+
20
+ li {
21
+ display: flex;
22
+ justify-content: flex-start;
23
+ align-items: center;
24
+ flex-wrap: wrap;
25
+ border-radius: 6px;
26
+ border: 1px solid rgba(var(--primary-rgb), 0.2);
27
+ box-shadow: 0px 0px 10px rgba(var(--primary-rgb), 0.1);
28
+ padding: 0.8rem 5rem 0.8rem 2rem;
29
+ margin-bottom: 0.5rem;
30
+ font-size: 0.9rem;
31
+ font-weight: 500;
32
+ transition: all 0.2s ease;
33
+
34
+ box-sizing: border-box;
35
+ cursor: pointer;
36
+ list-style: none;
37
+ position: relative;
38
+ color: var(--paragraph-color);
39
+ user-select: none;
40
+
41
+ &:hover {
42
+ border-color: rgba(var(--primary-rgb), 0.4);
43
+ box-shadow: 0px 0px 12px rgba(var(--primary-rgb), 0.2);
44
+ transform: translateY(-1px);
45
+ }
46
+
47
+ > svg:first-child {
48
+ width: 100%;
49
+ max-width: 18px;
50
+ position: absolute;
51
+ left: 6px;
52
+ top: 50%;
53
+ transform: translateY(-50%);
54
+ color: var(--primary-color);
55
+ }
56
+ }
57
+
58
+ .sortimg {
59
+ width: 40px;
60
+ position: relative;
61
+ height: 40px;
62
+ margin-left: auto;
63
+ border-radius: 4px;
64
+ overflow: hidden;
65
+
66
+ img {
67
+ display: block;
68
+ object-fit: cover;
69
+ height: 100%;
70
+ width: 100%;
71
+ overflow: hidden;
72
+ }
73
+ }
74
+ }
@@ -3206,6 +3206,22 @@ const DataGrid = forwardRef(
3206
3206
  };
3207
3207
  }, []);
3208
3208
 
3209
+ // Reset state when ajaxSetting changes (e.g., when navigating to a new page)
3210
+ useEffect(() => {
3211
+ // Reset selected state
3212
+ setSelected({});
3213
+
3214
+ // Reset filter value if needed
3215
+ if (setFilterData) {
3216
+ setFilterData([]);
3217
+ }
3218
+
3219
+ // Force reload of data
3220
+ if (gridRef.current && gridRef.current.paginationProps) {
3221
+ gridRef.current.paginationProps.reload();
3222
+ }
3223
+ }, [ajaxSetting?.url]);
3224
+
3209
3225
  // Auto-refresh effect
3210
3226
  useEffect(() => {
3211
3227
  // Clear any existing timer
@@ -3258,9 +3274,15 @@ const DataGrid = forwardRef(
3258
3274
  <div className={styles.dataGridContainer}>
3259
3275
  {limit && limit > 0 ? (
3260
3276
  <ReactDataGrid
3277
+ key={`datagrid-${ajaxSetting?.url || ''}-${
3278
+ ajaxSetting?.groupBy ? 'grouped' : 'ungrouped'
3279
+ }`}
3261
3280
  {...tableSetting}
3262
3281
  columns={gridColumns}
3263
3282
  dataSource={dataSource}
3283
+ {...(ajaxSetting && ajaxSetting.groupBy
3284
+ ? { defaultGroupBy: ajaxSetting.groupBy }
3285
+ : {})}
3264
3286
  filterValue={filterValue}
3265
3287
  limit={limit}
3266
3288
  enableColumnAutosize={false}
@@ -424,7 +424,7 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
424
424
  const sizeClassMap = {
425
425
  full: styles.fwBuilderItem,
426
426
  half: styles.halfBuilderItem,
427
- quarter: styles.qtrBuilderItem,
427
+ quarter: styles.halfBuilderItem, // Changed to halfBuilderItem to ensure max 2 per row
428
428
  };
429
429
 
430
430
  return `${styles.formBuilderItem} ${
@@ -855,6 +855,12 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
855
855
  <div className={styles.grid}>
856
856
  <div className={styles.grid__subrow}>
857
857
  <div className={styles.grid__subnav}>
858
+ <div
859
+ className={styles.gridtxt__header}
860
+ style={{ marginBottom: '10px' }}
861
+ >
862
+ <span>Form Fields</span>
863
+ </div>
858
864
  {data.detail && data.detail.length > 0 ? (
859
865
  <div className={styles.sortableListContainer}>
860
866
  <SortableList
@@ -869,7 +875,19 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
869
875
  useDragHandle
870
876
  />
871
877
  </div>
872
- ) : null}
878
+ ) : (
879
+ <div
880
+ style={{
881
+ padding: '10px',
882
+ textAlign: 'center',
883
+ color: 'rgba(var(--paragraph-color-rgb), 0.7)',
884
+ fontSize: '0.9rem',
885
+ }}
886
+ >
887
+ No fields added yet. Click "Add Field" to get
888
+ started.
889
+ </div>
890
+ )}
873
891
  </div>
874
892
  <div className={styles.grid__subcontent}>
875
893
  <div className={styles.formSplit}>
@@ -1165,7 +1183,7 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1165
1183
  'description'
1166
1184
  );
1167
1185
  }}
1168
- onInit={(evt, editor) =>
1186
+ onInit={(_, editor) =>
1169
1187
  (editorRef.current = editor)
1170
1188
  }
1171
1189
  value={
@@ -22,19 +22,19 @@
22
22
  &__subrow {
23
23
  width: 100%;
24
24
  display: flex;
25
- flex-wrap: wrap;
25
+ flex-wrap: nowrap;
26
26
  height: auto;
27
27
  }
28
28
 
29
29
  &__subnav {
30
- flex: 1;
30
+ flex: 0 0 20%;
31
31
  background: white;
32
32
  border-radius: var(--br);
33
33
  box-sizing: border-box;
34
- padding: 5px;
35
- margin: 8px 0;
34
+ padding: 8px;
35
+ margin: 8px 8px 8px 0;
36
36
  height: auto;
37
- box-shadow: 0 10px 50px rgba(var(--primary-rgb), 0.05);
37
+ box-shadow: 0 5px 15px rgba(var(--primary-rgb), 0.05);
38
38
 
39
39
  > ul {
40
40
  width: 100%;
@@ -114,7 +114,7 @@
114
114
  }
115
115
 
116
116
  &__subcontent {
117
- flex: 0 1 85%;
117
+ flex: 0 1 80%;
118
118
  background: white;
119
119
  border-radius: var(--br);
120
120
  border: 1px solid var(--border-color);
@@ -138,14 +138,14 @@
138
138
  display: block;
139
139
  background: var(--border-color);
140
140
  box-sizing: border-box;
141
- padding: 10px 20px;
142
- border-top-left-radius: var(--br);
143
- border-top-right-radius: var(--br);
141
+ padding: 10px 15px;
142
+ border-radius: var(--br);
144
143
  margin-bottom: 0;
145
- font-weight: 700;
144
+ font-weight: 600;
146
145
  text-align: center;
147
146
  color: var(--paragraph-color);
148
147
  position: relative;
148
+ font-size: 0.95rem;
149
149
 
150
150
  span {
151
151
  font-weight: 700;
@@ -257,7 +257,7 @@ input[type='file']:hover {
257
257
  }
258
258
 
259
259
  .formBuilderItem {
260
- width: 25%;
260
+ width: 50%;
261
261
  padding: 0.35em;
262
262
  position: relative;
263
263
  box-sizing: border-box;
@@ -318,7 +318,7 @@ input[type='file']:hover {
318
318
  }
319
319
 
320
320
  .qtrBuilderItem {
321
- flex-basis: 25%;
321
+ flex-basis: 50%;
322
322
  }
323
323
 
324
324
  .halfBuilderItem {
@@ -871,45 +871,73 @@ select:not(:placeholder-shown) + .fi__span {
871
871
  .sortableListContainer {
872
872
  max-height: 800px; /* Optional for scrollable content */
873
873
  overflow-y: auto;
874
+ padding: 0.75rem;
875
+ border-radius: var(--br);
876
+ background-color: rgba(var(--primary-rgb), 0.02);
874
877
  }
875
878
 
876
879
  .dragitem {
877
880
  list-style: none;
878
881
  display: flex;
879
- justify-content: center;
882
+ justify-content: flex-start;
880
883
  align-items: center;
881
884
  flex-wrap: wrap;
882
- border-radius: 5px;
885
+ border-radius: 6px;
883
886
  border: 1px solid rgba(var(--primary-rgb), 0.2);
884
- box-shadow: 0px 0px 30px rgba(var(--primary-rgb), 0.1);
885
- padding: 1.25rem 1.25rem 1.25rem 2rem;
887
+ box-shadow: 0px 0px 15px rgba(var(--primary-rgb), 0.15);
888
+ padding: 0.8rem 5rem 0.8rem 2rem;
886
889
  box-sizing: border-box;
887
890
  cursor: grab;
888
891
  list-style: none;
889
892
  color: var(--paragraph-color);
893
+ font-size: 0.9rem;
894
+ font-weight: 500;
890
895
 
891
- svg {
896
+ > svg:first-child {
892
897
  width: 100%;
893
898
  max-width: 18px;
894
899
  position: absolute;
895
- left: 5px;
896
- top: 5px;
900
+ left: 6px;
901
+ top: 50%;
902
+ transform: translateY(-50%);
903
+ color: var(--primary-color);
897
904
  }
898
905
 
899
- .edit {
900
- width: 100%;
901
- max-width: 18px;
906
+ .actionIcons {
902
907
  position: absolute;
903
- left: 5px;
904
- top: 30px;
908
+ right: 10px;
909
+ top: 50%;
910
+ transform: translateY(-50%);
911
+ display: flex;
912
+ gap: 12px;
913
+ align-items: center;
914
+ z-index: 10;
915
+ }
916
+
917
+ .edit {
918
+ width: 22px;
919
+ height: 22px;
920
+ color: var(--primary-color);
921
+ transition: color 0.2s ease;
922
+ cursor: pointer;
923
+ display: block;
924
+
925
+ &:hover {
926
+ color: var(--secondary-color);
927
+ }
905
928
  }
906
929
 
907
930
  .delete {
908
- width: 100%;
909
- max-width: 18px;
910
- position: absolute;
911
- left: 5px;
912
- top: 60px;
931
+ width: 22px;
932
+ height: 22px;
933
+ color: var(--primary-color);
934
+ transition: color 0.2s ease;
935
+ cursor: pointer;
936
+ display: block;
937
+
938
+ &:hover {
939
+ color: #d32f2f;
940
+ }
913
941
  }
914
942
 
915
943
  .sortimg {
@@ -124,28 +124,50 @@
124
124
  color: var(--paragraph-color);
125
125
  font-size: 0.8rem;
126
126
 
127
- svg {
127
+ > svg:first-child {
128
128
  width: 100%;
129
129
  max-width: 16px;
130
130
  position: absolute;
131
131
  left: 4px;
132
- top: 4px;
132
+ top: 50%;
133
+ transform: translateY(-50%);
133
134
  }
134
135
 
135
- .pencil-edit {
136
- width: 100%;
137
- max-width: 16px;
136
+ .actionIcons {
138
137
  position: absolute;
139
- left: 4px;
140
- top: 24px;
138
+ right: 10px;
139
+ top: 50%;
140
+ transform: translateY(-50%);
141
+ display: flex;
142
+ gap: 12px;
143
+ align-items: center;
144
+ z-index: 10;
145
+ }
146
+
147
+ .pencil-edit {
148
+ width: 22px;
149
+ height: 22px;
150
+ color: var(--primary-color);
151
+ transition: color 0.2s ease;
152
+ cursor: pointer;
153
+ display: block;
154
+
155
+ &:hover {
156
+ color: var(--secondary-color);
157
+ }
141
158
  }
142
159
 
143
160
  .trash-delete {
144
- width: 100%;
145
- max-width: 16px;
146
- position: absolute;
147
- left: 4px;
148
- top: 44px;
161
+ width: 22px;
162
+ height: 22px;
163
+ color: var(--primary-color);
164
+ transition: color 0.2s ease;
165
+ cursor: pointer;
166
+ display: block;
167
+
168
+ &:hover {
169
+ color: #d32f2f;
170
+ }
149
171
  }
150
172
 
151
173
  .sortimg {
@@ -65,9 +65,9 @@ const SortableItem = ({
65
65
 
66
66
  const EditButton = () => (
67
67
  <Pencil
68
- className={styles['edit']}
69
- strokeWidth={1}
70
- size={22}
68
+ className={styles.edit}
69
+ strokeWidth={2}
70
+ size={24}
71
71
  onClick={(e) => {
72
72
  e.preventDefault();
73
73
  e.stopPropagation();
@@ -93,22 +93,26 @@ const SortableItem = ({
93
93
 
94
94
  {renderImage(value)}
95
95
 
96
- {renderEditButton()}
96
+ {(handleEdit || handleDelete) && (
97
+ <div className={styles.actionIcons}>
98
+ {renderEditButton()}
97
99
 
98
- {handleDelete && (
99
- <TrashCan
100
- className={styles.delete}
101
- strokeWidth={1}
102
- size={22}
103
- onClick={(e) => {
104
- e.preventDefault();
105
- e.stopPropagation();
106
- handleDelete(
107
- value.id || containerIndex,
108
- value.label || value.file_name
109
- );
110
- }}
111
- />
100
+ {handleDelete && (
101
+ <TrashCan
102
+ className={styles.delete}
103
+ strokeWidth={2}
104
+ size={24}
105
+ onClick={(e) => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ handleDelete(
109
+ value.id || containerIndex,
110
+ value.label || value.file_name
111
+ );
112
+ }}
113
+ />
114
+ )}
115
+ </div>
112
116
  )}
113
117
  </li>
114
118
  );
@@ -1,15 +1,36 @@
1
- .edit {
2
- width: 100%;
3
- max-width: 16px;
1
+ .actionIcons {
4
2
  position: absolute;
5
- left: 4px !important;
6
- top: 24px !important;
3
+ right: 10px;
4
+ top: 50%;
5
+ transform: translateY(-50%);
6
+ display: flex;
7
+ gap: 12px;
8
+ align-items: center;
9
+ z-index: 10;
10
+ }
11
+
12
+ .edit {
13
+ width: 22px;
14
+ height: 22px;
15
+ color: var(--primary-color);
16
+ transition: color 0.2s ease;
17
+ cursor: pointer;
18
+ display: block;
19
+
20
+ &:hover {
21
+ color: var(--secondary-color);
22
+ }
7
23
  }
8
24
 
9
25
  .delete {
10
- width: 100%;
11
- max-width: 16px;
12
- position: absolute;
13
- left: 4px !important;
14
- top: 24px !important;
26
+ width: 22px;
27
+ height: 22px;
28
+ color: var(--primary-color);
29
+ transition: color 0.2s ease;
30
+ cursor: pointer;
31
+ display: block;
32
+
33
+ &:hover {
34
+ color: #d32f2f;
35
+ }
15
36
  }
@@ -10,22 +10,26 @@
10
10
  list-style: none;
11
11
 
12
12
  @media (min-width: 576px) {
13
- grid-template-columns: repeat(2, 1fr);
13
+ grid-template-columns: repeat(1, 1fr);
14
14
  }
15
15
 
16
16
  @media (min-width: 992px) {
17
- grid-template-columns: repeat(3, 1fr);
17
+ grid-template-columns: repeat(1, 1fr);
18
18
  }
19
19
 
20
20
  li {
21
21
  display: flex;
22
- justify-content: center;
22
+ justify-content: flex-start;
23
23
  align-items: center;
24
24
  flex-wrap: wrap;
25
- border-radius: 4px;
25
+ border-radius: 6px;
26
26
  border: 1px solid rgba(var(--primary-rgb), 0.2);
27
27
  box-shadow: 0px 0px 10px rgba(var(--primary-rgb), 0.1);
28
- padding: 0.6rem 0.6rem 0.6rem 1.5rem;
28
+ padding: 0.8rem 5rem 0.8rem 2rem;
29
+ margin-bottom: 0.5rem;
30
+ font-size: 0.9rem;
31
+ font-weight: 500;
32
+ transition: all 0.2s ease;
29
33
 
30
34
  box-sizing: border-box;
31
35
  cursor: pointer;
@@ -34,12 +38,20 @@
34
38
  color: var(--paragraph-color);
35
39
  user-select: none;
36
40
 
37
- svg {
41
+ &:hover {
42
+ border-color: rgba(var(--primary-rgb), 0.4);
43
+ box-shadow: 0px 0px 12px rgba(var(--primary-rgb), 0.2);
44
+ transform: translateY(-1px);
45
+ }
46
+
47
+ > svg:first-child {
38
48
  width: 100%;
39
- max-width: 16px;
49
+ max-width: 18px;
40
50
  position: absolute;
41
- left: 4px;
42
- top: 4px;
51
+ left: 6px;
52
+ top: 50%;
53
+ transform: translateY(-50%);
54
+ color: var(--primary-color);
43
55
  }
44
56
  }
45
57