@visns-studio/visns-components 5.7.0 → 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.
package/package.json CHANGED
@@ -2,8 +2,6 @@
2
2
  "dependencies": {
3
3
  "@emotion/is-prop-valid": "^1.3.1",
4
4
  "@fontsource/barlow": "^5.2.5",
5
- "@inovua/reactdatagrid-community": "^5.10.2",
6
- "@inovua/reactdatagrid-enterprise": "^5.10.2",
7
5
  "@nivo/bar": "^0.88.0",
8
6
  "@nivo/core": "^0.88.0",
9
7
  "@nivo/line": "^0.88.0",
@@ -16,7 +14,6 @@
16
14
  "akar-icons": "^1.9.31",
17
15
  "array-move": "^4.0.0",
18
16
  "awesome-debounce-promise": "^2.1.0",
19
- "axios": "^1.9.0",
20
17
  "browser-image-compression": "^2.0.2",
21
18
  "dayjs": "^1.11.13",
22
19
  "fabric": "^6.6.4",
@@ -83,7 +80,7 @@
83
80
  "react-dom": "^17.0.0 || ^18.0.0"
84
81
  },
85
82
  "name": "@visns-studio/visns-components",
86
- "version": "5.7.0",
83
+ "version": "5.7.1",
87
84
  "description": "Various packages to assist in the development of our Custom Applications.",
88
85
  "main": "src/index.js",
