@visns-studio/visns-components 5.4.3 → 5.4.5

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "@emotion/is-prop-valid": "^1.3.1",
4
- "@fontsource/barlow": "^5.1.1",
4
+ "@fontsource/barlow": "^5.2.5",
5
5
  "@inovua/reactdatagrid-community": "^5.10.2",
6
6
  "@inovua/reactdatagrid-enterprise": "^5.10.2",
7
7
  "@nivo/bar": "^0.88.0",
@@ -78,7 +78,7 @@
78
78
  "react-dom": "^17.0.0 || ^18.0.0"
79
79
  },
80
80
  "name": "@visns-studio/visns-components",
81
- "version": "5.4.3",
81
+ "version": "5.4.5",
82
82
  "description": "Various packages to assist in the development of our Custom Applications.",
83
83
  "main": "src/index.js",
84
84
  "files": [
@@ -23,8 +23,9 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
23
23
  const swapy = useRef(null);
24
24
  const container = useRef(null);
25
25
  const navigate = useNavigate();
26
+ const initialLoadRef = useRef(true); // Ref to track initial load
26
27
  const [dashboardSetting, setDashboardSetting] = useState(
27
- userProfile.dashboard_setting || setting
28
+ dynamicDashboard ? userProfile.dashboard_setting || setting : setting
28
29
  );
29
30
  const [data, setData] = useState({});
30
31
  const [dropdowns, setDropdowns] = useState({});
@@ -84,6 +85,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
84
85
  try {
85
86
  const promises = dashboardSetting.widgets.map(async (widget) => {
86
87
  const widgetFilters = appliedFilters[widget.id] || {};
88
+
87
89
  if (widget.filters) {
88
90
  for (const filter of widget.filters) {
89
91
  if (filter.url) {
@@ -110,12 +112,12 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
110
112
  }
111
113
  }
112
114
  }
113
-
114
115
  if (widget.api?.url && widget.api?.method) {
115
116
  const requestParams = {
116
- ...widgetFilters,
117
117
  ...(widget.api.params || {}),
118
+ ...widgetFilters,
118
119
  };
120
+
119
121
  return CustomFetch(
120
122
  widget.api.url,
121
123
  widget.api.method,
@@ -124,11 +126,21 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
124
126
  }
125
127
  return null;
126
128
  });
129
+
127
130
  const results = await Promise.all(promises);
131
+
128
132
  const fetchedData = dashboardSetting.widgets.reduce(
129
133
  (acc, widget, index) => {
130
134
  if (results[index]) {
131
- acc[widget.id] = results[index]?.data;
135
+ // Check if the response indicates an error
136
+ if (results[index]?.data?.value) {
137
+ console.error(
138
+ `API error for widget ${widget.id}:`,
139
+ results[index].data.value
140
+ );
141
+ } else {
142
+ acc[widget.id] = results[index]?.data;
143
+ }
132
144
  }
133
145
  return acc;
134
146
  },
@@ -140,19 +152,41 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
140
152
  }
141
153
  };
142
154
 
155
+ // Single useEffect for initial data fetch
143
156
  useEffect(() => {
144
- // Check if any widget has a "filters" property.
145
- const widgetHasFilters = dashboardSetting?.widgets?.some((widget) =>
146
- widget.hasOwnProperty('filters')
147
- );
157
+ // This will run once after filters are initialized
158
+ if (Object.keys(filters).length > 0 && initialLoadRef.current) {
159
+ // Fetch data once with initial filters
160
+ fetchData(filters);
161
+ // Mark initial load as complete
162
+ initialLoadRef.current = false;
163
+ }
164
+ }, [filters]);
165
+
166
+ // Handle filter changes after initial load
167
+ const isInitialFilterChangeRef = useRef(true);
148
168
 
149
- // If a widget has filters and our filters object is empty, do nothing.
150
- if (widgetHasFilters && Object.keys(filters).length === 0) {
169
+ useEffect(() => {
170
+ // Skip the first filter change (which happens during initialization)
171
+ if (isInitialFilterChangeRef.current) {
172
+ isInitialFilterChangeRef.current = false;
151
173
  return;
152
174
  }
153
175
 
154
- fetchData(filters);
155
- }, [filters, dashboardSetting]);
176
+ // Only fetch data if filters have been initialized
177
+ if (!initialLoadRef.current && Object.keys(filters).length > 0) {
178
+ fetchData(filters);
179
+ }
180
+ }, [filters]);
181
+
182
+ // Handle dashboardSetting changes
183
+ useEffect(() => {
184
+ // Skip the initial render
185
+ if (!initialLoadRef.current) {
186
+ // Only fetch data if it's not the initial load
187
+ fetchData(filters);
188
+ }
189
+ }, [dashboardSetting]);
156
190
 
157
191
  const openModal = (widgetId) => {
158
192
  setModalContent(widgetId);
@@ -453,10 +487,12 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
453
487
  <button
454
488
  className={`${styles.btn} ${styles['btn-secondary']}`}
455
489
  onClick={() => {
490
+ // Clear filters
456
491
  setFilters((prevFilters) => ({
457
492
  ...prevFilters,
458
493
  [widget.id]: {},
459
494
  }));
495
+
460
496
  fetchData();
461
497
  }}
462
498
  >
@@ -470,11 +506,22 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
470
506
 
471
507
  const renderWidget = (widget) => {
472
508
  const widgetData = data[widget.id] || [];
473
- if (
474
- widgetData.length > 0 ||
475
- widgetData?.data?.length > 0 ||
476
- widgetData
477
- ) {
509
+
510
+ // More detailed check for valid data
511
+ const hasData =
512
+ // Array with items
513
+ (Array.isArray(widgetData) && widgetData.length > 0) ||
514
+ // Object with data array
515
+ (widgetData?.data &&
516
+ Array.isArray(widgetData.data) &&
517
+ widgetData.data.length > 0) ||
518
+ // Simple value for counter widgets
519
+ (widget.type === 'counter' &&
520
+ widgetData !== null &&
521
+ widgetData !== undefined);
522
+
523
+ if (hasData || widget.type === 'bar') {
524
+ // Always try to render bar charts for debugging
478
525
  switch (widget.type) {
479
526
  case 'counter':
480
527
  return (
@@ -524,8 +571,33 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
524
571
  let colors = {};
525
572
  let keys = [];
526
573
 
527
- // Handle new data structure (from your first example)
574
+ // Handle specific structure with bar.data property
528
575
  if (
576
+ widgetData.bar &&
577
+ widgetData.bar.data &&
578
+ Array.isArray(widgetData.bar.data)
579
+ ) {
580
+ barData = widgetData.bar.data;
581
+
582
+ // Use the keys from the data
583
+ if (widgetData.keys && Array.isArray(widgetData.keys)) {
584
+ keys = widgetData.keys;
585
+ } else if (barData[0]) {
586
+ // Extract keys from the first item, excluding indexBy field (usually 'supervisor')
587
+ const indexField =
588
+ widget.props.indexBy || 'supervisor';
589
+ keys = Object.keys(barData[0] || {}).filter(
590
+ (key) => key !== indexField
591
+ );
592
+ }
593
+
594
+ // Set the indexBy field based on the data structure
595
+ if (barData[0] && barData[0].supervisor) {
596
+ widget.props.indexBy = 'supervisor';
597
+ }
598
+ }
599
+ // Handle new data structure (from your first example)
600
+ else if (
529
601
  widgetData.data &&
530
602
  Array.isArray(widgetData.data) &&
531
603
  widgetData.colors
@@ -533,9 +605,13 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
533
605
  barData = widgetData.data;
534
606
  colors = widgetData.colors;
535
607
  // Extract keys from the first data item, excluding 'month' or any other index field
536
- keys = Object.keys(barData[0] || {}).filter(
537
- (key) => key !== 'month'
538
- );
608
+ if (barData[0]) {
609
+ keys = Object.keys(barData[0] || {}).filter(
610
+ (key) => key !== 'month'
611
+ );
612
+ } else {
613
+ console.log(`No data items found in barData`);
614
+ }
539
615
  }
540
616
  // Handle newer data structure (from your second example)
541
617
  else if (
@@ -545,6 +621,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
545
621
  ) {
546
622
  barData = widgetData.data;
547
623
  keys = widgetData.keys;
624
+
548
625
  // Extract colors from the first data item
549
626
  if (barData[0]) {
550
627
  keys.forEach((key) => {
@@ -555,25 +632,103 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
555
632
  });
556
633
  }
557
634
  }
635
+ // Handle array of arrays structure
636
+ else if (
637
+ Array.isArray(widgetData) &&
638
+ widgetData.length > 0 &&
639
+ Array.isArray(widgetData[0])
640
+ ) {
641
+ barData = widgetData[0];
642
+ // Try to extract keys from the first item
643
+ if (barData[0]) {
644
+ keys = Object.keys(barData[0] || {}).filter(
645
+ (key) => key !== 'month'
646
+ );
647
+ }
648
+ }
558
649
  // Handle legacy data structure
559
650
  else {
560
651
  barData =
561
652
  widgetData.bar?.data ||
562
653
  widgetData.data ||
563
654
  widgetData;
655
+
656
+ // Check if barData is an array of arrays and use the first one
657
+ if (
658
+ Array.isArray(barData) &&
659
+ barData.length > 0 &&
660
+ Array.isArray(barData[0])
661
+ ) {
662
+ barData = barData[0];
663
+ }
664
+
564
665
  if (widgetData.keys) {
565
- keys = widgetData.keys;
666
+ keys = Array.isArray(widgetData.keys)
667
+ ? widgetData.keys
668
+ : [];
669
+ } else if (barData && barData[0]) {
670
+ // Try to extract keys from the first item
671
+ keys = Object.keys(barData[0] || {}).filter(
672
+ (key) => key !== 'month'
673
+ );
566
674
  }
567
675
  }
568
676
 
569
- // Set up colors if they exist
570
- if (Object.keys(colors).length > 0) {
677
+ // Always use static colors from props if available
678
+ // First check if we have stored original colors
679
+ if (
680
+ widget.props._originalColors &&
681
+ Array.isArray(widget.props._originalColors)
682
+ ) {
683
+ // Use the stored original colors
684
+ const originalColors = widget.props._originalColors;
685
+
686
+ // Create a direct mapping of keys to colors
687
+ const colorMap = {};
688
+ if (Array.isArray(keys)) {
689
+ keys.forEach((key, index) => {
690
+ colorMap[key] =
691
+ originalColors[
692
+ index % originalColors.length
693
+ ];
694
+ });
695
+ }
696
+ }
697
+ // Otherwise use the colors from the props
698
+ else if (
699
+ widget.props.colors &&
700
+ Array.isArray(widget.props.colors) &&
701
+ widget.props.colors.length > 0
702
+ ) {
703
+ // Store the original colors array
704
+ const originalColors = [...widget.props.colors];
705
+
706
+ // Create a direct mapping of keys to colors
707
+ const colorMap = {};
708
+ if (Array.isArray(keys)) {
709
+ keys.forEach((key, index) => {
710
+ colorMap[key] =
711
+ originalColors[
712
+ index % originalColors.length
713
+ ];
714
+ });
715
+ }
716
+
717
+ // Store the original colors array in a separate property
718
+ // This ensures it's preserved for future renders
719
+ widget.props._originalColors = originalColors;
720
+ }
721
+ // Fallback to API colors if no static colors in props
722
+ else if (Object.keys(colors).length > 0) {
571
723
  widget.props.colors = ({ id }) => colors[id];
572
724
  }
573
725
 
574
726
  // Set keys in props if they exist
575
- if (keys.length > 0) {
727
+ if (Array.isArray(keys) && keys.length > 0) {
576
728
  widget.props.keys = keys;
729
+ } else {
730
+ // Set default keys if none are found
731
+ widget.props.keys = ['value'];
577
732
  }
578
733
 
579
734
  // Handle legend setup
@@ -604,19 +759,60 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
604
759
  widget.props.tooltip = CustomTooltip;
605
760
  }
606
761
 
607
- if (barData?.length > 0) {
762
+ // Ensure barData is an array and has items
763
+ if (Array.isArray(barData) && barData.length > 0) {
764
+ // Make sure we have an indexBy field in each data item
765
+ const indexField = widget.props.indexBy || 'month';
766
+ const hasIndexField = barData.every(
767
+ (item) =>
768
+ item &&
769
+ typeof item === 'object' &&
770
+ indexField in item
771
+ );
772
+
773
+ if (!hasIndexField) {
774
+ console.warn(
775
+ `Bar data items missing required index field '${indexField}'`
776
+ );
777
+ // Add a default index if missing
778
+ barData = barData.map((item, i) => ({
779
+ ...item,
780
+ [indexField]:
781
+ item[indexField] || `Item ${i + 1}`,
782
+ }));
783
+ }
784
+
608
785
  return (
609
786
  <div style={{ height: widget.height || '600px' }}>
610
787
  <ResponsiveBar
611
788
  key={`${widget.id}-${widget.size}`}
612
789
  data={barData}
790
+ indexBy={indexField}
791
+ {...widget.props}
792
+ />
793
+ </div>
794
+ );
795
+ } else {
796
+ // Create dummy data if no data is available
797
+ const dummyData = [
798
+ {
799
+ month: 'No Data',
800
+ value: 0,
801
+ },
802
+ ];
803
+
804
+ return (
805
+ <div style={{ height: widget.height || '600px' }}>
806
+ <ResponsiveBar
807
+ key={`${widget.id}-${widget.size}`}
808
+ data={dummyData}
613
809
  indexBy="month"
810
+ keys={['value']}
614
811
  {...widget.props}
615
812
  />
616
813
  </div>
617
814
  );
618
815
  }
619
- break;
620
816
  }
621
817
  case 'line':
622
818
  if (widgetData.length > 0) {
@@ -1070,10 +1070,14 @@ function GenericDetail({
1070
1070
  case 'dropzone':
1071
1071
  const downloadFile = (file) => {
1072
1072
  const { id, file_url, file_name } = file;
1073
+ const toastId = toast.loading(
1074
+ 'Downloading file...'
1075
+ );
1073
1076
 
1074
1077
  // If file_url exists, use it for the download, else use the id to download
1075
1078
  const urlToDownload =
1076
- file_url || `/ajax/files/download/${id}`;
1079
+ file_url ||
1080
+ `/ajax/files/downloadContent/${id}`;
1077
1081
 
1078
1082
  // Use fetch to retrieve the file as a blob
1079
1083
  fetch(urlToDownload, {
@@ -1083,19 +1087,129 @@ function GenericDetail({
1083
1087
  'application/octet-stream', // Specify content type as binary
1084
1088
  },
1085
1089
  })
1086
- .then((response) => response.blob()) // Convert response to Blob
1090
+ .then(async (response) => {
1091
+ if (!response.ok) {
1092
+ // Try to extract error message from response if possible
1093
+ // Clone the response to read it as JSON first
1094
+ const clonedResponse =
1095
+ response.clone();
1096
+
1097
+ try {
1098
+ // First try to parse as JSON
1099
+ const errorData =
1100
+ await clonedResponse.json();
1101
+
1102
+ if (
1103
+ errorData &&
1104
+ errorData.error
1105
+ ) {
1106
+ throw new Error(
1107
+ errorData.error
1108
+ );
1109
+ } else if (
1110
+ errorData &&
1111
+ errorData.message
1112
+ ) {
1113
+ throw new Error(
1114
+ errorData.message
1115
+ );
1116
+ } else if (
1117
+ errorData &&
1118
+ errorData.value
1119
+ ) {
1120
+ throw new Error(
1121
+ errorData.value
1122
+ );
1123
+ } else {
1124
+ // JSON parsed but no known error fields
1125
+ throw new Error(
1126
+ `HTTP error! Status: ${response.status}`
1127
+ );
1128
+ }
1129
+ } catch (jsonError) {
1130
+ // If we can't parse as JSON, try to get text
1131
+ try {
1132
+ const textResponse =
1133
+ response.clone();
1134
+ const errorText =
1135
+ await textResponse.text();
1136
+
1137
+ // Check if the text looks like JSON but wasn't parsed correctly
1138
+ if (
1139
+ errorText.includes(
1140
+ '"error":'
1141
+ )
1142
+ ) {
1143
+ // Try to extract the error message with regex
1144
+ const errorMatch =
1145
+ errorText.match(
1146
+ /"error":"([^"]+)"/i
1147
+ );
1148
+ if (
1149
+ errorMatch &&
1150
+ errorMatch[1]
1151
+ ) {
1152
+ throw new Error(
1153
+ errorMatch[1]
1154
+ );
1155
+ }
1156
+ }
1157
+
1158
+ // If we got text but couldn't extract a specific error
1159
+ if (
1160
+ errorText &&
1161
+ errorText.length < 100
1162
+ ) {
1163
+ throw new Error(
1164
+ errorText
1165
+ );
1166
+ }
1167
+ } catch (textError) {
1168
+ // If all else fails, fall back to status
1169
+ throw new Error(
1170
+ `HTTP error! Status: ${response.status}`
1171
+ );
1172
+ }
1173
+ }
1174
+ }
1175
+ return response.blob(); // Convert response to Blob
1176
+ })
1087
1177
  .then((blob) => {
1178
+ // Validate that we have a valid blob with content
1179
+ if (!blob || blob.size === 0) {
1180
+ throw new Error(
1181
+ 'Downloaded file is empty or invalid'
1182
+ );
1183
+ }
1184
+
1088
1185
  // Use FileSaver to download the file with the specified name
1089
1186
  saveAs(blob, file_name);
1187
+
1188
+ toast.update(toastId, {
1189
+ render: 'Download successful!',
1190
+ type: 'success',
1191
+ isLoading: false,
1192
+ autoClose: 5000,
1193
+ closeButton: true,
1194
+ });
1090
1195
  })
1091
1196
  .catch((error) => {
1092
1197
  console.error(
1093
1198
  'Error downloading the file:',
1094
1199
  error
1095
1200
  );
1201
+
1202
+ toast.update(toastId, {
1203
+ render: `Error downloading file: ${
1204
+ error.message || 'Unknown error'
1205
+ }`,
1206
+ type: 'error',
1207
+ isLoading: false,
1208
+ autoClose: 5000,
1209
+ closeButton: true,
1210
+ });
1096
1211
  });
1097
1212
  };
1098
-
1099
1213
  const deleteFile = (e, id, name) => {
1100
1214
  if (e) {
1101
1215
  e.stopPropagation();
@@ -434,42 +434,109 @@ const GenericDynamic = ({
434
434
 
435
435
  const handleCustomButton = async (s) => {
436
436
  let payload = {};
437
- switch (s.type) {
438
- case 'save':
439
- if (s.payload?.length > 0) {
440
- s.payload.forEach((p) => {
441
- if (data[p]) {
442
- payload[p] = data[p];
443
- }
444
- });
445
- }
437
+ let toastId;
446
438
 
447
- const resSave = await Download(s.url, s.method, payload);
439
+ try {
440
+ switch (s.type) {
441
+ case 'save':
442
+ toastId = toast.loading('Downloading file...');
443
+
444
+ if (s.payload?.length > 0) {
445
+ s.payload.forEach((p) => {
446
+ if (data[p]) {
447
+ payload[p] = data[p];
448
+ }
449
+ });
450
+ }
448
451
 
449
- if (resSave.data) {
450
- saveAs(resSave.data);
451
- }
452
- break;
453
- case 'fetch':
454
- if (s.payload?.length > 0) {
455
- s.payload.forEach((p) => {
456
- if (data[p]) {
457
- payload[p] = data[p];
452
+ const resSave = await Download(s.url, s.method, payload);
453
+
454
+ if (resSave.data) {
455
+ // Check if response has a value property which indicates an error
456
+ if (resSave.data.value) {
457
+ toast.update(toastId, {
458
+ render: `Error: ${resSave.data.value}`,
459
+ type: 'error',
460
+ isLoading: false,
461
+ autoClose: 5000,
462
+ closeButton: true,
463
+ });
464
+ } else {
465
+ saveAs(resSave.data);
466
+ toast.update(toastId, {
467
+ render: 'Download successful!',
468
+ type: 'success',
469
+ isLoading: false,
470
+ autoClose: 5000,
471
+ closeButton: true,
472
+ });
458
473
  }
459
- });
460
- }
474
+ } else {
475
+ toast.update(toastId, {
476
+ render: 'Error: No data received',
477
+ type: 'error',
478
+ isLoading: false,
479
+ autoClose: 5000,
480
+ closeButton: true,
481
+ });
482
+ }
483
+ break;
484
+
485
+ case 'fetch':
486
+ toastId = toast.loading('Fetching data...');
461
487
 
462
- const resFetch = await CustomFetch(s.url, s.method, payload);
488
+ if (s.payload?.length > 0) {
489
+ s.payload.forEach((p) => {
490
+ if (data[p]) {
491
+ payload[p] = data[p];
492
+ }
493
+ });
494
+ }
463
495
 
464
- if (resFetch.data.error === '') {
465
- toast.success(
466
- s.message ? s.message : 'Form fetched successfully'
496
+ const resFetch = await CustomFetch(
497
+ s.url,
498
+ s.method,
499
+ payload
467
500
  );
468
- }
469
- break;
470
- default:
471
- console.info(s, 'default');
472
- break;
501
+
502
+ if (resFetch.data.error === '') {
503
+ toast.update(toastId, {
504
+ render: s.message
505
+ ? s.message
506
+ : 'Form fetched successfully',
507
+ type: 'success',
508
+ isLoading: false,
509
+ autoClose: 5000,
510
+ closeButton: true,
511
+ });
512
+ } else {
513
+ toast.update(toastId, {
514
+ render: `Error: ${resFetch.data.error}`,
515
+ type: 'error',
516
+ isLoading: false,
517
+ autoClose: 5000,
518
+ closeButton: true,
519
+ });
520
+ }
521
+ break;
522
+
523
+ default:
524
+ console.info(s, 'default');
525
+ break;
526
+ }
527
+ } catch (error) {
528
+ console.error('Error in handleCustomButton:', error);
529
+ if (toastId) {
530
+ toast.update(toastId, {
531
+ render: `Error: ${error.message || 'Unknown error'}`,
532
+ type: 'error',
533
+ isLoading: false,
534
+ autoClose: 5000,
535
+ closeButton: true,
536
+ });
537
+ } else {
538
+ toast.error(`Error: ${error.message || 'Unknown error'}`);
539
+ }
473
540
  }
474
541
  };
475
542