@visns-studio/visns-components 5.11.10 → 5.11.12
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 +4 -4
- package/src/components/cms/DataGrid.jsx +22 -4
- package/src/components/crm/Autocomplete.jsx +3 -3
- package/src/components/crm/DataGrid.jsx +26 -4
- package/src/components/crm/Field.jsx +66 -21
- package/src/components/crm/columns/ColumnRenderers.jsx +73 -8
- package/src/components/crm/generic/GenericGrid.jsx +52 -60
- package/src/components/crm/generic/styles/GenericIndex.module.scss +52 -0
- package/src/utils/columnsMetadataUtils.js +277 -0
- package/src/utils/relationshipSortingFallback.js +37 -0
- package/src/utils/relationshipSortingUtils.js +289 -0
- package/src/utils/relationshipSortingUtilsSimple.js +51 -0
|
@@ -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
|
+
};
|