@visns-studio/visns-components 5.6.3 → 5.6.4

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
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^17.0.0 || ^18.0.0"
85
85
  },
86
86
  "name": "@visns-studio/visns-components",
87
- "version": "5.6.3",
87
+ "version": "5.6.4",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -35,28 +35,64 @@ const CustomDownload = (
35
35
  }
36
36
  }
37
37
 
38
- options = {
39
- method: method,
40
- data: formData,
41
- headers: headers,
42
- url: url,
43
- responseType: 'blob',
44
- };
45
-
38
+ // First make a request to check if the request is valid
46
39
  return trackPromise(
47
40
  new Promise((resolve, reject) => {
48
- axios(options)
41
+ // First, make a request without responseType: 'blob' to check for errors
42
+ axios({
43
+ method: method,
44
+ data: formData,
45
+ headers: headers,
46
+ url: url,
47
+ })
49
48
  .then((response) => {
50
- if (successCallback) {
51
- successCallback(response.data);
52
- } else {
53
- const error = response.data.error;
49
+ // If the response is JSON and contains an error, handle it
50
+ if (response.data && response.data.success === false) {
51
+ let errorMessage =
52
+ response.data.message || 'Unknown error';
53
+ if (response.data.error) {
54
+ errorMessage += ': ' + response.data.error;
55
+ }
54
56
 
55
- if (error && error !== '') {
56
- toast.error(<div>{parse(error)}</div>);
57
+ if (errorCallback) {
58
+ errorCallback(errorMessage);
59
+ } else {
60
+ toast.error(<div>{parse(errorMessage)}</div>);
57
61
  }
62
+
63
+ // Resolve with the error response
64
+ resolve(response);
65
+ } else {
66
+ // If no error, make the actual blob request
67
+ axios({
68
+ method: method,
69
+ data: formData,
70
+ headers: headers,
71
+ url: url,
72
+ responseType: 'blob',
73
+ })
74
+ .then((blobResponse) => {
75
+ if (successCallback) {
76
+ successCallback(blobResponse.data);
77
+ }
78
+ resolve(blobResponse);
79
+ })
80
+ .catch((blobError) => {
81
+ let errorMessage =
82
+ blobError.message ||
83
+ 'Error downloading file';
84
+
85
+ if (errorCallback) {
86
+ errorCallback(errorMessage);
87
+ } else {
88
+ toast.error(
89
+ <div>{parse(errorMessage)}</div>
90
+ );
91
+ }
92
+
93
+ reject(blobError);
94
+ });
58
95
  }
59
- resolve(response);
60
96
  })
61
97
  .catch((error) => {
62
98
  let errorMessage = '';
@@ -177,6 +177,96 @@ const isJsonValue = (value) => {
177
177
  return typeof value === 'object' && value !== null;
178
178
  };
179
179
 
180
+ // Helper function to validate filter criteria
181
+ const validateFilterCriteria = (filterCriteria) => {
182
+ // If it's already a flat array (legacy format)
183
+ if (Array.isArray(filterCriteria)) {
184
+ // Check each filter in the array
185
+ for (let i = 0; i < filterCriteria.length; i++) {
186
+ const filter = filterCriteria[i];
187
+
188
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
189
+ if (
190
+ filter.operator === 'IS NULL' ||
191
+ filter.operator === 'IS NOT NULL'
192
+ ) {
193
+ continue;
194
+ }
195
+
196
+ // Check if any required field is missing
197
+ if (
198
+ !filter.table ||
199
+ !filter.column ||
200
+ !filter.operator ||
201
+ filter.value === undefined ||
202
+ filter.value === null ||
203
+ filter.value === ''
204
+ ) {
205
+ return {
206
+ valid: false,
207
+ message: `Filter #${
208
+ i + 1
209
+ } is missing required fields (table, column, operator, or value)`,
210
+ };
211
+ }
212
+ }
213
+ return { valid: true };
214
+ }
215
+
216
+ // If it's the new structure with groups and operators
217
+ if (filterCriteria.groups && Array.isArray(filterCriteria.groups)) {
218
+ // Process each group
219
+ for (
220
+ let groupIndex = 0;
221
+ groupIndex < filterCriteria.groups.length;
222
+ groupIndex++
223
+ ) {
224
+ const group = filterCriteria.groups[groupIndex];
225
+
226
+ // Only validate non-empty groups
227
+ if (group.filters && group.filters.length > 0) {
228
+ // Check all filters in this group
229
+ for (
230
+ let filterIndex = 0;
231
+ filterIndex < group.filters.length;
232
+ filterIndex++
233
+ ) {
234
+ const filter = group.filters[filterIndex];
235
+
236
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
237
+ if (
238
+ filter.operator === 'IS NULL' ||
239
+ filter.operator === 'IS NOT NULL'
240
+ ) {
241
+ continue;
242
+ }
243
+
244
+ // Check if any required field is missing
245
+ if (
246
+ !filter.table ||
247
+ !filter.column ||
248
+ !filter.operator ||
249
+ filter.value === undefined ||
250
+ filter.value === null ||
251
+ filter.value === ''
252
+ ) {
253
+ return {
254
+ valid: false,
255
+ message: `Filter #${filterIndex + 1} in group #${
256
+ groupIndex + 1
257
+ } is missing required fields (table, column, operator, or value)`,
258
+ };
259
+ }
260
+ }
261
+ }
262
+ }
263
+ return { valid: true };
264
+ }
265
+
266
+ // If it's neither a flat array nor the new structure, consider it valid (empty)
267
+ return { valid: true };
268
+ };
269
+
180
270
  // Helper function to convert the enhanced filter structure to a flat array for the backend
181
271
  const flattenFilterCriteria = (filterCriteria) => {
182
272
  // If it's already a flat array (legacy format), return it as is
@@ -750,9 +840,40 @@ const GenericReport = ({ setting = {} }) => {
750
840
  setSelectedColumns(updatedColumns);
751
841
  };
752
842
 
843
+ // Start editing a column name
844
+ const handleStartEditColumn = (index) => {
845
+ const column = selectedColumns[index];
846
+ setEditingColumnIndex(index);
847
+ setEditingColumnName(column.alias || column.displayName);
848
+ };
849
+
850
+ // Save the edited column name
851
+ const handleSaveColumnName = () => {
852
+ if (editingColumnIndex !== null) {
853
+ const updatedColumns = [...selectedColumns];
854
+ updatedColumns[editingColumnIndex] = {
855
+ ...updatedColumns[editingColumnIndex],
856
+ alias: editingColumnName,
857
+ };
858
+ setSelectedColumns(updatedColumns);
859
+ setEditingColumnIndex(null);
860
+ setEditingColumnName('');
861
+ }
862
+ };
863
+
864
+ // Cancel column name editing
865
+ const handleCancelEditColumn = () => {
866
+ setEditingColumnIndex(null);
867
+ setEditingColumnName('');
868
+ };
869
+
753
870
  // State for drag and drop
754
871
  const [draggedItemIndex, setDraggedItemIndex] = useState(null);
755
872
 
873
+ // State for column renaming
874
+ const [editingColumnIndex, setEditingColumnIndex] = useState(null);
875
+ const [editingColumnName, setEditingColumnName] = useState('');
876
+
756
877
  // Handle drag start for column reordering
757
878
  const handleDragStart = (e, index) => {
758
879
  setDraggedItemIndex(index);
@@ -1336,7 +1457,7 @@ const GenericReport = ({ setting = {} }) => {
1336
1457
  }
1337
1458
  };
1338
1459
 
1339
- // Export report as CSV or Excel
1460
+ // Export report as CSV, Excel or PDF
1340
1461
  const handleExportReport = async (format = 'csv') => {
1341
1462
  if (selectedColumns.length === 0) {
1342
1463
  toast.error('Please select at least one column for your report');
@@ -1348,6 +1469,15 @@ const GenericReport = ({ setting = {} }) => {
1348
1469
  return;
1349
1470
  }
1350
1471
 
1472
+ // Validate filter criteria
1473
+ if (filterCriteria) {
1474
+ const validationResult = validateFilterCriteria(filterCriteria);
1475
+ if (!validationResult.valid) {
1476
+ toast.error(validationResult.message);
1477
+ return;
1478
+ }
1479
+ }
1480
+
1351
1481
  setIsLoading(true);
1352
1482
  const toastId = toast.loading(
1353
1483
  `Exporting report as ${format.toUpperCase()}...`
@@ -1368,8 +1498,8 @@ const GenericReport = ({ setting = {} }) => {
1368
1498
  sourceColumn: join.sourceColumn,
1369
1499
  targetColumn: join.targetColumn,
1370
1500
  })),
1371
- filters: [],
1372
- sorting: [],
1501
+ filters: flattenFilterCriteria(filterCriteria),
1502
+ sorting: sortCriteria,
1373
1503
  };
1374
1504
 
1375
1505
  // Prepare export request data
@@ -1395,16 +1525,66 @@ const GenericReport = ({ setting = {} }) => {
1395
1525
  const date = moment().format('YYYYMMDD');
1396
1526
  const fileName = `${date}_${reportName}.${format}`;
1397
1527
 
1398
- // Use FileSaver.js to save the file
1399
- saveAs(res.data, fileName);
1528
+ // For PDF (HTML) format, open in a new window for printing
1529
+ if (format === 'pdf') {
1530
+ // Convert blob to URL
1531
+ const blob = new Blob([res.data], { type: 'text/html' });
1532
+ const url = URL.createObjectURL(blob);
1533
+
1534
+ // Open in a new window
1535
+ const printWindow = window.open(url, '_blank');
1536
+
1537
+ // Add print instructions
1538
+ if (printWindow) {
1539
+ printWindow.onload = function () {
1540
+ // Add print button at the top
1541
+ const printButton =
1542
+ printWindow.document.createElement('button');
1543
+ printButton.innerHTML = 'Print PDF';
1544
+ printButton.style.position = 'fixed';
1545
+ printButton.style.top = '10px';
1546
+ printButton.style.right = '10px';
1547
+ printButton.style.zIndex = '9999';
1548
+ printButton.style.padding = '8px 16px';
1549
+ printButton.style.backgroundColor = '#4CAF50';
1550
+ printButton.style.color = 'white';
1551
+ printButton.style.border = 'none';
1552
+ printButton.style.borderRadius = '4px';
1553
+ printButton.style.cursor = 'pointer';
1554
+
1555
+ printButton.onclick = function () {
1556
+ // Hide the button before printing
1557
+ this.style.display = 'none';
1558
+ printWindow.print();
1559
+ this.style.display = 'block';
1560
+ };
1400
1561
 
1401
- toast.update(toastId, {
1402
- render: `Report exported successfully as ${format.toUpperCase()}`,
1403
- type: 'success',
1404
- isLoading: false,
1405
- autoClose: 5000,
1406
- closeButton: true,
1407
- });
1562
+ printWindow.document.body.insertBefore(
1563
+ printButton,
1564
+ printWindow.document.body.firstChild
1565
+ );
1566
+ };
1567
+ }
1568
+
1569
+ toast.update(toastId, {
1570
+ render: `Report opened in a new window. Use browser's print function to save as PDF.`,
1571
+ type: 'success',
1572
+ isLoading: false,
1573
+ autoClose: 5000,
1574
+ closeButton: true,
1575
+ });
1576
+ } else {
1577
+ // Use FileSaver.js to save the file for CSV and Excel
1578
+ saveAs(res.data, fileName);
1579
+
1580
+ toast.update(toastId, {
1581
+ render: `Report exported successfully as ${format.toUpperCase()}`,
1582
+ type: 'success',
1583
+ isLoading: false,
1584
+ autoClose: 5000,
1585
+ closeButton: true,
1586
+ });
1587
+ }
1408
1588
  } else {
1409
1589
  throw new Error('Invalid response format');
1410
1590
  }
@@ -1666,6 +1846,15 @@ const GenericReport = ({ setting = {} }) => {
1666
1846
  return;
1667
1847
  }
1668
1848
 
1849
+ // Validate filter criteria
1850
+ if (filterCriteria) {
1851
+ const validationResult = validateFilterCriteria(filterCriteria);
1852
+ if (!validationResult.valid) {
1853
+ toast.error(validationResult.message);
1854
+ return;
1855
+ }
1856
+ }
1857
+
1669
1858
  setIsLoading(true);
1670
1859
  toast.info('Executing query...');
1671
1860
 
@@ -1845,6 +2034,15 @@ const GenericReport = ({ setting = {} }) => {
1845
2034
  return;
1846
2035
  }
1847
2036
 
2037
+ // Validate filter criteria
2038
+ if (filterCriteria) {
2039
+ const validationResult = validateFilterCriteria(filterCriteria);
2040
+ if (!validationResult.valid) {
2041
+ toast.error(validationResult.message);
2042
+ return;
2043
+ }
2044
+ }
2045
+
1848
2046
  setIsLoading(true);
1849
2047
 
1850
2048
  try {
@@ -2248,7 +2446,12 @@ const GenericReport = ({ setting = {} }) => {
2248
2446
  )}
2249
2447
  </div>
2250
2448
 
2251
- <h3>Selected Columns</h3>
2449
+ <div className={styles.columnSectionHeader}>
2450
+ <h3>Selected Columns</h3>
2451
+ <span className={styles.instructionText}>
2452
+ Click the pencil icon (✎) to rename columns
2453
+ </span>
2454
+ </div>
2252
2455
  {selectedColumns.length > 0 ? (
2253
2456
  <div className={styles.selectedColumns}>
2254
2457
  {selectedColumns.map((column, index) => (
@@ -2276,10 +2479,86 @@ const GenericReport = ({ setting = {} }) => {
2276
2479
  >
2277
2480
  ⋮⋮
2278
2481
  </div>
2279
- <span>{column.displayName}</span>
2482
+ <span>
2483
+ {editingColumnIndex ===
2484
+ index ? (
2485
+ <div
2486
+ className={
2487
+ styles.inlineEditContainer
2488
+ }
2489
+ >
2490
+ <input
2491
+ type="text"
2492
+ className={
2493
+ styles.columnNameInput
2494
+ }
2495
+ value={
2496
+ editingColumnName
2497
+ }
2498
+ onChange={(e) =>
2499
+ setEditingColumnName(
2500
+ e.target
2501
+ .value
2502
+ )
2503
+ }
2504
+ autoFocus
2505
+ onKeyDown={(e) => {
2506
+ if (
2507
+ e.key ===
2508
+ 'Enter'
2509
+ ) {
2510
+ handleSaveColumnName();
2511
+ } else if (
2512
+ e.key ===
2513
+ 'Escape'
2514
+ ) {
2515
+ handleCancelEditColumn();
2516
+ }
2517
+ }}
2518
+ />
2519
+ <button
2520
+ className={
2521
+ styles.saveBtn
2522
+ }
2523
+ onClick={
2524
+ handleSaveColumnName
2525
+ }
2526
+ title="Save"
2527
+ >
2528
+
2529
+ </button>
2530
+ <button
2531
+ className={
2532
+ styles.cancelBtn
2533
+ }
2534
+ onClick={
2535
+ handleCancelEditColumn
2536
+ }
2537
+ title="Cancel"
2538
+ >
2539
+
2540
+ </button>
2541
+ </div>
2542
+ ) : (
2543
+ column.alias ||
2544
+ column.displayName
2545
+ )}
2546
+ </span>
2280
2547
  <div
2281
2548
  className={styles.columnActions}
2282
2549
  >
2550
+ <button
2551
+ className={styles.editBtn}
2552
+ onClick={() =>
2553
+ handleStartEditColumn(
2554
+ index
2555
+ )
2556
+ }
2557
+ aria-label="Edit column name"
2558
+ title="Edit name"
2559
+ >
2560
+
2561
+ </button>
2283
2562
  <button
2284
2563
  className={styles.removeBtn}
2285
2564
  onClick={() =>
@@ -3890,6 +4169,7 @@ const GenericReport = ({ setting = {} }) => {
3890
4169
  {
3891
4170
  keyInfo.percentage
3892
4171
  }
4172
+
3893
4173
  %
3894
4174
  </span>
3895
4175
  </div>
@@ -4033,6 +4313,17 @@ const GenericReport = ({ setting = {} }) => {
4033
4313
  >
4034
4314
  Export as Excel
4035
4315
  </button>
4316
+ <button
4317
+ className={`${styles.btn} ${styles.btnSecondary}`}
4318
+ onClick={() => handleExportReport('pdf')}
4319
+ disabled={
4320
+ isLoading ||
4321
+ selectedColumns.length === 0 ||
4322
+ !reportName
4323
+ }
4324
+ >
4325
+ Export as PDF
4326
+ </button>
4036
4327
  <button
4037
4328
  className={`${styles.btn} ${styles.btnPrimary}`}
4038
4329
  onClick={handleSaveReport}
@@ -911,6 +911,55 @@
911
911
  text-overflow: ellipsis;
912
912
  white-space: nowrap;
913
913
  }
914
+
915
+ .inlineEditContainer {
916
+ display: flex;
917
+ flex: 1;
918
+ align-items: center;
919
+ gap: 4px;
920
+ width: 100%;
921
+ }
922
+
923
+ .columnNameInput {
924
+ flex: 1;
925
+ padding: 4px 8px;
926
+ border: 1px solid var(--primary-color, #2563eb);
927
+ border-radius: 4px;
928
+ font-size: 0.9em;
929
+ color: var(--paragraph-color, #4b5563);
930
+ min-width: 50px;
931
+ max-width: 200px;
932
+ }
933
+
934
+ .saveBtn,
935
+ .cancelBtn {
936
+ background: none;
937
+ border: none;
938
+ cursor: pointer;
939
+ padding: 2px 4px;
940
+ border-radius: 4px;
941
+ display: flex;
942
+ align-items: center;
943
+ justify-content: center;
944
+ transition: all 0.2s ease;
945
+ font-size: 0.9em;
946
+ min-width: 20px;
947
+ min-height: 20px;
948
+ }
949
+
950
+ .saveBtn {
951
+ color: var(--success-color, #10b981);
952
+ &:hover {
953
+ background: rgba(16, 185, 129, 0.1);
954
+ }
955
+ }
956
+
957
+ .cancelBtn {
958
+ color: var(--danger-color, #ef4444);
959
+ &:hover {
960
+ background: rgba(239, 68, 68, 0.1);
961
+ }
962
+ }
914
963
  }
915
964
 
916
965
  .selectedColumnItem:hover {
@@ -949,6 +998,25 @@
949
998
  flex-shrink: 0;
950
999
  }
951
1000
 
1001
+ .editBtn {
1002
+ background: none;
1003
+ border: none;
1004
+ color: var(--primary-color, #2563eb);
1005
+ cursor: pointer;
1006
+ font-size: 1em;
1007
+ display: flex;
1008
+ align-items: center;
1009
+ justify-content: center;
1010
+ padding: 4px;
1011
+ border-radius: 4px;
1012
+ min-width: 24px;
1013
+ min-height: 24px;
1014
+ }
1015
+
1016
+ .editBtn:hover {
1017
+ background: rgba(var(--primary-rgb, 37, 99, 235), 0.1);
1018
+ }
1019
+
952
1020
  .removeBtn {
953
1021
  background: none;
954
1022
  border: none;
@@ -1034,6 +1102,25 @@
1034
1102
  margin: 0;
1035
1103
  }
1036
1104
 
1105
+ .columnSectionHeader {
1106
+ display: flex;
1107
+ align-items: center;
1108
+ margin-bottom: 10px;
1109
+ flex-wrap: wrap;
1110
+ gap: 8px;
1111
+ }
1112
+
1113
+ .columnSectionHeader h3 {
1114
+ margin: 0;
1115
+ margin-right: 8px;
1116
+ }
1117
+
1118
+ .instructionText {
1119
+ font-size: 0.8em;
1120
+ color: var(--muted-color, #6b7280);
1121
+ font-style: italic;
1122
+ }
1123
+
1037
1124
  .btnSmall {
1038
1125
  padding: 4px 8px;
1039
1126
  font-size: 0.8em;