@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 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.18.1",
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.18.1",
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.0",
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.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
- return { data, count: total };
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
- return { data, count: total };
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
- fetchOptions();
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, formData]);
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={(evt, editor) => (editorRef.current = editor)}
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
- // Set the headers from the static data
979
- const headerEntries = Object.entries(staticData.header);
980
- setGridHeaders(
981
- headerEntries.map(([key, config]) => ({
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
- headerEntries.forEach(([key, config]) => {
994
- if (config.filter) {
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
- // Set the headers from the response with the new structure
1050
- const headerEntries = Object.entries(
1051
- response.data.data.header
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
- headerEntries.forEach(([key, config]) => {
1067
- if (config.filter) {
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
- <th
2632
- key={`header-${index}`}
2633
- className={`${styles.gridHeader} ${styles.sortableHeader}`}
2634
- onClick={() =>
2635
- requestSort(header.key)
2636
- }
2637
- style={{ cursor: 'pointer' }}
2638
- >
2639
- <div className={styles.headerContent}>
2640
- {header.label}
2641
- <span
2642
- className={
2643
- styles.sortIndicator
2644
- }
2645
- style={getSortIndicatorStyle(
2646
- header.key
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
- {sortConfig.key ===
2650
- header.key &&
2651
- sortConfig.direction === 'asc'
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
+ };