@visns-studio/visns-components 5.15.9 → 5.15.10
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
|
@@ -9,10 +9,11 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
|
|
|
9
9
|
|
|
10
10
|
VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
|
|
11
11
|
|
|
12
|
-
## Recent Updates (v5.15.
|
|
12
|
+
## Recent Updates (v5.15.10)
|
|
13
13
|
|
|
14
14
|
### Latest Enhancements
|
|
15
15
|
|
|
16
|
+
- **CameraPlacement Legacy Import**: Added comprehensive legacy JSON file import functionality with support for multiple data formats, automatic image loading, and seamless database integration
|
|
16
17
|
- **CameraPlacement Module**: Enhanced interactive camera placement tool with improved UI consistency, better icon visibility, drag-and-drop image upload, professional export capabilities, and complete Verkada database integration
|
|
17
18
|
- **Complete StandardModal Migration**: All components now use StandardModal instead of reactjs-popup for consistent modal behavior across the entire library
|
|
18
19
|
- **HTML Content Support**: Terms and conditions now properly parse HTML content from TinyMCE editors with clean rendering
|
|
@@ -2461,6 +2462,15 @@ import { CameraPlacement } from 'visns-components';
|
|
|
2461
2462
|
<CameraPlacement />
|
|
2462
2463
|
```
|
|
2463
2464
|
|
|
2465
|
+
**Legacy Import Workflow:**
|
|
2466
|
+
1. **Click Import Button**: Orange folder icon (📁) in the header controls
|
|
2467
|
+
2. **Select JSON File**: Choose legacy camera placement JSON file (.json extension)
|
|
2468
|
+
3. **Automatic Processing**: Smart parser handles multiple formats automatically
|
|
2469
|
+
4. **Import Validation**: Real-time validation with detailed error messages if needed
|
|
2470
|
+
5. **Data Loading**: Cameras, background images, and project metadata loaded instantly
|
|
2471
|
+
6. **Review & Adjust**: Fine-tune camera positions if coordinate scaling differs
|
|
2472
|
+
7. **Save to Database**: Use existing save functionality to persist imported data
|
|
2473
|
+
|
|
2464
2474
|
**Key Features:**
|
|
2465
2475
|
- **Enhanced User Interface**: Modern icon-based controls with tooltips, consistent color theming using project CSS variables
|
|
2466
2476
|
- **Dual Mode Support**: Switch between Mapbox map integration and image-based placement with seamless transitions
|
|
@@ -2470,10 +2480,14 @@ import { CameraPlacement } from 'visns-components';
|
|
|
2470
2480
|
- **FOV Visualization**: Intelligent field-of-view triangles with inverse distance scaling for realistic coverage display
|
|
2471
2481
|
- **Interactive Placement**: Drag-and-drop camera positioning with rotation handles for directional adjustment
|
|
2472
2482
|
- **Save/Load Projects**: Export/import camera placements as JSON files with project metadata and timestamps
|
|
2483
|
+
- **Legacy JSON Import**: One-click import of legacy camera placement files with automatic format detection and base64 image support
|
|
2473
2484
|
- **Professional Exports**: Generate high-quality PNG reports with side-by-side camera specifications and installation details
|
|
2474
2485
|
- **SweetAlert Integration**: Professional modal dialogs for confirmations, error handling, and success notifications
|
|
2475
2486
|
|
|
2476
|
-
**Recent Improvements (v5.15.
|
|
2487
|
+
**Recent Improvements (v5.15.10):**
|
|
2488
|
+
- **Legacy JSON Import**: Comprehensive import functionality supporting multiple legacy data formats with automatic image loading and base64 processing
|
|
2489
|
+
- **Enhanced Controls Layout**: Expanded controls container to accommodate new import button with proper spacing and responsive design
|
|
2490
|
+
- **Smart Data Parser**: Intelligent JSON parsing that handles 4+ different legacy format structures with fallback handling
|
|
2477
2491
|
- **Icon Visibility**: Fixed save button icon visibility with improved color contrast and explicit styling
|
|
2478
2492
|
- **UI Consistency**: All buttons now use project-standard color variables for consistent theming
|
|
2479
2493
|
- **Tooltips**: CSS-based tooltips for all control buttons with descriptive text
|
|
@@ -2488,6 +2502,30 @@ import { CameraPlacement } from 'visns-components';
|
|
|
2488
2502
|
- **Advanced Models**: Support for optical zoom, telephoto lenses, 4K resolution, and specialized security features
|
|
2489
2503
|
- **Real-time Updates**: Live camera specification display in sidebar with direction controls and coordinate tracking
|
|
2490
2504
|
|
|
2505
|
+
**Legacy Import System (v5.15.10+):**
|
|
2506
|
+
- **Multi-Format Support**: Handles 4+ different legacy JSON structures automatically with intelligent format detection
|
|
2507
|
+
- **Base64 Image Processing**: Seamless import of large images (4MB+) with base64 data URL support
|
|
2508
|
+
- **Smart Data Validation**: Comprehensive validation with user-friendly error messages and detailed debugging
|
|
2509
|
+
- **Format Flexibility**: Supports various naming conventions (`image.src`, `uploadedImage`, `backgroundImage`, etc.)
|
|
2510
|
+
- **One-Click Migration**: Import legacy projects with cameras, images, and metadata in a single operation
|
|
2511
|
+
- **Database Integration**: Imported data immediately compatible with save/load database functionality
|
|
2512
|
+
- **Visual Import Feedback**: Loading indicators, success dialogs with import summaries, and error handling
|
|
2513
|
+
|
|
2514
|
+
**Supported Legacy Formats:**
|
|
2515
|
+
```javascript
|
|
2516
|
+
// Format 1: Standard export with image object
|
|
2517
|
+
{ "image": { "src": "data:image/...", "name": "..." }, "cameras": [...] }
|
|
2518
|
+
|
|
2519
|
+
// Format 2: Nested placement structure
|
|
2520
|
+
{ "placement": { "cameras": [...], "image": "...", "projectName": "..." } }
|
|
2521
|
+
|
|
2522
|
+
// Format 3: Simple camera array
|
|
2523
|
+
[{ "id": 1, "name": "Camera 1", "x": 100, "y": 200, ... }]
|
|
2524
|
+
|
|
2525
|
+
// Format 4: Auto-detected custom structures
|
|
2526
|
+
{ "customData": { "cameraArray": [...] }, "backgroundImage": "..." }
|
|
2527
|
+
```
|
|
2528
|
+
|
|
2491
2529
|
**Technical Capabilities:**
|
|
2492
2530
|
- **Map Integration**: Mapbox GL JS with geocoding search, satellite imagery, and custom marker styling
|
|
2493
2531
|
- **Image Processing**: Support for floor plans and site diagrams with HTML5 file API and FileReader integration
|
package/package.json
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
95
95
|
},
|
|
96
96
|
"name": "@visns-studio/visns-components",
|
|
97
|
-
"version": "5.15.
|
|
97
|
+
"version": "5.15.10",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
|
@@ -48,6 +48,7 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
48
48
|
const geocoderRef = useRef(null);
|
|
49
49
|
const imageContainer = useRef(null);
|
|
50
50
|
const fileInputRef = useRef(null);
|
|
51
|
+
const jsonFileInputRef = useRef(null);
|
|
51
52
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
52
53
|
const [isRotating, setIsRotating] = useState(false);
|
|
53
54
|
|
|
@@ -186,6 +187,282 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
186
187
|
[map]
|
|
187
188
|
);
|
|
188
189
|
|
|
190
|
+
// Handle legacy JSON file upload
|
|
191
|
+
const handleJsonFileUpload = useCallback(async (event) => {
|
|
192
|
+
const file = event.target.files[0];
|
|
193
|
+
if (!file) return;
|
|
194
|
+
|
|
195
|
+
if (!file.name.toLowerCase().endsWith('.json')) {
|
|
196
|
+
Swal.fire({
|
|
197
|
+
icon: 'error',
|
|
198
|
+
title: 'Invalid File Type',
|
|
199
|
+
text: 'Please select a JSON file (.json extension)',
|
|
200
|
+
confirmButtonColor: '#ef4444',
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const text = await file.text();
|
|
207
|
+
const jsonData = JSON.parse(text);
|
|
208
|
+
|
|
209
|
+
// Show loading indicator
|
|
210
|
+
Swal.fire({
|
|
211
|
+
title: 'Processing Legacy Data...',
|
|
212
|
+
text: 'Converting and validating JSON data',
|
|
213
|
+
allowOutsideClick: false,
|
|
214
|
+
showConfirmButton: false,
|
|
215
|
+
didOpen: () => {
|
|
216
|
+
Swal.showLoading();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Process and validate the JSON data
|
|
221
|
+
const result = await processLegacyJsonData(jsonData);
|
|
222
|
+
|
|
223
|
+
Swal.close();
|
|
224
|
+
|
|
225
|
+
if (result.success) {
|
|
226
|
+
// Load the processed data
|
|
227
|
+
setProjectName(result.data.projectName);
|
|
228
|
+
setMode(result.data.mode);
|
|
229
|
+
setCameras(result.data.cameras);
|
|
230
|
+
|
|
231
|
+
if (result.data.uploadedImage) {
|
|
232
|
+
setUploadedImage(result.data.uploadedImage);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Clear the selected project since this is new legacy data
|
|
236
|
+
setSelectedProject(null);
|
|
237
|
+
setSidebarTab('cameras');
|
|
238
|
+
|
|
239
|
+
Swal.fire({
|
|
240
|
+
icon: 'success',
|
|
241
|
+
title: 'Legacy Data Loaded!',
|
|
242
|
+
html: `
|
|
243
|
+
<div style="text-align: left;">
|
|
244
|
+
<p><strong>Successfully imported:</strong></p>
|
|
245
|
+
<ul style="margin: 10px 0; padding-left: 20px;">
|
|
246
|
+
<li>Project: "${result.data.projectName}"</li>
|
|
247
|
+
<li>Cameras: ${result.data.cameras.length}</li>
|
|
248
|
+
<li>Mode: ${result.data.mode}</li>
|
|
249
|
+
${result.data.uploadedImage ? '<li>Image: Yes</li>' : '<li>Image: No</li>'}
|
|
250
|
+
</ul>
|
|
251
|
+
<p style="color: #6b7280; font-size: 14px; margin-top: 15px;">
|
|
252
|
+
<strong>Next steps:</strong> Review the imported data and save it to the database using the Save button.
|
|
253
|
+
</p>
|
|
254
|
+
</div>
|
|
255
|
+
`,
|
|
256
|
+
confirmButtonColor: '#3b82f6',
|
|
257
|
+
confirmButtonText: 'Continue',
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error(result.error);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
Swal.fire({
|
|
264
|
+
icon: 'error',
|
|
265
|
+
title: 'Import Failed',
|
|
266
|
+
html: `
|
|
267
|
+
<div style="text-align: left;">
|
|
268
|
+
<p>Failed to import legacy JSON file:</p>
|
|
269
|
+
<div style="background: #f3f4f6; padding: 10px; border-radius: 4px; margin: 10px 0; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;">
|
|
270
|
+
${error.message}
|
|
271
|
+
</div>
|
|
272
|
+
<p style="color: #6b7280; font-size: 14px;">
|
|
273
|
+
Please check that the JSON file contains valid camera placement data.
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
`,
|
|
277
|
+
confirmButtonColor: '#ef4444',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Clear the file input
|
|
282
|
+
event.target.value = '';
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
// Process legacy JSON data and convert to current format
|
|
286
|
+
const processLegacyJsonData = useCallback(async (jsonData) => {
|
|
287
|
+
try {
|
|
288
|
+
// Validate basic structure
|
|
289
|
+
if (!jsonData || typeof jsonData !== 'object') {
|
|
290
|
+
throw new Error('Invalid JSON structure - expected an object');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let cameras = [];
|
|
294
|
+
let projectName = '';
|
|
295
|
+
let mode = 'image';
|
|
296
|
+
let uploadedImage = null;
|
|
297
|
+
|
|
298
|
+
// Handle different legacy JSON formats
|
|
299
|
+
|
|
300
|
+
// Format 1: Direct camera placement export with cameras array
|
|
301
|
+
if (jsonData.cameras && Array.isArray(jsonData.cameras)) {
|
|
302
|
+
cameras = jsonData.cameras.map((camera, index) => {
|
|
303
|
+
// Ensure required fields exist with fallbacks
|
|
304
|
+
const id = camera.id || Date.now() + index;
|
|
305
|
+
const name = camera.name || camera.customName || `Camera ${index + 1}`;
|
|
306
|
+
const x = typeof camera.x === 'number' ? camera.x : (camera.position?.x || 0);
|
|
307
|
+
const y = typeof camera.y === 'number' ? camera.y : (camera.position?.y || 0);
|
|
308
|
+
const direction = typeof camera.direction === 'number' ? camera.direction : (camera.rotation || 0);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
id: id,
|
|
312
|
+
name: name,
|
|
313
|
+
customName: camera.customName || '',
|
|
314
|
+
x: x,
|
|
315
|
+
y: y,
|
|
316
|
+
direction: direction,
|
|
317
|
+
verkadaModel: camera.verkadaModel || camera.model || null,
|
|
318
|
+
timestamp: camera.timestamp || new Date().toISOString(),
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
projectName = jsonData.projectName || jsonData.name || jsonData.title || 'Imported Legacy Project';
|
|
323
|
+
mode = jsonData.mode || 'image';
|
|
324
|
+
|
|
325
|
+
// Handle different image data formats - extract src string directly
|
|
326
|
+
if (jsonData.image && jsonData.image.src) {
|
|
327
|
+
// Format: { "image": { "src": "data:image/png;base64,..." } }
|
|
328
|
+
uploadedImage = jsonData.image.src;
|
|
329
|
+
} else if (jsonData.uploadedImage && jsonData.uploadedImage.src) {
|
|
330
|
+
// Format: { "uploadedImage": { "src": "...", "name": "..." } }
|
|
331
|
+
uploadedImage = jsonData.uploadedImage.src;
|
|
332
|
+
} else if (jsonData.uploadedImage && typeof jsonData.uploadedImage === 'string') {
|
|
333
|
+
// Format: { "uploadedImage": "data:image/..." }
|
|
334
|
+
uploadedImage = jsonData.uploadedImage;
|
|
335
|
+
} else if (jsonData.backgroundImage) {
|
|
336
|
+
// Format: { "backgroundImage": "data:image/..." }
|
|
337
|
+
uploadedImage = jsonData.backgroundImage;
|
|
338
|
+
} else {
|
|
339
|
+
uploadedImage = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Format 2: Legacy format with different structure
|
|
344
|
+
else if (jsonData.placement || jsonData.cameraPlacement) {
|
|
345
|
+
const placement = jsonData.placement || jsonData.cameraPlacement;
|
|
346
|
+
|
|
347
|
+
if (placement.cameras && Array.isArray(placement.cameras)) {
|
|
348
|
+
cameras = placement.cameras.map((camera, index) => ({
|
|
349
|
+
id: camera.id || Date.now() + index,
|
|
350
|
+
name: camera.name || `Camera ${index + 1}`,
|
|
351
|
+
customName: camera.customName || '',
|
|
352
|
+
x: camera.x || 0,
|
|
353
|
+
y: camera.y || 0,
|
|
354
|
+
direction: camera.direction || 0,
|
|
355
|
+
verkadaModel: camera.verkadaModel || null,
|
|
356
|
+
timestamp: new Date().toISOString(),
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
projectName = placement.projectName || placement.name || 'Imported Legacy Project';
|
|
361
|
+
mode = placement.mode || 'image';
|
|
362
|
+
|
|
363
|
+
// Handle different image data formats - extract src string directly
|
|
364
|
+
if (placement.image && placement.image.src) {
|
|
365
|
+
uploadedImage = placement.image.src;
|
|
366
|
+
} else if (placement.image && typeof placement.image === 'string') {
|
|
367
|
+
uploadedImage = placement.image;
|
|
368
|
+
} else if (placement.backgroundImage) {
|
|
369
|
+
uploadedImage = placement.backgroundImage;
|
|
370
|
+
} else {
|
|
371
|
+
uploadedImage = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Format 3: Simple array of cameras
|
|
376
|
+
else if (Array.isArray(jsonData)) {
|
|
377
|
+
cameras = jsonData.map((camera, index) => ({
|
|
378
|
+
id: camera.id || Date.now() + index,
|
|
379
|
+
name: camera.name || `Camera ${index + 1}`,
|
|
380
|
+
customName: camera.customName || '',
|
|
381
|
+
x: camera.x || 0,
|
|
382
|
+
y: camera.y || 0,
|
|
383
|
+
direction: camera.direction || 0,
|
|
384
|
+
verkadaModel: camera.verkadaModel || null,
|
|
385
|
+
timestamp: new Date().toISOString(),
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
projectName = 'Imported Legacy Project';
|
|
389
|
+
mode = 'image';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Format 4: Check for other common legacy formats
|
|
393
|
+
else {
|
|
394
|
+
// Look for any array that might contain camera data
|
|
395
|
+
const possibleCameraArrays = Object.values(jsonData).filter(value =>
|
|
396
|
+
Array.isArray(value) && value.length > 0 &&
|
|
397
|
+
value[0] && typeof value[0] === 'object'
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (possibleCameraArrays.length > 0) {
|
|
401
|
+
const cameraArray = possibleCameraArrays[0];
|
|
402
|
+
cameras = cameraArray.map((camera, index) => ({
|
|
403
|
+
id: camera.id || Date.now() + index,
|
|
404
|
+
name: camera.name || `Camera ${index + 1}`,
|
|
405
|
+
customName: camera.customName || '',
|
|
406
|
+
x: camera.x || 0,
|
|
407
|
+
y: camera.y || 0,
|
|
408
|
+
direction: camera.direction || 0,
|
|
409
|
+
verkadaModel: camera.verkadaModel || null,
|
|
410
|
+
timestamp: new Date().toISOString(),
|
|
411
|
+
}));
|
|
412
|
+
|
|
413
|
+
projectName = jsonData.name || jsonData.title || 'Imported Legacy Project';
|
|
414
|
+
mode = jsonData.mode || 'image';
|
|
415
|
+
|
|
416
|
+
// Handle different image data formats - extract src string directly
|
|
417
|
+
if (jsonData.image && jsonData.image.src) {
|
|
418
|
+
uploadedImage = jsonData.image.src;
|
|
419
|
+
} else if (jsonData.image && typeof jsonData.image === 'string') {
|
|
420
|
+
uploadedImage = jsonData.image;
|
|
421
|
+
} else if (jsonData.backgroundImage) {
|
|
422
|
+
uploadedImage = jsonData.backgroundImage;
|
|
423
|
+
} else {
|
|
424
|
+
uploadedImage = null;
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
throw new Error('No valid camera data found in JSON file. Expected arrays of camera objects.');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Validate that we have at least some cameras
|
|
432
|
+
if (!cameras || cameras.length === 0) {
|
|
433
|
+
throw new Error('No cameras found in the JSON file');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Validate camera data
|
|
437
|
+
const invalidCameras = cameras.filter(camera =>
|
|
438
|
+
typeof camera.x !== 'number' ||
|
|
439
|
+
typeof camera.y !== 'number' ||
|
|
440
|
+
isNaN(camera.x) ||
|
|
441
|
+
isNaN(camera.y)
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
if (invalidCameras.length > 0) {
|
|
445
|
+
throw new Error(`${invalidCameras.length} cameras have invalid position data (x, y coordinates must be numbers)`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
success: true,
|
|
450
|
+
data: {
|
|
451
|
+
projectName: projectName,
|
|
452
|
+
mode: mode,
|
|
453
|
+
cameras: cameras,
|
|
454
|
+
uploadedImage: uploadedImage
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
} catch (error) {
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
error: error.message
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}, []);
|
|
465
|
+
|
|
189
466
|
// Initialize Mapbox GL map
|
|
190
467
|
useEffect(() => {
|
|
191
468
|
if (!mapContainer.current || mode !== 'map') return;
|
|
@@ -575,7 +852,7 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
575
852
|
const loadProject = useCallback(async (lightweightProject) => {
|
|
576
853
|
try {
|
|
577
854
|
// Show loading indicator
|
|
578
|
-
|
|
855
|
+
Swal.fire({
|
|
579
856
|
title: 'Loading Project...',
|
|
580
857
|
text: 'Fetching project data',
|
|
581
858
|
allowOutsideClick: false,
|
|
@@ -1194,6 +1471,12 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
1194
1471
|
tooltip="Upload Image"
|
|
1195
1472
|
className={styles.uploadBtn}
|
|
1196
1473
|
/>
|
|
1474
|
+
<IconButton
|
|
1475
|
+
icon={FolderOpen}
|
|
1476
|
+
onClick={() => jsonFileInputRef.current?.click()}
|
|
1477
|
+
tooltip="Import Legacy JSON File"
|
|
1478
|
+
className={styles.importBtn}
|
|
1479
|
+
/>
|
|
1197
1480
|
<IconButton
|
|
1198
1481
|
icon={Save}
|
|
1199
1482
|
onClick={saveToDatabase}
|
|
@@ -2289,7 +2572,7 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
2289
2572
|
</div>
|
|
2290
2573
|
</div>
|
|
2291
2574
|
|
|
2292
|
-
{/* Hidden file
|
|
2575
|
+
{/* Hidden file inputs */}
|
|
2293
2576
|
<input
|
|
2294
2577
|
type="file"
|
|
2295
2578
|
ref={fileInputRef}
|
|
@@ -2297,6 +2580,13 @@ const CameraPlacement = ({ userProfile }) => {
|
|
|
2297
2580
|
accept="image/*"
|
|
2298
2581
|
style={{ display: 'none' }}
|
|
2299
2582
|
/>
|
|
2583
|
+
<input
|
|
2584
|
+
type="file"
|
|
2585
|
+
ref={jsonFileInputRef}
|
|
2586
|
+
onChange={handleJsonFileUpload}
|
|
2587
|
+
accept=".json,application/json"
|
|
2588
|
+
style={{ display: 'none' }}
|
|
2589
|
+
/>
|
|
2300
2590
|
|
|
2301
2591
|
{showModal && selectedCamera && (
|
|
2302
2592
|
<CameraSelectionModal
|
|
@@ -1082,6 +1082,20 @@ const GenericReportImproved = ({
|
|
|
1082
1082
|
exportUrl: '',
|
|
1083
1083
|
});
|
|
1084
1084
|
|
|
1085
|
+
// State for unique field selection and row exclusion
|
|
1086
|
+
const [uniqueFieldConfig, setUniqueFieldConfig] = useState({
|
|
1087
|
+
enabled: false,
|
|
1088
|
+
selectedField: '',
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
// Debug logging for uniqueFieldConfig changes
|
|
1092
|
+
useEffect(() => {
|
|
1093
|
+
console.log('🔍 [EFFECT] uniqueFieldConfig changed:', uniqueFieldConfig);
|
|
1094
|
+
}, [uniqueFieldConfig]);
|
|
1095
|
+
const [excludedRows, setExcludedRows] = useState(new Set());
|
|
1096
|
+
const [showRowExclusionModal, setShowRowExclusionModal] = useState(false);
|
|
1097
|
+
const [exportType, setExportType] = useState('regular');
|
|
1098
|
+
|
|
1085
1099
|
// Note: Removed table search functionality to simplify user experience
|
|
1086
1100
|
|
|
1087
1101
|
// Extract additional settings with defaults
|
|
@@ -2291,6 +2305,17 @@ const GenericReportImproved = ({
|
|
|
2291
2305
|
let payload;
|
|
2292
2306
|
|
|
2293
2307
|
if (isDynamicTemplate) {
|
|
2308
|
+
// Enhanced unique field configuration for grouped templates
|
|
2309
|
+
const groupedUniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
|
|
2310
|
+
enabled: true,
|
|
2311
|
+
field: uniqueFieldConfig.selectedField,
|
|
2312
|
+
distinct: true,
|
|
2313
|
+
distinctField: uniqueFieldConfig.selectedField
|
|
2314
|
+
} : {
|
|
2315
|
+
enabled: false,
|
|
2316
|
+
distinct: false
|
|
2317
|
+
};
|
|
2318
|
+
|
|
2294
2319
|
// For dynamic templates, send the complete template configuration
|
|
2295
2320
|
payload = {
|
|
2296
2321
|
dynamic_template: template,
|
|
@@ -2299,7 +2324,16 @@ const GenericReportImproved = ({
|
|
|
2299
2324
|
joins: joins,
|
|
2300
2325
|
filters: flattenFilterCriteria(filterCriteria),
|
|
2301
2326
|
sorting: sortCriteria,
|
|
2327
|
+
// Enhanced unique field configuration
|
|
2328
|
+
unique: groupedUniqueConfig,
|
|
2302
2329
|
};
|
|
2330
|
+
|
|
2331
|
+
// Debug log for grouped reports
|
|
2332
|
+
if (uniqueFieldConfig.enabled) {
|
|
2333
|
+
console.group('🔍 [DEBUG] Grouped Report - Unique Field Configuration');
|
|
2334
|
+
console.log('Grouped unique config:', groupedUniqueConfig);
|
|
2335
|
+
console.groupEnd();
|
|
2336
|
+
}
|
|
2303
2337
|
} else {
|
|
2304
2338
|
// For predefined templates, use the existing approach
|
|
2305
2339
|
payload = {
|
|
@@ -2337,6 +2371,28 @@ const GenericReportImproved = ({
|
|
|
2337
2371
|
const page = Math.floor(skip / limit) + 1;
|
|
2338
2372
|
|
|
2339
2373
|
// Use the same payload format as GenericReport (with query wrapper)
|
|
2374
|
+
// Debug the unique field state at query time
|
|
2375
|
+
console.group('🔍 [DEBUG] DataSource - Unique Field State Check');
|
|
2376
|
+
console.log('uniqueFieldConfig:', uniqueFieldConfig);
|
|
2377
|
+
console.log('uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
|
|
2378
|
+
console.log('uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
|
|
2379
|
+
console.log('Condition evaluation:', uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField);
|
|
2380
|
+
|
|
2381
|
+
// Check DOM state vs React state
|
|
2382
|
+
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
|
|
2383
|
+
console.log('🔍 [DEBUG] All checkboxes in DOM:', Array.from(checkboxes).map((cb, i) => `${i}: ${cb.checked}`));
|
|
2384
|
+
console.groupEnd();
|
|
2385
|
+
|
|
2386
|
+
const uniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
|
|
2387
|
+
enabled: true,
|
|
2388
|
+
field: uniqueFieldConfig.selectedField,
|
|
2389
|
+
distinct: true, // Explicit DISTINCT flag
|
|
2390
|
+
distinctField: uniqueFieldConfig.selectedField
|
|
2391
|
+
} : {
|
|
2392
|
+
enabled: false,
|
|
2393
|
+
distinct: false
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2340
2396
|
const queryConfig = {
|
|
2341
2397
|
mainTable: selectedTable,
|
|
2342
2398
|
columns: selectedColumns.map((col) => {
|
|
@@ -2368,8 +2424,20 @@ const GenericReportImproved = ({
|
|
|
2368
2424
|
})),
|
|
2369
2425
|
filters: flattenFilterCriteria(filterCriteria),
|
|
2370
2426
|
sorting: sortCriteria,
|
|
2427
|
+
// Enhanced unique field configuration
|
|
2428
|
+
unique: uniqueConfig,
|
|
2371
2429
|
};
|
|
2372
2430
|
|
|
2431
|
+
// Debug log for unique field configuration
|
|
2432
|
+
if (uniqueFieldConfig.enabled) {
|
|
2433
|
+
console.group('🔍 [DEBUG] Unique Field Configuration');
|
|
2434
|
+
console.log('Unique config enabled:', uniqueFieldConfig.enabled);
|
|
2435
|
+
console.log('Selected field:', uniqueFieldConfig.selectedField);
|
|
2436
|
+
console.log('Query unique config:', uniqueConfig);
|
|
2437
|
+
console.log('Expected SQL: SELECT DISTINCT', uniqueFieldConfig.selectedField, '...');
|
|
2438
|
+
console.groupEnd();
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2373
2441
|
// Add sorting from DataGrid if provided
|
|
2374
2442
|
if (sortInfo) {
|
|
2375
2443
|
queryConfig.sorting = [
|
|
@@ -2461,6 +2529,11 @@ const GenericReportImproved = ({
|
|
|
2461
2529
|
executeUrl,
|
|
2462
2530
|
setting,
|
|
2463
2531
|
});
|
|
2532
|
+
|
|
2533
|
+
// CRITICAL DEBUG: Check uniqueFieldConfig state at execute time
|
|
2534
|
+
console.log('🚨 [CRITICAL] uniqueFieldConfig at executeReport start:', uniqueFieldConfig);
|
|
2535
|
+
console.log('🚨 [CRITICAL] uniqueFieldConfig.enabled:', uniqueFieldConfig.enabled);
|
|
2536
|
+
console.log('🚨 [CRITICAL] uniqueFieldConfig.selectedField:', uniqueFieldConfig.selectedField);
|
|
2464
2537
|
|
|
2465
2538
|
if (!selectedTable || selectedColumns.length === 0) {
|
|
2466
2539
|
toast.warning(
|
|
@@ -2885,6 +2958,12 @@ const GenericReportImproved = ({
|
|
|
2885
2958
|
});
|
|
2886
2959
|
}
|
|
2887
2960
|
|
|
2961
|
+
// Restore unique field configuration if it exists
|
|
2962
|
+
if (config.unique) {
|
|
2963
|
+
console.log('🔄 [RESTORE] Restoring unique field config:', config.unique);
|
|
2964
|
+
setUniqueFieldConfig(config.unique);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2888
2967
|
if (config.filters) {
|
|
2889
2968
|
// Ensure filters have the proper structure
|
|
2890
2969
|
if (config.filters.groups) {
|
|
@@ -4181,6 +4260,96 @@ const GenericReportImproved = ({
|
|
|
4181
4260
|
)}
|
|
4182
4261
|
</div>
|
|
4183
4262
|
)}
|
|
4263
|
+
|
|
4264
|
+
{/* Unique Field Configuration */}
|
|
4265
|
+
{selectedColumns.length > 0 && (
|
|
4266
|
+
<div className={styles.uniqueFieldConfig}>
|
|
4267
|
+
<div className={styles.configHeader}>
|
|
4268
|
+
<h4>
|
|
4269
|
+
<Filter size={16} />
|
|
4270
|
+
Remove Duplicates (Optional)
|
|
4271
|
+
</h4>
|
|
4272
|
+
<p>Use SQL DISTINCT to prevent duplicate rows based on a specific field</p>
|
|
4273
|
+
</div>
|
|
4274
|
+
<div className={styles.configContent}>
|
|
4275
|
+
<label className={styles.checkboxLabel}>
|
|
4276
|
+
<input
|
|
4277
|
+
type="checkbox"
|
|
4278
|
+
checked={uniqueFieldConfig.enabled}
|
|
4279
|
+
onChange={(e) => {
|
|
4280
|
+
console.log('🔍 [DEBUG] Unique field toggled:', e.target.checked);
|
|
4281
|
+
console.log('🔍 [DEBUG] Current uniqueFieldConfig before change:', uniqueFieldConfig);
|
|
4282
|
+
const newConfig = {
|
|
4283
|
+
...uniqueFieldConfig,
|
|
4284
|
+
enabled: e.target.checked,
|
|
4285
|
+
selectedField: e.target.checked ? uniqueFieldConfig.selectedField : ''
|
|
4286
|
+
};
|
|
4287
|
+
console.log('🔍 [DEBUG] New unique field config:', newConfig);
|
|
4288
|
+
setUniqueFieldConfig(newConfig);
|
|
4289
|
+
// Log immediately after setState
|
|
4290
|
+
setTimeout(() => {
|
|
4291
|
+
console.log('🔍 [DEBUG] State after setTimeout:', uniqueFieldConfig);
|
|
4292
|
+
}, 100);
|
|
4293
|
+
}}
|
|
4294
|
+
/>
|
|
4295
|
+
<span className={styles.checkboxText}>
|
|
4296
|
+
Enable DISTINCT filtering
|
|
4297
|
+
{uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField && (
|
|
4298
|
+
<span className={styles.activeIndicator}>
|
|
4299
|
+
<CircleCheck size={12} />
|
|
4300
|
+
Active on {uniqueFieldConfig.selectedField}
|
|
4301
|
+
</span>
|
|
4302
|
+
)}
|
|
4303
|
+
</span>
|
|
4304
|
+
</label>
|
|
4305
|
+
|
|
4306
|
+
{uniqueFieldConfig.enabled && (
|
|
4307
|
+
<div className={styles.uniqueFieldSelector}>
|
|
4308
|
+
<label className={styles.fieldLabel}>
|
|
4309
|
+
Select field to make unique:
|
|
4310
|
+
</label>
|
|
4311
|
+
<select
|
|
4312
|
+
value={uniqueFieldConfig.selectedField}
|
|
4313
|
+
onChange={(e) => {
|
|
4314
|
+
console.log('🔍 [DEBUG] Unique field selected:', e.target.value);
|
|
4315
|
+
console.log('🔍 [DEBUG] Available dropdown options:', Array.from(e.target.options).map(opt => ({value: opt.value, text: opt.text})));
|
|
4316
|
+
console.log('🔍 [DEBUG] Selected option text:', e.target.options[e.target.selectedIndex]?.text);
|
|
4317
|
+
console.log('🔍 [DEBUG] Current uniqueFieldConfig before field change:', uniqueFieldConfig);
|
|
4318
|
+
console.log('🔍 [DEBUG] Current selectedColumns:', selectedColumns);
|
|
4319
|
+
const newConfig = {
|
|
4320
|
+
...uniqueFieldConfig,
|
|
4321
|
+
selectedField: e.target.value
|
|
4322
|
+
};
|
|
4323
|
+
console.log('🔍 [DEBUG] Updated unique field config with selected field:', newConfig);
|
|
4324
|
+
setUniqueFieldConfig(newConfig);
|
|
4325
|
+
// Log immediately after setState
|
|
4326
|
+
setTimeout(() => {
|
|
4327
|
+
console.log('🔍 [DEBUG] Field state after setTimeout:', uniqueFieldConfig);
|
|
4328
|
+
}, 100);
|
|
4329
|
+
}}
|
|
4330
|
+
className={styles.fieldSelect}
|
|
4331
|
+
required
|
|
4332
|
+
>
|
|
4333
|
+
<option value="">Choose a field...</option>
|
|
4334
|
+
{selectedColumns.map((col) => (
|
|
4335
|
+
<option
|
|
4336
|
+
key={`${col.table}-${col.column}`}
|
|
4337
|
+
value={`${col.table}.${col.column}`}
|
|
4338
|
+
>
|
|
4339
|
+
{col.displayName || col.column} ({col.table})
|
|
4340
|
+
</option>
|
|
4341
|
+
))}
|
|
4342
|
+
</select>
|
|
4343
|
+
<div className={styles.fieldHelp}>
|
|
4344
|
+
<strong>SQL Impact:</strong> This will add DISTINCT to your query based on the selected field.
|
|
4345
|
+
<br />
|
|
4346
|
+
<code>SELECT DISTINCT {uniqueFieldConfig.selectedField || '[field]'}, ... FROM {selectedTable}</code>
|
|
4347
|
+
</div>
|
|
4348
|
+
</div>
|
|
4349
|
+
)}
|
|
4350
|
+
</div>
|
|
4351
|
+
</div>
|
|
4352
|
+
)}
|
|
4184
4353
|
</div>
|
|
4185
4354
|
);
|
|
4186
4355
|
};
|
|
@@ -6483,6 +6652,17 @@ const GenericReportImproved = ({
|
|
|
6483
6652
|
|
|
6484
6653
|
try {
|
|
6485
6654
|
// Use exact same payload format as GenericReport
|
|
6655
|
+
// Enhanced unique field configuration for exports
|
|
6656
|
+
const exportUniqueConfig = uniqueFieldConfig.enabled && uniqueFieldConfig.selectedField ? {
|
|
6657
|
+
enabled: true,
|
|
6658
|
+
field: uniqueFieldConfig.selectedField,
|
|
6659
|
+
distinct: true,
|
|
6660
|
+
distinctField: uniqueFieldConfig.selectedField
|
|
6661
|
+
} : {
|
|
6662
|
+
enabled: false,
|
|
6663
|
+
distinct: false
|
|
6664
|
+
};
|
|
6665
|
+
|
|
6486
6666
|
const queryConfig = {
|
|
6487
6667
|
mainTable: selectedTable,
|
|
6488
6668
|
columns: selectedColumns.map((col) => ({
|
|
@@ -6503,8 +6683,21 @@ const GenericReportImproved = ({
|
|
|
6503
6683
|
})),
|
|
6504
6684
|
filters: flattenFilterCriteria(filterCriteria),
|
|
6505
6685
|
sorting: sortCriteria,
|
|
6686
|
+
// Enhanced unique field configuration
|
|
6687
|
+
unique: exportUniqueConfig,
|
|
6688
|
+
// Add excluded rows for server-side filtering
|
|
6689
|
+
excludedRows: Array.from(excludedRows),
|
|
6506
6690
|
};
|
|
6507
6691
|
|
|
6692
|
+
// Debug log for exports
|
|
6693
|
+
if (uniqueFieldConfig.enabled) {
|
|
6694
|
+
console.group('🔍 [DEBUG] Export - Unique Field Configuration');
|
|
6695
|
+
console.log('Export format:', format);
|
|
6696
|
+
console.log('Export unique config:', exportUniqueConfig);
|
|
6697
|
+
console.log('Query config:', queryConfig);
|
|
6698
|
+
console.groupEnd();
|
|
6699
|
+
}
|
|
6700
|
+
|
|
6508
6701
|
const exportData = {
|
|
6509
6702
|
query: queryConfig,
|
|
6510
6703
|
format: format === 'excel' ? 'xlsx' : format, // Convert excel to xlsx
|
|
@@ -6663,23 +6856,78 @@ const GenericReportImproved = ({
|
|
|
6663
6856
|
}
|
|
6664
6857
|
};
|
|
6665
6858
|
|
|
6666
|
-
// Handle
|
|
6667
|
-
const
|
|
6859
|
+
// Handle row exclusion modal
|
|
6860
|
+
const handleExportWithExclusion = (exportType = 'regular') => {
|
|
6668
6861
|
if (previewData.length === 0) {
|
|
6669
6862
|
toast.error('No data available to export');
|
|
6670
6863
|
return;
|
|
6671
6864
|
}
|
|
6672
6865
|
|
|
6866
|
+
setShowRowExclusionModal(true);
|
|
6867
|
+
setExportType(exportType);
|
|
6868
|
+
};
|
|
6869
|
+
|
|
6870
|
+
const confirmExportWithExclusion = async () => {
|
|
6871
|
+
setShowRowExclusionModal(false);
|
|
6872
|
+
|
|
6873
|
+
if (exportType === 'grouped') {
|
|
6874
|
+
await performGroupedExport();
|
|
6875
|
+
} else {
|
|
6876
|
+
await performRegularExport();
|
|
6877
|
+
}
|
|
6878
|
+
};
|
|
6879
|
+
|
|
6880
|
+
// Handle grouped report export to Excel
|
|
6881
|
+
const handleRegularExport = async () => {
|
|
6882
|
+
handleExportWithExclusion('regular');
|
|
6883
|
+
};
|
|
6884
|
+
|
|
6885
|
+
// Filter out excluded rows from preview data
|
|
6886
|
+
const getFilteredPreviewData = () => {
|
|
6887
|
+
if (excludedRows.size === 0) {
|
|
6888
|
+
return previewData;
|
|
6889
|
+
}
|
|
6890
|
+
|
|
6891
|
+
return previewData.filter((row, index) => {
|
|
6892
|
+
const rowId = row.id || `preview-row-${index}`;
|
|
6893
|
+
return !excludedRows.has(rowId);
|
|
6894
|
+
});
|
|
6895
|
+
};
|
|
6896
|
+
|
|
6897
|
+
// Perform the actual regular export
|
|
6898
|
+
const performRegularExport = async () => {
|
|
6899
|
+
const filteredData = getFilteredPreviewData();
|
|
6900
|
+
|
|
6901
|
+
if (filteredData.length === 0) {
|
|
6902
|
+
toast.error('No data available to export after exclusions');
|
|
6903
|
+
return;
|
|
6904
|
+
}
|
|
6905
|
+
|
|
6673
6906
|
try {
|
|
6907
|
+
// Temporarily replace previewData with filtered data for export
|
|
6908
|
+
const originalPreviewData = previewData;
|
|
6909
|
+
setPreviewData(filteredData);
|
|
6910
|
+
|
|
6674
6911
|
// Use the existing client-side Excel export functionality
|
|
6675
6912
|
await exportReport('excel');
|
|
6913
|
+
|
|
6914
|
+
// Restore original data
|
|
6915
|
+
setPreviewData(originalPreviewData);
|
|
6676
6916
|
} catch (error) {
|
|
6677
6917
|
console.error('Regular export failed:', error);
|
|
6678
6918
|
toast.error('Export failed. Please try again.');
|
|
6919
|
+
|
|
6920
|
+
// Restore original data on error as well
|
|
6921
|
+
setPreviewData(previewData);
|
|
6679
6922
|
}
|
|
6680
6923
|
};
|
|
6681
6924
|
|
|
6682
6925
|
const handleGroupedExport = async () => {
|
|
6926
|
+
handleExportWithExclusion('grouped');
|
|
6927
|
+
};
|
|
6928
|
+
|
|
6929
|
+
// Perform the actual grouped export
|
|
6930
|
+
const performGroupedExport = async () => {
|
|
6683
6931
|
if (!selectedTemplate) {
|
|
6684
6932
|
toast.error('No template selected for export');
|
|
6685
6933
|
return;
|
|
@@ -6830,9 +7078,17 @@ const GenericReportImproved = ({
|
|
|
6830
7078
|
groupHeaderRow[0] = `${groupDisplayName}: ${group.groupDisplayName}`;
|
|
6831
7079
|
worksheetData.push(groupHeaderRow);
|
|
6832
7080
|
|
|
6833
|
-
// Add data rows for this group
|
|
7081
|
+
// Add data rows for this group (excluding excluded rows)
|
|
6834
7082
|
const allRows = [...group.rows, ...(group.emptyRows || [])];
|
|
6835
|
-
allRows.forEach((row) => {
|
|
7083
|
+
allRows.forEach((row, rowIndex) => {
|
|
7084
|
+
// Create a unique identifier for the row
|
|
7085
|
+
const rowId = row.id || `group-${groupIndex}-row-${rowIndex}`;
|
|
7086
|
+
|
|
7087
|
+
// Skip excluded rows
|
|
7088
|
+
if (excludedRows.has(rowId)) {
|
|
7089
|
+
return;
|
|
7090
|
+
}
|
|
7091
|
+
|
|
6836
7092
|
const dataRow = selectedColumns.map((column) =>
|
|
6837
7093
|
getCellValue(row, column)
|
|
6838
7094
|
);
|
|
@@ -6977,6 +7233,7 @@ const GenericReportImproved = ({
|
|
|
6977
7233
|
isGrouped: groupingConfig.enabled,
|
|
6978
7234
|
customCalculatedFields: customCalculatedFields,
|
|
6979
7235
|
customUrls: customUrls,
|
|
7236
|
+
unique: uniqueFieldConfig,
|
|
6980
7237
|
},
|
|
6981
7238
|
};
|
|
6982
7239
|
|
|
@@ -7400,7 +7657,7 @@ const GenericReportImproved = ({
|
|
|
7400
7657
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
7401
7658
|
onClick={() => {
|
|
7402
7659
|
resetPagination();
|
|
7403
|
-
|
|
7660
|
+
// Don't reset hasAutoExecuted to avoid triggering useEffect
|
|
7404
7661
|
executeReport();
|
|
7405
7662
|
}}
|
|
7406
7663
|
disabled={isLoading}
|
|
@@ -8597,6 +8854,123 @@ const GenericReportImproved = ({
|
|
|
8597
8854
|
</div>
|
|
8598
8855
|
</div>
|
|
8599
8856
|
)}
|
|
8857
|
+
|
|
8858
|
+
{/* Row Exclusion Modal */}
|
|
8859
|
+
{showRowExclusionModal && (
|
|
8860
|
+
<div className={styles.modalOverlay}>
|
|
8861
|
+
<div className={styles.rowExclusionModal}>
|
|
8862
|
+
<div className={styles.modalHeader}>
|
|
8863
|
+
<h3>Select Rows to Exclude</h3>
|
|
8864
|
+
<p>Choose which rows you don't want to include in the export</p>
|
|
8865
|
+
<button
|
|
8866
|
+
className={styles.modalCloseButton}
|
|
8867
|
+
onClick={() => setShowRowExclusionModal(false)}
|
|
8868
|
+
>
|
|
8869
|
+
✕
|
|
8870
|
+
</button>
|
|
8871
|
+
</div>
|
|
8872
|
+
|
|
8873
|
+
<div className={styles.modalBody}>
|
|
8874
|
+
<div className={styles.rowExclusionList}>
|
|
8875
|
+
{(exportType === 'grouped' && groupedData?.groupedData?.groups ?
|
|
8876
|
+
groupedData.groupedData.groups.flatMap((group, groupIndex) =>
|
|
8877
|
+
[...group.rows, ...(group.emptyRows || [])].map((row, rowIndex) => {
|
|
8878
|
+
const rowId = row.id || `group-${groupIndex}-row-${rowIndex}`;
|
|
8879
|
+
const isExcluded = excludedRows.has(rowId);
|
|
8880
|
+
|
|
8881
|
+
return (
|
|
8882
|
+
<label key={rowId} className={styles.rowExclusionItem}>
|
|
8883
|
+
<input
|
|
8884
|
+
type="checkbox"
|
|
8885
|
+
checked={isExcluded}
|
|
8886
|
+
onChange={(e) => {
|
|
8887
|
+
const newExcludedRows = new Set(excludedRows);
|
|
8888
|
+
if (e.target.checked) {
|
|
8889
|
+
newExcludedRows.add(rowId);
|
|
8890
|
+
} else {
|
|
8891
|
+
newExcludedRows.delete(rowId);
|
|
8892
|
+
}
|
|
8893
|
+
setExcludedRows(newExcludedRows);
|
|
8894
|
+
}}
|
|
8895
|
+
/>
|
|
8896
|
+
<div className={styles.rowPreview}>
|
|
8897
|
+
{selectedColumns.slice(0, 3).map((col) => {
|
|
8898
|
+
const value = row[col.displayName] || row[col.column] || row[`${col.table}_${col.column}`] || '';
|
|
8899
|
+
return (
|
|
8900
|
+
<span key={col.column} className={styles.rowValue}>
|
|
8901
|
+
{String(value).substring(0, 50)}{String(value).length > 50 ? '...' : ''}
|
|
8902
|
+
</span>
|
|
8903
|
+
);
|
|
8904
|
+
})}
|
|
8905
|
+
</div>
|
|
8906
|
+
</label>
|
|
8907
|
+
);
|
|
8908
|
+
})
|
|
8909
|
+
)
|
|
8910
|
+
:
|
|
8911
|
+
previewData.map((row, index) => {
|
|
8912
|
+
const rowId = row.id || `preview-row-${index}`;
|
|
8913
|
+
const isExcluded = excludedRows.has(rowId);
|
|
8914
|
+
|
|
8915
|
+
return (
|
|
8916
|
+
<label key={rowId} className={styles.rowExclusionItem}>
|
|
8917
|
+
<input
|
|
8918
|
+
type="checkbox"
|
|
8919
|
+
checked={isExcluded}
|
|
8920
|
+
onChange={(e) => {
|
|
8921
|
+
const newExcludedRows = new Set(excludedRows);
|
|
8922
|
+
if (e.target.checked) {
|
|
8923
|
+
newExcludedRows.add(rowId);
|
|
8924
|
+
} else {
|
|
8925
|
+
newExcludedRows.delete(rowId);
|
|
8926
|
+
}
|
|
8927
|
+
setExcludedRows(newExcludedRows);
|
|
8928
|
+
}}
|
|
8929
|
+
/>
|
|
8930
|
+
<div className={styles.rowPreview}>
|
|
8931
|
+
{selectedColumns.slice(0, 3).map((col) => {
|
|
8932
|
+
const value = row[col.displayName] || row[col.column] || row[`${col.table}_${col.column}`] || '';
|
|
8933
|
+
return (
|
|
8934
|
+
<span key={col.column} className={styles.rowValue}>
|
|
8935
|
+
{String(value).substring(0, 50)}{String(value).length > 50 ? '...' : ''}
|
|
8936
|
+
</span>
|
|
8937
|
+
);
|
|
8938
|
+
})}
|
|
8939
|
+
</div>
|
|
8940
|
+
</label>
|
|
8941
|
+
);
|
|
8942
|
+
})
|
|
8943
|
+
)}
|
|
8944
|
+
</div>
|
|
8945
|
+
|
|
8946
|
+
<div className={styles.exclusionSummary}>
|
|
8947
|
+
<p>
|
|
8948
|
+
{excludedRows.size} of {(exportType === 'grouped' && groupedData?.groupedData?.groups ?
|
|
8949
|
+
groupedData.groupedData.groups.reduce((total, group) => total + group.rows.length + (group.emptyRows?.length || 0), 0)
|
|
8950
|
+
: previewData.length)} rows will be excluded from export
|
|
8951
|
+
</p>
|
|
8952
|
+
</div>
|
|
8953
|
+
</div>
|
|
8954
|
+
|
|
8955
|
+
<div className={styles.modalFooter}>
|
|
8956
|
+
<button
|
|
8957
|
+
className={styles.cancelButton}
|
|
8958
|
+
onClick={() => setShowRowExclusionModal(false)}
|
|
8959
|
+
>
|
|
8960
|
+
Cancel
|
|
8961
|
+
</button>
|
|
8962
|
+
<button
|
|
8963
|
+
className={styles.confirmButton}
|
|
8964
|
+
onClick={confirmExportWithExclusion}
|
|
8965
|
+
>
|
|
8966
|
+
Export ({(exportType === 'grouped' && groupedData?.groupedData?.groups ?
|
|
8967
|
+
groupedData.groupedData.groups.reduce((total, group) => total + group.rows.length + (group.emptyRows?.length || 0), 0) - excludedRows.size
|
|
8968
|
+
: previewData.length - excludedRows.size)} rows)
|
|
8969
|
+
</button>
|
|
8970
|
+
</div>
|
|
8971
|
+
</div>
|
|
8972
|
+
</div>
|
|
8973
|
+
)}
|
|
8600
8974
|
</div>
|
|
8601
8975
|
);
|
|
8602
8976
|
};
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
border-bottom: 1px solid rgba(var(--primary-rgb), 0.7);
|
|
19
19
|
z-index: 100;
|
|
20
20
|
position: relative;
|
|
21
|
-
min-width:
|
|
21
|
+
min-width: 1060px;
|
|
22
22
|
|
|
23
23
|
.headerContent {
|
|
24
24
|
display: flex;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
gap: 12px;
|
|
47
47
|
align-items: center;
|
|
48
48
|
flex-shrink: 0;
|
|
49
|
-
min-width:
|
|
49
|
+
min-width: 540px;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
.projectInput {
|
|
@@ -169,6 +169,17 @@
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
&.importBtn {
|
|
173
|
+
background: var(--warning-color, #f59e0b);
|
|
174
|
+
border-color: rgba(var(--warning-rgb, 245, 158, 11), 1);
|
|
175
|
+
|
|
176
|
+
&:hover:not(:disabled) {
|
|
177
|
+
background: rgba(var(--warning-rgb, 245, 158, 11), 0.8);
|
|
178
|
+
transform: translateY(-2px);
|
|
179
|
+
box-shadow: 0 4px 12px rgba(var(--warning-rgb, 245, 158, 11), 0.3);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
&.saveFileBtn {
|
|
173
184
|
background: var(--secondary-color, #6b7280);
|
|
174
185
|
border-color: rgba(var(--secondary-rgb, 107, 114, 128), 1);
|
|
@@ -3831,3 +3831,345 @@
|
|
|
3831
3831
|
font-size: 0.9rem;
|
|
3832
3832
|
}
|
|
3833
3833
|
}
|
|
3834
|
+
|
|
3835
|
+
/* Unique Field Configuration Styling */
|
|
3836
|
+
.uniqueFieldConfig {
|
|
3837
|
+
margin-top: 24px;
|
|
3838
|
+
background-color: rgba(106, 90, 205, 0.05); // Subtle purple background
|
|
3839
|
+
border: 1px solid rgba(106, 90, 205, 0.15);
|
|
3840
|
+
border-radius: var(--radius);
|
|
3841
|
+
overflow: hidden;
|
|
3842
|
+
transition: all 0.3s ease;
|
|
3843
|
+
|
|
3844
|
+
&:hover {
|
|
3845
|
+
border-color: rgba(106, 90, 205, 0.25);
|
|
3846
|
+
box-shadow: 0 2px 8px rgba(106, 90, 205, 0.1);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
.configHeader {
|
|
3851
|
+
padding: 16px 20px 12px 20px;
|
|
3852
|
+
background: linear-gradient(135deg, rgba(106, 90, 205, 0.08) 0%, rgba(106, 90, 205, 0.03) 100%);
|
|
3853
|
+
border-bottom: 1px solid rgba(106, 90, 205, 0.1);
|
|
3854
|
+
|
|
3855
|
+
h4 {
|
|
3856
|
+
margin: 0 0 6px 0;
|
|
3857
|
+
font-size: 1rem;
|
|
3858
|
+
font-weight: 600;
|
|
3859
|
+
color: #4c46a6;
|
|
3860
|
+
display: flex;
|
|
3861
|
+
align-items: center;
|
|
3862
|
+
gap: 8px;
|
|
3863
|
+
|
|
3864
|
+
svg {
|
|
3865
|
+
color: #6a5acd;
|
|
3866
|
+
flex-shrink: 0;
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
&::before {
|
|
3870
|
+
content: '';
|
|
3871
|
+
width: 3px;
|
|
3872
|
+
height: 18px;
|
|
3873
|
+
background: linear-gradient(45deg, #6a5acd, #9370db);
|
|
3874
|
+
border-radius: 2px;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
p {
|
|
3879
|
+
margin: 0;
|
|
3880
|
+
font-size: 0.875rem;
|
|
3881
|
+
color: var(--paragraph-color);
|
|
3882
|
+
opacity: 0.8;
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
.configContent {
|
|
3887
|
+
padding: 20px;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
.checkboxLabel {
|
|
3891
|
+
display: flex;
|
|
3892
|
+
align-items: flex-start;
|
|
3893
|
+
cursor: pointer;
|
|
3894
|
+
font-size: 0.875rem;
|
|
3895
|
+
color: var(--header-color);
|
|
3896
|
+
margin-bottom: 16px;
|
|
3897
|
+
padding: 12px;
|
|
3898
|
+
background-color: rgba(255, 255, 255, 0.3);
|
|
3899
|
+
border-radius: var(--radius);
|
|
3900
|
+
border: 1px solid rgba(106, 90, 205, 0.1);
|
|
3901
|
+
transition: all 0.2s ease;
|
|
3902
|
+
|
|
3903
|
+
&:hover {
|
|
3904
|
+
background-color: rgba(255, 255, 255, 0.5);
|
|
3905
|
+
border-color: rgba(106, 90, 205, 0.2);
|
|
3906
|
+
transform: translateY(-1px);
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
input[type="checkbox"] {
|
|
3910
|
+
margin-right: 12px;
|
|
3911
|
+
width: 18px;
|
|
3912
|
+
height: 18px;
|
|
3913
|
+
cursor: pointer;
|
|
3914
|
+
accent-color: #6a5acd;
|
|
3915
|
+
margin-top: 1px; // Align with text
|
|
3916
|
+
flex-shrink: 0;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
.checkboxText {
|
|
3921
|
+
font-weight: 500;
|
|
3922
|
+
color: var(--header-color);
|
|
3923
|
+
line-height: 1.4;
|
|
3924
|
+
|
|
3925
|
+
.activeIndicator {
|
|
3926
|
+
display: inline-flex;
|
|
3927
|
+
align-items: center;
|
|
3928
|
+
gap: 4px;
|
|
3929
|
+
margin-left: 8px;
|
|
3930
|
+
padding: 2px 8px;
|
|
3931
|
+
background-color: rgba(40, 167, 69, 0.1);
|
|
3932
|
+
color: #28a745;
|
|
3933
|
+
font-size: 0.75rem;
|
|
3934
|
+
font-weight: 600;
|
|
3935
|
+
border-radius: 12px;
|
|
3936
|
+
border: 1px solid rgba(40, 167, 69, 0.2);
|
|
3937
|
+
|
|
3938
|
+
svg {
|
|
3939
|
+
color: #28a745;
|
|
3940
|
+
flex-shrink: 0;
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
.uniqueFieldSelector {
|
|
3946
|
+
margin-left: 30px; // Align with checkbox text
|
|
3947
|
+
padding-left: 16px;
|
|
3948
|
+
border-left: 2px solid rgba(106, 90, 205, 0.2);
|
|
3949
|
+
animation: slideDown 0.3s ease-out;
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
@keyframes slideDown {
|
|
3953
|
+
from {
|
|
3954
|
+
opacity: 0;
|
|
3955
|
+
transform: translateY(-10px);
|
|
3956
|
+
}
|
|
3957
|
+
to {
|
|
3958
|
+
opacity: 1;
|
|
3959
|
+
transform: translateY(0);
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
.fieldLabel {
|
|
3964
|
+
display: block;
|
|
3965
|
+
font-size: 0.875rem;
|
|
3966
|
+
font-weight: 500;
|
|
3967
|
+
color: var(--header-color);
|
|
3968
|
+
margin-bottom: 8px;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
.fieldSelect {
|
|
3972
|
+
width: 100%;
|
|
3973
|
+
padding: 12px 16px;
|
|
3974
|
+
border: 2px solid rgba(106, 90, 205, 0.2);
|
|
3975
|
+
border-radius: var(--radius);
|
|
3976
|
+
font-size: 0.875rem;
|
|
3977
|
+
background-color: var(--bg-color);
|
|
3978
|
+
color: var(--header-color);
|
|
3979
|
+
transition: all 0.2s ease;
|
|
3980
|
+
cursor: pointer;
|
|
3981
|
+
|
|
3982
|
+
&:focus {
|
|
3983
|
+
outline: none;
|
|
3984
|
+
border-color: #6a5acd;
|
|
3985
|
+
box-shadow: 0 0 0 3px rgba(106, 90, 205, 0.15);
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
&:hover {
|
|
3989
|
+
border-color: rgba(106, 90, 205, 0.3);
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
option {
|
|
3993
|
+
padding: 8px;
|
|
3994
|
+
color: var(--header-color);
|
|
3995
|
+
background-color: var(--bg-color);
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
.fieldHelp {
|
|
4000
|
+
margin-top: 8px;
|
|
4001
|
+
font-size: 0.75rem;
|
|
4002
|
+
color: var(--paragraph-color);
|
|
4003
|
+
line-height: 1.4;
|
|
4004
|
+
padding: 8px 12px;
|
|
4005
|
+
background-color: rgba(106, 90, 205, 0.05);
|
|
4006
|
+
border-radius: var(--radius);
|
|
4007
|
+
border-left: 3px solid rgba(106, 90, 205, 0.3);
|
|
4008
|
+
|
|
4009
|
+
code {
|
|
4010
|
+
display: block;
|
|
4011
|
+
margin-top: 6px;
|
|
4012
|
+
padding: 6px 8px;
|
|
4013
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
4014
|
+
border-radius: 4px;
|
|
4015
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
4016
|
+
font-size: 0.7rem;
|
|
4017
|
+
color: #4c46a6;
|
|
4018
|
+
border: 1px solid rgba(106, 90, 205, 0.15);
|
|
4019
|
+
white-space: pre-wrap;
|
|
4020
|
+
word-break: break-word;
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
strong {
|
|
4024
|
+
color: var(--header-color);
|
|
4025
|
+
font-weight: 600;
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
|
|
4029
|
+
/* Row Exclusion Modal Styling */
|
|
4030
|
+
.rowExclusionModal {
|
|
4031
|
+
background-color: var(--bg-color);
|
|
4032
|
+
border-radius: var(--radius);
|
|
4033
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
|
4034
|
+
max-width: 800px;
|
|
4035
|
+
width: 90vw;
|
|
4036
|
+
max-height: 80vh;
|
|
4037
|
+
display: flex;
|
|
4038
|
+
flex-direction: column;
|
|
4039
|
+
|
|
4040
|
+
.modalHeader {
|
|
4041
|
+
padding: 24px;
|
|
4042
|
+
border-bottom: 1px solid var(--border-color);
|
|
4043
|
+
background: linear-gradient(135deg, rgba(220, 38, 127, 0.05) 0%, rgba(220, 38, 127, 0.02) 100%);
|
|
4044
|
+
|
|
4045
|
+
h3 {
|
|
4046
|
+
margin: 0 0 8px 0;
|
|
4047
|
+
font-size: 1.25rem;
|
|
4048
|
+
color: var(--header-color);
|
|
4049
|
+
font-weight: 600;
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
p {
|
|
4053
|
+
margin: 0;
|
|
4054
|
+
color: var(--paragraph-color);
|
|
4055
|
+
font-size: 0.875rem;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
.modalBody {
|
|
4060
|
+
flex: 1;
|
|
4061
|
+
overflow: hidden;
|
|
4062
|
+
display: flex;
|
|
4063
|
+
flex-direction: column;
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
.rowExclusionList {
|
|
4067
|
+
flex: 1;
|
|
4068
|
+
overflow-y: auto;
|
|
4069
|
+
padding: 16px 24px;
|
|
4070
|
+
max-height: 400px;
|
|
4071
|
+
}
|
|
4072
|
+
|
|
4073
|
+
.rowExclusionItem {
|
|
4074
|
+
display: flex;
|
|
4075
|
+
align-items: center;
|
|
4076
|
+
padding: 12px;
|
|
4077
|
+
margin-bottom: 8px;
|
|
4078
|
+
border-radius: var(--radius);
|
|
4079
|
+
border: 1px solid var(--border-color);
|
|
4080
|
+
background-color: var(--tertiary-color);
|
|
4081
|
+
cursor: pointer;
|
|
4082
|
+
transition: all 0.2s ease;
|
|
4083
|
+
|
|
4084
|
+
&:hover {
|
|
4085
|
+
background-color: rgba(var(--primary-rgb), 0.05);
|
|
4086
|
+
border-color: rgba(var(--primary-rgb), 0.2);
|
|
4087
|
+
transform: translateX(4px);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
input[type="checkbox"] {
|
|
4091
|
+
margin-right: 12px;
|
|
4092
|
+
width: 16px;
|
|
4093
|
+
height: 16px;
|
|
4094
|
+
cursor: pointer;
|
|
4095
|
+
accent-color: var(--primary-color);
|
|
4096
|
+
flex-shrink: 0;
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
.rowPreview {
|
|
4101
|
+
display: flex;
|
|
4102
|
+
flex-direction: column;
|
|
4103
|
+
gap: 4px;
|
|
4104
|
+
flex: 1;
|
|
4105
|
+
min-width: 0;
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
.rowValue {
|
|
4109
|
+
font-size: 0.875rem;
|
|
4110
|
+
color: var(--header-color);
|
|
4111
|
+
white-space: nowrap;
|
|
4112
|
+
overflow: hidden;
|
|
4113
|
+
text-overflow: ellipsis;
|
|
4114
|
+
|
|
4115
|
+
&:not(:first-child) {
|
|
4116
|
+
color: var(--paragraph-color);
|
|
4117
|
+
font-size: 0.8rem;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
.exclusionSummary {
|
|
4122
|
+
padding: 16px 24px;
|
|
4123
|
+
background-color: rgba(var(--primary-rgb), 0.05);
|
|
4124
|
+
border-top: 1px solid var(--border-color);
|
|
4125
|
+
|
|
4126
|
+
p {
|
|
4127
|
+
margin: 0;
|
|
4128
|
+
font-size: 0.875rem;
|
|
4129
|
+
color: var(--primary-color);
|
|
4130
|
+
text-align: center;
|
|
4131
|
+
font-weight: 500;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
.modalFooter {
|
|
4136
|
+
padding: 16px 24px;
|
|
4137
|
+
border-top: 1px solid var(--border-color);
|
|
4138
|
+
display: flex;
|
|
4139
|
+
gap: 12px;
|
|
4140
|
+
justify-content: flex-end;
|
|
4141
|
+
background-color: var(--tertiary-color);
|
|
4142
|
+
|
|
4143
|
+
button {
|
|
4144
|
+
padding: 10px 20px;
|
|
4145
|
+
border-radius: var(--radius);
|
|
4146
|
+
font-size: 0.875rem;
|
|
4147
|
+
font-weight: 500;
|
|
4148
|
+
cursor: pointer;
|
|
4149
|
+
transition: all 0.2s ease;
|
|
4150
|
+
border: 1px solid transparent;
|
|
4151
|
+
|
|
4152
|
+
&.cancelButton {
|
|
4153
|
+
background-color: transparent;
|
|
4154
|
+
color: var(--paragraph-color);
|
|
4155
|
+
border-color: var(--border-color);
|
|
4156
|
+
|
|
4157
|
+
&:hover {
|
|
4158
|
+
background-color: var(--border-color);
|
|
4159
|
+
color: var(--header-color);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
&.confirmButton {
|
|
4164
|
+
background-color: var(--primary-color);
|
|
4165
|
+
color: white;
|
|
4166
|
+
|
|
4167
|
+
&:hover {
|
|
4168
|
+
background-color: var(--primary-hover, var(--primary-color));
|
|
4169
|
+
transform: translateY(-1px);
|
|
4170
|
+
box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3);
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|