@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 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** - File upload with drag and drop support
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
- File upload component with drag and drop support.
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
- onDrop={handleFileDrop}
1348
- accept=".jpg,.png,.pdf"
1349
- maxSize={5242880} // 5MB
1350
- multiple={true}
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.18",
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 { X as CircleX, File, Trash2 as TrashCan } from 'lucide-react';
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
- function VisnsDropZone({ fetchData, files, settings }) {
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
- settings.url,
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
- if (typeof fetchData === 'function') {
64
- fetchData();
65
-
66
- toast.success(
67
- 'File has been successfully uploaded.'
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
- `/ajax/files/delete`,
339
+ finalDeleteUrl,
134
340
  'POST',
135
341
  {
136
342
  file_id: id,
137
343
  },
138
344
  function (result) {
139
345
  if (result.error === '') {
140
- if (typeof fetchData === 'function') {
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
- if (typeof fetchData === 'function') {
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
- {data && data.length > 0
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
- <File
261
- strokeWidth={2}
262
- size={20}
263
- className={
264
- styles.dropzoneFileIcon
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
- <div className={styles.progress}>
1818
- <motion.div
1819
- className={styles.progress__bar}
1820
- initial={{ width: '0%' }}
1821
- animate={{
1822
- width: loadingProgress + '%',
1823
- }}
1824
- transition={{
1825
- duration: 2,
1826
- ease: [0.165, 0.84, 0.44, 1.0],
1827
- }}
1828
- />
1829
- </div>
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
- setFiles(
1919
- res
1920
- .data[
1921
- activeTabConfig
1922
- .file_relationship
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
- setData(
1927
- res.data
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
- setFiles(
1946
- res
1947
- .data
1948
- .files
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
- setData(
1952
- res.data
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
- {activeTabConfig.filetype ===
2021
- 'image' ? (
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
- {files.map((a, b) => (
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':