89
86
  "files": [
@@ -1,6 +1,5 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
- import axios from 'axios';
4
3
  import debounce from 'lodash.debounce';
5
4
  import moment from 'moment';
6
5
  import numeral from 'numeral';
@@ -12,6 +11,7 @@ import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
12
11
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
13
12
  import { toast } from 'react-toastify';
14
13
  import Toggle from 'react-toggle';
14
+ import fetchUtil from '../../utils/fetchUtil';
15
15
  import { confirmDialog } from '../utils/ConfirmDialog';
16
16
  import {
17
17
  Backspace,
@@ -81,7 +81,7 @@ const loadData = async (
81
81
  }
82
82
 
83
83
  try {
84
- const response = await axios.post(url, params);
84
+ const response = await fetchUtil.post(url, params);
85
85
  const { data, total } = response.data;
86
86
  return { data, count: total };
87
87
  } catch (error) {
@@ -1,10 +1,16 @@
1
- import React, { useState, useEffect } from 'react';
2
- import axios from 'axios';
1
+ import React, { useState, useEffect, useCallback, useMemo, memo } from 'react';
3
2
  import AwesomeDebouncePromise from 'awesome-debounce-promise';
4
3
  import { ToggleOffFill, ToggleOnFill } from 'akar-icons';
5
4
  import styles from './styles/Autocomplete.module.scss';
5
+ import fetchUtil from '../../utils/fetchUtil';
6
6
 
7
- const VisnsAutocomplete = (props) => {
7
+ // Create the debounced function outside the component to prevent recreation
8
+ const createDebouncedSearch = (fn) =>
9
+ AwesomeDebouncePromise(fn, 500, {
10
+ key: (id) => id,
11
+ });
12
+
13
+ const VisnsAutocomplete = memo((props) => {
8
14
  const {
9
15
  api,
10
16
  country,
@@ -15,160 +21,188 @@ const VisnsAutocomplete = (props) => {
15
21
  setFormData,
16
22
  style,
17
23
  } = props;
24
+
18
25
  const [search, setSearch] = useState('');
19
26
  const [results, setResults] = useState([]);
20
27
  const [isLoading, setIsLoading] = useState(false);
21
28
  const [overwrite, setOverwrite] = useState(false);
22
29
 
23
- const performSearch = (id, value) => {
24
- if (id > 0) {
25
- if (value === '') {
26
- setResults([]);
27
- setIsLoading(false);
28
- } else {
29
- let url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${value}.json?types=address,poi&access_token=${api}`;
30
+ // Memoize URL building logic to avoid recalculations
31
+ const buildSearchUrl = useCallback(
32
+ (value) => {
33
+ if (!value || !api) return null;
30
34
 
31
- if (country) {
32
- url += `&country=${country}`;
33
- }
35
+ let url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${value}.json?types=address,poi&access_token=${api}`;
34
36
 
35
- if (mapbox) {
36
- if (mapbox.proximity) {
37
- url += `&proximity=${mapbox.proximity}`;
38
- }
37
+ if (country) {
38
+ url += `&country=${country}`;
39
+ }
39
40
 
40
- if (mapbox.bbox) {
41
- url += `&bbox=${mapbox.bbox}`;
42
- }
41
+ if (mapbox) {
42
+ if (mapbox.proximity) {
43
+ url += `&proximity=${mapbox.proximity}`;
43
44
  }
44
45
 
45
- axios
46
- .get(url)
47
- .then((response) => {
48
- setResults(response.data.features);
49
- setIsLoading(false);
50
- })
51
- .catch(function (error) {
52
- console.log(error);
53
- });
46
+ if (mapbox.bbox) {
47
+ url += `&bbox=${mapbox.bbox}`;
48
+ }
54
49
  }
55
- }
56
- };
57
50
 
58
- const handleSearchChange = async (e) => {
59
- if (e) {
60
- setSearch(e.target.value);
51
+ return url;
52
+ },
53
+ [api, country, mapbox]
54
+ );
55
+
56
+ const performSearch = useCallback(
57
+ (id, value) => {
58
+ if (id <= 0 || !value) {
59
+ setResults([]);
60
+ setIsLoading(false);
61
+ return;
62
+ }
63
+
64
+ const url = buildSearchUrl(value);
65
+ if (!url) return;
66
+
67
+ fetchUtil
68
+ .get(url)
69
+ .then((response) => {
70
+ setResults(response.data.features || []);
71
+ setIsLoading(false);
72
+ })
73
+ .catch((error) => {
74
+ console.log(error);
75
+ setIsLoading(false);
76
+ });
77
+ },
78
+ [buildSearchUrl]
79
+ );
80
+
81
+ // Create the debounced search function using our memoized performSearch
82
+ const performSearchDebounced = useMemo(
83
+ () => createDebouncedSearch(performSearch),
84
+ [performSearch]
85
+ );
86
+
87
+ const handleSearchChange = useCallback(
88
+ async (e) => {
89
+ if (!e) return;
90
+
91
+ const newValue = e.target.value;
92
+ setSearch(newValue);
61
93
  setFormData((prevState) => ({
62
94
  ...prevState,
63
- [field]: e.target.value,
95
+ [field]: newValue,
64
96
  }));
65
97
 
66
- if (overwrite === false) {
98
+ if (!overwrite) {
67
99
  setIsLoading(true);
100
+ await performSearchDebounced(1, newValue);
68
101
  }
102
+ },
103
+ [field, overwrite, performSearchDebounced, setFormData]
104
+ );
69
105
 
70
- if (overwrite === false) {
71
- await performSearchDebounced(1, e.target.value);
72
- }
73
- }
74
- };
75
-
76
- const performSearchDebounced = AwesomeDebouncePromise(performSearch, 500, {
77
- key: (id, value) => id,
78
- });
106
+ const handleItemClicked = useCallback(
107
+ (place) => {
108
+ const address = place.address || '';
109
+ const text = place.text || '';
110
+ const search = address && text ? `${address} ${text}` : text;
79
111
 
80
- const handleItemClicked = (place) => {
81
- const address =
82
- place.address !== undefined && place.address !== null
83
- ? place.address
84
- : '';
85
- const text =
86
- place.text !== undefined && place.text !== null ? place.text : '';
87
-
88
- const search = address && text ? `${address} ${text}` : text;
89
- setSearch(search);
90
- setResults([]);
91
- onSelect(place, field);
92
- };
93
-
94
- const handleOverwrite = () => {
95
- if (overwrite === false) {
96
- setOverwrite(true);
112
+ setSearch(search);
97
113
  setResults([]);
98
- } else {
99
- setOverwrite(false);
100
- }
101
- };
114
+ onSelect(place, field);
115
+ },
116
+ [field, onSelect]
117
+ );
102
118
 
103
- useEffect(() => {
104
- if (overwrite === false) {
105
- if (search !== '') {
106
- performSearch();
119
+ const handleOverwrite = useCallback(() => {
120
+ setOverwrite((prevState) => {
121
+ if (!prevState) {
122
+ setResults([]);
107
123
  }
124
+ return !prevState;
125
+ });
126
+ }, []);
127
+
128
+ // Effect to handle overwrite toggle
129
+ useEffect(() => {
130
+ if (!overwrite && search) {
131
+ setIsLoading(true);
132
+ performSearch(1, search);
108
133
  }
109
- }, [overwrite]);
134
+ }, [overwrite, search, performSearch]);
110
135
 
136
+ // Effect to sync with inputValue prop
111
137
  useEffect(() => {
112
- setSearch(inputValue);
138
+ if (inputValue !== undefined) {
139
+ setSearch(inputValue);
140
+ }
113
141
  }, [inputValue]);
114
142
 
143
+ // Memoize the input style to prevent recalculation
144
+ const inputStyle = useMemo(
145
+ () => ({
146
+ height: style?.multi_select_height || '52px',
147
+ }),
148
+ [style]
149
+ );
150
+
115
151
  return (
116
- <>
117
- <div className={styles.AutocompletePlace}>
118
- <input
119
- className={styles['AutocompletePlace-input']}
120
- style={{
121
- height:
122
- style && style.multi_select_height
123
- ? style.multi_select_height
124
- : '52px',
125
- }}
126
- type="text"
127
- value={search || ''}
128
- onChange={handleSearchChange}
129
- placeholder="Type an address"
130
- autoComplete="off"
152
+ <div className={styles.AutocompletePlace}>
153
+ <input
154
+ className={styles['AutocompletePlace-input']}
155
+ style={inputStyle}
156
+ type="text"
157
+ value={search || ''}
158
+ onChange={handleSearchChange}
159
+ placeholder="Type an address"
160
+ autoComplete="off"
161
+ />
162
+
163
+ {overwrite ? (
164
+ <ToggleOnFill
165
+ data-tooltip-id="system-tooltip"
166
+ data-tooltip-content="Enable Autocomplete"
167
+ strokeWidth={2}
168
+ size={24}
169
+ onClick={handleOverwrite}
170
+ className={styles.toggleActive}
131
171
  />
132
- {overwrite === false ? (
133
- <ToggleOffFill
134
- data-tooltip-id="system-tooltip"
135
- data-tooltip-content="Disable Autocomplete"
136
- strokeWidth={2}
137
- size={24}
138
- onClick={handleOverwrite}
139
- />
140
- ) : (
141
- <ToggleOnFill
142
- data-tooltip-id="system-tooltip"
143
- data-tooltip-content="Enable Autocomplete"
144
- strokeWidth={2}
145
- size={24}
146
- onClick={handleOverwrite}
147
- className={styles.toggleActive}
148
- />
149
- )}
150
- {results.length > 0 ? (
151
- <ul className="AutocompletePlace-results">
152
- {results.map((place) => (
153
- <li
154
- key={place.id}
155
- className={styles['AutocompletePlace-items']}
156
- onClick={() => handleItemClicked(place)}
157
- >
158
- {place.place_name}
159
- </li>
160
- ))}
161
-
162
- {isLoading && (
163
- <li className={styles['AutocompletePlace-items']}>
164
- Loading...
165
- </li>
166
- )}
167
- </ul>
168
- ) : null}
169
- </div>
170
- </>
172
+ ) : (
173
+ <ToggleOffFill
174
+ data-tooltip-id="system-tooltip"
175
+ data-tooltip-content="Disable Autocomplete"
176
+ strokeWidth={2}
177
+ size={24}
178
+ onClick={handleOverwrite}
179
+ />
180
+ )}
181
+
182
+ {results.length > 0 && (
183
+ <ul className="AutocompletePlace-results">
184
+ {results.map((place) => (
185
+ <li
186
+ key={place.id}
187
+ className={styles['AutocompletePlace-items']}
188
+ onClick={() => handleItemClicked(place)}
189
+ >
190
+ {place.place_name}
191
+ </li>
192
+ ))}
193
+
194
+ {isLoading && (
195
+ <li className={styles['AutocompletePlace-items']}>
196
+ Loading...
197
+ </li>
198
+ )}
199
+ </ul>
200
+ )}
201
+ </div>
171
202
  );
172
- };
203
+ });
204
+
205
+ // Add display name for debugging
206
+ VisnsAutocomplete.displayName = 'VisnsAutocomplete';
173
207
 
174
208
  export default VisnsAutocomplete;
@@ -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';
@@ -26,6 +25,7 @@ import { toast } from 'react-toastify';
26
25
  import imageCompression from 'browser-image-compression';
27
26
  import Vapor from 'laravel-vapor';
28
27
  import Lightbox from 'yet-another-react-lightbox';
28
+ import fetchUtil from '../../utils/fetchUtil';
29
29
  import { confirmDialog } from '../utils/ConfirmDialog';
30
30
  import {
31
31
  Alarm,
@@ -118,7 +118,7 @@ const loadData = async (
118
118
  }
119
119
 
120
120
  try {
121
- const response = await axios.post(url, params);
121
+ const response = await fetchUtil.post(url, params);
122
122
  const { data, total } = response.data;
123
123
  return { data, count: total };
124
124
  } catch (error) {
@@ -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 {