@visns-studio/visns-components 5.11.10 → 5.11.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Fallback version that restores original behavior
3
+ */
4
+
5
+ export const DEFAULT_INTELLIGENT_SORTING_CONFIG = {
6
+ enableIntelligentSorting: false, // Disabled by default for safety
7
+ maxRelationshipDepth: 3,
8
+ customSortableFields: [],
9
+ customUnsortableFields: [],
10
+ respectExplicitSortable: true,
11
+ logAnalysis: false
12
+ };
13
+
14
+ export const isRelationshipColumnSortable = (column, options = {}) => {
15
+ // If intelligent sorting is disabled, use original behavior
16
+ if (options.enableIntelligentSorting === false) {
17
+ // Original behavior: return false for relation columns unless explicitly set
18
+ return column.sortable === true;
19
+ }
20
+
21
+ // Respect explicit configuration
22
+ if (column.hasOwnProperty('sortable')) {
23
+ return column.sortable === true;
24
+ }
25
+
26
+ // Default to false (original behavior) for safety
27
+ return false;
28
+ };
29
+
30
+ export const getRelationshipPath = (column) => {
31
+ if (!Array.isArray(column.id)) {
32
+ return column.nameFrom || 'unknown';
33
+ }
34
+
35
+ const relationPath = column.id.join('.');
36
+ return column.nameFrom ? `${relationPath}.${column.nameFrom}` : relationPath;
37
+ };