@visns-studio/visns-components 5.11.11 → 5.11.13
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 +249 -111
- package/src/components/crm/generic/styles/GenericIndex.module.scss +63 -0
- package/src/utils/columnsMetadataUtils.js +329 -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.13",
|
|
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, getColumnCellClasses, getColumnCellStyles } from '../../../utils/columnsMetadataUtils';
|
|
16
17
|
import styles from './styles/GenericIndex.module.scss';
|
|
17
18
|
|
|
18
19
|
// ContactTooltip component for enhanced contact display
|
|
@@ -106,6 +107,82 @@ const ContactTooltip = ({ contacts, children }) => {
|
|
|
106
107
|
);
|
|
107
108
|
};
|
|
108
109
|
|
|
110
|
+
// CellContentWithTooltip component for handling text truncation and tooltips
|
|
111
|
+
const CellContentWithTooltip = ({ content, header, className }) => {
|
|
112
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
113
|
+
const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 });
|
|
114
|
+
const cellRef = useRef(null);
|
|
115
|
+
const tooltipId = `cell-tooltip-${Math.random().toString(36).substr(2, 9)}`;
|
|
116
|
+
|
|
117
|
+
// Show tooltip by default for text content, but exclude date columns which have their own tooltip system
|
|
118
|
+
const isDateColumn = header.type === 'date' || header.dataType === 'date' || header.key?.includes('date') || header.key?.includes('anniversary');
|
|
119
|
+
const shouldShowTooltip = !isDateColumn && header.tooltip !== false && (header.truncate !== false || header.tooltip);
|
|
120
|
+
|
|
121
|
+
const handleMouseEnter = (e) => {
|
|
122
|
+
if (!shouldShowTooltip) return;
|
|
123
|
+
|
|
124
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
125
|
+
setTooltipPosition({
|
|
126
|
+
x: rect.left + rect.width / 2,
|
|
127
|
+
y: rect.top - 10,
|
|
128
|
+
});
|
|
129
|
+
setShowTooltip(true);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleMouseLeave = () => {
|
|
133
|
+
setShowTooltip(false);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Determine if cell should allow wrapping based on header configuration
|
|
137
|
+
const allowWrap = header.allowWrap === true || header.truncate === false;
|
|
138
|
+
const cellClassName = `${className} ${allowWrap ? '' : ''}`;
|
|
139
|
+
|
|
140
|
+
if (shouldShowTooltip) {
|
|
141
|
+
return (
|
|
142
|
+
<>
|
|
143
|
+
<span
|
|
144
|
+
ref={cellRef}
|
|
145
|
+
className={cellClassName}
|
|
146
|
+
data-allow-wrap={allowWrap}
|
|
147
|
+
onMouseEnter={handleMouseEnter}
|
|
148
|
+
onMouseLeave={handleMouseLeave}
|
|
149
|
+
title={content} // Fallback native tooltip
|
|
150
|
+
>
|
|
151
|
+
{content}
|
|
152
|
+
</span>
|
|
153
|
+
{showTooltip && (
|
|
154
|
+
<div
|
|
155
|
+
className={styles.cellTooltip}
|
|
156
|
+
style={{
|
|
157
|
+
position: 'fixed',
|
|
158
|
+
left: `${tooltipPosition.x}px`,
|
|
159
|
+
top: `${tooltipPosition.y}px`,
|
|
160
|
+
transform: 'translateX(-50%) translateY(-100%)',
|
|
161
|
+
zIndex: 999999,
|
|
162
|
+
backgroundColor: 'white',
|
|
163
|
+
color: 'black',
|
|
164
|
+
border: '1px solid black',
|
|
165
|
+
borderRadius: '4px',
|
|
166
|
+
padding: '16px 20px',
|
|
167
|
+
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
168
|
+
fontSize: '14px',
|
|
169
|
+
maxWidth: '500px',
|
|
170
|
+
minWidth: '200px',
|
|
171
|
+
whiteSpace: 'pre-wrap',
|
|
172
|
+
wordBreak: 'break-word',
|
|
173
|
+
lineHeight: '1.4'
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{content}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return <span className={cellClassName} data-allow-wrap={allowWrap}>{content}</span>;
|
|
184
|
+
};
|
|
185
|
+
|
|
109
186
|
const GenericGrid = ({
|
|
110
187
|
config,
|
|
111
188
|
userProfile,
|
|
@@ -825,6 +902,14 @@ const GenericGrid = ({
|
|
|
825
902
|
|
|
826
903
|
// Handle sort request
|
|
827
904
|
const requestSort = useCallback((key) => {
|
|
905
|
+
// Find the header configuration for this key to check if it's sortable
|
|
906
|
+
const header = gridHeaders.find((h) => h.key === key);
|
|
907
|
+
|
|
908
|
+
// If the column is not sortable, don't proceed with sorting
|
|
909
|
+
if (header && header.sortable === false) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
828
913
|
// If clicking the same column, toggle direction
|
|
829
914
|
// Otherwise, start with ascending sort for the new column
|
|
830
915
|
setSortConfig((prevConfig) => {
|
|
@@ -835,7 +920,7 @@ const GenericGrid = ({
|
|
|
835
920
|
|
|
836
921
|
return { key, direction: newDirection };
|
|
837
922
|
});
|
|
838
|
-
}, []);
|
|
923
|
+
}, [gridHeaders]);
|
|
839
924
|
|
|
840
925
|
// Function to filter data based on filter values
|
|
841
926
|
const filterData = useCallback(
|
|
@@ -930,6 +1015,11 @@ const GenericGrid = ({
|
|
|
930
1015
|
);
|
|
931
1016
|
}
|
|
932
1017
|
|
|
1018
|
+
case 'dropdown':
|
|
1019
|
+
const rowStrDropdown = String(rowValue).toLowerCase();
|
|
1020
|
+
const filterStrDropdown = String(value).toLowerCase();
|
|
1021
|
+
return rowStrDropdown === filterStrDropdown;
|
|
1022
|
+
|
|
933
1023
|
default:
|
|
934
1024
|
return true;
|
|
935
1025
|
}
|
|
@@ -941,10 +1031,27 @@ const GenericGrid = ({
|
|
|
941
1031
|
|
|
942
1032
|
// Handle filter change
|
|
943
1033
|
const handleFilterChange = useCallback((key, value) => {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1034
|
+
console.group('🔍 Filter Change Debug');
|
|
1035
|
+
console.log('Filter key:', key);
|
|
1036
|
+
console.log('Filter value:', value);
|
|
1037
|
+
console.log('Value type:', typeof value);
|
|
1038
|
+
console.log('Value length:', value?.length);
|
|
1039
|
+
console.log('Is empty value:', !value || value === '');
|
|
1040
|
+
|
|
1041
|
+
setFilterValues((prev) => {
|
|
1042
|
+
console.log('Previous filter values:', prev);
|
|
1043
|
+
|
|
1044
|
+
const newFilterValues = {
|
|
1045
|
+
...prev,
|
|
1046
|
+
[key]: value,
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
console.log('New filter values:', newFilterValues);
|
|
1050
|
+
console.log('Active filters count:', Object.keys(newFilterValues).filter(k => newFilterValues[k]).length);
|
|
1051
|
+
console.groupEnd();
|
|
1052
|
+
|
|
1053
|
+
return newFilterValues;
|
|
1054
|
+
});
|
|
948
1055
|
}, []);
|
|
949
1056
|
|
|
950
1057
|
// Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
|
|
@@ -975,24 +1082,16 @@ const GenericGrid = ({
|
|
|
975
1082
|
const staticData = settings.staticData;
|
|
976
1083
|
|
|
977
1084
|
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
|
-
);
|
|
1085
|
+
// Process static headers using the metadata utilities
|
|
1086
|
+
const mockResponse = { data: { data: staticData } };
|
|
1087
|
+
const processedHeaders = processGridHeaders(mockResponse);
|
|
1088
|
+
setGridHeaders(processedHeaders);
|
|
990
1089
|
|
|
991
1090
|
// Initialize filter values
|
|
992
1091
|
const initialFilterValues = {};
|
|
993
|
-
|
|
994
|
-
if (
|
|
995
|
-
initialFilterValues[key] = '';
|
|
1092
|
+
processedHeaders.forEach((header) => {
|
|
1093
|
+
if (header.filter) {
|
|
1094
|
+
initialFilterValues[header.key] = '';
|
|
996
1095
|
}
|
|
997
1096
|
});
|
|
998
1097
|
setFilterValues(initialFilterValues);
|
|
@@ -1046,26 +1145,15 @@ const GenericGrid = ({
|
|
|
1046
1145
|
if (response.data.data) {
|
|
1047
1146
|
// Check if the response has the expected structure with header and rows
|
|
1048
1147
|
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
|
-
);
|
|
1148
|
+
// Process headers using the metadata utilities
|
|
1149
|
+
const processedHeaders = processGridHeaders(response.data);
|
|
1150
|
+
setGridHeaders(processedHeaders);
|
|
1063
1151
|
|
|
1064
1152
|
// Initialize filter values
|
|
1065
1153
|
const initialFilterValues = {};
|
|
1066
|
-
|
|
1067
|
-
if (
|
|
1068
|
-
initialFilterValues[key] = '';
|
|
1154
|
+
processedHeaders.forEach((header) => {
|
|
1155
|
+
if (header.filter) {
|
|
1156
|
+
initialFilterValues[header.key] = '';
|
|
1069
1157
|
}
|
|
1070
1158
|
});
|
|
1071
1159
|
setFilterValues(initialFilterValues);
|
|
@@ -2627,34 +2715,36 @@ const GenericGrid = ({
|
|
|
2627
2715
|
<tr>
|
|
2628
2716
|
{/* Use gridHeaders if available, otherwise fall back to headers from columns */}
|
|
2629
2717
|
{gridHeaders.length > 0
|
|
2630
|
-
? gridHeaders.map((header, index) =>
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
{
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2718
|
+
? gridHeaders.map((header, index) => {
|
|
2719
|
+
const headerClasses = getColumnHeaderClasses(header);
|
|
2720
|
+
const headerStyles = getColumnHeaderStyles(header);
|
|
2721
|
+
const tooltip = getColumnHeaderTooltip(header);
|
|
2722
|
+
|
|
2723
|
+
return (
|
|
2724
|
+
<th
|
|
2725
|
+
key={`header-${index}`}
|
|
2726
|
+
className={`${styles.gridHeader} ${headerClasses.join(' ')}`}
|
|
2727
|
+
onClick={header.sortable !== false ? () => requestSort(header.key) : undefined}
|
|
2728
|
+
style={headerStyles}
|
|
2729
|
+
title={tooltip}
|
|
2730
|
+
>
|
|
2731
|
+
<div className={styles.headerContent}>
|
|
2732
|
+
{header.label}
|
|
2733
|
+
{header.sortable !== false && (
|
|
2734
|
+
<span
|
|
2735
|
+
className={styles.sortIndicator}
|
|
2736
|
+
style={getSortIndicatorStyle(header.key)}
|
|
2737
|
+
>
|
|
2738
|
+
{sortConfig.key === header.key &&
|
|
2739
|
+
sortConfig.direction === 'asc'
|
|
2740
|
+
? ' ▲'
|
|
2741
|
+
: ' ▼'}
|
|
2742
|
+
</span>
|
|
2647
2743
|
)}
|
|
2648
|
-
>
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
? ' ▲'
|
|
2653
|
-
: ' ▼'}
|
|
2654
|
-
</span>
|
|
2655
|
-
</div>
|
|
2656
|
-
</th>
|
|
2657
|
-
))
|
|
2744
|
+
</div>
|
|
2745
|
+
</th>
|
|
2746
|
+
);
|
|
2747
|
+
})
|
|
2658
2748
|
: headers.map((header, index) => (
|
|
2659
2749
|
<th
|
|
2660
2750
|
key={`header-${index}`}
|
|
@@ -2673,26 +2763,82 @@ const GenericGrid = ({
|
|
|
2673
2763
|
className={styles.filterCell}
|
|
2674
2764
|
>
|
|
2675
2765
|
{header.filter ? (
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
?
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2766
|
+
header.filter.type === 'dropdown' ? (
|
|
2767
|
+
<div className={styles.filterInput}>
|
|
2768
|
+
<MultiSelect
|
|
2769
|
+
placeholder={`Filter ${header.label}...`}
|
|
2770
|
+
inputValue={filterValues[header.key] ? { value: filterValues[header.key], label: filterValues[header.key] } : null}
|
|
2771
|
+
multi={false}
|
|
2772
|
+
options={header.filter.options || []}
|
|
2773
|
+
settings={{}}
|
|
2774
|
+
style={{
|
|
2775
|
+
control: (provided) => ({
|
|
2776
|
+
...provided,
|
|
2777
|
+
minHeight: '32px',
|
|
2778
|
+
height: '32px',
|
|
2779
|
+
border: 'none',
|
|
2780
|
+
boxShadow: 'none',
|
|
2781
|
+
backgroundColor: 'transparent',
|
|
2782
|
+
}),
|
|
2783
|
+
valueContainer: (provided) => ({
|
|
2784
|
+
...provided,
|
|
2785
|
+
height: '30px',
|
|
2786
|
+
padding: '0 6px',
|
|
2787
|
+
}),
|
|
2788
|
+
input: (provided) => ({
|
|
2789
|
+
...provided,
|
|
2790
|
+
margin: '0px',
|
|
2791
|
+
}),
|
|
2792
|
+
indicatorSeparator: () => ({
|
|
2793
|
+
display: 'none',
|
|
2794
|
+
}),
|
|
2795
|
+
indicatorsContainer: (provided) => ({
|
|
2796
|
+
...provided,
|
|
2797
|
+
height: '30px',
|
|
2798
|
+
}),
|
|
2799
|
+
}}
|
|
2800
|
+
onChange={(selectedOption) => {
|
|
2801
|
+
console.log('=== DROPDOWN FILTER DEBUG ===');
|
|
2802
|
+
console.log('Header key:', header.key);
|
|
2803
|
+
console.log('Header:', header);
|
|
2804
|
+
console.log('Selected option:', selectedOption);
|
|
2805
|
+
console.log('Selected option type:', typeof selectedOption);
|
|
2806
|
+
console.log('Is array?', Array.isArray(selectedOption));
|
|
2807
|
+
console.log('Current filterValues:', filterValues);
|
|
2808
|
+
|
|
2809
|
+
// Fix: MultiSelect returns a single object when multi=false, not an array
|
|
2810
|
+
const newValue = selectedOption ? selectedOption.value : '';
|
|
2811
|
+
console.log('New value to set:', newValue);
|
|
2812
|
+
|
|
2813
|
+
handleFilterChange(header.key, newValue);
|
|
2814
|
+
|
|
2815
|
+
console.log('Filter change called with:', header.key, newValue);
|
|
2816
|
+
console.log('=== END DROPDOWN DEBUG ===');
|
|
2817
|
+
}}
|
|
2818
|
+
/>
|
|
2819
|
+
</div>
|
|
2820
|
+
) : (
|
|
2821
|
+
<input
|
|
2822
|
+
type={
|
|
2823
|
+
header.filter.type ===
|
|
2824
|
+
'date'
|
|
2825
|
+
? 'date'
|
|
2826
|
+
: 'text'
|
|
2827
|
+
}
|
|
2828
|
+
className={styles.filterInput}
|
|
2829
|
+
placeholder={`Filter ${header.label}...`}
|
|
2830
|
+
value={
|
|
2831
|
+
filterValues[header.key] ||
|
|
2832
|
+
''
|
|
2833
|
+
}
|
|
2834
|
+
onChange={(e) =>
|
|
2835
|
+
handleFilterChange(
|
|
2836
|
+
header.key,
|
|
2837
|
+
e.target.value
|
|
2838
|
+
)
|
|
2839
|
+
}
|
|
2840
|
+
/>
|
|
2841
|
+
)
|
|
2696
2842
|
) : null}
|
|
2697
2843
|
</th>
|
|
2698
2844
|
))}
|
|
@@ -2715,7 +2861,11 @@ const GenericGrid = ({
|
|
|
2715
2861
|
{/* Use gridHeaders if available, otherwise fall back to columns */}
|
|
2716
2862
|
{gridHeaders.length > 0
|
|
2717
2863
|
? gridHeaders.map(
|
|
2718
|
-
(header, colIndex) =>
|
|
2864
|
+
(header, colIndex) => {
|
|
2865
|
+
const cellClasses = getColumnCellClasses(header);
|
|
2866
|
+
const cellStyles = getColumnCellStyles(header);
|
|
2867
|
+
|
|
2868
|
+
return (
|
|
2719
2869
|
<td
|
|
2720
2870
|
key={`cell-${rowIndex}-${colIndex}`}
|
|
2721
2871
|
className={`${
|
|
@@ -2732,7 +2882,8 @@ const GenericGrid = ({
|
|
|
2732
2882
|
)
|
|
2733
2883
|
? styles.clickableCell
|
|
2734
2884
|
: ''
|
|
2735
|
-
}`}
|
|
2885
|
+
} ${cellClasses.join(' ')}`}
|
|
2886
|
+
style={cellStyles}
|
|
2736
2887
|
onClick={
|
|
2737
2888
|
header.onClick &&
|
|
2738
2889
|
!quarterCellHasContent(
|
|
@@ -2757,12 +2908,8 @@ const GenericGrid = ({
|
|
|
2757
2908
|
row
|
|
2758
2909
|
) ? (
|
|
2759
2910
|
header.onClick ? (
|
|
2760
|
-
<
|
|
2761
|
-
|
|
2762
|
-
styles.cellWithValue
|
|
2763
|
-
}
|
|
2764
|
-
>
|
|
2765
|
-
{formatCellContent(
|
|
2911
|
+
<CellContentWithTooltip
|
|
2912
|
+
content={formatCellContent(
|
|
2766
2913
|
row[
|
|
2767
2914
|
header
|
|
2768
2915
|
.key
|
|
@@ -2781,24 +2928,12 @@ const GenericGrid = ({
|
|
|
2781
2928
|
},
|
|
2782
2929
|
row
|
|
2783
2930
|
)}
|
|
2784
|
-
|
|
2931
|
+
header={header}
|
|
2932
|
+
className={styles.cellWithValue}
|
|
2933
|
+
/>
|
|
2785
2934
|
) : (
|
|
2786
|
-
<
|
|
2787
|
-
|
|
2788
|
-
styles.cellContent
|
|
2789
|
-
}
|
|
2790
|
-
data-type={
|
|
2791
|
-
header.type ||
|
|
2792
|
-
(typeof row[
|
|
2793
|
-
header
|
|
2794
|
-
.key
|
|
2795
|
-
] ===
|
|
2796
|
-
'number'
|
|
2797
|
-
? 'number'
|
|
2798
|
-
: '')
|
|
2799
|
-
}
|
|
2800
|
-
>
|
|
2801
|
-
{formatCellContent(
|
|
2935
|
+
<CellContentWithTooltip
|
|
2936
|
+
content={formatCellContent(
|
|
2802
2937
|
row[
|
|
2803
2938
|
header
|
|
2804
2939
|
.key
|
|
@@ -2817,7 +2952,9 @@ const GenericGrid = ({
|
|
|
2817
2952
|
},
|
|
2818
2953
|
row
|
|
2819
2954
|
)}
|
|
2820
|
-
|
|
2955
|
+
header={header}
|
|
2956
|
+
className={styles.cellContent}
|
|
2957
|
+
/>
|
|
2821
2958
|
)
|
|
2822
2959
|
) : header.onClick &&
|
|
2823
2960
|
isQuarterCellClickable(
|
|
@@ -2837,7 +2974,8 @@ const GenericGrid = ({
|
|
|
2837
2974
|
''
|
|
2838
2975
|
)}
|
|
2839
2976
|
</td>
|
|
2840
|
-
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2841
2979
|
)
|
|
2842
2980
|
: columns.map((col, colIndex) => (
|
|
2843
2981
|
<td
|
|
@@ -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;
|
|
@@ -564,6 +616,10 @@
|
|
|
564
616
|
.cellContent {
|
|
565
617
|
display: inline-block;
|
|
566
618
|
width: 100%;
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
text-overflow: ellipsis;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
max-width: 200px;
|
|
567
623
|
|
|
568
624
|
/* Ensure date fields have uniform width */
|
|
569
625
|
&[data-type='date'] {
|
|
@@ -572,6 +628,13 @@
|
|
|
572
628
|
max-width: 120px;
|
|
573
629
|
display: inline-block;
|
|
574
630
|
}
|
|
631
|
+
|
|
632
|
+
/* Allow wrapping for specific content types that need it */
|
|
633
|
+
&[data-allow-wrap='true'] {
|
|
634
|
+
white-space: normal;
|
|
635
|
+
max-width: none;
|
|
636
|
+
text-overflow: initial;
|
|
637
|
+
}
|
|
575
638
|
}
|
|
576
639
|
|
|
577
640
|
/* Date with contacts display */
|
|
@@ -0,0 +1,329 @@
|
|
|
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
|
+
width: columnMeta.width || null,
|
|
100
|
+
truncate: columnMeta.truncate === true
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Merges column configuration with metadata to create enhanced column objects
|
|
106
|
+
*
|
|
107
|
+
* @param {Array} columns - Original column configuration array
|
|
108
|
+
* @param {Object} metadata - Columns metadata object
|
|
109
|
+
* @returns {Array} - Enhanced column array with metadata applied
|
|
110
|
+
*/
|
|
111
|
+
export const mergeColumnsWithMetadata = (columns, metadata) => {
|
|
112
|
+
if (!Array.isArray(columns)) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
117
|
+
return columns;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return columns.map(column => {
|
|
121
|
+
const columnKey = column.key || column.id || column.name;
|
|
122
|
+
|
|
123
|
+
if (!columnKey) {
|
|
124
|
+
return column;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const columnProperties = getColumnProperties(columnKey, metadata);
|
|
128
|
+
const isSortable = isColumnSortable(columnKey, metadata);
|
|
129
|
+
const displayType = getColumnDisplayType(columnKey, metadata);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
...column,
|
|
133
|
+
sortable: isSortable,
|
|
134
|
+
displayType,
|
|
135
|
+
virtual: columnProperties.virtual,
|
|
136
|
+
computed: columnProperties.computed,
|
|
137
|
+
tooltip: columnProperties.tooltip,
|
|
138
|
+
description: columnProperties.description,
|
|
139
|
+
searchable: columnProperties.searchable,
|
|
140
|
+
filterable: columnProperties.filterable,
|
|
141
|
+
width: columnProperties.width,
|
|
142
|
+
truncate: columnProperties.truncate,
|
|
143
|
+
// Keep original metadata for reference
|
|
144
|
+
_metadata: metadata[columnKey] || {}
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Processes grid headers from API response and applies metadata
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} response - API response containing headers and metadata
|
|
153
|
+
* @returns {Array} - Processed headers with metadata applied
|
|
154
|
+
*/
|
|
155
|
+
export const processGridHeaders = (response) => {
|
|
156
|
+
// Extract headers from different possible response structures
|
|
157
|
+
const headers = response?.data?.data?.header ||
|
|
158
|
+
response?.data?.header ||
|
|
159
|
+
response?.header ||
|
|
160
|
+
{};
|
|
161
|
+
|
|
162
|
+
const metadata = extractColumnsMetadata(response);
|
|
163
|
+
|
|
164
|
+
if (!headers || typeof headers !== 'object') {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Convert headers object to array format expected by components
|
|
169
|
+
const headerEntries = Object.entries(headers);
|
|
170
|
+
|
|
171
|
+
return headerEntries.map(([key, config]) => {
|
|
172
|
+
const isSortable = isColumnSortable(key, metadata);
|
|
173
|
+
const displayType = getColumnDisplayType(key, metadata);
|
|
174
|
+
const properties = getColumnProperties(key, metadata);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
key,
|
|
178
|
+
label: config.label || key,
|
|
179
|
+
sort: config.sort || key,
|
|
180
|
+
type: config.type || 'text',
|
|
181
|
+
filter: config.filter || null,
|
|
182
|
+
onClick: config.onClick || null,
|
|
183
|
+
// Apply metadata
|
|
184
|
+
sortable: isSortable,
|
|
185
|
+
displayType,
|
|
186
|
+
virtual: properties.virtual,
|
|
187
|
+
computed: properties.computed,
|
|
188
|
+
tooltip: properties.tooltip,
|
|
189
|
+
description: properties.description,
|
|
190
|
+
searchable: properties.searchable,
|
|
191
|
+
filterable: properties.filterable,
|
|
192
|
+
width: properties.width,
|
|
193
|
+
truncate: properties.truncate,
|
|
194
|
+
// Keep original metadata for reference
|
|
195
|
+
_metadata: metadata[key] || {}
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Gets CSS classes for a column header based on its metadata
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} header - Header configuration object with metadata
|
|
204
|
+
* @returns {Array} - Array of CSS class names
|
|
205
|
+
*/
|
|
206
|
+
export const getColumnHeaderClasses = (header) => {
|
|
207
|
+
const classes = [];
|
|
208
|
+
|
|
209
|
+
if (header.sortable === false) {
|
|
210
|
+
classes.push('non-sortable');
|
|
211
|
+
} else {
|
|
212
|
+
classes.push('sortable');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (header.virtual) {
|
|
216
|
+
classes.push('virtual-column');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (header.computed) {
|
|
220
|
+
classes.push('computed-column');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (header.displayType) {
|
|
224
|
+
classes.push(`column-type-${header.displayType}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (header.truncate) {
|
|
228
|
+
classes.push('truncate-text');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return classes;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Gets inline styles for a column header based on its metadata
|
|
236
|
+
*
|
|
237
|
+
* @param {Object} header - Header configuration object with metadata
|
|
238
|
+
* @returns {Object} - Style object for the header
|
|
239
|
+
*/
|
|
240
|
+
export const getColumnHeaderStyles = (header) => {
|
|
241
|
+
const styles = {};
|
|
242
|
+
|
|
243
|
+
if (header.sortable === false) {
|
|
244
|
+
styles.cursor = 'default';
|
|
245
|
+
styles.opacity = 0.7;
|
|
246
|
+
} else {
|
|
247
|
+
styles.cursor = 'pointer';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (header.width) {
|
|
251
|
+
styles.width = header.width;
|
|
252
|
+
styles.minWidth = header.width;
|
|
253
|
+
styles.maxWidth = header.width;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return styles;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Gets tooltip text for a column header based on its metadata
|
|
261
|
+
*
|
|
262
|
+
* @param {Object} header - Header configuration object with metadata
|
|
263
|
+
* @returns {string|null} - Tooltip text or null if no tooltip needed
|
|
264
|
+
*/
|
|
265
|
+
export const getColumnHeaderTooltip = (header) => {
|
|
266
|
+
if (header.tooltip) {
|
|
267
|
+
return header.tooltip;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (header.sortable === false) {
|
|
271
|
+
if (header.virtual) {
|
|
272
|
+
return 'This is a virtual column and cannot be sorted';
|
|
273
|
+
}
|
|
274
|
+
if (header.computed) {
|
|
275
|
+
return 'This is a computed column and cannot be sorted';
|
|
276
|
+
}
|
|
277
|
+
return 'This column cannot be sorted';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Gets CSS classes for a column cell based on its metadata
|
|
285
|
+
*
|
|
286
|
+
* @param {Object} header - Header configuration object with metadata
|
|
287
|
+
* @returns {Array} - Array of CSS class names
|
|
288
|
+
*/
|
|
289
|
+
export const getColumnCellClasses = (header) => {
|
|
290
|
+
const classes = [];
|
|
291
|
+
|
|
292
|
+
if (header.truncate) {
|
|
293
|
+
classes.push('truncate-text');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return classes;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Gets inline styles for a column cell based on its metadata
|
|
301
|
+
*
|
|
302
|
+
* @param {Object} header - Header configuration object with metadata
|
|
303
|
+
* @returns {Object} - Style object for the cell
|
|
304
|
+
*/
|
|
305
|
+
export const getColumnCellStyles = (header) => {
|
|
306
|
+
const styles = {};
|
|
307
|
+
|
|
308
|
+
if (header.width) {
|
|
309
|
+
styles.width = header.width;
|
|
310
|
+
styles.minWidth = header.width;
|
|
311
|
+
styles.maxWidth = header.width;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return styles;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export default {
|
|
318
|
+
extractColumnsMetadata,
|
|
319
|
+
isColumnSortable,
|
|
320
|
+
getColumnDisplayType,
|
|
321
|
+
getColumnProperties,
|
|
322
|
+
mergeColumnsWithMetadata,
|
|
323
|
+
processGridHeaders,
|
|
324
|
+
getColumnHeaderClasses,
|
|
325
|
+
getColumnHeaderStyles,
|
|
326
|
+
getColumnHeaderTooltip,
|
|
327
|
+
getColumnCellClasses,
|
|
328
|
+
getColumnCellStyles
|
|
329
|
+
};
|