@visns-studio/visns-components 5.12.0 → 5.12.2
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
|
@@ -625,6 +625,197 @@ You can add summary calculations to grouped data:
|
|
|
625
625
|
/>
|
|
626
626
|
```
|
|
627
627
|
|
|
628
|
+
### Group-Level Bulk Edit Operations (Enhanced v5.13.0+)
|
|
629
|
+
|
|
630
|
+
The DataGrid component now supports powerful bulk editing capabilities through group-based operations, allowing users to efficiently manage multiple items simultaneously.
|
|
631
|
+
|
|
632
|
+
#### How Group Bulk Edit Works
|
|
633
|
+
|
|
634
|
+
When data is grouped using the `groupBy` or `defaultGroupBy` properties, each group header can display action icons for common operations. The new bulk edit functionality adds an "edit" icon that opens a form modal for updating all items in the group simultaneously.
|
|
635
|
+
|
|
636
|
+
#### Configuration Structure
|
|
637
|
+
|
|
638
|
+
Group operations are configured through the `ajaxSetting.groupBySetting.icons` array:
|
|
639
|
+
|
|
640
|
+
```jsx
|
|
641
|
+
<DataGrid
|
|
642
|
+
ajaxSetting={{
|
|
643
|
+
url: '/api/data',
|
|
644
|
+
groupBy: ['category'],
|
|
645
|
+
groupBySetting: {
|
|
646
|
+
icons: [
|
|
647
|
+
{
|
|
648
|
+
id: 'edit',
|
|
649
|
+
label: 'Edit Group',
|
|
650
|
+
formModal: {
|
|
651
|
+
title: 'Bulk Edit Items',
|
|
652
|
+
url: '/ajax/data/bulkFormUpdate',
|
|
653
|
+
method: 'POST',
|
|
654
|
+
groupKey: 'category',
|
|
655
|
+
fields: ['assigned_user_id', 'status'],
|
|
656
|
+
data: {
|
|
657
|
+
stage: 'processing'
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
show: [
|
|
661
|
+
{
|
|
662
|
+
id: 'status',
|
|
663
|
+
value: 'pending'
|
|
664
|
+
}
|
|
665
|
+
]
|
|
666
|
+
}
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
}}
|
|
670
|
+
// Other props...
|
|
671
|
+
/>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
#### Configuration Properties
|
|
675
|
+
|
|
676
|
+
**Icon Configuration:**
|
|
677
|
+
- `id`: Must be set to `'edit'` to enable bulk edit functionality
|
|
678
|
+
- `label`: Display text for the action button
|
|
679
|
+
- `formModal`: Configuration object for the bulk edit form
|
|
680
|
+
- `show`: Optional conditions to control when the icon appears
|
|
681
|
+
|
|
682
|
+
**Form Modal Configuration:**
|
|
683
|
+
- `title`: Title displayed in the bulk edit modal
|
|
684
|
+
- `url`: API endpoint for bulk update requests
|
|
685
|
+
- `method`: HTTP method (typically 'POST')
|
|
686
|
+
- `groupKey`: Field name used for grouping (matches the groupBy value)
|
|
687
|
+
- `fields`: Array of field IDs that can be bulk edited
|
|
688
|
+
- `data`: Additional data to send with the bulk update request
|
|
689
|
+
|
|
690
|
+
#### Form Integration
|
|
691
|
+
|
|
692
|
+
The bulk edit feature integrates seamlessly with the existing Form component. When the edit icon is clicked:
|
|
693
|
+
|
|
694
|
+
1. A form modal opens with the specified title
|
|
695
|
+
2. Only the fields listed in the `fields` array are displayed
|
|
696
|
+
3. The form shows an indication that this is a bulk operation
|
|
697
|
+
4. On submission, all items in the group are updated simultaneously
|
|
698
|
+
|
|
699
|
+
#### Backend Integration
|
|
700
|
+
|
|
701
|
+
The bulk edit functionality expects a specific API endpoint structure:
|
|
702
|
+
|
|
703
|
+
```javascript
|
|
704
|
+
// POST /ajax/data/bulkFormUpdate
|
|
705
|
+
{
|
|
706
|
+
"bulkEdit": true,
|
|
707
|
+
"groupValue": "Category Name",
|
|
708
|
+
"groupKey": "category",
|
|
709
|
+
"rowIds": [1, 2, 3, 4],
|
|
710
|
+
"assigned_user_id": 5,
|
|
711
|
+
"status": "in_progress",
|
|
712
|
+
// Additional form fields...
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Expected Response:
|
|
716
|
+
{
|
|
717
|
+
"error": "",
|
|
718
|
+
"message": "Bulk update completed successfully",
|
|
719
|
+
"data": {
|
|
720
|
+
"updated_count": 4,
|
|
721
|
+
"group_value": "Category Name",
|
|
722
|
+
"updated_items": [...]
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### Features and Benefits
|
|
728
|
+
|
|
729
|
+
**User Experience:**
|
|
730
|
+
- **No Confirmation Dialog**: Edit actions open the form immediately (unlike timer operations)
|
|
731
|
+
- **No Toast Notifications**: Silent operation that focuses on the form interaction
|
|
732
|
+
- **Visual Group Context**: Modal title includes group information and item count
|
|
733
|
+
- **Field Filtering**: Only relevant fields are shown for bulk editing
|
|
734
|
+
|
|
735
|
+
**Technical Features:**
|
|
736
|
+
- **Transaction Safety**: All updates wrapped in database transactions
|
|
737
|
+
- **Partial Updates**: Only changed fields are updated
|
|
738
|
+
- **Audit Trail**: Full change tracking for compliance
|
|
739
|
+
- **Error Handling**: Graceful handling of validation errors and conflicts
|
|
740
|
+
- **Performance Optimized**: Efficient bulk operations with minimal database calls
|
|
741
|
+
|
|
742
|
+
#### Advanced Example
|
|
743
|
+
|
|
744
|
+
```jsx
|
|
745
|
+
// Manufacturing workflow with bulk edit capabilities
|
|
746
|
+
<DataGrid
|
|
747
|
+
ajaxSetting={{
|
|
748
|
+
url: '/ajax/manufacturing/items',
|
|
749
|
+
groupBy: ['work_order'],
|
|
750
|
+
groupBySetting: {
|
|
751
|
+
icons: [
|
|
752
|
+
// Bulk edit for user assignment
|
|
753
|
+
{
|
|
754
|
+
id: 'edit',
|
|
755
|
+
label: 'Assign Operator',
|
|
756
|
+
formModal: {
|
|
757
|
+
title: 'Bulk Assign Operator',
|
|
758
|
+
url: '/ajax/manufacturing/bulkAssign',
|
|
759
|
+
method: 'POST',
|
|
760
|
+
groupKey: 'work_order',
|
|
761
|
+
fields: ['operator_id', 'priority'],
|
|
762
|
+
data: {
|
|
763
|
+
stage: 'production',
|
|
764
|
+
update_type: 'assignment'
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
show: [
|
|
768
|
+
{ id: 'status', value: 'ready' }
|
|
769
|
+
]
|
|
770
|
+
},
|
|
771
|
+
// Timer operations (existing functionality)
|
|
772
|
+
{
|
|
773
|
+
id: 'clock',
|
|
774
|
+
label: 'Start Production',
|
|
775
|
+
fetch: {
|
|
776
|
+
url: '/ajax/manufacturing/groupTimer',
|
|
777
|
+
method: 'POST',
|
|
778
|
+
data: {
|
|
779
|
+
action: 'start_production',
|
|
780
|
+
timestamp: 'now()'
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
]
|
|
785
|
+
}
|
|
786
|
+
}}
|
|
787
|
+
columns={[
|
|
788
|
+
{ name: 'item_code', headerName: 'Item Code' },
|
|
789
|
+
{ name: 'operator', headerName: 'Operator' },
|
|
790
|
+
{ name: 'status', headerName: 'Status' },
|
|
791
|
+
{ name: 'priority', headerName: 'Priority' }
|
|
792
|
+
]}
|
|
793
|
+
form={{
|
|
794
|
+
fields: [
|
|
795
|
+
{
|
|
796
|
+
id: 'operator_id',
|
|
797
|
+
label: 'Production Operator',
|
|
798
|
+
type: 'dropdown-ajax',
|
|
799
|
+
url: '/ajax/users/operators',
|
|
800
|
+
required: true
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
id: 'priority',
|
|
804
|
+
label: 'Priority Level',
|
|
805
|
+
type: 'select',
|
|
806
|
+
options: [
|
|
807
|
+
{ value: 'normal', label: 'Normal' },
|
|
808
|
+
{ value: 'high', label: 'High' },
|
|
809
|
+
{ value: 'urgent', label: 'Urgent' }
|
|
810
|
+
]
|
|
811
|
+
}
|
|
812
|
+
]
|
|
813
|
+
}}
|
|
814
|
+
/>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
This implementation provides a powerful and intuitive way to manage bulk operations in grouped data scenarios, significantly improving workflow efficiency for users managing large datasets.
|
|
818
|
+
|
|
628
819
|
##### Customizing Group Rendering
|
|
629
820
|
|
|
630
821
|
You can customize how groups are displayed:
|
package/package.json
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"@tinymce/miniature": "^6.0.0",
|
|
15
15
|
"@tinymce/tinymce-react": "^6.2.1",
|
|
16
16
|
"@uiw/react-color": "^2.6.0",
|
|
17
|
-
"@visns-studio/visns-datagrid-community": "
|
|
18
|
-
"@visns-studio/visns-datagrid-enterprise": "
|
|
17
|
+
"@visns-studio/visns-datagrid-community": "1.1.0",
|
|
18
|
+
"@visns-studio/visns-datagrid-enterprise": "1.1.0",
|
|
19
19
|
"@vitejs/plugin-react": "^4.6.0",
|
|
20
20
|
"add": "^2.0.6",
|
|
21
21
|
"array-move": "^4.0.0",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
88
88
|
},
|
|
89
89
|
"name": "@visns-studio/visns-components",
|
|
90
|
-
"version": "5.12.
|
|
90
|
+
"version": "5.12.2",
|
|
91
91
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
92
92
|
"main": "src/index.js",
|
|
93
93
|
"files": [
|
package/src/components/Field.jsx
CHANGED
|
@@ -1027,7 +1027,7 @@ function Field({
|
|
|
1027
1027
|
<div className={styles['file-image-preview']}>
|
|
1028
1028
|
<img
|
|
1029
1029
|
src={fileImageSrc}
|
|
1030
|
-
alt={file.name || file.file_name || 'Preview'}
|
|
1030
|
+
alt={file.name || file.file_name || file.filename || 'Preview'}
|
|
1031
1031
|
className={styles['file-thumbnail']}
|
|
1032
1032
|
onError={(e) => {
|
|
1033
1033
|
e.target.style.display = 'none';
|
|
@@ -1050,6 +1050,7 @@ function Field({
|
|
|
1050
1050
|
<span className={styles['file-name']}>
|
|
1051
1051
|
{file.name ||
|
|
1052
1052
|
file.file_name ||
|
|
1053
|
+
file.filename ||
|
|
1053
1054
|
''}
|
|
1054
1055
|
</span>
|
|
1055
1056
|
|
|
@@ -1107,7 +1108,7 @@ function Field({
|
|
|
1107
1108
|
{inputValue ? getFileIcon(inputValue) : '📁'}
|
|
1108
1109
|
</span>
|
|
1109
1110
|
<span className={styles['file-button-text']}>
|
|
1110
|
-
{inputValue?.filename || inputValue?.name
|
|
1111
|
+
{inputValue?.filename || inputValue?.name || inputValue?.file_name
|
|
1111
1112
|
? isImage
|
|
1112
1113
|
? 'Replace Image...'
|
|
1113
1114
|
: 'Replace File...'
|
|
@@ -1133,7 +1134,7 @@ function Field({
|
|
|
1133
1134
|
<div className={styles['image-preview-container']}>
|
|
1134
1135
|
<img
|
|
1135
1136
|
src={imageSrc}
|
|
1136
|
-
alt={inputValue.filename || inputValue.name || 'Preview'}
|
|
1137
|
+
alt={inputValue.filename || inputValue.name || inputValue.file_name || 'Preview'}
|
|
1137
1138
|
className={styles['image-preview']}
|
|
1138
1139
|
onError={(e) => {
|
|
1139
1140
|
e.target.style.display = 'none';
|
|
@@ -1145,14 +1146,14 @@ function Field({
|
|
|
1145
1146
|
|
|
1146
1147
|
<div className={styles['file-details']}>
|
|
1147
1148
|
<span className={styles['file-name-display']}>
|
|
1148
|
-
{inputValue.filename || inputValue.name || ''}
|
|
1149
|
+
{inputValue.filename || inputValue.name || inputValue.file_name || ''}
|
|
1149
1150
|
</span>
|
|
1150
1151
|
|
|
1151
|
-
{inputValue.key && inputValue.uuid && (
|
|
1152
|
+
{((inputValue.key && inputValue.uuid) || inputValue.file_url || inputValue.file_path || inputValue.id) && (
|
|
1152
1153
|
<button
|
|
1153
1154
|
className={styles['download-button']}
|
|
1154
1155
|
data-tooltip-id="system-tooltip"
|
|
1155
|
-
data-tooltip-content={`Download ${inputValue.filename || inputValue.name}`}
|
|
1156
|
+
data-tooltip-content={`Download ${inputValue.filename || inputValue.name || inputValue.file_name}`}
|
|
1156
1157
|
onClick={(e) => {
|
|
1157
1158
|
e.preventDefault();
|
|
1158
1159
|
onFileDownload(inputValue);
|
package/src/components/Form.jsx
CHANGED
|
@@ -443,11 +443,65 @@ function Form({
|
|
|
443
443
|
}
|
|
444
444
|
};
|
|
445
445
|
|
|
446
|
-
const handleFileDownload = async (
|
|
446
|
+
const handleFileDownload = async (fileData) => {
|
|
447
447
|
try {
|
|
448
|
-
console.info(
|
|
448
|
+
console.info('File download data:', fileData);
|
|
449
|
+
|
|
450
|
+
if (!fileData) {
|
|
451
|
+
toast.error('No file data available for download');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Handle different file data structures
|
|
456
|
+
let downloadUrl = null;
|
|
457
|
+
let fileName = fileData.filename || fileData.name || fileData.file_name || 'download';
|
|
458
|
+
|
|
459
|
+
// Priority order for download methods:
|
|
460
|
+
// 1. Direct file URL (S3, CDN, etc.)
|
|
461
|
+
if (fileData.file_url) {
|
|
462
|
+
downloadUrl = fileData.file_url;
|
|
463
|
+
}
|
|
464
|
+
// 2. File path (construct URL if needed)
|
|
465
|
+
else if (fileData.file_path || fileData.file_full_path) {
|
|
466
|
+
const filePath = fileData.file_full_path || fileData.file_path;
|
|
467
|
+
// If it's already a full URL, use it directly
|
|
468
|
+
if (filePath.startsWith('http')) {
|
|
469
|
+
downloadUrl = filePath;
|
|
470
|
+
} else {
|
|
471
|
+
// Construct URL - adjust this based on your API structure
|
|
472
|
+
downloadUrl = `/api/files/download?path=${encodeURIComponent(filePath)}`;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// 3. Legacy key/uuid method
|
|
476
|
+
else if (fileData.key && fileData.uuid) {
|
|
477
|
+
downloadUrl = `/api/files/download/${fileData.uuid}/${fileData.key}`;
|
|
478
|
+
}
|
|
479
|
+
// 4. File ID method
|
|
480
|
+
else if (fileData.id) {
|
|
481
|
+
downloadUrl = `/api/files/download/${fileData.id}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!downloadUrl) {
|
|
485
|
+
toast.error('Unable to determine download URL for this file');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Create a temporary anchor element to trigger download
|
|
490
|
+
const link = document.createElement('a');
|
|
491
|
+
link.href = downloadUrl;
|
|
492
|
+
link.download = fileName;
|
|
493
|
+
link.target = '_blank';
|
|
494
|
+
|
|
495
|
+
// Add to DOM, click, and remove
|
|
496
|
+
document.body.appendChild(link);
|
|
497
|
+
link.click();
|
|
498
|
+
document.body.removeChild(link);
|
|
499
|
+
|
|
500
|
+
toast.success(`Downloading ${fileName}...`);
|
|
501
|
+
|
|
449
502
|
} catch (error) {
|
|
450
|
-
console.error(error);
|
|
503
|
+
console.error('File download error:', error);
|
|
504
|
+
toast.error('Failed to download file');
|
|
451
505
|
}
|
|
452
506
|
};
|
|
453
507
|
|
|
@@ -1721,6 +1775,7 @@ function Form({
|
|
|
1721
1775
|
handleChangeSignature
|
|
1722
1776
|
}
|
|
1723
1777
|
onChangeToggle={handleChangeToggle}
|
|
1778
|
+
onFileDownload={handleFileDownload}
|
|
1724
1779
|
settings={item}
|
|
1725
1780
|
setFormData={setFormData}
|
|
1726
1781
|
style={style}
|
|
@@ -163,6 +163,7 @@ const GenericReportForm = ({ config, userProfile, onExport, onFormChange }) => {
|
|
|
163
163
|
onChangeRicheditor={() => {}}
|
|
164
164
|
onChangeSelect={handleChangeSelect}
|
|
165
165
|
onChangeToggle={() => {}}
|
|
166
|
+
onFileDownload={() => console.warn('File download not implemented in GenericReportForm')}
|
|
166
167
|
settings={item}
|
|
167
168
|
setFormData={setFormData}
|
|
168
169
|
style={userProfile?.settings?.style}
|