@visns-studio/visns-components 5.11.9 → 5.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/package.json +2 -2
- package/src/components/crm/DataGrid.jsx +3 -0
- package/src/components/crm/Field.jsx +209 -5
- package/src/components/crm/Form.jsx +9 -2
- package/src/components/crm/MultiSelect.jsx +54 -7
- package/src/components/crm/QuickAction.jsx +15 -1
- package/src/components/crm/columns/ColumnRenderers.jsx +73 -8
- package/src/components/crm/generic/GenericDashboard.jsx +1 -1
- package/src/components/crm/styles/BusinessCardOcr.module.scss +3 -2
- package/src/components/crm/styles/Field.module.scss +102 -12
- package/src/components/crm/styles/MultiSelect.module.scss +22 -1
- package/src/utils/relationshipSortingFallback.js +37 -0
- package/src/utils/relationshipSortingUtils.js +289 -0
- package/src/utils/relationshipSortingUtilsSimple.js +51 -0
package/README.md
CHANGED
|
@@ -321,6 +321,7 @@ The DataGrid component utilizes a modular column renderer system with 28 special
|
|
|
321
321
|
- **Extensibility**: Easy to add new column types or modify existing ones
|
|
322
322
|
- **Maintainability**: Centralized location for all column rendering logic
|
|
323
323
|
- **Performance**: Optimized rendering for each specific column type
|
|
324
|
+
- **Intelligent Sorting**: Automatic detection of sortable relationship and JSON fields
|
|
324
325
|
|
|
325
326
|
**Supported Column Types:**
|
|
326
327
|
- `boolean` - Yes/No display with tooltips
|
|
@@ -354,6 +355,133 @@ The DataGrid component utilizes a modular column renderer system with 28 special
|
|
|
354
355
|
|
|
355
356
|
All column renderers are exported from `@visns-studio/visns-components` and can be used individually or as part of the DataGrid component.
|
|
356
357
|
|
|
358
|
+
#### Intelligent Relationship Sorting
|
|
359
|
+
|
|
360
|
+
The DataGrid component includes intelligent sorting capabilities that automatically detect and enable sorting for relationship and JSON fields. This feature works seamlessly with the backend `HasRelationshipSorting` trait.
|
|
361
|
+
|
|
362
|
+
**Automatic Sortable Detection:**
|
|
363
|
+
|
|
364
|
+
```jsx
|
|
365
|
+
// The DataGrid automatically detects sortable fields based on naming patterns
|
|
366
|
+
<DataGrid
|
|
367
|
+
columns={[
|
|
368
|
+
{
|
|
369
|
+
name: 'user.profile.name', // Relationship field - automatically sortable
|
|
370
|
+
headerName: 'User Name',
|
|
371
|
+
nameFrom: ['user', 'profile', 'name']
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'settings.theme', // JSON field - automatically sortable
|
|
375
|
+
headerName: 'Theme Preference',
|
|
376
|
+
nameFrom: ['settings', 'theme']
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: 'client.company', // BelongsTo relationship - automatically sortable
|
|
380
|
+
headerName: 'Company',
|
|
381
|
+
nameFrom: ['client', 'company']
|
|
382
|
+
}
|
|
383
|
+
]}
|
|
384
|
+
settings={{
|
|
385
|
+
intelligentSorting: true // Enable intelligent sorting (default: true)
|
|
386
|
+
}}
|
|
387
|
+
/>
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Manual Sortable Control:**
|
|
391
|
+
|
|
392
|
+
```jsx
|
|
393
|
+
// Override automatic detection with explicit sortable configuration
|
|
394
|
+
<DataGrid
|
|
395
|
+
columns={[
|
|
396
|
+
{
|
|
397
|
+
name: 'user.profile.name',
|
|
398
|
+
headerName: 'User Name',
|
|
399
|
+
sortable: true, // Explicitly enable sorting
|
|
400
|
+
nameFrom: ['user', 'profile', 'name']
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'complex.calculation',
|
|
404
|
+
headerName: 'Complex Field',
|
|
405
|
+
sortable: false, // Explicitly disable sorting
|
|
406
|
+
nameFrom: ['complex', 'calculation']
|
|
407
|
+
}
|
|
408
|
+
]}
|
|
409
|
+
/>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Supported Sorting Patterns:**
|
|
413
|
+
|
|
414
|
+
The intelligent sorting system recognizes these field patterns as sortable:
|
|
415
|
+
|
|
416
|
+
- **Relationship Fields**: `user.profile.name`, `order.customer.company`
|
|
417
|
+
- **JSON Fields**: `settings.theme`, `metadata.tags`, `data.preferences.language`
|
|
418
|
+
- **Standard Fields**: Any direct model attribute
|
|
419
|
+
- **Nested Relationships**: `post.author.profile.display_name`
|
|
420
|
+
|
|
421
|
+
**Field Name Detection:**
|
|
422
|
+
|
|
423
|
+
The sorting system intelligently determines sortable fields using:
|
|
424
|
+
|
|
425
|
+
1. **nameFrom Array**: Primary method for complex field paths
|
|
426
|
+
```jsx
|
|
427
|
+
nameFrom: ['user', 'profile', 'full_name'] // → user.profile.full_name
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
2. **Dot Notation**: Direct string paths
|
|
431
|
+
```jsx
|
|
432
|
+
name: 'client.address.city' // → client.address.city
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
3. **Pattern Recognition**: Automatic detection of common relationship patterns
|
|
436
|
+
- Fields ending with `_id` that have corresponding relationships
|
|
437
|
+
- JSON field patterns like `data.*`, `settings.*`, `metadata.*`
|
|
438
|
+
- Nested object notation
|
|
439
|
+
|
|
440
|
+
**Configuration Options:**
|
|
441
|
+
|
|
442
|
+
```jsx
|
|
443
|
+
<DataGrid
|
|
444
|
+
settings={{
|
|
445
|
+
// Global intelligent sorting toggle
|
|
446
|
+
intelligentSorting: true, // default: true
|
|
447
|
+
|
|
448
|
+
// Additional sorting configuration
|
|
449
|
+
sortingConfig: {
|
|
450
|
+
// Custom sortable field patterns
|
|
451
|
+
additionalPatterns: [
|
|
452
|
+
/^custom_data\./, // Match custom_data.* fields
|
|
453
|
+
/^config\./ // Match config.* fields
|
|
454
|
+
],
|
|
455
|
+
|
|
456
|
+
// Fields to exclude from automatic sorting
|
|
457
|
+
excludePatterns: [
|
|
458
|
+
/^temp\./, // Exclude temp.* fields
|
|
459
|
+
/^cache\./ // Exclude cache.* fields
|
|
460
|
+
]
|
|
461
|
+
}
|
|
462
|
+
}}
|
|
463
|
+
/>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Backend Integration:**
|
|
467
|
+
|
|
468
|
+
The frontend intelligent sorting works seamlessly with the backend `HasRelationshipSorting` trait:
|
|
469
|
+
|
|
470
|
+
```jsx
|
|
471
|
+
// Frontend automatically generates these API calls:
|
|
472
|
+
// GET /ajax/users/table?orderBy=profile.name&order=asc
|
|
473
|
+
// GET /ajax/orders/table?orderBy=customer.company&order=desc
|
|
474
|
+
// GET /ajax/products/table?orderBy=metadata.category&order=asc
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Benefits:**
|
|
478
|
+
|
|
479
|
+
- **Zero Configuration**: Most relationship and JSON fields work automatically
|
|
480
|
+
- **Performance Optimized**: Uses backend subqueries instead of joins
|
|
481
|
+
- **Type Safety**: Automatic validation of sortable field patterns
|
|
482
|
+
- **Fallback Support**: Graceful handling of unsupported sort fields
|
|
483
|
+
- **Developer Friendly**: Clear visual indicators for sortable columns
|
|
484
|
+
|
|
357
485
|
#### DataGrid Props
|
|
358
486
|
|
|
359
487
|
| Prop | Description | Type | Default |
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@uiw/react-color": "^2.6.0",
|
|
17
17
|
"@visns-studio/visns-datagrid-community": "^1.0.14",
|
|
18
18
|
"@visns-studio/visns-datagrid-enterprise": "^1.0.14",
|
|
19
|
-
"@vitejs/plugin-react": "^4.
|
|
19
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
20
20
|
"add": "^2.0.6",
|
|
21
21
|
"array-move": "^4.0.0",
|
|
22
22
|
"awesome-debounce-promise": "^2.1.0",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
88
88
|
},
|
|
89
89
|
"name": "@visns-studio/visns-components",
|
|
90
|
-
"version": "5.11.
|
|
90
|
+
"version": "5.11.11",
|
|
91
91
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
92
92
|
"main": "src/index.js",
|
|
93
93
|
"files": [
|
|
@@ -41,6 +41,7 @@ import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
|
|
|
41
41
|
import { toast } from 'react-toastify';
|
|
42
42
|
import fetchUtil from '../../utils/fetchUtil';
|
|
43
43
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
44
|
+
import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../../utils/relationshipSortingUtils';
|
|
44
45
|
import {
|
|
45
46
|
AlarmClock,
|
|
46
47
|
RotateCcw,
|
|
@@ -2592,6 +2593,7 @@ const DataGrid = forwardRef(
|
|
|
2592
2593
|
filterEditor,
|
|
2593
2594
|
filterEditorProps,
|
|
2594
2595
|
relationName,
|
|
2596
|
+
intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
2595
2597
|
});
|
|
2596
2598
|
case 'relationArray':
|
|
2597
2599
|
return renderRelationArrayColumn({
|
|
@@ -2600,6 +2602,7 @@ const DataGrid = forwardRef(
|
|
|
2600
2602
|
filterEditor,
|
|
2601
2603
|
filterEditorProps,
|
|
2602
2604
|
relationName,
|
|
2605
|
+
intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
2603
2606
|
});
|
|
2604
2607
|
case 'richtext':
|
|
2605
2608
|
return renderRichTextColumn({
|
|
@@ -549,10 +549,10 @@ function Field({
|
|
|
549
549
|
);
|
|
550
550
|
case 'async-dropdown-ajax':
|
|
551
551
|
return (
|
|
552
|
-
<div className={styles.dropdownContainer}>
|
|
552
|
+
<div className={`${styles.dropdownContainer} ${settings.create ? styles.hasButton : ''}`}>
|
|
553
553
|
<VisnsAsyncSelect
|
|
554
554
|
settings={settings}
|
|
555
|
-
className={inputClass[settings.id]}
|
|
555
|
+
className={`${inputClass[settings.id]} ${settings.create ? styles.selectFullWidth : ''}`}
|
|
556
556
|
multi={
|
|
557
557
|
settings.hasOwnProperty('multi')
|
|
558
558
|
? settings.multi
|
|
@@ -568,6 +568,35 @@ function Field({
|
|
|
568
568
|
dataOptions={dataOptions}
|
|
569
569
|
isCreatable={settings.isCreatable || false}
|
|
570
570
|
/>
|
|
571
|
+
{settings.create && (
|
|
572
|
+
<button
|
|
573
|
+
className={styles.buttonRight}
|
|
574
|
+
onClick={(e) => {
|
|
575
|
+
e.preventDefault();
|
|
576
|
+
let allow = false;
|
|
577
|
+
|
|
578
|
+
if (settings.create.parent_id) {
|
|
579
|
+
if (
|
|
580
|
+
formData[settings.create.parent_id]
|
|
581
|
+
) {
|
|
582
|
+
allow = true;
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
allow = true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (allow) {
|
|
589
|
+
handleAddEntry(settings.create);
|
|
590
|
+
} else {
|
|
591
|
+
toast.warning(
|
|
592
|
+
'Please select a parent item first'
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}}
|
|
596
|
+
>
|
|
597
|
+
<CirclePlus strokeWidth={2} size={16} />
|
|
598
|
+
</button>
|
|
599
|
+
)}
|
|
571
600
|
</div>
|
|
572
601
|
);
|
|
573
602
|
case 'canvas':
|
|
@@ -778,7 +807,7 @@ function Field({
|
|
|
778
807
|
case 'dropdown':
|
|
779
808
|
case 'dropdown-ajax':
|
|
780
809
|
return (
|
|
781
|
-
<div className={styles.dropdownContainer}>
|
|
810
|
+
<div className={`${styles.dropdownContainer} ${settings.create ? styles.hasButton : ''}`}>
|
|
782
811
|
<Select
|
|
783
812
|
settings={settings}
|
|
784
813
|
className={
|
|
@@ -1287,7 +1316,7 @@ function Field({
|
|
|
1287
1316
|
case 'multi-dropdown':
|
|
1288
1317
|
case 'multi-dropdown-ajax':
|
|
1289
1318
|
return (
|
|
1290
|
-
<div className={styles.dropdownContainer}>
|
|
1319
|
+
<div className={`${styles.dropdownContainer} ${settings.create ? styles.hasButton : ''}`}>
|
|
1291
1320
|
<MultiSelect
|
|
1292
1321
|
settings={settings}
|
|
1293
1322
|
className={
|
|
@@ -2257,8 +2286,178 @@ function Field({
|
|
|
2257
2286
|
}
|
|
2258
2287
|
};
|
|
2259
2288
|
|
|
2260
|
-
const handleReload = () => {
|
|
2289
|
+
const handleReload = (createdItem = null) => {
|
|
2261
2290
|
setCounter(counter + 1);
|
|
2291
|
+
|
|
2292
|
+
// Auto-select the newly created item if provided
|
|
2293
|
+
if (createdItem && onChange) {
|
|
2294
|
+
console.log('Auto-selecting created item:', createdItem);
|
|
2295
|
+
|
|
2296
|
+
// Analyze existing options to understand the expected format
|
|
2297
|
+
const analyzeOptionsFormat = () => {
|
|
2298
|
+
let existingOptions = [];
|
|
2299
|
+
|
|
2300
|
+
// Get options based on dropdown type
|
|
2301
|
+
if (['dropdown', 'multi-dropdown'].includes(settings.type) && settings.options) {
|
|
2302
|
+
existingOptions = settings.options;
|
|
2303
|
+
} else if (['dropdown-ajax', 'multi-dropdown-ajax', 'async-dropdown-ajax'].includes(settings.type) && options) {
|
|
2304
|
+
existingOptions = options;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (existingOptions.length === 0) return null;
|
|
2308
|
+
|
|
2309
|
+
// Analyze the first few options to understand the label pattern
|
|
2310
|
+
const sampleOption = existingOptions[0];
|
|
2311
|
+
return {
|
|
2312
|
+
hasLabel: !!sampleOption.label,
|
|
2313
|
+
hasName: !!sampleOption.name,
|
|
2314
|
+
hasFirstname: !!(sampleOption.firstname || sampleOption.surname),
|
|
2315
|
+
hasTitle: !!sampleOption.title,
|
|
2316
|
+
hasDescription: !!sampleOption.description,
|
|
2317
|
+
labelPattern: sampleOption.label || sampleOption.name || 'name'
|
|
2318
|
+
};
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
const formatAnalysis = analyzeOptionsFormat();
|
|
2322
|
+
|
|
2323
|
+
// Determine the label for the created item based on existing options format or common patterns
|
|
2324
|
+
const getItemLabel = (item) => {
|
|
2325
|
+
// If we have format analysis, try to match the existing pattern
|
|
2326
|
+
if (formatAnalysis) {
|
|
2327
|
+
if (formatAnalysis.hasLabel && item.label) return item.label;
|
|
2328
|
+
if (formatAnalysis.hasName && item.name) return item.name;
|
|
2329
|
+
if (formatAnalysis.hasFirstname && (item.firstname || item.surname)) {
|
|
2330
|
+
return `${item.firstname || ''} ${item.surname || ''}`.trim();
|
|
2331
|
+
}
|
|
2332
|
+
if (formatAnalysis.hasTitle && item.title) return item.title;
|
|
2333
|
+
if (formatAnalysis.hasDescription && item.description) return item.description;
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// Fallback to common patterns
|
|
2337
|
+
if (item.label) return item.label;
|
|
2338
|
+
if (item.name) return item.name;
|
|
2339
|
+
if (item.firstname || item.surname) {
|
|
2340
|
+
return `${item.firstname || ''} ${item.surname || ''}`.trim();
|
|
2341
|
+
}
|
|
2342
|
+
if (item.title) return item.title;
|
|
2343
|
+
if (item.description) return item.description;
|
|
2344
|
+
return `Item ${item.id}`;
|
|
2345
|
+
};
|
|
2346
|
+
|
|
2347
|
+
const itemLabel = getItemLabel(createdItem);
|
|
2348
|
+
|
|
2349
|
+
// Create a properly formatted item for MultiSelect/Select components
|
|
2350
|
+
// Match the format of existing options as closely as possible
|
|
2351
|
+
const formattedItem = {
|
|
2352
|
+
id: createdItem.id,
|
|
2353
|
+
value: createdItem.id,
|
|
2354
|
+
label: itemLabel,
|
|
2355
|
+
...createdItem // Preserve all original properties
|
|
2356
|
+
};
|
|
2357
|
+
|
|
2358
|
+
console.log('Format analysis:', formatAnalysis);
|
|
2359
|
+
console.log('Formatted item for selection:', formattedItem);
|
|
2360
|
+
|
|
2361
|
+
// For different dropdown types, handle the selection appropriately
|
|
2362
|
+
if (['dropdown', 'dropdown-ajax'].includes(settings.type)) {
|
|
2363
|
+
// For single select fields
|
|
2364
|
+
if (onChangeSelect) {
|
|
2365
|
+
onChangeSelect(formattedItem, { name: settings.id });
|
|
2366
|
+
}
|
|
2367
|
+
// Also update form data with the ID for single selects
|
|
2368
|
+
if (setFormData) {
|
|
2369
|
+
setFormData((prevState) => ({
|
|
2370
|
+
...prevState,
|
|
2371
|
+
[settings.id]: createdItem.id
|
|
2372
|
+
}));
|
|
2373
|
+
}
|
|
2374
|
+
} else if (['multi-dropdown', 'multi-dropdown-ajax', 'async-dropdown-ajax'].includes(settings.type)) {
|
|
2375
|
+
// For multi-select fields, we need to handle existing selections
|
|
2376
|
+
if (onChangeSelect) {
|
|
2377
|
+
// Get current value to determine if it's multi-select
|
|
2378
|
+
const currentValue = inputValue || [];
|
|
2379
|
+
let newValue;
|
|
2380
|
+
|
|
2381
|
+
if (settings.hasOwnProperty('multi') && settings.multi === false) {
|
|
2382
|
+
// Single select in multi-dropdown component
|
|
2383
|
+
newValue = formattedItem;
|
|
2384
|
+
} else {
|
|
2385
|
+
// True multi-select - add to existing selections
|
|
2386
|
+
const currentArray = Array.isArray(currentValue) ? currentValue :
|
|
2387
|
+
(currentValue ? [currentValue] : []);
|
|
2388
|
+
|
|
2389
|
+
// Check if item already exists (avoid duplicates)
|
|
2390
|
+
const existsIndex = currentArray.findIndex(item =>
|
|
2391
|
+
(item.id && item.id === createdItem.id) ||
|
|
2392
|
+
(item.value && item.value === createdItem.id)
|
|
2393
|
+
);
|
|
2394
|
+
|
|
2395
|
+
if (existsIndex === -1) {
|
|
2396
|
+
// Add new item to the array
|
|
2397
|
+
newValue = [...currentArray, formattedItem];
|
|
2398
|
+
} else {
|
|
2399
|
+
// Replace existing item
|
|
2400
|
+
newValue = [...currentArray];
|
|
2401
|
+
newValue[existsIndex] = formattedItem;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
onChangeSelect(newValue, { name: settings.id });
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// Update form data with the appropriate format
|
|
2409
|
+
if (setFormData) {
|
|
2410
|
+
setFormData((prevState) => {
|
|
2411
|
+
const currentValue = prevState[settings.id] || [];
|
|
2412
|
+
const currentArray = Array.isArray(currentValue) ? currentValue :
|
|
2413
|
+
(currentValue ? [currentValue] : []);
|
|
2414
|
+
|
|
2415
|
+
// Check if we're in multi mode
|
|
2416
|
+
if (settings.hasOwnProperty('multi') && settings.multi === false) {
|
|
2417
|
+
return {
|
|
2418
|
+
...prevState,
|
|
2419
|
+
[settings.id]: formattedItem
|
|
2420
|
+
};
|
|
2421
|
+
} else {
|
|
2422
|
+
// Check for duplicates
|
|
2423
|
+
const existsIndex = currentArray.findIndex(item =>
|
|
2424
|
+
(item.id && item.id === createdItem.id) ||
|
|
2425
|
+
(item.value && item.value === createdItem.id)
|
|
2426
|
+
);
|
|
2427
|
+
|
|
2428
|
+
let newArray;
|
|
2429
|
+
if (existsIndex === -1) {
|
|
2430
|
+
newArray = [...currentArray, formattedItem];
|
|
2431
|
+
} else {
|
|
2432
|
+
newArray = [...currentArray];
|
|
2433
|
+
newArray[existsIndex] = formattedItem;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
return {
|
|
2437
|
+
...prevState,
|
|
2438
|
+
[settings.id]: newArray
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
} else {
|
|
2444
|
+
// For regular input fields, use onChange
|
|
2445
|
+
const syntheticEvent = {
|
|
2446
|
+
target: {
|
|
2447
|
+
dataset: { name: settings.id },
|
|
2448
|
+
value: createdItem.id
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
onChange(syntheticEvent);
|
|
2452
|
+
|
|
2453
|
+
if (setFormData) {
|
|
2454
|
+
setFormData((prevState) => ({
|
|
2455
|
+
...prevState,
|
|
2456
|
+
[settings.id]: createdItem.id
|
|
2457
|
+
}));
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2262
2461
|
};
|
|
2263
2462
|
|
|
2264
2463
|
useEffect(() => {
|
|
@@ -2294,6 +2493,8 @@ function Field({
|
|
|
2294
2493
|
open={childFormShow}
|
|
2295
2494
|
onClose={childFormClose}
|
|
2296
2495
|
closeOnDocumentClick={false}
|
|
2496
|
+
overlayStyle={{ zIndex: 10001 }}
|
|
2497
|
+
contentStyle={{ zIndex: 10002 }}
|
|
2297
2498
|
>
|
|
2298
2499
|
<Form
|
|
2299
2500
|
ajaxSetting={childForm.ajaxSetting}
|
|
@@ -2309,12 +2510,15 @@ function Field({
|
|
|
2309
2510
|
paramValue={null}
|
|
2310
2511
|
style={style}
|
|
2311
2512
|
updateForm={setChildForm}
|
|
2513
|
+
userProfile={userProfile}
|
|
2312
2514
|
/>
|
|
2313
2515
|
</Popup>
|
|
2314
2516
|
<Popup
|
|
2315
2517
|
open={canvasPopupOpen}
|
|
2316
2518
|
onClose={() => setCanvasPopupOpen(false)}
|
|
2317
2519
|
closeOnDocumentClick={false}
|
|
2520
|
+
overlayStyle={{ zIndex: 10001 }}
|
|
2521
|
+
contentStyle={{ zIndex: 10002 }}
|
|
2318
2522
|
>
|
|
2319
2523
|
<div className={`${styles.modalwrap} ${styles['top--modal']}`}>
|
|
2320
2524
|
<div className={styles.modal}>
|
|
@@ -1068,7 +1068,11 @@ function Form({
|
|
|
1068
1068
|
location.reload();
|
|
1069
1069
|
}, 1000);
|
|
1070
1070
|
}
|
|
1071
|
-
if (fetchTable)
|
|
1071
|
+
if (fetchTable) {
|
|
1072
|
+
// Pass the created item data to fetchTable for auto-selection
|
|
1073
|
+
const createdItem = res.data.data || res.data;
|
|
1074
|
+
fetchTable(createdItem);
|
|
1075
|
+
}
|
|
1072
1076
|
if (closeModal) {
|
|
1073
1077
|
closeModal();
|
|
1074
1078
|
if (type === 'saveAnother') {
|
|
@@ -1107,7 +1111,10 @@ function Form({
|
|
|
1107
1111
|
|
|
1108
1112
|
if (resSaveAnother.data.error === '') {
|
|
1109
1113
|
toast.success(saveAnother.message);
|
|
1110
|
-
fetchTable
|
|
1114
|
+
if (fetchTable) {
|
|
1115
|
+
const createdItem = res.data.data || res.data;
|
|
1116
|
+
fetchTable(createdItem);
|
|
1117
|
+
}
|
|
1111
1118
|
}
|
|
1112
1119
|
} else {
|
|
1113
1120
|
fetchData();
|
|
@@ -62,6 +62,43 @@ const CustomMultiValue = (props) => {
|
|
|
62
62
|
);
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
// Custom MultiValueRemove component with debug logging
|
|
66
|
+
const CustomMultiValueRemove = (props) => {
|
|
67
|
+
const { innerProps, data } = props;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<components.MultiValueRemove
|
|
71
|
+
{...props}
|
|
72
|
+
innerProps={{
|
|
73
|
+
...innerProps,
|
|
74
|
+
onClick: (e) => {
|
|
75
|
+
console.log('MultiSelect: Remove button clicked for item:', data);
|
|
76
|
+
console.log('MultiSelect: Event details:', e);
|
|
77
|
+
|
|
78
|
+
// Prevent event bubbling
|
|
79
|
+
e.stopPropagation();
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
|
|
82
|
+
// Call the original handler
|
|
83
|
+
if (innerProps && innerProps.onClick) {
|
|
84
|
+
console.log('MultiSelect: Calling original onClick handler');
|
|
85
|
+
innerProps.onClick(e);
|
|
86
|
+
} else {
|
|
87
|
+
console.log('MultiSelect: No original onClick handler found');
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
onMouseDown: (e) => {
|
|
91
|
+
console.log('MultiSelect: Remove button mouse down');
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
if (innerProps && innerProps.onMouseDown) {
|
|
94
|
+
innerProps.onMouseDown(e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
65
102
|
function MultiSelect({
|
|
66
103
|
className,
|
|
67
104
|
inputValue = [], // Default to an empty array
|
|
@@ -230,12 +267,22 @@ function MultiSelect({
|
|
|
230
267
|
SelectList,
|
|
231
268
|
Option: CustomOption,
|
|
232
269
|
MultiValue: (props) => <CustomMultiValue {...props} settings={settings} />,
|
|
270
|
+
MultiValueRemove: CustomMultiValueRemove,
|
|
233
271
|
}}
|
|
234
272
|
options={selectOptions}
|
|
235
273
|
menuPortalTarget={document.body}
|
|
236
274
|
menuPlacement={menuPlacement}
|
|
237
275
|
onMenuOpen={handleMenuOpen}
|
|
238
276
|
onChange={(inputValue, action) => {
|
|
277
|
+
// Debug logging for delete actions
|
|
278
|
+
if (action.action === 'remove-value') {
|
|
279
|
+
console.log('MultiSelect: Individual item delete:', {
|
|
280
|
+
removedValue: action.removedValue,
|
|
281
|
+
newValue: inputValue,
|
|
282
|
+
action: action
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
239
286
|
// Special handling for limit=1: keep only the most recent selection
|
|
240
287
|
if (
|
|
241
288
|
settings.limit === 1 &&
|
|
@@ -333,7 +380,7 @@ function MultiSelect({
|
|
|
333
380
|
flex: '1',
|
|
334
381
|
minWidth: '0',
|
|
335
382
|
}),
|
|
336
|
-
multiValueRemove: (base) => ({
|
|
383
|
+
multiValueRemove: (base, state) => ({
|
|
337
384
|
...base,
|
|
338
385
|
padding: '0 4px',
|
|
339
386
|
display: 'flex',
|
|
@@ -341,12 +388,12 @@ function MultiSelect({
|
|
|
341
388
|
justifyContent: 'center',
|
|
342
389
|
margin: '0',
|
|
343
390
|
borderRadius: '0 4px 4px 0',
|
|
344
|
-
backgroundColor: 'transparent',
|
|
345
|
-
|
|
346
|
-
'
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
391
|
+
backgroundColor: state.isFocused ? '#dc2626' : 'transparent',
|
|
392
|
+
color: state.isFocused ? 'white' : '#666',
|
|
393
|
+
transition: 'all 0.2s ease',
|
|
394
|
+
cursor: 'pointer',
|
|
395
|
+
minWidth: '16px',
|
|
396
|
+
minHeight: '16px',
|
|
350
397
|
}),
|
|
351
398
|
}}
|
|
352
399
|
value={selectValue}
|
|
@@ -408,7 +408,21 @@ const QuickAction = ({ setting }) => {
|
|
|
408
408
|
</div>
|
|
409
409
|
</div>
|
|
410
410
|
|
|
411
|
-
<Popup
|
|
411
|
+
<Popup
|
|
412
|
+
open={modalShow}
|
|
413
|
+
onClose={modalClose}
|
|
414
|
+
closeOnDocumentClick={false}
|
|
415
|
+
contentStyle={{
|
|
416
|
+
width: '80vw',
|
|
417
|
+
maxWidth: '1200px',
|
|
418
|
+
minWidth: '600px',
|
|
419
|
+
padding: 0,
|
|
420
|
+
border: 'none',
|
|
421
|
+
borderRadius: '8px',
|
|
422
|
+
overflow: 'hidden',
|
|
423
|
+
maxHeight: '90vh',
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
412
426
|
<Form
|
|
413
427
|
closeModal={modalClose}
|
|
414
428
|
fetchTable={() => {}}
|
|
@@ -4,6 +4,11 @@ import parse from 'html-react-parser';
|
|
|
4
4
|
import { Download, AlarmClock } from 'lucide-react';
|
|
5
5
|
import { toast } from 'react-toastify';
|
|
6
6
|
import CellWithTooltip from '../cells/CellWithTooltip';
|
|
7
|
+
import {
|
|
8
|
+
isRelationshipColumnSortable,
|
|
9
|
+
getRelationshipPath,
|
|
10
|
+
DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
11
|
+
} from '../../../utils/relationshipSortingUtils';
|
|
7
12
|
|
|
8
13
|
// Boolean Column Renderer
|
|
9
14
|
export const renderBooleanColumn = ({
|
|
@@ -17,7 +22,7 @@ export const renderBooleanColumn = ({
|
|
|
17
22
|
filterEditor: filterEditor,
|
|
18
23
|
filterEditorProps: filterEditorProps,
|
|
19
24
|
name: `${column.type}.${column.id}`,
|
|
20
|
-
sortable:
|
|
25
|
+
sortable: true,
|
|
21
26
|
render: ({ data }) => {
|
|
22
27
|
const value = data[column.id] ? 'Yes' : 'No';
|
|
23
28
|
return (
|
|
@@ -146,7 +151,7 @@ export const renderArrayCountColumn = ({ column, commonProps }) => {
|
|
|
146
151
|
return {
|
|
147
152
|
...commonProps,
|
|
148
153
|
name: `${column.type}.${column.id}`,
|
|
149
|
-
sortable:
|
|
154
|
+
sortable: true,
|
|
150
155
|
render: ({ data }) => {
|
|
151
156
|
if (data && data[column.id]) {
|
|
152
157
|
return data[column.id].length;
|
|
@@ -713,11 +718,39 @@ export const renderRelationColumn = ({
|
|
|
713
718
|
filterEditor,
|
|
714
719
|
filterEditorProps,
|
|
715
720
|
relationName,
|
|
721
|
+
intelligentSortingConfig = DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
716
722
|
}) => {
|
|
723
|
+
// Intelligent sorting detection
|
|
724
|
+
const isSortable = isRelationshipColumnSortable(
|
|
725
|
+
column,
|
|
726
|
+
intelligentSortingConfig
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// Log sorting analysis in development
|
|
730
|
+
if (
|
|
731
|
+
intelligentSortingConfig.logAnalysis &&
|
|
732
|
+
process.env.NODE_ENV === 'development'
|
|
733
|
+
) {
|
|
734
|
+
const relationshipPath = getRelationshipPath(column);
|
|
735
|
+
console.log(
|
|
736
|
+
`Relationship sorting analysis for "${relationshipPath}":`,
|
|
737
|
+
{
|
|
738
|
+
sortable: isSortable,
|
|
739
|
+
fieldName: column.nameFrom,
|
|
740
|
+
relationshipDepth: Array.isArray(column.id)
|
|
741
|
+
? column.id.length
|
|
742
|
+
: 1,
|
|
743
|
+
explicitSortable: column.hasOwnProperty('sortable')
|
|
744
|
+
? column.sortable
|
|
745
|
+
: 'not specified',
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
717
750
|
return {
|
|
718
751
|
...commonProps,
|
|
719
752
|
name: relationName,
|
|
720
|
-
sortable:
|
|
753
|
+
sortable: isSortable,
|
|
721
754
|
filterEditor: filterEditor,
|
|
722
755
|
filterEditorProps: filterEditorProps,
|
|
723
756
|
render: ({ data }) => {
|
|
@@ -770,18 +803,22 @@ export const renderRelationColumn = ({
|
|
|
770
803
|
// Check if the value is a hex color and render with color preview
|
|
771
804
|
const isHexColor = (str) => {
|
|
772
805
|
if (!str || typeof str !== 'string') return false;
|
|
773
|
-
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(
|
|
806
|
+
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(
|
|
807
|
+
str
|
|
808
|
+
);
|
|
774
809
|
};
|
|
775
810
|
|
|
776
811
|
if (isHexColor(value)) {
|
|
777
812
|
return (
|
|
778
813
|
<div className="relation-color-container">
|
|
779
|
-
<div
|
|
814
|
+
<div
|
|
780
815
|
className="relation-color-preview"
|
|
781
816
|
style={{ backgroundColor: value }}
|
|
782
817
|
title={`Color: ${value}`}
|
|
783
818
|
></div>
|
|
784
|
-
<span className="relation-color-text">
|
|
819
|
+
<span className="relation-color-text">
|
|
820
|
+
{value}
|
|
821
|
+
</span>
|
|
785
822
|
</div>
|
|
786
823
|
);
|
|
787
824
|
}
|
|
@@ -812,11 +849,39 @@ export const renderRelationArrayColumn = ({
|
|
|
812
849
|
filterEditor,
|
|
813
850
|
filterEditorProps,
|
|
814
851
|
relationName,
|
|
852
|
+
intelligentSortingConfig = DEFAULT_INTELLIGENT_SORTING_CONFIG,
|
|
815
853
|
}) => {
|
|
854
|
+
// Intelligent sorting detection for relation arrays
|
|
855
|
+
const isSortable = isRelationshipColumnSortable(
|
|
856
|
+
column,
|
|
857
|
+
intelligentSortingConfig
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
// Log sorting analysis in development
|
|
861
|
+
if (
|
|
862
|
+
intelligentSortingConfig.logAnalysis &&
|
|
863
|
+
process.env.NODE_ENV === 'development'
|
|
864
|
+
) {
|
|
865
|
+
const relationshipPath = getRelationshipPath(column);
|
|
866
|
+
console.log(
|
|
867
|
+
`Relationship array sorting analysis for "${relationshipPath}":`,
|
|
868
|
+
{
|
|
869
|
+
sortable: isSortable,
|
|
870
|
+
fieldName: column.nameFrom,
|
|
871
|
+
relationshipDepth: Array.isArray(column.id)
|
|
872
|
+
? column.id.length
|
|
873
|
+
: 1,
|
|
874
|
+
explicitSortable: column.hasOwnProperty('sortable')
|
|
875
|
+
? column.sortable
|
|
876
|
+
: 'not specified',
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
|
|
816
881
|
return {
|
|
817
882
|
...commonProps,
|
|
818
883
|
name: relationName,
|
|
819
|
-
sortable:
|
|
884
|
+
sortable: isSortable,
|
|
820
885
|
filterEditor: filterEditor,
|
|
821
886
|
filterEditorProps: filterEditorProps,
|
|
822
887
|
render: ({ data }) => {
|
|
@@ -1284,7 +1349,7 @@ export const renderCreatedByColumn = ({ column, commonProps }) => {
|
|
|
1284
1349
|
return {
|
|
1285
1350
|
...commonProps,
|
|
1286
1351
|
name: `${column.type}.${column.id}`,
|
|
1287
|
-
sortable:
|
|
1352
|
+
sortable: true,
|
|
1288
1353
|
render: ({ data }) => {
|
|
1289
1354
|
if (
|
|
1290
1355
|
data.audits[0] &&
|
|
@@ -1355,7 +1355,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1355
1355
|
)}
|
|
1356
1356
|
<div className={styles.grid} ref={container}>
|
|
1357
1357
|
{renderWidgets()}
|
|
1358
|
-
<Popup open={modalShow} onClose={closeModal}>
|
|
1358
|
+
<Popup open={modalShow} onClose={closeModal} closeOnDocumentClick={false}>
|
|
1359
1359
|
<div className="modalwrap top--modal modalWide">
|
|
1360
1360
|
<div className="modal">
|
|
1361
1361
|
<div className="modal__header">
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
border-radius: var(--br, 8px);
|
|
21
21
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
|
22
22
|
overflow: hidden;
|
|
23
|
-
|
|
24
|
-
width:
|
|
23
|
+
width: 80vw;
|
|
24
|
+
max-width: 1200px;
|
|
25
|
+
min-width: 600px;
|
|
25
26
|
max-height: 90vh;
|
|
26
27
|
display: flex;
|
|
27
28
|
flex-direction: column;
|
|
@@ -3,28 +3,118 @@
|
|
|
3
3
|
width: 100%;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
/* Only apply flex layout when there's a create button */
|
|
7
|
+
.dropdownContainer:has(.buttonRight) {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: stretch;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
.selectFullWidth {
|
|
7
13
|
width: 90%;
|
|
8
14
|
}
|
|
9
15
|
|
|
16
|
+
/* Only style the select when there's a create button */
|
|
17
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth {
|
|
18
|
+
flex: 1;
|
|
19
|
+
width: auto;
|
|
20
|
+
border-top-right-radius: 0;
|
|
21
|
+
border-bottom-right-radius: 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Ensure the select component inside container integrates seamlessly - only when button exists */
|
|
25
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth > div,
|
|
26
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth .visns-select__control,
|
|
27
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth select {
|
|
28
|
+
border-top-right-radius: 0 !important;
|
|
29
|
+
border-bottom-right-radius: 0 !important;
|
|
30
|
+
border-right: none !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth .visns-select__control--is-focused,
|
|
34
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth select:focus {
|
|
35
|
+
border-right: none !important;
|
|
36
|
+
box-shadow: none !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Ensure consistent height between select and button */
|
|
40
|
+
.dropdownContainer:has(.buttonRight) .selectFullWidth select {
|
|
41
|
+
height: auto;
|
|
42
|
+
min-height: 3.1rem; /* Match typical input height */
|
|
43
|
+
}
|
|
44
|
+
|
|
10
45
|
.buttonRight {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
justify-content: center;
|
|
49
|
+
min-width: 42px;
|
|
50
|
+
min-height: 3.1rem;
|
|
51
|
+
background: rgba(var(--primary-color-rgb), 0.08);
|
|
52
|
+
color: var(--primary-color);
|
|
53
|
+
border: 1px solid rgba(var(--primary-color-rgb), 0.25);
|
|
54
|
+
border-left: none;
|
|
55
|
+
border-top-right-radius: var(--br);
|
|
56
|
+
border-bottom-right-radius: var(--br);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
transition: all 0.2s ease;
|
|
59
|
+
outline: none;
|
|
60
|
+
padding: 0;
|
|
61
|
+
position: relative;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.buttonRight::before {
|
|
65
|
+
content: '';
|
|
11
66
|
position: absolute;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
67
|
+
top: 20%;
|
|
68
|
+
left: 0;
|
|
69
|
+
width: 1px;
|
|
70
|
+
height: 60%;
|
|
71
|
+
background: rgba(var(--primary-color-rgb), 0.15);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.buttonRight:hover {
|
|
16
75
|
background: var(--primary-color);
|
|
17
76
|
color: var(--secondary-color);
|
|
18
|
-
|
|
19
|
-
border: none;
|
|
20
|
-
border-radius: var(--br);
|
|
21
|
-
cursor: pointer;
|
|
22
|
-
transition: background 0.3s;
|
|
77
|
+
border-color: var(--primary-color);
|
|
23
78
|
}
|
|
24
79
|
|
|
25
|
-
.buttonRight:hover {
|
|
26
|
-
|
|
27
|
-
|
|
80
|
+
.buttonRight:hover::before {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.buttonRight:focus {
|
|
85
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Fallback for browsers that don't support :has() */
|
|
89
|
+
.dropdownContainer.hasButton {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: stretch;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dropdownContainer.hasButton .selectFullWidth {
|
|
95
|
+
flex: 1;
|
|
96
|
+
width: auto;
|
|
97
|
+
border-top-right-radius: 0;
|
|
98
|
+
border-bottom-right-radius: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dropdownContainer.hasButton .selectFullWidth > div,
|
|
102
|
+
.dropdownContainer.hasButton .selectFullWidth .visns-select__control,
|
|
103
|
+
.dropdownContainer.hasButton .selectFullWidth select {
|
|
104
|
+
border-top-right-radius: 0 !important;
|
|
105
|
+
border-bottom-right-radius: 0 !important;
|
|
106
|
+
border-right: none !important;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.dropdownContainer.hasButton .selectFullWidth .visns-select__control--is-focused,
|
|
110
|
+
.dropdownContainer.hasButton .selectFullWidth select:focus {
|
|
111
|
+
border-right: none !important;
|
|
112
|
+
box-shadow: none !important;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.dropdownContainer.hasButton .selectFullWidth select {
|
|
116
|
+
height: auto;
|
|
117
|
+
min-height: 3.1rem;
|
|
28
118
|
}
|
|
29
119
|
|
|
30
120
|
.formcontainer {
|
|
@@ -158,12 +158,33 @@
|
|
|
158
158
|
margin: 0 !important;
|
|
159
159
|
border-radius: 0 4px 4px 0 !important;
|
|
160
160
|
background-color: transparent !important;
|
|
161
|
-
transition:
|
|
161
|
+
transition: all 0.2s ease !important;
|
|
162
|
+
cursor: pointer !important;
|
|
163
|
+
pointer-events: auto !important;
|
|
164
|
+
position: relative !important;
|
|
165
|
+
z-index: 10 !important;
|
|
166
|
+
min-width: 20px !important;
|
|
167
|
+
min-height: 20px !important;
|
|
168
|
+
border: 1px solid transparent !important;
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
.visns-select__multi-value__remove:hover {
|
|
165
172
|
background-color: #dc2626 !important;
|
|
166
173
|
color: white !important;
|
|
174
|
+
border-color: #dc2626 !important;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.visns-select__multi-value__remove:focus {
|
|
178
|
+
background-color: #dc2626 !important;
|
|
179
|
+
color: white !important;
|
|
180
|
+
border-color: #dc2626 !important;
|
|
181
|
+
outline: none !important;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.visns-select__multi-value__remove:active {
|
|
185
|
+
background-color: #b91c1c !important;
|
|
186
|
+
color: white !important;
|
|
187
|
+
transform: scale(0.95) !important;
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
/* Fix for modals */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback version that restores original behavior
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_INTELLIGENT_SORTING_CONFIG = {
|
|
6
|
+
enableIntelligentSorting: false, // Disabled by default for safety
|
|
7
|
+
maxRelationshipDepth: 3,
|
|
8
|
+
customSortableFields: [],
|
|
9
|
+
customUnsortableFields: [],
|
|
10
|
+
respectExplicitSortable: true,
|
|
11
|
+
logAnalysis: false
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isRelationshipColumnSortable = (column, options = {}) => {
|
|
15
|
+
// If intelligent sorting is disabled, use original behavior
|
|
16
|
+
if (options.enableIntelligentSorting === false) {
|
|
17
|
+
// Original behavior: return false for relation columns unless explicitly set
|
|
18
|
+
return column.sortable === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Respect explicit configuration
|
|
22
|
+
if (column.hasOwnProperty('sortable')) {
|
|
23
|
+
return column.sortable === true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Default to false (original behavior) for safety
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getRelationshipPath = (column) => {
|
|
31
|
+
if (!Array.isArray(column.id)) {
|
|
32
|
+
return column.nameFrom || 'unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const relationPath = column.id.join('.');
|
|
36
|
+
return column.nameFrom ? `${relationPath}.${column.nameFrom}` : relationPath;
|
|
37
|
+
};
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligent Relationship Sorting Utilities
|
|
3
|
+
*
|
|
4
|
+
* Automatically determines if relationship columns should be sortable
|
|
5
|
+
* based on column configuration patterns and field types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fields that are typically sortable
|
|
10
|
+
*/
|
|
11
|
+
const SORTABLE_FIELD_PATTERNS = [
|
|
12
|
+
// Text fields
|
|
13
|
+
'name', 'title', 'label', 'description', 'email', 'username', 'slug',
|
|
14
|
+
'first_name', 'last_name', 'full_name', 'display_name', 'company_name',
|
|
15
|
+
'organization', 'department', 'position', 'role', 'status', 'type',
|
|
16
|
+
'category', 'tag', 'code', 'reference', 'identifier', 'serial',
|
|
17
|
+
|
|
18
|
+
// Date fields
|
|
19
|
+
'created_at', 'updated_at', 'deleted_at', 'published_at', 'expires_at',
|
|
20
|
+
'start_date', 'end_date', 'due_date', 'birth_date', 'date', 'timestamp',
|
|
21
|
+
|
|
22
|
+
// Numeric fields
|
|
23
|
+
'id', 'order', 'sort_order', 'priority', 'weight', 'score', 'rating',
|
|
24
|
+
'amount', 'price', 'cost', 'total', 'quantity', 'count', 'number',
|
|
25
|
+
'percentage', 'rate', 'value', 'balance', 'age', 'duration',
|
|
26
|
+
|
|
27
|
+
// Boolean/Status fields (often useful for sorting)
|
|
28
|
+
'active', 'enabled', 'visible', 'published', 'featured', 'verified',
|
|
29
|
+
'approved', 'completed', 'archived', 'deleted'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fields that should NOT be sortable
|
|
34
|
+
*/
|
|
35
|
+
const UNSORTABLE_FIELD_PATTERNS = [
|
|
36
|
+
// Large text/blob fields
|
|
37
|
+
'content', 'body', 'text', 'html', 'markdown', 'notes', 'comments',
|
|
38
|
+
'description_long', 'bio', 'about', 'details', 'message', 'review',
|
|
39
|
+
|
|
40
|
+
// Binary/complex data
|
|
41
|
+
'data', 'metadata', 'settings', 'config', 'options', 'attributes',
|
|
42
|
+
'properties', 'json', 'xml', 'blob', 'binary', 'file', 'image',
|
|
43
|
+
'photo', 'avatar', 'logo', 'attachment', 'upload',
|
|
44
|
+
|
|
45
|
+
// Sensitive fields
|
|
46
|
+
'password', 'token', 'key', 'secret', 'hash', 'signature', 'checksum'
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Relationship depth limits for sorting
|
|
51
|
+
*/
|
|
52
|
+
const MAX_RELATIONSHIP_DEPTH = 3;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Determines if a relationship column should be sortable based on intelligent analysis
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} column - Column configuration object
|
|
58
|
+
* @param {Object} options - Additional options for sorting intelligence
|
|
59
|
+
* @returns {boolean} - Whether the column should be sortable
|
|
60
|
+
*/
|
|
61
|
+
export const isRelationshipColumnSortable = (column, options = {}) => {
|
|
62
|
+
const {
|
|
63
|
+
enableIntelligentSorting = true,
|
|
64
|
+
maxRelationshipDepth = MAX_RELATIONSHIP_DEPTH,
|
|
65
|
+
customSortableFields = [],
|
|
66
|
+
customUnsortableFields = [],
|
|
67
|
+
respectExplicitSortable = true
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
// If intelligent sorting is disabled, fall back to explicit configuration
|
|
71
|
+
if (!enableIntelligentSorting) {
|
|
72
|
+
return column.sortable === true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Respect explicit sortable configuration if specified
|
|
76
|
+
if (respectExplicitSortable && column.hasOwnProperty('sortable')) {
|
|
77
|
+
return column.sortable === true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Only analyze relation and relationArray types
|
|
81
|
+
if (!['relation', 'relationArray'].includes(column.type)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check relationship depth
|
|
86
|
+
const relationshipDepth = Array.isArray(column.id) ? column.id.length : 1;
|
|
87
|
+
if (relationshipDepth > maxRelationshipDepth) {
|
|
88
|
+
console.warn(`Relationship depth ${relationshipDepth} exceeds maximum ${maxRelationshipDepth} for sorting: ${getRelationshipPath(column)}`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Analyze the target field name
|
|
93
|
+
let fieldName = column.nameFrom;
|
|
94
|
+
if (!fieldName) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle array nameFrom (for relationArray columns)
|
|
99
|
+
if (Array.isArray(fieldName)) {
|
|
100
|
+
// Use the first field name for analysis
|
|
101
|
+
fieldName = fieldName[0];
|
|
102
|
+
if (!fieldName) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check custom configurations first
|
|
108
|
+
const customSortable = [...SORTABLE_FIELD_PATTERNS, ...customSortableFields];
|
|
109
|
+
const customUnsortable = [...UNSORTABLE_FIELD_PATTERNS, ...customUnsortableFields];
|
|
110
|
+
|
|
111
|
+
// Explicit unsortable fields take precedence
|
|
112
|
+
if (isFieldInPatterns(fieldName, customUnsortable)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if field matches sortable patterns
|
|
117
|
+
if (isFieldInPatterns(fieldName, customSortable)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Analyze field name patterns for common sortable types
|
|
122
|
+
return analyzeFieldNamePattern(fieldName);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Checks if a field name matches any of the given patterns
|
|
127
|
+
*
|
|
128
|
+
* @param {string} fieldName - The field name to check
|
|
129
|
+
* @param {Array} patterns - Array of patterns to match against
|
|
130
|
+
* @returns {boolean} - Whether the field matches any pattern
|
|
131
|
+
*/
|
|
132
|
+
const isFieldInPatterns = (fieldName, patterns) => {
|
|
133
|
+
// Ensure fieldName is a string
|
|
134
|
+
if (typeof fieldName !== 'string') {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const normalizedFieldName = fieldName.toLowerCase();
|
|
139
|
+
|
|
140
|
+
return patterns.some(pattern => {
|
|
141
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
142
|
+
|
|
143
|
+
// Exact match
|
|
144
|
+
if (normalizedFieldName === normalizedPattern) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Contains pattern (for composite field names)
|
|
149
|
+
if (normalizedFieldName.includes(normalizedPattern)) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Pattern contains field name (for abbreviated fields)
|
|
154
|
+
if (normalizedPattern.includes(normalizedFieldName)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return false;
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Analyzes field name patterns to determine if they're likely sortable
|
|
164
|
+
*
|
|
165
|
+
* @param {string} fieldName - The field name to analyze
|
|
166
|
+
* @returns {boolean} - Whether the field appears to be sortable
|
|
167
|
+
*/
|
|
168
|
+
const analyzeFieldNamePattern = (fieldName) => {
|
|
169
|
+
const normalizedName = fieldName.toLowerCase();
|
|
170
|
+
|
|
171
|
+
// Date-like patterns
|
|
172
|
+
if (/_at$|_date$|date_|_time$|time_/.test(normalizedName)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ID patterns
|
|
177
|
+
if (/^id$|_id$|^uuid$|_uuid$/.test(normalizedName)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Name patterns
|
|
182
|
+
if (/name|title|label/.test(normalizedName)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Order/priority patterns
|
|
187
|
+
if (/order|priority|sort|rank|position|sequence/.test(normalizedName)) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Status patterns
|
|
192
|
+
if (/status|state|active|enabled|visible/.test(normalizedName)) {
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Numeric patterns
|
|
197
|
+
if (/amount|price|cost|total|count|number|quantity|score|rating/.test(normalizedName)) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Default to false for unknown patterns to be conservative
|
|
202
|
+
return false;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Constructs the relationship path for a column (for logging/debugging)
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} column - Column configuration object
|
|
209
|
+
* @returns {string} - The relationship path (e.g., "user.profile.name")
|
|
210
|
+
*/
|
|
211
|
+
export const getRelationshipPath = (column) => {
|
|
212
|
+
if (!Array.isArray(column.id)) {
|
|
213
|
+
return column.nameFrom || 'unknown';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const relationPath = column.id.join('.');
|
|
217
|
+
return column.nameFrom ? `${relationPath}.${column.nameFrom}` : relationPath;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Analyzes all relationship columns in a column configuration array
|
|
222
|
+
* and provides sorting recommendations
|
|
223
|
+
*
|
|
224
|
+
* @param {Array} columns - Array of column configuration objects
|
|
225
|
+
* @param {Object} options - Options for analysis
|
|
226
|
+
* @returns {Object} - Analysis results with recommendations
|
|
227
|
+
*/
|
|
228
|
+
export const analyzeRelationshipSorting = (columns, options = {}) => {
|
|
229
|
+
const results = {
|
|
230
|
+
totalRelationColumns: 0,
|
|
231
|
+
sortableRelationColumns: 0,
|
|
232
|
+
unsortableRelationColumns: 0,
|
|
233
|
+
recommendations: [],
|
|
234
|
+
sortableColumns: [],
|
|
235
|
+
unsortableColumns: []
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
columns.forEach(column => {
|
|
239
|
+
if (['relation', 'relationArray'].includes(column.type)) {
|
|
240
|
+
results.totalRelationColumns++;
|
|
241
|
+
|
|
242
|
+
const isSortable = isRelationshipColumnSortable(column, options);
|
|
243
|
+
const relationshipPath = getRelationshipPath(column);
|
|
244
|
+
|
|
245
|
+
if (isSortable) {
|
|
246
|
+
results.sortableRelationColumns++;
|
|
247
|
+
results.sortableColumns.push({
|
|
248
|
+
column,
|
|
249
|
+
path: relationshipPath,
|
|
250
|
+
reason: 'Matches sortable field pattern'
|
|
251
|
+
});
|
|
252
|
+
} else {
|
|
253
|
+
results.unsortableRelationColumns++;
|
|
254
|
+
results.unsortableColumns.push({
|
|
255
|
+
column,
|
|
256
|
+
path: relationshipPath,
|
|
257
|
+
reason: 'Does not match sortable patterns or exceeds depth limit'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Generate recommendations
|
|
264
|
+
if (results.unsortableRelationColumns > 0) {
|
|
265
|
+
results.recommendations.push(
|
|
266
|
+
`Consider adding explicit sortable: true to ${results.unsortableRelationColumns} relationship columns if sorting is needed`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (results.sortableRelationColumns > 10) {
|
|
271
|
+
results.recommendations.push(
|
|
272
|
+
'Large number of sortable relationship columns detected - consider performance impact'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return results;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Default configuration for intelligent sorting
|
|
281
|
+
*/
|
|
282
|
+
export const DEFAULT_INTELLIGENT_SORTING_CONFIG = {
|
|
283
|
+
enableIntelligentSorting: true,
|
|
284
|
+
maxRelationshipDepth: MAX_RELATIONSHIP_DEPTH,
|
|
285
|
+
customSortableFields: [],
|
|
286
|
+
customUnsortableFields: [],
|
|
287
|
+
respectExplicitSortable: true,
|
|
288
|
+
logAnalysis: process.env.NODE_ENV === 'development'
|
|
289
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified version for testing import issues
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_INTELLIGENT_SORTING_CONFIG = {
|
|
6
|
+
enableIntelligentSorting: true,
|
|
7
|
+
maxRelationshipDepth: 3,
|
|
8
|
+
customSortableFields: [],
|
|
9
|
+
customUnsortableFields: [],
|
|
10
|
+
respectExplicitSortable: true,
|
|
11
|
+
logAnalysis: false
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isRelationshipColumnSortable = (column, options = {}) => {
|
|
15
|
+
// Simple implementation for testing
|
|
16
|
+
if (options.enableIntelligentSorting === false) {
|
|
17
|
+
return column.sortable === true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (column.hasOwnProperty('sortable')) {
|
|
21
|
+
return column.sortable === true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Basic field name analysis
|
|
25
|
+
const fieldName = column.nameFrom;
|
|
26
|
+
if (!fieldName) return false;
|
|
27
|
+
|
|
28
|
+
const sortablePatterns = ['name', 'title', 'email', 'created_at', 'updated_at', 'id'];
|
|
29
|
+
const unsortablePatterns = ['bio', 'content', 'data', 'notes', 'description'];
|
|
30
|
+
|
|
31
|
+
const normalizedName = fieldName.toLowerCase();
|
|
32
|
+
|
|
33
|
+
if (unsortablePatterns.some(pattern => normalizedName.includes(pattern))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (sortablePatterns.some(pattern => normalizedName.includes(pattern))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const getRelationshipPath = (column) => {
|
|
45
|
+
if (!Array.isArray(column.id)) {
|
|
46
|
+
return column.nameFrom || 'unknown';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const relationPath = column.id.join('.');
|
|
50
|
+
return column.nameFrom ? `${relationPath}.${column.nameFrom}` : relationPath;
|
|
51
|
+
};
|