@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 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.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
- 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, 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
- setFilterValues((prev) => ({
945
- ...prev,
946
- [key]: value,
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
- // 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
- );
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
- headerEntries.forEach(([key, config]) => {
994
- if (config.filter) {
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
- // 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
- );
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
- headerEntries.forEach(([key, config]) => {
1067
- if (config.filter) {
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
- <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
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
- {sortConfig.key ===
2650
- header.key &&
2651
- sortConfig.direction === 'asc'
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
- <input
2677
- type={
2678
- header.filter.type ===
2679
- 'date'
2680
- ? 'date'
2681
- : 'text'
2682
- }
2683
- className={styles.filterInput}
2684
- placeholder={`Filter ${header.label}...`}
2685
- value={
2686
- filterValues[header.key] ||
2687
- ''
2688
- }
2689
- onChange={(e) =>
2690
- handleFilterChange(
2691
- header.key,
2692
- e.target.value
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
- <span
2761
- className={
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
- </span>
2931
+ header={header}
2932
+ className={styles.cellWithValue}
2933
+ />
2785
2934
  ) : (
2786
- <span
2787
- className={
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
- </span>
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
+ };