@visns-studio/visns-components 5.11.11 → 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/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 +23 -4
- package/src/components/crm/Field.jsx +66 -21
- 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/package.json
CHANGED
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"dayjs": "^1.11.13",
|
|
25
25
|
"fabric": "^6.7.0",
|
|
26
26
|
"file-saver": "^2.0.5",
|
|
27
|
-
"framer-motion": "^12.
|
|
27
|
+
"framer-motion": "^12.19.1",
|
|
28
28
|
"html-react-parser": "^5.2.5",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
30
|
"lodash.debounce": "^4.0.8",
|
|
31
31
|
"lucide-react": "^0.518.0",
|
|
32
32
|
"moment": "^2.30.1",
|
|
33
|
-
"motion": "^12.
|
|
33
|
+
"motion": "^12.19.1",
|
|
34
34
|
"numeral": "^2.0.6",
|
|
35
35
|
"pluralize": "^8.0.0",
|
|
36
36
|
"qrcode.react": "^4.2.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"reactjs-popup": "^2.0.6",
|
|
59
59
|
"style-loader": "^4.0.0",
|
|
60
60
|
"swapy": "^1.0.5",
|
|
61
|
-
"sweetalert2": "^11.22.
|
|
61
|
+
"sweetalert2": "^11.22.1",
|
|
62
62
|
"tesseract.js": "^6.0.1",
|
|
63
63
|
"truncate": "^3.0.0",
|
|
64
64
|
"uuid": "^11.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.12",
|
|
91
91
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
92
92
|
"main": "src/index.js",
|
|
93
93
|
"files": [
|
|
@@ -36,6 +36,7 @@ import 'react-toggle/style.css';
|
|
|
36
36
|
|
|
37
37
|
import CustomFetch from '../crm/Fetch';
|
|
38
38
|
import Form from './Form';
|
|
39
|
+
import { extractColumnsMetadata, isColumnSortable } from '../../utils/columnsMetadataUtils';
|
|
39
40
|
|
|
40
41
|
const loadData = async (
|
|
41
42
|
{ skip, limit, sortInfo, currentData, filterValue, groupBy },
|
|
@@ -83,10 +84,14 @@ const loadData = async (
|
|
|
83
84
|
try {
|
|
84
85
|
const response = await fetchUtil.post(url, params);
|
|
85
86
|
const { data, total } = response.data;
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
// Extract columns metadata if available
|
|
89
|
+
const metadata = extractColumnsMetadata(response);
|
|
90
|
+
|
|
91
|
+
return { data, count: total, metadata };
|
|
87
92
|
} catch (error) {
|
|
88
93
|
console.error('Error fetching data:', error);
|
|
89
|
-
return { data: [], count: 0 };
|
|
94
|
+
return { data: [], count: 0, metadata: {} };
|
|
90
95
|
}
|
|
91
96
|
};
|
|
92
97
|
|
|
@@ -146,9 +151,16 @@ const DataGrid = ({
|
|
|
146
151
|
|
|
147
152
|
sqlResult.then((res) => {
|
|
148
153
|
setDataCount(res.count);
|
|
154
|
+
// Update metadata when data is loaded
|
|
155
|
+
if (res.metadata) {
|
|
156
|
+
setColumnsMetadata(res.metadata);
|
|
157
|
+
}
|
|
149
158
|
});
|
|
150
159
|
|
|
151
|
-
return sqlResult
|
|
160
|
+
return sqlResult.then(res => ({
|
|
161
|
+
data: res.data,
|
|
162
|
+
count: res.count
|
|
163
|
+
}));
|
|
152
164
|
},
|
|
153
165
|
[ajaxSetting, dataReload, dSearch]
|
|
154
166
|
);
|
|
@@ -157,6 +169,7 @@ const DataGrid = ({
|
|
|
157
169
|
const [gridColumns, setGridColumns] = useState([]);
|
|
158
170
|
const [limit, setLimit] = useState(ajaxSetting.take || 10);
|
|
159
171
|
const [search, setSearch] = useState('');
|
|
172
|
+
const [columnsMetadata, setColumnsMetadata] = useState({});
|
|
160
173
|
|
|
161
174
|
/** Table Functions */
|
|
162
175
|
const handleChangeSearch = (e) => {
|
|
@@ -455,9 +468,14 @@ const DataGrid = ({
|
|
|
455
468
|
let style;
|
|
456
469
|
let value;
|
|
457
470
|
|
|
471
|
+
// Check if column is sortable based on metadata
|
|
472
|
+
const columnKey = column.id;
|
|
473
|
+
const sortable = isColumnSortable(columnKey, columnsMetadata);
|
|
474
|
+
|
|
458
475
|
const commonProps = {
|
|
459
476
|
header: column.label,
|
|
460
477
|
defaultFlex: 1,
|
|
478
|
+
sortable: sortable,
|
|
461
479
|
link: column.link ? column.link : {},
|
|
462
480
|
minWidth:
|
|
463
481
|
column.minWidth && column.minWidth > 0
|
|
@@ -1384,7 +1402,7 @@ const DataGrid = ({
|
|
|
1384
1402
|
});
|
|
1385
1403
|
|
|
1386
1404
|
setFilterValue(newFilterValue);
|
|
1387
|
-
}, [columns, filterDataSource]);
|
|
1405
|
+
}, [columns, filterDataSource, columnsMetadata]);
|
|
1388
1406
|
|
|
1389
1407
|
useEffect(() => {
|
|
1390
1408
|
if (setFilterData) {
|
|
@@ -178,11 +178,11 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
178
178
|
|
|
179
179
|
// Effect to handle overwrite toggle
|
|
180
180
|
useEffect(() => {
|
|
181
|
-
if (!overwrite && search) {
|
|
181
|
+
if (!overwrite && search && search.length > 1 && isFocused) {
|
|
182
182
|
setIsLoading(true);
|
|
183
183
|
performSearch(1, search);
|
|
184
184
|
}
|
|
185
|
-
}, [overwrite, search, performSearch]);
|
|
185
|
+
}, [overwrite, search, performSearch, isFocused]);
|
|
186
186
|
|
|
187
187
|
// Effect to sync with inputValue prop
|
|
188
188
|
useEffect(() => {
|
|
@@ -274,7 +274,7 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
274
274
|
)}
|
|
275
275
|
</div>
|
|
276
276
|
|
|
277
|
-
{isFocused && (search.length > 1 || isLoading) && (
|
|
277
|
+
{isFocused && !overwrite && (search.length > 1 || isLoading) && (
|
|
278
278
|
<ul
|
|
279
279
|
ref={resultsRef}
|
|
280
280
|
className={styles['AutocompletePlace-results']}
|
|
@@ -42,6 +42,7 @@ import { toast } from 'react-toastify';
|
|
|
42
42
|
import fetchUtil from '../../utils/fetchUtil';
|
|
43
43
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
44
44
|
import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../../utils/relationshipSortingUtils';
|
|
45
|
+
import { extractColumnsMetadata, isColumnSortable } from '../../utils/columnsMetadataUtils';
|
|
45
46
|
import {
|
|
46
47
|
AlarmClock,
|
|
47
48
|
RotateCcw,
|
|
@@ -205,10 +206,14 @@ const loadData = async (
|
|
|
205
206
|
try {
|
|
206
207
|
const response = await fetchUtil.post(url, params);
|
|
207
208
|
const { data, total } = response.data;
|
|
208
|
-
|
|
209
|
+
|
|
210
|
+
// Extract columns metadata if available
|
|
211
|
+
const metadata = extractColumnsMetadata(response);
|
|
212
|
+
|
|
213
|
+
return { data, count: total, metadata };
|
|
209
214
|
} catch (error) {
|
|
210
215
|
console.error('Error fetching data:', error);
|
|
211
|
-
return { data: [], count: 0 };
|
|
216
|
+
return { data: [], count: 0, metadata: {} };
|
|
212
217
|
}
|
|
213
218
|
};
|
|
214
219
|
|
|
@@ -377,6 +382,11 @@ const DataGrid = forwardRef(
|
|
|
377
382
|
}
|
|
378
383
|
|
|
379
384
|
sqlResult.then((res) => {
|
|
385
|
+
// Update metadata when data is loaded
|
|
386
|
+
if (res.metadata) {
|
|
387
|
+
setColumnsMetadata(res.metadata);
|
|
388
|
+
}
|
|
389
|
+
|
|
380
390
|
// Sort data by grouping field if grouping is enabled
|
|
381
391
|
if (ajaxSetting?.groupBy?.length > 0) {
|
|
382
392
|
const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
|
|
@@ -434,13 +444,17 @@ const DataGrid = forwardRef(
|
|
|
434
444
|
}, 10);
|
|
435
445
|
});
|
|
436
446
|
|
|
437
|
-
return sqlResult
|
|
447
|
+
return sqlResult.then(res => ({
|
|
448
|
+
data: res.data,
|
|
449
|
+
count: res.count
|
|
450
|
+
}));
|
|
438
451
|
},
|
|
439
452
|
[ajaxSetting, dSearch, collapsedGroups]
|
|
440
453
|
);
|
|
441
454
|
const [filterDataSource, setFilterDataSource] = useState([]);
|
|
442
455
|
const [filterValue, setFilterValue] = useState([]);
|
|
443
456
|
const [gridColumns, setGridColumns] = useState([]);
|
|
457
|
+
const [columnsMetadata, setColumnsMetadata] = useState({});
|
|
444
458
|
const gridRef = useRef(null);
|
|
445
459
|
const [limit, setLimit] = useState(0);
|
|
446
460
|
const [search, setSearch] = useState('');
|
|
@@ -2354,9 +2368,14 @@ const DataGrid = forwardRef(
|
|
|
2354
2368
|
let columnStyle;
|
|
2355
2369
|
let value;
|
|
2356
2370
|
|
|
2371
|
+
// Check if column is sortable based on metadata
|
|
2372
|
+
const columnKey = column.id;
|
|
2373
|
+
const sortable = isColumnSortable(columnKey, columnsMetadata);
|
|
2374
|
+
|
|
2357
2375
|
const commonProps = {
|
|
2358
2376
|
header: column.label,
|
|
2359
2377
|
defaultFlex: 1,
|
|
2378
|
+
sortable: sortable,
|
|
2360
2379
|
link: column.link ?? {},
|
|
2361
2380
|
minWidth: column.minWidth ?? undefined,
|
|
2362
2381
|
maxWidth: column.maxWidth ?? undefined,
|
|
@@ -2836,7 +2855,7 @@ const DataGrid = forwardRef(
|
|
|
2836
2855
|
.catch((error) => {
|
|
2837
2856
|
console.error('Error in fetching dropdown data: ', error);
|
|
2838
2857
|
});
|
|
2839
|
-
}, [columns, filterDataSource]);
|
|
2858
|
+
}, [columns, filterDataSource, columnsMetadata]);
|
|
2840
2859
|
|
|
2841
2860
|
useEffect(() => {
|
|
2842
2861
|
if (setFilterData) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
|
4
4
|
import DatePicker from 'react-datepicker';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
import Toggle from 'react-toggle';
|
|
@@ -189,22 +189,6 @@ function Field({
|
|
|
189
189
|
const fetchOptions = () => {
|
|
190
190
|
let filter = {};
|
|
191
191
|
|
|
192
|
-
if (settings.where?.length > 0) {
|
|
193
|
-
const processedWhere = settings.where.map((whereItem) => {
|
|
194
|
-
if (whereItem.getKey && formData[whereItem.getKey]) {
|
|
195
|
-
return {
|
|
196
|
-
...whereItem,
|
|
197
|
-
value: formData[whereItem.getKey],
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
return whereItem;
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
filter = {
|
|
204
|
-
where: processedWhere,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
192
|
if (settings.fields?.length > 0) {
|
|
209
193
|
filter.fields = settings.fields;
|
|
210
194
|
}
|
|
@@ -261,7 +245,10 @@ function Field({
|
|
|
261
245
|
case 'dropdown-ajax':
|
|
262
246
|
case 'multi-dropdown-ajax':
|
|
263
247
|
case 'radio-ajax':
|
|
264
|
-
|
|
248
|
+
// Only fetch if there are no dependent fields (no 'where' conditions)
|
|
249
|
+
if (!settings.where || settings.where.length === 0) {
|
|
250
|
+
fetchOptions();
|
|
251
|
+
}
|
|
265
252
|
break;
|
|
266
253
|
default:
|
|
267
254
|
break;
|
|
@@ -285,7 +272,66 @@ function Field({
|
|
|
285
272
|
);
|
|
286
273
|
setCanvasUrl(canvasType.url);
|
|
287
274
|
}
|
|
288
|
-
}, [settings, counter
|
|
275
|
+
}, [settings, counter]);
|
|
276
|
+
|
|
277
|
+
// Get dependency field keys once
|
|
278
|
+
const dependencyKeys = useMemo(() => {
|
|
279
|
+
return settings.where?.map(whereItem => whereItem.getKey).filter(Boolean) || [];
|
|
280
|
+
}, [settings.where]);
|
|
281
|
+
|
|
282
|
+
// Separate useEffect for handling dependent form data changes for dropdown refetch
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (['dropdown-ajax', 'multi-dropdown-ajax', 'radio-ajax'].includes(settings.type) && settings.where?.length > 0) {
|
|
285
|
+
let filter = {};
|
|
286
|
+
|
|
287
|
+
const processedWhere = settings.where.map((whereItem) => {
|
|
288
|
+
if (whereItem.getKey && formData[whereItem.getKey]) {
|
|
289
|
+
return {
|
|
290
|
+
...whereItem,
|
|
291
|
+
value: formData[whereItem.getKey],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return whereItem;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
filter = { where: processedWhere };
|
|
298
|
+
|
|
299
|
+
if (settings.fields?.length > 0) {
|
|
300
|
+
filter.fields = settings.fields;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (settings.order && settings.order.sortBy && settings.order.sort) {
|
|
304
|
+
filter.sortBy = settings.order.sortBy;
|
|
305
|
+
filter.sort = settings.order.sort;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (settings?.customParams) {
|
|
309
|
+
filter = { ...filter, ...settings.customParams };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
CustomFetch(settings.url, 'POST', filter, function (result) {
|
|
313
|
+
setOptions(result.data);
|
|
314
|
+
|
|
315
|
+
let dataArray = [];
|
|
316
|
+
result.data.forEach((a) => {
|
|
317
|
+
let _tempObject = a;
|
|
318
|
+
if (!_.isEmpty(_tempObject)) {
|
|
319
|
+
let modifiedObject = {};
|
|
320
|
+
Object.keys(_tempObject).forEach(function (c) {
|
|
321
|
+
if (c !== 'id' && c !== 'label') {
|
|
322
|
+
modifiedObject['data-' + c] = _tempObject[c];
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
dataArray.push(modifiedObject);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (dataArray.length > 0) {
|
|
330
|
+
setDataOptions(dataArray);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}, [...dependencyKeys.map(key => formData[key])]);
|
|
289
335
|
|
|
290
336
|
const fetchChildDropdownData = (s, v) => {
|
|
291
337
|
let filter = {};
|
|
@@ -2126,7 +2172,6 @@ function Field({
|
|
|
2126
2172
|
</span>
|
|
2127
2173
|
);
|
|
2128
2174
|
}
|
|
2129
|
-
break;
|
|
2130
2175
|
case 'plaintextheading':
|
|
2131
2176
|
return <h2>{settings.label ? settings.label : ''}</h2>;
|
|
2132
2177
|
case 'section':
|
|
@@ -2143,7 +2188,7 @@ function Field({
|
|
|
2143
2188
|
onEditorChange={(value, e) => {
|
|
2144
2189
|
onChangeRicheditor(e, value, settings.id);
|
|
2145
2190
|
}}
|
|
2146
|
-
onInit={(
|
|
2191
|
+
onInit={(_, editor) => (editorRef.current = editor)}
|
|
2147
2192
|
value={inputValue || ''}
|
|
2148
2193
|
init={{
|
|
2149
2194
|
branding: false,
|
|
@@ -13,6 +13,7 @@ import { showContactSelectorModal } from './ContactSelectorModal';
|
|
|
13
13
|
import { showDateRangeSelectorModal } from './DateRangeSelectorModal';
|
|
14
14
|
import { showAlternativeActionModal } from './AlternativeActionModal';
|
|
15
15
|
import { showReasonCollectorModal } from './ReasonCollectorModal';
|
|
16
|
+
import { processGridHeaders, getColumnHeaderClasses, getColumnHeaderStyles, getColumnHeaderTooltip } from '../../../utils/columnsMetadataUtils';
|
|
16
17
|
import styles from './styles/GenericIndex.module.scss';
|
|
17
18
|
|
|
18
19
|
// ContactTooltip component for enhanced contact display
|
|
@@ -825,6 +826,14 @@ const GenericGrid = ({
|
|
|
825
826
|
|
|
826
827
|
// Handle sort request
|
|
827
828
|
const requestSort = useCallback((key) => {
|
|
829
|
+
// Find the header configuration for this key to check if it's sortable
|
|
830
|
+
const header = gridHeaders.find((h) => h.key === key);
|
|
831
|
+
|
|
832
|
+
// If the column is not sortable, don't proceed with sorting
|
|
833
|
+
if (header && header.sortable === false) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
828
837
|
// If clicking the same column, toggle direction
|
|
829
838
|
// Otherwise, start with ascending sort for the new column
|
|
830
839
|
setSortConfig((prevConfig) => {
|
|
@@ -835,7 +844,7 @@ const GenericGrid = ({
|
|
|
835
844
|
|
|
836
845
|
return { key, direction: newDirection };
|
|
837
846
|
});
|
|
838
|
-
}, []);
|
|
847
|
+
}, [gridHeaders]);
|
|
839
848
|
|
|
840
849
|
// Function to filter data based on filter values
|
|
841
850
|
const filterData = useCallback(
|
|
@@ -975,24 +984,16 @@ const GenericGrid = ({
|
|
|
975
984
|
const staticData = settings.staticData;
|
|
976
985
|
|
|
977
986
|
if (staticData.header && staticData.rows) {
|
|
978
|
-
//
|
|
979
|
-
const
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
key,
|
|
983
|
-
label: config.label || key,
|
|
984
|
-
sort: config.sort || key,
|
|
985
|
-
type: config.type || 'text',
|
|
986
|
-
filter: config.filter || null,
|
|
987
|
-
onClick: config.onClick || null,
|
|
988
|
-
}))
|
|
989
|
-
);
|
|
987
|
+
// Process static headers using the metadata utilities
|
|
988
|
+
const mockResponse = { data: { data: staticData } };
|
|
989
|
+
const processedHeaders = processGridHeaders(mockResponse);
|
|
990
|
+
setGridHeaders(processedHeaders);
|
|
990
991
|
|
|
991
992
|
// Initialize filter values
|
|
992
993
|
const initialFilterValues = {};
|
|
993
|
-
|
|
994
|
-
if (
|
|
995
|
-
initialFilterValues[key] = '';
|
|
994
|
+
processedHeaders.forEach((header) => {
|
|
995
|
+
if (header.filter) {
|
|
996
|
+
initialFilterValues[header.key] = '';
|
|
996
997
|
}
|
|
997
998
|
});
|
|
998
999
|
setFilterValues(initialFilterValues);
|
|
@@ -1046,26 +1047,15 @@ const GenericGrid = ({
|
|
|
1046
1047
|
if (response.data.data) {
|
|
1047
1048
|
// Check if the response has the expected structure with header and rows
|
|
1048
1049
|
if (response.data.data.header && response.data.data.rows) {
|
|
1049
|
-
//
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
);
|
|
1053
|
-
setGridHeaders(
|
|
1054
|
-
headerEntries.map(([key, config]) => ({
|
|
1055
|
-
key,
|
|
1056
|
-
label: config.label || key,
|
|
1057
|
-
sort: config.sort || key,
|
|
1058
|
-
type: config.type || 'text',
|
|
1059
|
-
filter: config.filter || null,
|
|
1060
|
-
onClick: config.onClick || null,
|
|
1061
|
-
}))
|
|
1062
|
-
);
|
|
1050
|
+
// Process headers using the metadata utilities
|
|
1051
|
+
const processedHeaders = processGridHeaders(response.data);
|
|
1052
|
+
setGridHeaders(processedHeaders);
|
|
1063
1053
|
|
|
1064
1054
|
// Initialize filter values
|
|
1065
1055
|
const initialFilterValues = {};
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
initialFilterValues[key] = '';
|
|
1056
|
+
processedHeaders.forEach((header) => {
|
|
1057
|
+
if (header.filter) {
|
|
1058
|
+
initialFilterValues[header.key] = '';
|
|
1069
1059
|
}
|
|
1070
1060
|
});
|
|
1071
1061
|
setFilterValues(initialFilterValues);
|
|
@@ -2627,34 +2617,36 @@ const GenericGrid = ({
|
|
|
2627
2617
|
<tr>
|
|
2628
2618
|
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
2629
2619
|
{gridHeaders.length > 0
|
|
2630
|
-
? gridHeaders.map((header, index) =>
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
{
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2620
|
+
? gridHeaders.map((header, index) => {
|
|
2621
|
+
const headerClasses = getColumnHeaderClasses(header);
|
|
2622
|
+
const headerStyles = getColumnHeaderStyles(header);
|
|
2623
|
+
const tooltip = getColumnHeaderTooltip(header);
|
|
2624
|
+
|
|
2625
|
+
return (
|
|
2626
|
+
<th
|
|
2627
|
+
key={`header-${index}`}
|
|
2628
|
+
className={`${styles.gridHeader} ${headerClasses.join(' ')}`}
|
|
2629
|
+
onClick={header.sortable !== false ? () => requestSort(header.key) : undefined}
|
|
2630
|
+
style={headerStyles}
|
|
2631
|
+
title={tooltip}
|
|
2632
|
+
>
|
|
2633
|
+
<div className={styles.headerContent}>
|
|
2634
|
+
{header.label}
|
|
2635
|
+
{header.sortable !== false && (
|
|
2636
|
+
<span
|
|
2637
|
+
className={styles.sortIndicator}
|
|
2638
|
+
style={getSortIndicatorStyle(header.key)}
|
|
2639
|
+
>
|
|
2640
|
+
{sortConfig.key === header.key &&
|
|
2641
|
+
sortConfig.direction === 'asc'
|
|
2642
|
+
? ' ▲'
|
|
2643
|
+
: ' ▼'}
|
|
2644
|
+
</span>
|
|
2647
2645
|
)}
|
|
2648
|
-
>
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
? ' ▲'
|
|
2653
|
-
: ' ▼'}
|
|
2654
|
-
</span>
|
|
2655
|
-
</div>
|
|
2656
|
-
</th>
|
|
2657
|
-
))
|
|
2646
|
+
</div>
|
|
2647
|
+
</th>
|
|
2648
|
+
);
|
|
2649
|
+
})
|
|
2658
2650
|
: headers.map((header, index) => (
|
|
2659
2651
|
<th
|
|
2660
2652
|
key={`header-${index}`}
|
|
@@ -502,6 +502,58 @@
|
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
+
// Non-sortable column styles
|
|
506
|
+
.non-sortable {
|
|
507
|
+
cursor: default !important;
|
|
508
|
+
opacity: 0.8;
|
|
509
|
+
user-select: none;
|
|
510
|
+
|
|
511
|
+
&:hover {
|
|
512
|
+
background-color: var(--primary-color, #007bff) !important;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.headerContent {
|
|
516
|
+
opacity: 0.9;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Virtual column styles
|
|
521
|
+
.virtual-column {
|
|
522
|
+
position: relative;
|
|
523
|
+
|
|
524
|
+
&::after {
|
|
525
|
+
content: '🔗';
|
|
526
|
+
position: absolute;
|
|
527
|
+
top: 4px;
|
|
528
|
+
right: 4px;
|
|
529
|
+
font-size: 0.7em;
|
|
530
|
+
opacity: 0.6;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Computed column styles
|
|
535
|
+
.computed-column {
|
|
536
|
+
position: relative;
|
|
537
|
+
|
|
538
|
+
&::after {
|
|
539
|
+
content: '🧮';
|
|
540
|
+
position: absolute;
|
|
541
|
+
top: 4px;
|
|
542
|
+
right: 4px;
|
|
543
|
+
font-size: 0.7em;
|
|
544
|
+
opacity: 0.6;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Column type indicators
|
|
549
|
+
.column-type-virtual {
|
|
550
|
+
border-left: 3px solid rgba(255, 193, 7, 0.7);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.column-type-computed {
|
|
554
|
+
border-left: 3px solid rgba(40, 167, 69, 0.7);
|
|
555
|
+
}
|
|
556
|
+
|
|
505
557
|
.headerContent {
|
|
506
558
|
display: flex;
|
|
507
559
|
align-items: center;
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for handling columns_metadata from API responses
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to parse and apply metadata from the backend
|
|
5
|
+
* that controls column behavior, especially for virtual columns that should not be sortable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts and normalizes columns metadata from API response
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} response - API response object
|
|
12
|
+
* @returns {Object} - Normalized metadata object with column keys as properties
|
|
13
|
+
*/
|
|
14
|
+
export const extractColumnsMetadata = (response) => {
|
|
15
|
+
// Handle different possible response structures
|
|
16
|
+
const metadata = response?.data?.columns_metadata ||
|
|
17
|
+
response?.columns_metadata ||
|
|
18
|
+
response?.metadata?.columns ||
|
|
19
|
+
{};
|
|
20
|
+
|
|
21
|
+
// Ensure we have a valid object
|
|
22
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return metadata;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a column is sortable based on metadata
|
|
31
|
+
*
|
|
32
|
+
* @param {string} columnKey - The column key/identifier
|
|
33
|
+
* @param {Object} metadata - Columns metadata object
|
|
34
|
+
* @returns {boolean} - Whether the column is sortable
|
|
35
|
+
*/
|
|
36
|
+
export const isColumnSortable = (columnKey, metadata) => {
|
|
37
|
+
// If no metadata available, assume sortable (backward compatibility)
|
|
38
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const columnMeta = metadata[columnKey];
|
|
43
|
+
|
|
44
|
+
// If no specific metadata for this column, assume sortable
|
|
45
|
+
if (!columnMeta || typeof columnMeta !== 'object') {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check the sortable property, default to true if not specified
|
|
50
|
+
return columnMeta.sortable !== false;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Gets the display type for a column based on metadata
|
|
55
|
+
*
|
|
56
|
+
* @param {string} columnKey - The column key/identifier
|
|
57
|
+
* @param {Object} metadata - Columns metadata object
|
|
58
|
+
* @returns {string} - Column display type (e.g., 'virtual', 'computed', 'regular')
|
|
59
|
+
*/
|
|
60
|
+
export const getColumnDisplayType = (columnKey, metadata) => {
|
|
61
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
62
|
+
return 'regular';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const columnMeta = metadata[columnKey];
|
|
66
|
+
|
|
67
|
+
if (!columnMeta || typeof columnMeta !== 'object') {
|
|
68
|
+
return 'regular';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return columnMeta.type || 'regular';
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets additional column properties from metadata
|
|
76
|
+
*
|
|
77
|
+
* @param {string} columnKey - The column key/identifier
|
|
78
|
+
* @param {Object} metadata - Columns metadata object
|
|
79
|
+
* @returns {Object} - Additional properties like tooltip, description, etc.
|
|
80
|
+
*/
|
|
81
|
+
export const getColumnProperties = (columnKey, metadata) => {
|
|
82
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const columnMeta = metadata[columnKey];
|
|
87
|
+
|
|
88
|
+
if (!columnMeta || typeof columnMeta !== 'object') {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
tooltip: columnMeta.tooltip || null,
|
|
94
|
+
description: columnMeta.description || null,
|
|
95
|
+
virtual: columnMeta.virtual === true,
|
|
96
|
+
computed: columnMeta.computed === true,
|
|
97
|
+
searchable: columnMeta.searchable !== false,
|
|
98
|
+
filterable: columnMeta.filterable !== false
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merges column configuration with metadata to create enhanced column objects
|
|
104
|
+
*
|
|
105
|
+
* @param {Array} columns - Original column configuration array
|
|
106
|
+
* @param {Object} metadata - Columns metadata object
|
|
107
|
+
* @returns {Array} - Enhanced column array with metadata applied
|
|
108
|
+
*/
|
|
109
|
+
export const mergeColumnsWithMetadata = (columns, metadata) => {
|
|
110
|
+
if (!Array.isArray(columns)) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
115
|
+
return columns;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return columns.map(column => {
|
|
119
|
+
const columnKey = column.key || column.id || column.name;
|
|
120
|
+
|
|
121
|
+
if (!columnKey) {
|
|
122
|
+
return column;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const columnProperties = getColumnProperties(columnKey, metadata);
|
|
126
|
+
const isSortable = isColumnSortable(columnKey, metadata);
|
|
127
|
+
const displayType = getColumnDisplayType(columnKey, metadata);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...column,
|
|
131
|
+
sortable: isSortable,
|
|
132
|
+
displayType,
|
|
133
|
+
virtual: columnProperties.virtual,
|
|
134
|
+
computed: columnProperties.computed,
|
|
135
|
+
tooltip: columnProperties.tooltip,
|
|
136
|
+
description: columnProperties.description,
|
|
137
|
+
searchable: columnProperties.searchable,
|
|
138
|
+
filterable: columnProperties.filterable,
|
|
139
|
+
// Keep original metadata for reference
|
|
140
|
+
_metadata: metadata[columnKey] || {}
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Processes grid headers from API response and applies metadata
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} response - API response containing headers and metadata
|
|
149
|
+
* @returns {Array} - Processed headers with metadata applied
|
|
150
|
+
*/
|
|
151
|
+
export const processGridHeaders = (response) => {
|
|
152
|
+
// Extract headers from different possible response structures
|
|
153
|
+
const headers = response?.data?.data?.header ||
|
|
154
|
+
response?.data?.header ||
|
|
155
|
+
response?.header ||
|
|
156
|
+
{};
|
|
157
|
+
|
|
158
|
+
const metadata = extractColumnsMetadata(response);
|
|
159
|
+
|
|
160
|
+
if (!headers || typeof headers !== 'object') {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Convert headers object to array format expected by components
|
|
165
|
+
const headerEntries = Object.entries(headers);
|
|
166
|
+
|
|
167
|
+
return headerEntries.map(([key, config]) => {
|
|
168
|
+
const isSortable = isColumnSortable(key, metadata);
|
|
169
|
+
const displayType = getColumnDisplayType(key, metadata);
|
|
170
|
+
const properties = getColumnProperties(key, metadata);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
key,
|
|
174
|
+
label: config.label || key,
|
|
175
|
+
sort: config.sort || key,
|
|
176
|
+
type: config.type || 'text',
|
|
177
|
+
filter: config.filter || null,
|
|
178
|
+
onClick: config.onClick || null,
|
|
179
|
+
// Apply metadata
|
|
180
|
+
sortable: isSortable,
|
|
181
|
+
displayType,
|
|
182
|
+
virtual: properties.virtual,
|
|
183
|
+
computed: properties.computed,
|
|
184
|
+
tooltip: properties.tooltip,
|
|
185
|
+
description: properties.description,
|
|
186
|
+
searchable: properties.searchable,
|
|
187
|
+
filterable: properties.filterable,
|
|
188
|
+
// Keep original metadata for reference
|
|
189
|
+
_metadata: metadata[key] || {}
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Gets CSS classes for a column header based on its metadata
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} header - Header configuration object with metadata
|
|
198
|
+
* @returns {Array} - Array of CSS class names
|
|
199
|
+
*/
|
|
200
|
+
export const getColumnHeaderClasses = (header) => {
|
|
201
|
+
const classes = [];
|
|
202
|
+
|
|
203
|
+
if (header.sortable === false) {
|
|
204
|
+
classes.push('non-sortable');
|
|
205
|
+
} else {
|
|
206
|
+
classes.push('sortable');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (header.virtual) {
|
|
210
|
+
classes.push('virtual-column');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (header.computed) {
|
|
214
|
+
classes.push('computed-column');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (header.displayType) {
|
|
218
|
+
classes.push(`column-type-${header.displayType}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return classes;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Gets inline styles for a column header based on its metadata
|
|
226
|
+
*
|
|
227
|
+
* @param {Object} header - Header configuration object with metadata
|
|
228
|
+
* @returns {Object} - Style object for the header
|
|
229
|
+
*/
|
|
230
|
+
export const getColumnHeaderStyles = (header) => {
|
|
231
|
+
const styles = {};
|
|
232
|
+
|
|
233
|
+
if (header.sortable === false) {
|
|
234
|
+
styles.cursor = 'default';
|
|
235
|
+
styles.opacity = 0.7;
|
|
236
|
+
} else {
|
|
237
|
+
styles.cursor = 'pointer';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return styles;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gets tooltip text for a column header based on its metadata
|
|
245
|
+
*
|
|
246
|
+
* @param {Object} header - Header configuration object with metadata
|
|
247
|
+
* @returns {string|null} - Tooltip text or null if no tooltip needed
|
|
248
|
+
*/
|
|
249
|
+
export const getColumnHeaderTooltip = (header) => {
|
|
250
|
+
if (header.tooltip) {
|
|
251
|
+
return header.tooltip;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (header.sortable === false) {
|
|
255
|
+
if (header.virtual) {
|
|
256
|
+
return 'This is a virtual column and cannot be sorted';
|
|
257
|
+
}
|
|
258
|
+
if (header.computed) {
|
|
259
|
+
return 'This is a computed column and cannot be sorted';
|
|
260
|
+
}
|
|
261
|
+
return 'This column cannot be sorted';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export default {
|
|
268
|
+
extractColumnsMetadata,
|
|
269
|
+
isColumnSortable,
|
|
270
|
+
getColumnDisplayType,
|
|
271
|
+
getColumnProperties,
|
|
272
|
+
mergeColumnsWithMetadata,
|
|
273
|
+
processGridHeaders,
|
|
274
|
+
getColumnHeaderClasses,
|
|
275
|
+
getColumnHeaderStyles,
|
|
276
|
+
getColumnHeaderTooltip
|
|
277
|
+
};
|