@visns-studio/visns-components 5.6.14 → 5.7.1

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.
@@ -10,7 +10,6 @@ import React, {
10
10
  useState,
11
11
  } from 'react';
12
12
  import { useNavigate } from 'react-router-dom';
13
- import axios from 'axios';
14
13
  import debounce from 'lodash.debounce';
15
14
  import moment from 'moment';
16
15
  import numeral from 'numeral';
@@ -22,11 +21,12 @@ import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
22
21
  import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
23
22
  import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
24
23
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
25
- import { confirmAlert } from 'react-confirm-alert';
26
24
  import { toast } from 'react-toastify';
27
25
  import imageCompression from 'browser-image-compression';
28
26
  import Vapor from 'laravel-vapor';
29
27
  import Lightbox from 'yet-another-react-lightbox';
28
+ import fetchUtil from '../../utils/fetchUtil';
29
+ import { confirmDialog } from '../utils/ConfirmDialog';
30
30
  import {
31
31
  Alarm,
32
32
  ArrowCounterClockwise,
@@ -59,7 +59,6 @@ import {
59
59
  import styles from './styles/DataGrid.module.scss';
60
60
 
61
61
  import '@visns-studio/visns-datagrid-enterprise/index.css';
62
- import 'react-confirm-alert/src/react-confirm-alert.css';
63
62
  import 'yet-another-react-lightbox/styles.css';
64
63
 
65
64
  import CustomFetch from './Fetch';
@@ -119,7 +118,7 @@ const loadData = async (
119
118
  }
120
119
 
121
120
  try {
122
- const response = await axios.post(url, params);
121
+ const response = await fetchUtil.post(url, params);
123
122
  const { data, total } = response.data;
124
123
  return { data, count: total };
125
124
  } catch (error) {
@@ -493,43 +492,15 @@ const DataGrid = forwardRef(
493
492
 
494
493
  const handleTimer = async (id, key, data) => {
495
494
  try {
496
- // Special handling for galvanizing timers to generate dip numbers
497
- if (
498
- key === 'galvanizing_start_1' ||
499
- key === 'galvanizing_start_2'
500
- ) {
501
- // For galvanizing_start_1 and galvanizing_start_2, we need to generate dip numbers
502
- const payload = {
503
- [key]: moment().toDate(),
504
- timer_type: key, // Pass the timer type to the backend
505
- };
506
-
507
- const res = await CustomFetch(
508
- `${form.url}/${id}/timer`,
509
- 'PUT',
510
- payload
511
- );
495
+ const res = await CustomFetch(`${form.url}/${id}`, 'PUT', {
496
+ [key]: moment().toDate(),
497
+ });
512
498
 
513
- if (res.data.error === '') {
514
- toast.success(
515
- 'Timer has been updated and dip number generated.'
516
- );
517
- handleReload();
518
- } else {
519
- toast.error(res.data.error);
520
- }
499
+ if (res.data.error === '') {
500
+ toast.success('Timer has been updated.');
501
+ handleReload();
521
502
  } else {
522
- // Normal timer handling for other stages
523
- const res = await CustomFetch(`${form.url}/${id}`, 'PUT', {
524
- [key]: moment().toDate(),
525
- });
526
-
527
- if (res.data.error === '') {
528
- toast.success('Timer has been updated.');
529
- handleReload();
530
- } else {
531
- toast.error(res.data.error);
532
- }
503
+ toast.error(res.data.error);
533
504
  }
534
505
  } catch (error) {
535
506
  console.info(error);
@@ -538,32 +509,74 @@ const DataGrid = forwardRef(
538
509
 
539
510
  const handleSelectionChange = useCallback(
540
511
  (param) => {
541
- if (param.selected === true) {
542
- const s = {};
543
-
544
- param.data.forEach((obj) => {
545
- s[obj.id] = obj;
546
- });
512
+ // If this is a new selection (not a deselection)
513
+ if (
514
+ param.selected === true ||
515
+ (typeof param.selected === 'object' &&
516
+ Object.keys(param.selected).length > 0)
517
+ ) {
518
+ // Show confirmation dialog for selection
519
+ const selectedCount =
520
+ param.selected === true ? param.data.length : 1;
521
+ const itemText = selectedCount === 1 ? 'item' : 'items';
522
+
523
+ confirmDialog({
524
+ title: 'Selection Confirmation',
525
+ message: `You have selected ${selectedCount} ${itemText}. Do you want to proceed with this selection?`,
526
+ data:
527
+ param.selected === true
528
+ ? param.data
529
+ : param.selected, // Pass the selected data
530
+ buttons: [
531
+ {
532
+ label: 'Yes',
533
+ onClick: () => {
534
+ // Process the selection as before
535
+ if (param.selected === true) {
536
+ const s = {};
537
+ param.data.forEach((obj) => {
538
+ s[obj.id] = obj;
539
+ });
540
+ setSelected(s);
541
+ } else if (
542
+ typeof param.selected === 'object'
543
+ ) {
544
+ const selectedKey = Object.keys(
545
+ param.selected
546
+ )[0];
547
+ const isSelectedAlready =
548
+ selected.hasOwnProperty(
549
+ selectedKey
550
+ );
547
551
 
548
- setSelected(s);
549
- } else if (typeof param.selected === 'object') {
550
- // Assuming param.selected is an object where keys are the ids of the selections
551
- const selectedKey = Object.keys(param.selected)[0]; // Get the key of the selected object
552
- const isSelectedAlready =
553
- selected.hasOwnProperty(selectedKey); // Check if this key is already in selected
552
+ if (isSelectedAlready) {
553
+ setSelected({});
554
+ } else {
555
+ setSelected((prevSelected) => ({
556
+ ...prevSelected,
557
+ [selectedKey]:
558
+ param.selected[selectedKey],
559
+ }));
560
+ }
561
+ }
554
562
 
555
- if (isSelectedAlready) {
556
- // If the key already exists, reset selected
557
- setSelected({});
558
- } else {
559
- // If it does not exist, update selected with the new object
560
- setSelected((prevSelected) => ({
561
- ...prevSelected,
562
- [selectedKey]: param.selected[selectedKey],
563
- }));
564
- }
563
+ // Show a success message
564
+ toast.success(
565
+ `Selection of ${selectedCount} ${itemText} confirmed`
566
+ );
567
+ },
568
+ },
569
+ {
570
+ label: 'No',
571
+ onClick: () => {
572
+ // Clear selection if user cancels
573
+ setSelected({});
574
+ },
575
+ },
576
+ ],
577
+ });
565
578
  } else {
566
- // Handle other cases as needed
579
+ // Handle deselection directly without confirmation
567
580
  setSelected(param.selected);
568
581
  }
569
582
  },
@@ -645,18 +658,86 @@ const DataGrid = forwardRef(
645
658
  case 'file':
646
659
  case 'image':
647
660
  case 'undo':
648
- CustomFetch(s.url, 'POST', { id: d[s.key] }, (result) => {
649
- toast.success(result.message);
661
+ // Create a user-friendly action name from the ID
662
+ const actionName =
663
+ s.title || s.id.charAt(0).toUpperCase() + s.id.slice(1);
664
+
665
+ // Create a message based on the action type
666
+ let message = `Are you sure you want to ${actionName.toLowerCase()}?`;
667
+
668
+ // Add item identifier if available
669
+ const itemName = d.name || d.label || d.title || d.id;
670
+ if (itemName) {
671
+ message += ` Item: "${itemName}"`;
672
+ }
673
+
674
+ // Show confirmation dialog
675
+ confirmDialog({
676
+ title: actionName,
677
+ message: message,
678
+ data: d, // Pass the row data for context
679
+ buttons: [
680
+ {
681
+ label: 'Yes',
682
+ onClick: () => {
683
+ CustomFetch(
684
+ s.url,
685
+ 'POST',
686
+ { id: d[s.key] },
687
+ (result) => {
688
+ toast.success(result.message);
689
+ }
690
+ );
691
+ },
692
+ },
693
+ {
694
+ label: 'No',
695
+ onClick: () => {},
696
+ },
697
+ ],
650
698
  });
651
699
  break;
652
700
  case 'clone':
653
701
  if (s.url && s.url !== '') {
654
- const cloneUrl = `${s.url}/${d[s.key]}/clone`;
655
-
656
- CustomFetch(cloneUrl, 'POST', {}, (result) => {
657
- toast.success(result.message);
702
+ // Create a message for clone action
703
+ let cloneMessage = `Are you sure you want to clone this item?`;
704
+
705
+ // Add item identifier if available
706
+ const cloneItemName =
707
+ d.name || d.label || d.title || d.id;
708
+ if (cloneItemName) {
709
+ cloneMessage += ` Item: "${cloneItemName}"`;
710
+ }
658
711
 
659
- handleReload();
712
+ // Show confirmation dialog
713
+ confirmDialog({
714
+ title: 'Clone Item',
715
+ message: cloneMessage,
716
+ data: d, // Pass the row data for context
717
+ buttons: [
718
+ {
719
+ label: 'Yes',
720
+ onClick: () => {
721
+ const cloneUrl = `${s.url}/${
722
+ d[s.key]
723
+ }/clone`;
724
+
725
+ CustomFetch(
726
+ cloneUrl,
727
+ 'POST',
728
+ {},
729
+ (result) => {
730
+ toast.success(result.message);
731
+ handleReload();
732
+ }
733
+ );
734
+ },
735
+ },
736
+ {
737
+ label: 'No',
738
+ onClick: () => {},
739
+ },
740
+ ],
660
741
  });
661
742
  }
662
743
  break;
@@ -748,9 +829,10 @@ const DataGrid = forwardRef(
748
829
  }?`;
749
830
  }
750
831
 
751
- confirmAlert({
832
+ confirmDialog({
752
833
  title: 'Delete Item',
753
834
  message: confirmMessage,
835
+ data: d, // Pass the row data
754
836
  buttons: [
755
837
  {
756
838
  label: 'Yes',
@@ -889,9 +971,10 @@ const DataGrid = forwardRef(
889
971
  modalOpen('update', d.id);
890
972
  break;
891
973
  default:
892
- confirmAlert({
974
+ confirmDialog({
893
975
  title: s.title,
894
976
  message: '',
977
+ data: d, // Pass the row data
895
978
  buttons: [
896
979
  {
897
980
  label: 'Yes',
@@ -1509,11 +1592,71 @@ const DataGrid = forwardRef(
1509
1592
  e.preventDefault();
1510
1593
  e.stopPropagation();
1511
1594
 
1512
- // Ensure both s and d are defined before calling handleSettingClick
1513
- if (s && d) {
1514
- handleSettingClick(s, d);
1515
- } else {
1595
+ // Ensure both s and d are defined before proceeding
1596
+ if (!s || !d) {
1516
1597
  console.warn('Missing settings or data for icon click');
1598
+ return;
1599
+ }
1600
+
1601
+ // Actions that should have confirmation dialogs
1602
+ const actionsRequiringConfirmation = [
1603
+ 'activate',
1604
+ 'arrowCycle',
1605
+ 'cloudUpload',
1606
+ 'clone',
1607
+ 'envelope',
1608
+ 'file',
1609
+ 'image',
1610
+ 'undo',
1611
+ 'restore',
1612
+ ];
1613
+
1614
+ // If this action requires confirmation, show a dialog
1615
+ if (actionsRequiringConfirmation.includes(s.id)) {
1616
+ // Get a user-friendly action name from the title or ID
1617
+ const actionName =
1618
+ s.title || s.id.charAt(0).toUpperCase() + s.id.slice(1);
1619
+
1620
+ // Create a message based on the action type
1621
+ let message = '';
1622
+ if (s.id === 'delete') {
1623
+ // Delete already has its own confirmation message
1624
+ handleSettingClick(s, d);
1625
+ return;
1626
+ } else {
1627
+ // For other actions, create a message based on the tooltip content
1628
+ const tooltipContent =
1629
+ s.active && iconStyle.color
1630
+ ? s.active.title
1631
+ : s.title;
1632
+ message = `Are you sure you want to ${tooltipContent.toLowerCase()}?`;
1633
+
1634
+ // Add item identifier if available
1635
+ const itemName = d.name || d.label || d.title || d.id;
1636
+ if (itemName) {
1637
+ message += ` Item: "${itemName}"`;
1638
+ }
1639
+ }
1640
+
1641
+ // Show the confirmation dialog
1642
+ confirmDialog({
1643
+ title: actionName,
1644
+ message: message,
1645
+ data: d, // Pass the row data for context
1646
+ buttons: [
1647
+ {
1648
+ label: 'Yes',
1649
+ onClick: () => handleSettingClick(s, d),
1650
+ },
1651
+ {
1652
+ label: 'No',
1653
+ onClick: () => {},
1654
+ },
1655
+ ],
1656
+ });
1657
+ } else {
1658
+ // For actions that don't need confirmation, proceed directly
1659
+ handleSettingClick(s, d);
1517
1660
  }
1518
1661
  };
1519
1662
 
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
- import axios from 'axios';
3
2
  import { trackPromise } from 'react-promise-tracker';
4
3
  import { toast } from 'react-toastify';
5
4
  import parse from 'html-react-parser';
5
+ import fetchUtil from '../../utils/fetchUtil';
6
6
 
7
7
  const CustomDownload = (
8
8
  url,
@@ -11,7 +11,18 @@ const CustomDownload = (
11
11
  successCallback = null,
12
12
  errorCallback = null
13
13
  ) => {
14
- let headers = {};
14
+ let headers = {
15
+ 'X-Requested-With': 'XMLHttpRequest',
16
+ };
17
+
18
+ // Add CSRF token if available
19
+ const csrfToken = document
20
+ .querySelector('meta[name="csrf-token"]')
21
+ ?.getAttribute('content');
22
+ if (csrfToken) {
23
+ headers['X-CSRF-TOKEN'] = csrfToken;
24
+ headers['X-XSRF-TOKEN'] = csrfToken;
25
+ }
15
26
  let options = {};
16
27
 
17
28
  if (method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
@@ -39,7 +50,7 @@ const CustomDownload = (
39
50
  return trackPromise(
40
51
  new Promise((resolve, reject) => {
41
52
  // First, make a request without responseType: 'blob' to check for errors
42
- axios({
53
+ fetchUtil({
43
54
  method: method,
44
55
  data: formData,
45
56
  headers: headers,
@@ -64,7 +75,7 @@ const CustomDownload = (
64
75
  resolve(response);
65
76
  } else {
66
77
  // If no error, make the actual blob request
67
- axios({
78
+ fetchUtil({
68
79
  method: method,
69
80
  data: formData,
70
81
  headers: headers,
@@ -1,5 +1,4 @@
1
1
  import React from 'react';
2
- import axios from 'axios';
3
2
  import { trackPromise } from 'react-promise-tracker';
4
3
  import { toast } from 'react-toastify';
5
4
  import parse from 'html-react-parser';
@@ -14,7 +13,17 @@ const CustomFetch = (
14
13
  const baseUrl = '';
15
14
  const headers = {
16
15
  'Content-Type': 'application/json',
16
+ 'X-Requested-With': 'XMLHttpRequest',
17
17
  };
18
+
19
+ // Add CSRF token if available
20
+ const csrfToken = document
21
+ .querySelector('meta[name="csrf-token"]')
22
+ ?.getAttribute('content');
23
+ if (csrfToken) {
24
+ headers['X-CSRF-TOKEN'] = csrfToken;
25
+ headers['X-XSRF-TOKEN'] = csrfToken;
26
+ }
18
27
  const MAX_PAYLOAD_SIZE_MB =
19
28
  parseFloat(import.meta.env.VITE_MAX_PAYLOAD_SIZE_MB) || 4.5;
20
29
  const MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE_MB * 1024 * 1024; // Convert MB to bytes
@@ -48,36 +57,80 @@ const CustomFetch = (
48
57
  };
49
58
  }
50
59
 
51
- const options = {
60
+ // Configure fetch options
61
+ const fetchOptions = {
52
62
  method: method,
53
- data: formData,
54
63
  headers: headers,
55
- url: baseUrl + url,
56
64
  };
57
65
 
66
+ // For GET and HEAD requests, convert formData to query parameters
67
+ if (method.toUpperCase() === 'GET' || method.toUpperCase() === 'HEAD') {
68
+ if (formData && Object.keys(formData).length > 0) {
69
+ // Convert formData to query string
70
+ const queryParams = new URLSearchParams();
71
+ Object.entries(formData).forEach(([key, value]) => {
72
+ if (value !== undefined && value !== null) {
73
+ queryParams.append(key, value);
74
+ }
75
+ });
76
+ const queryString = queryParams.toString();
77
+ if (queryString) {
78
+ url += (url.includes('?') ? '&' : '?') + queryString;
79
+ }
80
+ }
81
+ } else {
82
+ // For other methods, add body
83
+ fetchOptions.body = formData ? JSON.stringify(formData) : undefined;
84
+ }
85
+
58
86
  return trackPromise(
59
87
  new Promise((resolve, reject) => {
60
- axios(options)
61
- .then((response) => {
62
- if (successCallback) {
63
- successCallback(response.data);
88
+ fetch(baseUrl + url, fetchOptions)
89
+ .then(async (response) => {
90
+ // Parse the response data
91
+ const contentType = response.headers.get('content-type');
92
+ let data;
93
+
94
+ if (
95
+ contentType &&
96
+ contentType.includes('application/json')
97
+ ) {
98
+ data = await response.json();
64
99
  } else {
65
- const error = response.data.error;
100
+ data = await response.text();
101
+ }
102
+
103
+ // Create a response object similar to axios for compatibility
104
+ const responseObj = {
105
+ data: data,
106
+ status: response.status,
107
+ statusText: response.statusText,
108
+ headers: response.headers,
109
+ ok: response.ok,
110
+ };
66
111
 
112
+ if (!response.ok) {
113
+ // Handle error responses (4xx, 5xx)
114
+ const error = new Error(response.statusText);
115
+ error.response = responseObj;
116
+ throw error;
117
+ }
118
+
119
+ // Handle success
120
+ if (successCallback) {
121
+ successCallback(data);
122
+ } else {
123
+ const error = data.error;
67
124
  if (error && error !== '') {
68
125
  toast.error(<div>{parse(error)}</div>);
69
126
  }
70
127
  }
71
- resolve(response);
128
+ resolve(responseObj);
72
129
  })
73
130
  .catch((error) => {
74
131
  let errorMessage = '';
75
132
 
76
- if (
77
- error.response &&
78
- error.response.data &&
79
- error.response.data.message
80
- ) {
133
+ if (error.response && error.response.data) {
81
134
  const { data } = error.response;
82
135
 
83
136
  if (data.errors) {
@@ -88,7 +141,7 @@ const CustomFetch = (
88
141
  errorMessage += `${errorMsg}<br />`;
89
142
  });
90
143
  });
91
- } else {
144
+ } else if (data.message) {
92
145
  if (data.message === 'CSRF token mismatch.') {
93
146
  window.location.replace('/login');
94
147
  } else {
@@ -10,11 +10,10 @@ import parse from 'html-react-parser';
10
10
  import _ from 'lodash';
11
11
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
12
12
  import { CircleX } from 'akar-icons';
13
- import { confirmAlert } from 'react-confirm-alert';
14
13
  import imageCompression from 'browser-image-compression';
14
+ import { confirmDialog } from '../utils/ConfirmDialog';
15
15
 
16
16
  import '@visns-studio/visns-datagrid-enterprise/index.css';
17
- import 'react-confirm-alert/src/react-confirm-alert.css';
18
17
 
19
18
  import CustomFetch from './Fetch';
20
19
  import Download from './Download';
@@ -598,11 +597,12 @@ function Form({
598
597
 
599
598
  try {
600
599
  if (s.customButton.confirmation) {
601
- confirmAlert({
600
+ confirmDialog({
602
601
  title: '',
603
602
  message:
604
603
  s.customButton.confirmation.message ||
605
604
  'Are you sure you want to proceed?',
605
+ data: formData, // Add context data
606
606
  buttons: [
607
607
  {
608
608
  label: 'Yes',