@visns-studio/visns-components 5.13.18 → 5.13.19
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 +126 -6
- package/package.json +1 -1
- package/src/components/DropZone.jsx +347 -25
- package/src/components/generic/GenericDetail.jsx +158 -35
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ VISNS Components is a React-based UI component library that provides a set of re
|
|
|
33
33
|
- **Field Components** - Text inputs, selects, checkboxes, etc.
|
|
34
34
|
- **Multi-select** - Enhanced select with multiple selection support and selection limits
|
|
35
35
|
- **Autocomplete** - Mapbox integration for location selection
|
|
36
|
-
- **Dropzone** -
|
|
36
|
+
- **Dropzone** - Advanced file upload with drag and drop, smart data extraction, and file type icons
|
|
37
37
|
- **AsyncSelect** - Asynchronous data loading for select components
|
|
38
38
|
- **Creatable Select** - Select components that allow creating new options
|
|
39
39
|
- **GenericEditableTable** - Interactive table component with inline editing, color-coded columns, and multiple data types
|
|
@@ -179,6 +179,66 @@ The refactoring also introduced shared utility functions:
|
|
|
179
179
|
|
|
180
180
|
These utilities are available for use in custom components and provide consistent functionality across the library.
|
|
181
181
|
|
|
182
|
+
## DropZone Component Enhancements (v5.14.0+)
|
|
183
|
+
|
|
184
|
+
### Smart Data Extraction
|
|
185
|
+
|
|
186
|
+
The DropZone component has been significantly enhanced with intelligent data extraction capabilities for complex API responses:
|
|
187
|
+
|
|
188
|
+
**Problem Solved:**
|
|
189
|
+
When uploading files to nested relationships (e.g., project → estimation → design → lead → documents), the API returns different model structures at different stages:
|
|
190
|
+
- Initial load: `project.estimation.design.lead.documents`
|
|
191
|
+
- After upload: `lead.documents` (direct lead model response)
|
|
192
|
+
|
|
193
|
+
**Solution:**
|
|
194
|
+
The DropZone component now automatically adapts the data extraction key based on the API response structure:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
// Configuration
|
|
198
|
+
{
|
|
199
|
+
"key": "estimation.design.lead.documents",
|
|
200
|
+
"url": "/ajax/leads/{key}",
|
|
201
|
+
"urlKey": "estimation.design.lead.id",
|
|
202
|
+
"file_relationship": "documents"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Smart adaptation:
|
|
206
|
+
// Initial: extracts from project.estimation.design.lead.documents
|
|
207
|
+
// After upload: automatically adapts to extract from response.data.documents
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### File Type Icons
|
|
211
|
+
|
|
212
|
+
Enhanced visual file identification with dynamic icons based on file extensions:
|
|
213
|
+
|
|
214
|
+
- **📄 Documents**: PDF, DOC, DOCX, TXT, RTF, ODT
|
|
215
|
+
- **📊 Spreadsheets**: XLS, XLSX, CSV, ODS
|
|
216
|
+
- **🖼️ Images**: JPG, JPEG, PNG, GIF, BMP, SVG, WEBP, ICO
|
|
217
|
+
- **💻 Code Files**: JS, JSX, TS, TSX, HTML, CSS, PHP, PY, JAVA, etc.
|
|
218
|
+
- **📦 Archives**: ZIP, RAR, 7Z, TAR, GZ, BZ2
|
|
219
|
+
- **🎵 Audio**: MP3, WAV, FLAC, AAC, OGG, M4A
|
|
220
|
+
- **🎬 Video**: MP4, AVI, MKV, MOV, WMV, FLV, WEBM
|
|
221
|
+
|
|
222
|
+
### GenericDetail Integration
|
|
223
|
+
|
|
224
|
+
The GenericDetail component now uses the enhanced VisnsDropZone component instead of inline dropzone implementation, providing:
|
|
225
|
+
|
|
226
|
+
- Consistent file handling across all dropzone instances
|
|
227
|
+
- Automatic data refresh after file uploads
|
|
228
|
+
- Smart data extraction for nested relationships
|
|
229
|
+
- Improved state management and UI responsiveness
|
|
230
|
+
|
|
231
|
+
### Technical Implementation
|
|
232
|
+
|
|
233
|
+
**Key Features:**
|
|
234
|
+
- `getAdaptedDataKey()`: Intelligently adapts extraction paths based on response structure
|
|
235
|
+
- `getFileIcon()`: Returns appropriate Lucide React icons based on file extensions
|
|
236
|
+
- Enhanced `fetchData` integration: Automatic page refresh after successful uploads
|
|
237
|
+
- Comprehensive debug logging for troubleshooting data extraction issues
|
|
238
|
+
|
|
239
|
+
**Backward Compatibility:**
|
|
240
|
+
All existing DropZone implementations continue to work without modification. The smart features activate automatically when `dataKey` prop is provided.
|
|
241
|
+
|
|
182
242
|
## Component Documentation
|
|
183
243
|
|
|
184
244
|
### Authentication Components
|
|
@@ -1340,17 +1400,77 @@ Location autocomplete with Mapbox integration.
|
|
|
1340
1400
|
|
|
1341
1401
|
#### DropZone
|
|
1342
1402
|
|
|
1343
|
-
|
|
1403
|
+
Advanced file upload component with drag and drop support, smart data extraction, and file type icons.
|
|
1344
1404
|
|
|
1405
|
+
**Key Features:**
|
|
1406
|
+
- **Smart Data Extraction**: Automatically adapts to API response structures using nested keys
|
|
1407
|
+
- **File Type Icons**: Dynamic icons based on file extensions (PDF, images, documents, etc.)
|
|
1408
|
+
- **Nested API Support**: Handles complex data structures like `estimation.design.lead.documents`
|
|
1409
|
+
- **Auto-Refresh**: Intelligent file list updates after upload
|
|
1410
|
+
- **Gallery Mode**: Image preview and gallery display
|
|
1411
|
+
- **Progress Tracking**: Upload progress indication
|
|
1412
|
+
- **File Management**: Edit, delete, and download functionality
|
|
1413
|
+
|
|
1414
|
+
**Basic Usage:**
|
|
1345
1415
|
```jsx
|
|
1346
1416
|
<DropZone
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1417
|
+
fetchData={handleRefresh}
|
|
1418
|
+
files={currentFiles}
|
|
1419
|
+
settings={{
|
|
1420
|
+
url: '/ajax/files',
|
|
1421
|
+
method: 'POST',
|
|
1422
|
+
type: 'gallery', // or 'list'
|
|
1423
|
+
data: {
|
|
1424
|
+
file_relationship: 'documents'
|
|
1425
|
+
},
|
|
1426
|
+
folder: 'uploads',
|
|
1427
|
+
filetype: 'image',
|
|
1428
|
+
deleteUrl: '/ajax/files/delete'
|
|
1429
|
+
}}
|
|
1351
1430
|
/>
|
|
1352
1431
|
```
|
|
1353
1432
|
|
|
1433
|
+
**Smart Data Extraction:**
|
|
1434
|
+
```jsx
|
|
1435
|
+
<DropZone
|
|
1436
|
+
fetchData={handleRefresh}
|
|
1437
|
+
files={files}
|
|
1438
|
+
url="/ajax/leads/{key}"
|
|
1439
|
+
urlKey="estimation.design.lead.id"
|
|
1440
|
+
dataKey="documents" // Extracts files from response.data.documents
|
|
1441
|
+
entityData={projectData}
|
|
1442
|
+
routeParams={routeParams}
|
|
1443
|
+
settings={{
|
|
1444
|
+
method: 'PUT',
|
|
1445
|
+
data: {
|
|
1446
|
+
file_relationship: 'documents'
|
|
1447
|
+
}
|
|
1448
|
+
}}
|
|
1449
|
+
/>
|
|
1450
|
+
```
|
|
1451
|
+
|
|
1452
|
+
**Supported File Types & Icons:**
|
|
1453
|
+
- 📄 **Documents**: PDF, DOC, DOCX, TXT, RTF, ODT
|
|
1454
|
+
- 📊 **Spreadsheets**: XLS, XLSX, CSV, ODS
|
|
1455
|
+
- 🖼️ **Images**: JPG, JPEG, PNG, GIF, BMP, SVG, WEBP, ICO
|
|
1456
|
+
- 💻 **Code Files**: JS, JSX, TS, TSX, HTML, CSS, PHP, PY, JAVA, etc.
|
|
1457
|
+
- 📦 **Archives**: ZIP, RAR, 7Z, TAR, GZ, BZ2
|
|
1458
|
+
- 🎵 **Audio**: MP3, WAV, FLAC, AAC, OGG, M4A
|
|
1459
|
+
- 🎬 **Video**: MP4, AVI, MKV, MOV, WMV, FLV, WEBM
|
|
1460
|
+
|
|
1461
|
+
**Configuration Options:**
|
|
1462
|
+
- `fetchData`: Function called after successful uploads to refresh data
|
|
1463
|
+
- `files`: Array of current files to display
|
|
1464
|
+
- `url`: Upload endpoint (supports {key} placeholders)
|
|
1465
|
+
- `urlKey`: Dot notation path for URL parameter replacement
|
|
1466
|
+
- `dataKey`: Dot notation path for extracting files from API responses
|
|
1467
|
+
- `deleteUrl`: Endpoint for file deletion
|
|
1468
|
+
- `entityData`: Context data for URL parameter resolution
|
|
1469
|
+
- `routeParams`: Route parameters for URL processing
|
|
1470
|
+
- `settings.type`: 'gallery' for images, 'list' for documents
|
|
1471
|
+
- `settings.filetype`: File type restrictions
|
|
1472
|
+
- `settings.folder`: Upload folder designation
|
|
1473
|
+
|
|
1354
1474
|
### Association Management Components
|
|
1355
1475
|
|
|
1356
1476
|
#### AssociationManager
|
package/package.json
CHANGED
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
89
89
|
},
|
|
90
90
|
"name": "@visns-studio/visns-components",
|
|
91
|
-
"version": "5.13.
|
|
91
|
+
"version": "5.13.19",
|
|
92
92
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
93
93
|
"main": "src/index.js",
|
|
94
94
|
"files": [
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import React, { useEffect, useState, useCallback } from 'react';
|
|
2
2
|
import { motion } from 'framer-motion';
|
|
3
3
|
import { useDropzone } from 'react-dropzone';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
X as CircleX,
|
|
6
|
+
File,
|
|
7
|
+
Trash2 as TrashCan,
|
|
8
|
+
FileText,
|
|
9
|
+
FileSpreadsheet,
|
|
10
|
+
FileImage,
|
|
11
|
+
FileCode,
|
|
12
|
+
FileArchive,
|
|
13
|
+
FileAudio,
|
|
14
|
+
FileVideo
|
|
15
|
+
} from 'lucide-react';
|
|
5
16
|
import { toast } from 'react-toastify';
|
|
6
17
|
import { arrayMoveImmutable } from 'array-move';
|
|
7
18
|
import Popup from 'reactjs-popup';
|
|
@@ -12,8 +23,188 @@ import CustomFetch from './Fetch';
|
|
|
12
23
|
import Gallery from './Gallery';
|
|
13
24
|
import styles from './styles/DropZone.module.css';
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
// Function to get the appropriate file icon based on extension
|
|
27
|
+
const getFileIcon = (extension) => {
|
|
28
|
+
if (!extension) return File;
|
|
29
|
+
|
|
30
|
+
const ext = extension.toLowerCase().replace('.', '');
|
|
31
|
+
|
|
32
|
+
// Document types
|
|
33
|
+
if (['pdf', 'doc', 'docx', 'txt', 'rtf', 'odt'].includes(ext)) return FileText;
|
|
34
|
+
if (['xls', 'xlsx', 'csv', 'ods'].includes(ext)) return FileSpreadsheet;
|
|
35
|
+
|
|
36
|
+
// Image types
|
|
37
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico'].includes(ext)) return FileImage;
|
|
38
|
+
|
|
39
|
+
// Code types
|
|
40
|
+
if (['js', 'jsx', 'ts', 'tsx', 'html', 'css', 'scss', 'php', 'py', 'java', 'cpp', 'c', 'json', 'xml', 'yml', 'yaml'].includes(ext)) return FileCode;
|
|
41
|
+
|
|
42
|
+
// Archive types
|
|
43
|
+
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) return FileArchive;
|
|
44
|
+
|
|
45
|
+
// Audio types
|
|
46
|
+
if (['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'].includes(ext)) return FileAudio;
|
|
47
|
+
|
|
48
|
+
// Video types
|
|
49
|
+
if (['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm'].includes(ext)) return FileVideo;
|
|
50
|
+
|
|
51
|
+
// Default file icon
|
|
52
|
+
return File;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function VisnsDropZone({ fetchData, files, settings, url, urlKey, deleteUrl, entityData = {}, routeParams = {}, dataKey }) {
|
|
56
|
+
console.log('DropZone: Component initialized with props:', {
|
|
57
|
+
hasFetchData: typeof fetchData === 'function',
|
|
58
|
+
files,
|
|
59
|
+
filesCount: files ? files.length : 0,
|
|
60
|
+
settings,
|
|
61
|
+
url,
|
|
62
|
+
urlKey,
|
|
63
|
+
deleteUrl,
|
|
64
|
+
entityData,
|
|
65
|
+
routeParams,
|
|
66
|
+
dataKey
|
|
67
|
+
});
|
|
68
|
+
|
|
16
69
|
const [data, setData] = useState(files);
|
|
70
|
+
|
|
71
|
+
// Debug logging for data state changes
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
console.log('DropZone: Data state changed:', {
|
|
74
|
+
data,
|
|
75
|
+
dataCount: data ? data.length : 0,
|
|
76
|
+
dataType: typeof data,
|
|
77
|
+
isArray: Array.isArray(data),
|
|
78
|
+
firstItem: data && data.length > 0 ? data[0] : null
|
|
79
|
+
});
|
|
80
|
+
}, [data]);
|
|
81
|
+
|
|
82
|
+
// Helper function to get nested object value using dot notation
|
|
83
|
+
const getNestedValue = React.useCallback((obj, path) => {
|
|
84
|
+
if (!obj || !path) {
|
|
85
|
+
console.log('DropZone getNestedValue: Invalid input', { obj: !!obj, path });
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log('DropZone getNestedValue called:', {
|
|
90
|
+
objType: typeof obj,
|
|
91
|
+
objKeys: Object.keys(obj || {}),
|
|
92
|
+
path,
|
|
93
|
+
pathType: typeof path
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = path.split('.').reduce((current, key) => {
|
|
98
|
+
console.log('Accessing key:', key, 'from current:', {
|
|
99
|
+
currentType: typeof current,
|
|
100
|
+
currentValue: current,
|
|
101
|
+
hasKey: current && current.hasOwnProperty(key),
|
|
102
|
+
keyValue: current?.[key]
|
|
103
|
+
});
|
|
104
|
+
return current && current[key] !== undefined ? current[key] : null;
|
|
105
|
+
}, obj);
|
|
106
|
+
|
|
107
|
+
console.log('DropZone getNestedValue final result:', { path, result, resultType: typeof result });
|
|
108
|
+
return result;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('DropZone getNestedValue error:', error, { obj, path });
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// Process URL with key substitution and urlKey support
|
|
116
|
+
const processUrl = (baseUrl) => {
|
|
117
|
+
if (!baseUrl) return baseUrl;
|
|
118
|
+
|
|
119
|
+
let processedUrl = baseUrl;
|
|
120
|
+
|
|
121
|
+
// Handle {key} placeholder substitution
|
|
122
|
+
if (processedUrl.includes('{key}')) {
|
|
123
|
+
let keyValue = null;
|
|
124
|
+
|
|
125
|
+
if (urlKey) {
|
|
126
|
+
// New config: Get the value from entityData using urlKey path
|
|
127
|
+
keyValue = getNestedValue(entityData, urlKey);
|
|
128
|
+
console.log('DropZone {key} replacement (new config):', {
|
|
129
|
+
urlKey,
|
|
130
|
+
keyValue,
|
|
131
|
+
entityData
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
// Old config: Fallback to routeParams.dataId or entityData.id for backward compatibility
|
|
135
|
+
keyValue = routeParams.dataId || entityData.id;
|
|
136
|
+
console.log('DropZone {key} replacement (old config fallback):', {
|
|
137
|
+
keyValue,
|
|
138
|
+
routeParams,
|
|
139
|
+
entityDataId: entityData.id
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (keyValue) {
|
|
144
|
+
processedUrl = processedUrl.replace('{key}', keyValue);
|
|
145
|
+
console.log('DropZone: Successfully replaced {key} with:', keyValue);
|
|
146
|
+
} else {
|
|
147
|
+
console.warn('DropZone: Could not resolve {key} placeholder, leaving as-is', {
|
|
148
|
+
urlKey,
|
|
149
|
+
entityData,
|
|
150
|
+
routeParams,
|
|
151
|
+
originalUrl: baseUrl
|
|
152
|
+
});
|
|
153
|
+
// Don't replace if we can't find the value - this will leave the placeholder intact
|
|
154
|
+
// which should help identify the issue
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle {dataId} placeholder substitution
|
|
159
|
+
if (processedUrl.includes('{dataId}')) {
|
|
160
|
+
let dataId = routeParams.dataId || entityData.id;
|
|
161
|
+
|
|
162
|
+
console.log('DropZone {dataId} replacement:', {
|
|
163
|
+
dataId,
|
|
164
|
+
routeParams,
|
|
165
|
+
entityDataId: entityData.id
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (dataId) {
|
|
169
|
+
processedUrl = processedUrl.replace('{dataId}', dataId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.log('DropZone URL Processing:', {
|
|
174
|
+
originalUrl: baseUrl,
|
|
175
|
+
processedUrl,
|
|
176
|
+
hasUnreplacedPlaceholders: processedUrl.includes('{') || processedUrl.includes('}')
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return processedUrl;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Process the URL, preferring the direct url prop over settings.url
|
|
183
|
+
// For backward compatibility: if no url prop is passed, use settings.url without processing
|
|
184
|
+
// If url prop is passed (new config), process it for placeholders
|
|
185
|
+
const finalUrl = React.useMemo(() => {
|
|
186
|
+
const baseUrl = url || settings.url || '/ajax/files';
|
|
187
|
+
const processed = url ? processUrl(url) : baseUrl;
|
|
188
|
+
|
|
189
|
+
console.log('DropZone Final URL Processing:', {
|
|
190
|
+
inputUrl: url,
|
|
191
|
+
settingsUrl: settings.url,
|
|
192
|
+
baseUrl,
|
|
193
|
+
processedUrl: processed,
|
|
194
|
+
willProcess: !!url,
|
|
195
|
+
entityData,
|
|
196
|
+
urlKey,
|
|
197
|
+
routeParams
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return processed;
|
|
201
|
+
}, [url, settings.url, entityData, urlKey, routeParams]);
|
|
202
|
+
|
|
203
|
+
// Process deleteUrl similarly
|
|
204
|
+
const finalDeleteUrl = React.useMemo(() => {
|
|
205
|
+
const baseDeleteUrl = deleteUrl || settings.deleteUrl || '/ajax/files/delete';
|
|
206
|
+
return deleteUrl ? processUrl(deleteUrl) : baseDeleteUrl;
|
|
207
|
+
}, [deleteUrl, settings.deleteUrl, entityData, urlKey, routeParams]);
|
|
17
208
|
|
|
18
209
|
/** Modal States */
|
|
19
210
|
const [modalData, setModalData] = useState({
|
|
@@ -39,7 +230,14 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
39
230
|
};
|
|
40
231
|
|
|
41
232
|
const onDrop = useCallback((acceptedFiles) => {
|
|
233
|
+
console.log('DropZone: onDrop called with files:', {
|
|
234
|
+
acceptedFiles,
|
|
235
|
+
filesCount: acceptedFiles.length,
|
|
236
|
+
fileNames: acceptedFiles.map(f => f.name)
|
|
237
|
+
});
|
|
238
|
+
|
|
42
239
|
acceptedFiles.forEach((a) => {
|
|
240
|
+
console.log('DropZone: Processing file:', a.name);
|
|
43
241
|
Vapor.store(a, {
|
|
44
242
|
progress: (progress) => {
|
|
45
243
|
setLoadingProgress(progress * 100);
|
|
@@ -47,7 +245,7 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
47
245
|
}).then((response) => {
|
|
48
246
|
setLoadingProgress(0);
|
|
49
247
|
CustomFetch(
|
|
50
|
-
|
|
248
|
+
finalUrl,
|
|
51
249
|
settings.method,
|
|
52
250
|
{
|
|
53
251
|
uuid: response.uuid,
|
|
@@ -57,17 +255,25 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
57
255
|
filesize: a.size,
|
|
58
256
|
extension: response.extension,
|
|
59
257
|
...settings.data,
|
|
258
|
+
// Pass route parameters for URL processing
|
|
259
|
+
...routeParams,
|
|
60
260
|
},
|
|
61
261
|
function (result) {
|
|
262
|
+
console.log('DropZone: Upload response received:', {
|
|
263
|
+
result,
|
|
264
|
+
error: result.error,
|
|
265
|
+
hasData: !!result.data,
|
|
266
|
+
dataStructure: result.data ? Object.keys(result.data) : null
|
|
267
|
+
});
|
|
268
|
+
|
|
62
269
|
if (result.error === '') {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
}
|
|
270
|
+
console.log('DropZone: Upload successful, calling enhancedFetchData...');
|
|
271
|
+
enhancedFetchData();
|
|
272
|
+
toast.success(
|
|
273
|
+
'File has been successfully uploaded.'
|
|
274
|
+
);
|
|
70
275
|
} else {
|
|
276
|
+
console.error('DropZone: Upload failed with error:', result.error);
|
|
71
277
|
toast.error(String(result.error));
|
|
72
278
|
}
|
|
73
279
|
}
|
|
@@ -130,16 +336,14 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
130
336
|
label: 'Yes',
|
|
131
337
|
onClick: () => {
|
|
132
338
|
CustomFetch(
|
|
133
|
-
|
|
339
|
+
finalDeleteUrl,
|
|
134
340
|
'POST',
|
|
135
341
|
{
|
|
136
342
|
file_id: id,
|
|
137
343
|
},
|
|
138
344
|
function (result) {
|
|
139
345
|
if (result.error === '') {
|
|
140
|
-
|
|
141
|
-
fetchData();
|
|
142
|
-
}
|
|
346
|
+
enhancedFetchData();
|
|
143
347
|
}
|
|
144
348
|
}
|
|
145
349
|
);
|
|
@@ -176,9 +380,7 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
176
380
|
toast.success('File metadata has been successfully updated.');
|
|
177
381
|
modalClose();
|
|
178
382
|
|
|
179
|
-
|
|
180
|
-
fetchData();
|
|
181
|
-
}
|
|
383
|
+
enhancedFetchData();
|
|
182
384
|
} else {
|
|
183
385
|
toast.error(res.data.error);
|
|
184
386
|
}
|
|
@@ -209,7 +411,114 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
209
411
|
}
|
|
210
412
|
}, [data]);
|
|
211
413
|
|
|
414
|
+
// Smart dataKey adaptation function
|
|
415
|
+
const getAdaptedDataKey = React.useCallback((responseData, originalDataKey) => {
|
|
416
|
+
if (!originalDataKey || !responseData) return originalDataKey;
|
|
417
|
+
|
|
418
|
+
// First try the original dataKey
|
|
419
|
+
if (getNestedValue(responseData, originalDataKey)) {
|
|
420
|
+
console.log('DropZone: Original dataKey works:', originalDataKey);
|
|
421
|
+
return originalDataKey;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// If the original dataKey doesn't work, try to adapt it
|
|
425
|
+
// For nested keys like "estimation.design.lead.documents", the last part is usually the field we want
|
|
426
|
+
const keyParts = originalDataKey.split('.');
|
|
427
|
+
const lastPart = keyParts[keyParts.length - 1]; // e.g., "documents"
|
|
428
|
+
|
|
429
|
+
// Try the last part directly (most common case)
|
|
430
|
+
// e.g., "estimation.design.lead.documents" -> "documents"
|
|
431
|
+
if (responseData.hasOwnProperty(lastPart)) {
|
|
432
|
+
console.log('DropZone: Using last part of key directly:', lastPart);
|
|
433
|
+
return lastPart;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// If that doesn't work, try progressively shorter paths (but skip relationship parts)
|
|
437
|
+
// This handles cases where there might be some intermediate structure
|
|
438
|
+
for (let i = keyParts.length - 1; i >= 0; i--) {
|
|
439
|
+
const adaptedKey = keyParts.slice(i).join('.');
|
|
440
|
+
console.log('DropZone: Trying adapted key:', adaptedKey);
|
|
441
|
+
|
|
442
|
+
if (getNestedValue(responseData, adaptedKey)) {
|
|
443
|
+
console.log('DropZone: Adapted key works:', adaptedKey);
|
|
444
|
+
return adaptedKey;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.warn('DropZone: No suitable dataKey found, falling back to original:', originalDataKey);
|
|
449
|
+
return originalDataKey;
|
|
450
|
+
}, [getNestedValue]);
|
|
451
|
+
|
|
452
|
+
// Enhanced fetchData that handles smart data extraction from API responses
|
|
453
|
+
const enhancedFetchData = React.useCallback(() => {
|
|
454
|
+
console.log('DropZone: enhancedFetchData called!', {
|
|
455
|
+
hasFetchDataFunction: typeof fetchData === 'function',
|
|
456
|
+
finalUrl,
|
|
457
|
+
dataKey,
|
|
458
|
+
willCallOriginalFetchData: typeof fetchData === 'function',
|
|
459
|
+
willCallSmartFetch: !!(finalUrl && dataKey)
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
if (typeof fetchData === 'function') {
|
|
463
|
+
console.log('DropZone: Calling original fetchData function...');
|
|
464
|
+
// Call the original fetchData function
|
|
465
|
+
fetchData();
|
|
466
|
+
} else if (finalUrl && dataKey) {
|
|
467
|
+
// If we have a URL and dataKey, fetch the data ourselves and extract using the key
|
|
468
|
+
console.log('DropZone: Fetching data with smart extraction', {
|
|
469
|
+
url: finalUrl,
|
|
470
|
+
dataKey,
|
|
471
|
+
entityData
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
CustomFetch(
|
|
475
|
+
finalUrl,
|
|
476
|
+
'GET',
|
|
477
|
+
{},
|
|
478
|
+
function (result) {
|
|
479
|
+
if (result.error === '') {
|
|
480
|
+
console.log('DropZone: API response received', {
|
|
481
|
+
result,
|
|
482
|
+
originalDataKey: dataKey,
|
|
483
|
+
responseStructure: Object.keys(result.data || {})
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Get the adapted dataKey based on the response structure
|
|
487
|
+
const adaptedKey = getAdaptedDataKey(result.data, dataKey);
|
|
488
|
+
|
|
489
|
+
// Extract files using the adapted dataKey path
|
|
490
|
+
const extractedFiles = getNestedValue(result.data, adaptedKey) || [];
|
|
491
|
+
|
|
492
|
+
console.log('DropZone: About to set extracted files:', {
|
|
493
|
+
originalDataKey: dataKey,
|
|
494
|
+
adaptedDataKey: adaptedKey,
|
|
495
|
+
extractedFiles,
|
|
496
|
+
count: extractedFiles.length,
|
|
497
|
+
currentDataState: data,
|
|
498
|
+
currentDataCount: data ? data.length : 0
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
setData(extractedFiles);
|
|
502
|
+
|
|
503
|
+
console.log('DropZone: setData called with:', {
|
|
504
|
+
newData: extractedFiles,
|
|
505
|
+
newDataCount: extractedFiles.length
|
|
506
|
+
});
|
|
507
|
+
} else {
|
|
508
|
+
console.error('DropZone: API error', result.error);
|
|
509
|
+
toast.error(String(result.error));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}, [fetchData, finalUrl, dataKey, entityData, getNestedValue, getAdaptedDataKey]);
|
|
515
|
+
|
|
212
516
|
useEffect(() => {
|
|
517
|
+
console.log('DropZone: Files prop changed, updating data:', {
|
|
518
|
+
newFiles: files,
|
|
519
|
+
filesCount: files ? files.length : 0,
|
|
520
|
+
filesStructure: files && files.length > 0 ? Object.keys(files[0] || {}) : null
|
|
521
|
+
});
|
|
213
522
|
setData(files);
|
|
214
523
|
}, [files]);
|
|
215
524
|
|
|
@@ -243,7 +552,15 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
243
552
|
</div>
|
|
244
553
|
{settings.type !== 'gallery' ? (
|
|
245
554
|
<ul className={styles.dropzoneFiles}>
|
|
246
|
-
{
|
|
555
|
+
{(() => {
|
|
556
|
+
console.log('DropZone: Rendering files list:', {
|
|
557
|
+
data,
|
|
558
|
+
dataLength: data ? data.length : 0,
|
|
559
|
+
hasData: data && data.length > 0,
|
|
560
|
+
settingsType: settings.type
|
|
561
|
+
});
|
|
562
|
+
return data && data.length > 0;
|
|
563
|
+
})()
|
|
247
564
|
? data.map((a, b) => (
|
|
248
565
|
<li
|
|
249
566
|
onClick={() => {
|
|
@@ -257,13 +574,18 @@ function VisnsDropZone({ fetchData, files, settings }) {
|
|
|
257
574
|
styles.dropzoneFileContent
|
|
258
575
|
}
|
|
259
576
|
>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
577
|
+
{(() => {
|
|
578
|
+
const IconComponent = getFileIcon(a.file_extension);
|
|
579
|
+
return (
|
|
580
|
+
<IconComponent
|
|
581
|
+
strokeWidth={2}
|
|
582
|
+
size={20}
|
|
583
|
+
className={
|
|
584
|
+
styles.dropzoneFileIcon
|
|
585
|
+
}
|
|
586
|
+
/>
|
|
587
|
+
);
|
|
588
|
+
})()}
|
|
267
589
|
<span
|
|
268
590
|
className={
|
|
269
591
|
styles.dropzoneFileName
|
|
@@ -81,6 +81,7 @@ import StagePopupModal from './StagePopupModal';
|
|
|
81
81
|
import Table from '../DataGrid';
|
|
82
82
|
import TableFilter from '../TableFilter';
|
|
83
83
|
import CategorizedDropZone from '../CategorizedDropZone';
|
|
84
|
+
import VisnsDropZone from '../DropZone';
|
|
84
85
|
|
|
85
86
|
// Proposal components
|
|
86
87
|
import ProposalTemplateSectionManager from '../proposal/ProposalTemplateSectionManager';
|
|
@@ -240,6 +241,17 @@ function GenericDetail({
|
|
|
240
241
|
|
|
241
242
|
/** Fileupload states */
|
|
242
243
|
const [files, setFiles] = useState([]);
|
|
244
|
+
|
|
245
|
+
// Debug logging for files state changes
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
console.log('GenericDetail: Files state changed:', {
|
|
248
|
+
files,
|
|
249
|
+
filesCount: files ? files.length : 0,
|
|
250
|
+
filesType: typeof files,
|
|
251
|
+
isArray: Array.isArray(files),
|
|
252
|
+
firstFile: files && files.length > 0 ? files[0] : null
|
|
253
|
+
});
|
|
254
|
+
}, [files]);
|
|
243
255
|
const [loadingProgress, setLoadingProgress] = useState(0);
|
|
244
256
|
|
|
245
257
|
/** General States */
|
|
@@ -1699,6 +1711,49 @@ function GenericDetail({
|
|
|
1699
1711
|
);
|
|
1700
1712
|
}
|
|
1701
1713
|
case 'dropzone':
|
|
1714
|
+
// Replace URL parameters like {key} with actual values (same logic as categorized_dropzone)
|
|
1715
|
+
const replaceDropzoneUrlParams = (url) => {
|
|
1716
|
+
if (!url) return url;
|
|
1717
|
+
|
|
1718
|
+
// Handle the new urlKey parameter
|
|
1719
|
+
const urlKey =
|
|
1720
|
+
activeTabConfig.urlKey || 'dataId';
|
|
1721
|
+
|
|
1722
|
+
let replacementValue;
|
|
1723
|
+
if (urlKey === 'dataId') {
|
|
1724
|
+
// Use routeParams for dataId (backward compatibility)
|
|
1725
|
+
replacementValue =
|
|
1726
|
+
routeParams[urlParam] || '';
|
|
1727
|
+
} else {
|
|
1728
|
+
// Use data object for other keys, supporting dot notation
|
|
1729
|
+
replacementValue = urlKey
|
|
1730
|
+
.split('.')
|
|
1731
|
+
.reduce((obj, key) => {
|
|
1732
|
+
return obj && obj[key] !== undefined
|
|
1733
|
+
? obj[key]
|
|
1734
|
+
: '';
|
|
1735
|
+
}, data);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Replace both {key} and {dataId} patterns for backward compatibility
|
|
1739
|
+
return url
|
|
1740
|
+
.replace(/{key}/g, replacementValue)
|
|
1741
|
+
.replace(/{dataId}/g, replacementValue);
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
// Helper function to get nested object value using dot notation
|
|
1745
|
+
const getNestedValue = (obj, path) => {
|
|
1746
|
+
if (!obj || !path) return null;
|
|
1747
|
+
return path.split('.').reduce((current, key) => {
|
|
1748
|
+
return current && current[key] !== undefined ? current[key] : null;
|
|
1749
|
+
}, obj);
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
// Extract files using nested key support
|
|
1753
|
+
const files = activeTabConfig.key
|
|
1754
|
+
? getNestedValue(data, activeTabConfig.key) || []
|
|
1755
|
+
: [];
|
|
1756
|
+
|
|
1702
1757
|
const downloadFile = (file) => {
|
|
1703
1758
|
const { id, file_url, file_name } = file;
|
|
1704
1759
|
const toastId = toast.loading(
|
|
@@ -1807,6 +1862,12 @@ function GenericDetail({
|
|
|
1807
1862
|
}
|
|
1808
1863
|
};
|
|
1809
1864
|
|
|
1865
|
+
// Create a fetchData function that refetches the main data
|
|
1866
|
+
const handleFetchData = () => {
|
|
1867
|
+
console.log('GenericDetail: DropZone fetchData called, refetching main data...');
|
|
1868
|
+
setDataReload(!dataReload);
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1810
1871
|
return (
|
|
1811
1872
|
<div className={styles.gridtxt}>
|
|
1812
1873
|
<div className={styles.gridtxt__header}>
|
|
@@ -1814,19 +1875,29 @@ function GenericDetail({
|
|
|
1814
1875
|
? activeTabConfig.label
|
|
1815
1876
|
: 'Files'}
|
|
1816
1877
|
</div>
|
|
1817
|
-
<
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1878
|
+
<VisnsDropZone
|
|
1879
|
+
fetchData={handleFetchData}
|
|
1880
|
+
files={files}
|
|
1881
|
+
settings={{
|
|
1882
|
+
url: replaceDropzoneUrlParams(activeTabConfig.url),
|
|
1883
|
+
method: 'PUT',
|
|
1884
|
+
type: activeTabConfig.filetype === 'image' ? 'gallery' : 'list',
|
|
1885
|
+
data: {
|
|
1886
|
+
file_relationship: activeTabConfig.file_relationship,
|
|
1887
|
+
},
|
|
1888
|
+
folder: activeTabConfig.folder || 'uploads',
|
|
1889
|
+
filetype: activeTabConfig.filetype,
|
|
1890
|
+
deleteUrl: replaceDropzoneUrlParams(activeTabConfig.deleteUrl) || '/ajax/files/delete',
|
|
1891
|
+
showDescription: true,
|
|
1892
|
+
}}
|
|
1893
|
+
url={replaceDropzoneUrlParams(activeTabConfig.url)}
|
|
1894
|
+
urlKey={activeTabConfig.urlKey}
|
|
1895
|
+
deleteUrl={replaceDropzoneUrlParams(activeTabConfig.deleteUrl) || '/ajax/files/delete'}
|
|
1896
|
+
entityData={data}
|
|
1897
|
+
routeParams={routeParams}
|
|
1898
|
+
dataKey={activeTabConfig.key}
|
|
1899
|
+
/>
|
|
1900
|
+
{/* OLD INLINE DROPZONE - TO BE REMOVED
|
|
1830
1901
|
<Dropzone
|
|
1831
1902
|
accept={getAcceptTypes(
|
|
1832
1903
|
activeTabConfig.filetype
|
|
@@ -1888,16 +1959,28 @@ function GenericDetail({
|
|
|
1888
1959
|
};
|
|
1889
1960
|
|
|
1890
1961
|
CustomFetch(
|
|
1891
|
-
`${activeTabConfig.url}/${routeParams[urlParam]}`,
|
|
1962
|
+
replaceDropzoneUrlParams(activeTabConfig.url) || `${activeTabConfig.url}/${routeParams[urlParam]}`,
|
|
1892
1963
|
'PUT',
|
|
1893
1964
|
requestBody,
|
|
1894
1965
|
(res) => {
|
|
1966
|
+
console.log('GenericDetail: Upload response received:', {
|
|
1967
|
+
res,
|
|
1968
|
+
hasError: res.error && res.error !== '',
|
|
1969
|
+
error: res.error,
|
|
1970
|
+
hasData: !!res.data,
|
|
1971
|
+
dataStructure: res.data ? Object.keys(res.data) : null,
|
|
1972
|
+
fileRelationship: activeTabConfig.file_relationship,
|
|
1973
|
+
hasFileRelationshipData: res.data && activeTabConfig.file_relationship && res.data[activeTabConfig.file_relationship],
|
|
1974
|
+
fileRelationshipData: res.data && activeTabConfig.file_relationship ? res.data[activeTabConfig.file_relationship] : null
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1895
1977
|
// Check if there's an error in the response
|
|
1896
1978
|
if (
|
|
1897
1979
|
res.error &&
|
|
1898
1980
|
res.error !==
|
|
1899
1981
|
''
|
|
1900
1982
|
) {
|
|
1983
|
+
console.error('GenericDetail: Upload error:', res.error);
|
|
1901
1984
|
toast.update(
|
|
1902
1985
|
uploadToastId,
|
|
1903
1986
|
{
|
|
@@ -1915,17 +1998,18 @@ function GenericDetail({
|
|
|
1915
1998
|
.file_relationship
|
|
1916
1999
|
]
|
|
1917
2000
|
) {
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2001
|
+
const newFiles = res.data[activeTabConfig.file_relationship];
|
|
2002
|
+
console.log('GenericDetail: Setting files from file_relationship:', {
|
|
2003
|
+
fileRelationship: activeTabConfig.file_relationship,
|
|
2004
|
+
newFiles,
|
|
2005
|
+
filesCount: newFiles ? newFiles.length : 0
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
setFiles(newFiles);
|
|
2009
|
+
|
|
1925
2010
|
// Update the main data state with the new data
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
);
|
|
2011
|
+
console.log('GenericDetail: Updating main data state:', res.data);
|
|
2012
|
+
setData(res.data);
|
|
1929
2013
|
|
|
1930
2014
|
// Update the upload toast
|
|
1931
2015
|
toast.update(
|
|
@@ -1937,20 +2021,26 @@ function GenericDetail({
|
|
|
1937
2021
|
}
|
|
1938
2022
|
);
|
|
1939
2023
|
} else {
|
|
2024
|
+
console.log('GenericDetail: File relationship not found, trying fallback...', {
|
|
2025
|
+
hasDataFiles: !!(res?.data?.files),
|
|
2026
|
+
dataFiles: res?.data?.files
|
|
2027
|
+
});
|
|
2028
|
+
|
|
1940
2029
|
if (
|
|
1941
2030
|
res
|
|
1942
2031
|
?.data
|
|
1943
2032
|
?.files
|
|
1944
2033
|
) {
|
|
1945
|
-
|
|
1946
|
-
res
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2034
|
+
console.log('GenericDetail: Setting files from data.files fallback:', {
|
|
2035
|
+
files: res.data.files,
|
|
2036
|
+
filesCount: res.data.files.length
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
setFiles(res.data.files);
|
|
2040
|
+
|
|
1950
2041
|
// Update the main data state with the new data
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
);
|
|
2042
|
+
console.log('GenericDetail: Updating main data state (fallback):', res.data);
|
|
2043
|
+
setData(res.data);
|
|
1954
2044
|
|
|
1955
2045
|
// Update the upload toast
|
|
1956
2046
|
toast.update(
|
|
@@ -1961,6 +2051,22 @@ function GenericDetail({
|
|
|
1961
2051
|
autoClose: 3000,
|
|
1962
2052
|
}
|
|
1963
2053
|
);
|
|
2054
|
+
} else {
|
|
2055
|
+
console.warn('GenericDetail: No files found in upload response!', {
|
|
2056
|
+
response: res,
|
|
2057
|
+
expectedFileRelationship: activeTabConfig.file_relationship,
|
|
2058
|
+
hasDataFiles: !!(res?.data?.files),
|
|
2059
|
+
responseDataKeys: res?.data ? Object.keys(res.data) : null
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
toast.update(
|
|
2063
|
+
uploadToastId,
|
|
2064
|
+
{
|
|
2065
|
+
render: `${file.name} uploaded but files not found in response`,
|
|
2066
|
+
type: 'warning',
|
|
2067
|
+
autoClose: 5000,
|
|
2068
|
+
}
|
|
2069
|
+
);
|
|
1964
2070
|
}
|
|
1965
2071
|
}
|
|
1966
2072
|
}
|
|
@@ -2017,8 +2123,16 @@ function GenericDetail({
|
|
|
2017
2123
|
</p>
|
|
2018
2124
|
)}
|
|
2019
2125
|
</div>
|
|
2020
|
-
{
|
|
2021
|
-
|
|
2126
|
+
{(() => {
|
|
2127
|
+
const isImage = activeTabConfig.filetype === 'image';
|
|
2128
|
+
console.log('GenericDetail: Checking filetype for rendering:', {
|
|
2129
|
+
filetype: activeTabConfig.filetype,
|
|
2130
|
+
isImage,
|
|
2131
|
+
files,
|
|
2132
|
+
filesCount: files ? files.length : 0
|
|
2133
|
+
});
|
|
2134
|
+
return isImage;
|
|
2135
|
+
})() ? (
|
|
2022
2136
|
<div>
|
|
2023
2137
|
<ul
|
|
2024
2138
|
className={
|
|
@@ -2152,7 +2266,15 @@ function GenericDetail({
|
|
|
2152
2266
|
styles.dropzone__files
|
|
2153
2267
|
}
|
|
2154
2268
|
>
|
|
2155
|
-
{
|
|
2269
|
+
{(() => {
|
|
2270
|
+
console.log('GenericDetail: Rendering files list (non-gallery):', {
|
|
2271
|
+
files,
|
|
2272
|
+
filesCount: files ? files.length : 0,
|
|
2273
|
+
isArray: Array.isArray(files),
|
|
2274
|
+
willRender: files && Array.isArray(files) && files.length > 0
|
|
2275
|
+
});
|
|
2276
|
+
return files;
|
|
2277
|
+
})().map((a, b) => (
|
|
2156
2278
|
<li key={'tf-' + b}>
|
|
2157
2279
|
<div
|
|
2158
2280
|
className="file-info"
|
|
@@ -2208,6 +2330,7 @@ function GenericDetail({
|
|
|
2208
2330
|
</section>
|
|
2209
2331
|
)}
|
|
2210
2332
|
</Dropzone>
|
|
2333
|
+
*/}
|
|
2211
2334
|
</div>
|
|
2212
2335
|
);
|
|
2213
2336
|
case 'dynamicform':
|