@visns-studio/visns-components 5.11.12 → 5.11.14

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
@@ -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.12",
90
+ "version": "5.11.14",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -867,44 +867,15 @@ const DataGrid = forwardRef(
867
867
  case 'file':
868
868
  case 'image':
869
869
  case 'undo':
870
- // Create a user-friendly action name from the ID
871
- const actionName =
872
- s.title || s.id.charAt(0).toUpperCase() + s.id.slice(1);
873
-
874
- // Create a message based on the action type
875
- let message = `Are you sure you want to ${actionName.toLowerCase()}?`;
876
-
877
- // Add item identifier if available
878
- const itemName = d.name || d.label || d.title || d.id;
879
- if (itemName) {
880
- message += ` Item: "${itemName}"`;
881
- }
882
-
883
- // Show confirmation dialog
884
- confirmDialog({
885
- title: actionName,
886
- message: message,
887
- data: d, // Pass the row data for context
888
- buttons: [
889
- {
890
- label: 'Yes',
891
- onClick: () => {
892
- CustomFetch(
893
- s.url,
894
- 'POST',
895
- { id: d[s.key] },
896
- (result) => {
897
- toast.success(result.message);
898
- }
899
- );
900
- },
901
- },
902
- {
903
- label: 'No',
904
- onClick: () => {},
905
- },
906
- ],
907
- });
870
+ // Execute the action directly since confirmation was already handled
871
+ CustomFetch(
872
+ s.url,
873
+ 'POST',
874
+ { id: d[s.key] },
875
+ (result) => {
876
+ toast.success(result.message);
877
+ }
878
+ );
908
879
  break;
909
880
  case 'clone':
910
881
  if (s.url && s.url !== '') {
@@ -13,7 +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
+ import { processGridHeaders, getColumnHeaderClasses, getColumnHeaderStyles, getColumnHeaderTooltip, getColumnCellClasses, getColumnCellStyles } from '../../../utils/columnsMetadataUtils';
17
17
  import styles from './styles/GenericIndex.module.scss';
18
18
 
19
19
  // ContactTooltip component for enhanced contact display
@@ -107,6 +107,82 @@ const ContactTooltip = ({ contacts, children }) => {
107
107
  );
108
108
  };
109
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
+
110
186
  const GenericGrid = ({
111
187
  config,
112
188
  userProfile,
@@ -939,6 +1015,11 @@ const GenericGrid = ({
939
1015
  );
940
1016
  }
941
1017
 
1018
+ case 'dropdown':
1019
+ const rowStrDropdown = String(rowValue).toLowerCase();
1020
+ const filterStrDropdown = String(value).toLowerCase();
1021
+ return rowStrDropdown === filterStrDropdown;
1022
+
942
1023
  default:
943
1024
  return true;
944
1025
  }
@@ -950,10 +1031,27 @@ const GenericGrid = ({
950
1031
 
951
1032
  // Handle filter change
952
1033
  const handleFilterChange = useCallback((key, value) => {
953
- setFilterValues((prev) => ({
954
- ...prev,
955
- [key]: value,
956
- }));
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
+ });
957
1055
  }, []);
958
1056
 
959
1057
  // Effect to update filteredData and sortedData when gridData, filterValues, or sortConfig changes
@@ -2665,26 +2763,82 @@ const GenericGrid = ({
2665
2763
  className={styles.filterCell}
2666
2764
  >
2667
2765
  {header.filter ? (
2668
- <input
2669
- type={
2670
- header.filter.type ===
2671
- 'date'
2672
- ? 'date'
2673
- : 'text'
2674
- }
2675
- className={styles.filterInput}
2676
- placeholder={`Filter ${header.label}...`}
2677
- value={
2678
- filterValues[header.key] ||
2679
- ''
2680
- }
2681
- onChange={(e) =>
2682
- handleFilterChange(
2683
- header.key,
2684
- e.target.value
2685
- )
2686
- }
2687
- />
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
+ )
2688
2842
  ) : null}
2689
2843
  </th>
2690
2844
  ))}
@@ -2707,7 +2861,11 @@ const GenericGrid = ({
2707
2861
  {/* Use gridHeaders if available, otherwise fall back to columns */}
2708
2862
  {gridHeaders.length > 0
2709
2863
  ? gridHeaders.map(
2710
- (header, colIndex) => (
2864
+ (header, colIndex) => {
2865
+ const cellClasses = getColumnCellClasses(header);
2866
+ const cellStyles = getColumnCellStyles(header);
2867
+
2868
+ return (
2711
2869
  <td
2712
2870
  key={`cell-${rowIndex}-${colIndex}`}
2713
2871
  className={`${
@@ -2724,7 +2882,8 @@ const GenericGrid = ({
2724
2882
  )
2725
2883
  ? styles.clickableCell
2726
2884
  : ''
2727
- }`}
2885
+ } ${cellClasses.join(' ')}`}
2886
+ style={cellStyles}
2728
2887
  onClick={
2729
2888
  header.onClick &&
2730
2889
  !quarterCellHasContent(
@@ -2749,12 +2908,8 @@ const GenericGrid = ({
2749
2908
  row
2750
2909
  ) ? (
2751
2910
  header.onClick ? (
2752
- <span
2753
- className={
2754
- styles.cellWithValue
2755
- }
2756
- >
2757
- {formatCellContent(
2911
+ <CellContentWithTooltip
2912
+ content={formatCellContent(
2758
2913
  row[
2759
2914
  header
2760
2915
  .key
@@ -2773,24 +2928,12 @@ const GenericGrid = ({
2773
2928
  },
2774
2929
  row
2775
2930
  )}
2776
- </span>
2931
+ header={header}
2932
+ className={styles.cellWithValue}
2933
+ />
2777
2934
  ) : (
2778
- <span
2779
- className={
2780
- styles.cellContent
2781
- }
2782
- data-type={
2783
- header.type ||
2784
- (typeof row[
2785
- header
2786
- .key
2787
- ] ===
2788
- 'number'
2789
- ? 'number'
2790
- : '')
2791
- }
2792
- >
2793
- {formatCellContent(
2935
+ <CellContentWithTooltip
2936
+ content={formatCellContent(
2794
2937
  row[
2795
2938
  header
2796
2939
  .key
@@ -2809,7 +2952,9 @@ const GenericGrid = ({
2809
2952
  },
2810
2953
  row
2811
2954
  )}
2812
- </span>
2955
+ header={header}
2956
+ className={styles.cellContent}
2957
+ />
2813
2958
  )
2814
2959
  ) : header.onClick &&
2815
2960
  isQuarterCellClickable(
@@ -2829,7 +2974,8 @@ const GenericGrid = ({
2829
2974
  ''
2830
2975
  )}
2831
2976
  </td>
2832
- )
2977
+ );
2978
+ }
2833
2979
  )
2834
2980
  : columns.map((col, colIndex) => (
2835
2981
  <td
@@ -616,6 +616,10 @@
616
616
  .cellContent {
617
617
  display: inline-block;
618
618
  width: 100%;
619
+ overflow: hidden;
620
+ text-overflow: ellipsis;
621
+ white-space: nowrap;
622
+ max-width: 200px;
619
623
 
620
624
  /* Ensure date fields have uniform width */
621
625
  &[data-type='date'] {
@@ -624,6 +628,13 @@
624
628
  max-width: 120px;
625
629
  display: inline-block;
626
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
+ }
627
638
  }
628
639
 
629
640
  /* Date with contacts display */
@@ -95,7 +95,9 @@ export const getColumnProperties = (columnKey, metadata) => {
95
95
  virtual: columnMeta.virtual === true,
96
96
  computed: columnMeta.computed === true,
97
97
  searchable: columnMeta.searchable !== false,
98
- filterable: columnMeta.filterable !== false
98
+ filterable: columnMeta.filterable !== false,
99
+ width: columnMeta.width || null,
100
+ truncate: columnMeta.truncate === true
99
101
  };
100
102
  };
101
103
 
@@ -136,6 +138,8 @@ export const mergeColumnsWithMetadata = (columns, metadata) => {
136
138
  description: columnProperties.description,
137
139
  searchable: columnProperties.searchable,
138
140
  filterable: columnProperties.filterable,
141
+ width: columnProperties.width,
142
+ truncate: columnProperties.truncate,
139
143
  // Keep original metadata for reference
140
144
  _metadata: metadata[columnKey] || {}
141
145
  };
@@ -185,6 +189,8 @@ export const processGridHeaders = (response) => {
185
189
  description: properties.description,
186
190
  searchable: properties.searchable,
187
191
  filterable: properties.filterable,
192
+ width: properties.width,
193
+ truncate: properties.truncate,
188
194
  // Keep original metadata for reference
189
195
  _metadata: metadata[key] || {}
190
196
  };
@@ -218,6 +224,10 @@ export const getColumnHeaderClasses = (header) => {
218
224
  classes.push(`column-type-${header.displayType}`);
219
225
  }
220
226
 
227
+ if (header.truncate) {
228
+ classes.push('truncate-text');
229
+ }
230
+
221
231
  return classes;
222
232
  };
223
233
 
@@ -237,6 +247,12 @@ export const getColumnHeaderStyles = (header) => {
237
247
  styles.cursor = 'pointer';
238
248
  }
239
249
 
250
+ if (header.width) {
251
+ styles.width = header.width;
252
+ styles.minWidth = header.width;
253
+ styles.maxWidth = header.width;
254
+ }
255
+
240
256
  return styles;
241
257
  };
242
258
 
@@ -264,6 +280,40 @@ export const getColumnHeaderTooltip = (header) => {
264
280
  return null;
265
281
  };
266
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
+
267
317
  export default {
268
318
  extractColumnsMetadata,
269
319
  isColumnSortable,
@@ -273,5 +323,7 @@ export default {
273
323
  processGridHeaders,
274
324
  getColumnHeaderClasses,
275
325
  getColumnHeaderStyles,
276
- getColumnHeaderTooltip
326
+ getColumnHeaderTooltip,
327
+ getColumnCellClasses,
328
+ getColumnCellStyles
277
329
  };