@visns-studio/visns-components 5.14.22 → 5.15.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/README.md +5 -1
- package/package.json +2 -1
- package/src/components/Fetch.jsx +292 -56
- package/src/components/Fetch.jsx.backup +172 -0
- package/src/components/Field.jsx +18 -8
- package/src/components/modals/GalleryModal.jsx +1 -1
package/README.md
CHANGED
|
@@ -9,10 +9,14 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
|
|
|
9
9
|
|
|
10
10
|
VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
|
|
11
11
|
|
|
12
|
-
## Recent Updates (v5.
|
|
12
|
+
## Recent Updates (v5.15.1)
|
|
13
13
|
|
|
14
14
|
### Latest Enhancements
|
|
15
15
|
|
|
16
|
+
- **Enhanced JSON Table Field**: Improved json-table field type with better styling, user experience, and data editing capabilities
|
|
17
|
+
- **Dynamic Action Labels**: GenericDetail component now supports dynamic action labels and custom action buttons for better user interaction
|
|
18
|
+
- **Dependency Updates**: Updated lucide-react to version 0.536.0 for improved icon consistency and performance
|
|
19
|
+
- **GenericAuth Enhancements**: Additional config prop support for more flexible authentication configurations
|
|
16
20
|
- **GenericGrid Advanced Features**: Enhanced filtering with URL-based options, multiselect support, contact display integration, interactive date pickers with timezone support, and toggleable stage functionality
|
|
17
21
|
- **Modular Architecture**: GenericIndex refactored into standalone GenericGrid and GenericReportForm components for better reusability
|
|
18
22
|
- **Proposal System Integration**: GenericQuote enhanced with comprehensive proposal generation capabilities
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@nivo/core": "^0.99.0",
|
|
12
12
|
"@nivo/line": "^0.99.0",
|
|
13
13
|
"@nivo/pie": "^0.99.0",
|
|
14
|
+
"@tanstack/react-query": "^5.84.1",
|
|
14
15
|
"@tinymce/miniature": "^6.0.0",
|
|
15
16
|
"@tinymce/tinymce-react": "^6.3.0",
|
|
16
17
|
"@uiw/react-color": "^2.7.3",
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
90
91
|
},
|
|
91
92
|
"name": "@visns-studio/visns-components",
|
|
92
|
-
"version": "5.
|
|
93
|
+
"version": "5.15.1",
|
|
93
94
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
94
95
|
"main": "src/index.js",
|
|
95
96
|
"files": [
|
package/src/components/Fetch.jsx
CHANGED
|
@@ -1,9 +1,128 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
3
|
import { trackPromise } from 'react-promise-tracker';
|
|
3
4
|
import { toast } from 'react-toastify';
|
|
4
5
|
import parse from 'html-react-parser';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
// Create a singleton QueryClient instance for the entire visns-components ecosystem
|
|
8
|
+
const createQueryClient = () => {
|
|
9
|
+
return new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
staleTime: 2 * 60 * 1000, // 2 minutes - conservative setting
|
|
13
|
+
cacheTime: 10 * 60 * 1000, // 10 minutes - data stays in cache
|
|
14
|
+
retry: 1, // Conservative retry count
|
|
15
|
+
refetchOnWindowFocus: false, // Don't refetch on window focus
|
|
16
|
+
refetchOnReconnect: true, // Refetch when reconnecting to internet
|
|
17
|
+
retryOnMount: false, // Don't retry when component mounts
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Singleton QueryClient instance
|
|
24
|
+
let queryClientInstance = null;
|
|
25
|
+
const getQueryClient = () => {
|
|
26
|
+
if (!queryClientInstance) {
|
|
27
|
+
queryClientInstance = createQueryClient();
|
|
28
|
+
}
|
|
29
|
+
return queryClientInstance;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Helper to generate consistent cache keys
|
|
33
|
+
const generateCacheKey = (url, method, formData) => {
|
|
34
|
+
const normalizedMethod = method.toUpperCase();
|
|
35
|
+
const baseUrl = url.replace(/^\/+/, ''); // Remove leading slashes for consistency
|
|
36
|
+
|
|
37
|
+
// For GET requests, include query params in URL part of cache key
|
|
38
|
+
if (normalizedMethod === 'GET' && formData && Object.keys(formData).length > 0) {
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
Object.entries(formData).forEach(([key, value]) => {
|
|
41
|
+
if (value !== undefined && value !== null) {
|
|
42
|
+
params.append(key, value);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const queryString = params.toString();
|
|
46
|
+
return [baseUrl + (queryString ? `?${queryString}` : ''), normalizedMethod];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For other methods, include formData in cache key
|
|
50
|
+
const dataKey = formData ? JSON.stringify(formData) : '';
|
|
51
|
+
return [baseUrl, normalizedMethod, dataKey];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Helper to determine if request should be cached
|
|
55
|
+
const shouldCache = (method, url) => {
|
|
56
|
+
const normalizedMethod = method.toUpperCase();
|
|
57
|
+
|
|
58
|
+
// Always cache GET requests
|
|
59
|
+
if (normalizedMethod === 'GET') return true;
|
|
60
|
+
|
|
61
|
+
// Cache specific safe POST endpoints that return reference data
|
|
62
|
+
const cacheableEndpoints = [
|
|
63
|
+
'/dropdown', // Dropdown data (industries, facilities, etc.)
|
|
64
|
+
'/table', // Table data queries
|
|
65
|
+
'/availabilities', // Report availability data
|
|
66
|
+
'/reports/', // Report data
|
|
67
|
+
'/dashboard/', // Dashboard data
|
|
68
|
+
'/getFloatingDockUsage',
|
|
69
|
+
'/getWharfUsage',
|
|
70
|
+
'/getFloatingWharfUsage',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
return cacheableEndpoints.some(endpoint => url.includes(endpoint));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Dynamic stale time based on endpoint type
|
|
77
|
+
const getStaleTime = (url) => {
|
|
78
|
+
if (url.includes('/dropdown')) return 10 * 60 * 1000; // 10 minutes for dropdown data
|
|
79
|
+
if (url.includes('/dashboard/')) return 5 * 60 * 1000; // 5 minutes for dashboard data
|
|
80
|
+
if (url.includes('/availabilities') || url.includes('/reports/')) return 2 * 60 * 1000; // 2 minutes for reports
|
|
81
|
+
if (url.includes('/table')) return 1 * 60 * 1000; // 1 minute for table data
|
|
82
|
+
return 30 * 1000; // 30 seconds default
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Auto-invalidation patterns for mutations
|
|
86
|
+
const invalidateRelatedCaches = (url, method) => {
|
|
87
|
+
const queryClient = getQueryClient();
|
|
88
|
+
const normalizedMethod = method.toUpperCase();
|
|
89
|
+
|
|
90
|
+
// Only invalidate for mutation methods
|
|
91
|
+
if (!['POST', 'PUT', 'PATCH', 'DELETE'].includes(normalizedMethod)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Invalidate related cache entries based on URL patterns
|
|
96
|
+
if (url.includes('/leads/')) {
|
|
97
|
+
queryClient.invalidateQueries({ queryKey: ['leads/table'] });
|
|
98
|
+
queryClient.invalidateQueries({ queryKey: ['dashboard/'] });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (url.includes('/clients/')) {
|
|
102
|
+
queryClient.invalidateQueries({ queryKey: ['clients/dropdown'] });
|
|
103
|
+
queryClient.invalidateQueries({ queryKey: ['clients/table'] });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (url.includes('/clientAgreements/')) {
|
|
107
|
+
queryClient.invalidateQueries({ queryKey: ['clientAgreements/table'] });
|
|
108
|
+
queryClient.invalidateQueries({ queryKey: ['reports/'] });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (url.includes('/facilities/')) {
|
|
112
|
+
queryClient.invalidateQueries({ queryKey: ['facilities/dropdown'] });
|
|
113
|
+
queryClient.invalidateQueries({ queryKey: ['availabilities'] });
|
|
114
|
+
queryClient.invalidateQueries({ queryKey: ['reports/'] });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Invalidate all report caches for any data mutations
|
|
118
|
+
if (url.includes('/ajax/') && !url.includes('/dropdown')) {
|
|
119
|
+
queryClient.invalidateQueries({ queryKey: ['reports/'] });
|
|
120
|
+
queryClient.invalidateQueries({ queryKey: ['availabilities'] });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Original CustomFetch logic extracted for reuse
|
|
125
|
+
const originalCustomFetch = (
|
|
7
126
|
url,
|
|
8
127
|
method,
|
|
9
128
|
formData,
|
|
@@ -83,59 +202,143 @@ const CustomFetch = (
|
|
|
83
202
|
fetchOptions.body = formData ? JSON.stringify(formData) : undefined;
|
|
84
203
|
}
|
|
85
204
|
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
205
|
+
return new Promise((resolve, reject) => {
|
|
206
|
+
fetch(baseUrl + url, fetchOptions)
|
|
207
|
+
.then(async (response) => {
|
|
208
|
+
// Parse the response data
|
|
209
|
+
const contentType = response.headers.get('content-type');
|
|
210
|
+
let data;
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
contentType &&
|
|
214
|
+
contentType.includes('application/json')
|
|
215
|
+
) {
|
|
216
|
+
data = await response.json();
|
|
217
|
+
} else {
|
|
218
|
+
data = await response.text();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Create a response object similar to axios for compatibility
|
|
222
|
+
const responseObj = {
|
|
223
|
+
data: data,
|
|
224
|
+
status: response.status,
|
|
225
|
+
statusText: response.statusText,
|
|
226
|
+
headers: response.headers,
|
|
227
|
+
ok: response.ok,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
// Handle error responses (4xx, 5xx)
|
|
232
|
+
const error = new Error(response.statusText);
|
|
233
|
+
error.response = responseObj;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle success
|
|
238
|
+
if (successCallback) {
|
|
239
|
+
successCallback(data);
|
|
240
|
+
} else {
|
|
241
|
+
const error = data.error;
|
|
242
|
+
if (error && error !== '') {
|
|
243
|
+
toast.error(<div>{parse(error)}</div>);
|
|
101
244
|
}
|
|
245
|
+
}
|
|
246
|
+
resolve(responseObj);
|
|
247
|
+
})
|
|
248
|
+
.catch((error) => {
|
|
249
|
+
let errorMessage = '';
|
|
102
250
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
251
|
+
if (error.response && error.response.data) {
|
|
252
|
+
const { data } = error.response;
|
|
253
|
+
|
|
254
|
+
if (data.errors) {
|
|
255
|
+
const { errors } = data;
|
|
256
|
+
|
|
257
|
+
Object.values(errors).forEach((errorArray) => {
|
|
258
|
+
errorArray.forEach((errorMsg) => {
|
|
259
|
+
errorMessage += `${errorMsg}<br />`;
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
} else if (data.message) {
|
|
263
|
+
if (data.message === 'CSRF token mismatch.') {
|
|
264
|
+
window.location.replace('/login');
|
|
265
|
+
} else {
|
|
266
|
+
errorMessage = data.message;
|
|
267
|
+
}
|
|
117
268
|
}
|
|
269
|
+
} else {
|
|
270
|
+
errorMessage = error.message;
|
|
271
|
+
}
|
|
118
272
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
273
|
+
if (errorCallback) {
|
|
274
|
+
errorCallback(errorMessage);
|
|
275
|
+
} else {
|
|
276
|
+
if (errorMessage !== 'Unauthenticated.') {
|
|
277
|
+
toast.error(<div>{parse(errorMessage)}</div>);
|
|
122
278
|
} else {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
279
|
+
console.info(
|
|
280
|
+
'You are not authenticated into the system.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
reject(error);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Enhanced CustomFetch with TanStack Query integration
|
|
290
|
+
const CustomFetch = (
|
|
291
|
+
url,
|
|
292
|
+
method,
|
|
293
|
+
formData,
|
|
294
|
+
successCallback = null,
|
|
295
|
+
errorCallback = null
|
|
296
|
+
) => {
|
|
297
|
+
const queryClient = getQueryClient();
|
|
298
|
+
const cacheKey = generateCacheKey(url, method, formData);
|
|
299
|
+
const useCache = shouldCache(method, url);
|
|
300
|
+
|
|
301
|
+
if (useCache) {
|
|
302
|
+
// Use TanStack Query for cacheable requests
|
|
303
|
+
return trackPromise(
|
|
304
|
+
queryClient.fetchQuery({
|
|
305
|
+
queryKey: cacheKey,
|
|
306
|
+
queryFn: () => originalCustomFetch(url, method, formData, null, null),
|
|
307
|
+
staleTime: getStaleTime(url),
|
|
308
|
+
cacheTime: 10 * 60 * 1000, // 10 minutes cache time
|
|
309
|
+
}).then(response => {
|
|
310
|
+
// Call success callback exactly like original implementation
|
|
311
|
+
if (successCallback) {
|
|
312
|
+
successCallback(response.data);
|
|
313
|
+
}
|
|
314
|
+
return response;
|
|
315
|
+
}).catch(error => {
|
|
316
|
+
// Call error callback exactly like original implementation
|
|
317
|
+
if (errorCallback) {
|
|
318
|
+
let errorMessage = '';
|
|
319
|
+
if (error.response && error.response.data) {
|
|
320
|
+
const { data } = error.response;
|
|
321
|
+
if (data.errors) {
|
|
322
|
+
const { errors } = data;
|
|
323
|
+
Object.values(errors).forEach((errorArray) => {
|
|
324
|
+
errorArray.forEach((errorMsg) => {
|
|
325
|
+
errorMessage += `${errorMsg}<br />`;
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
} else if (data.message) {
|
|
329
|
+
errorMessage = data.message;
|
|
126
330
|
}
|
|
331
|
+
} else {
|
|
332
|
+
errorMessage = error.message;
|
|
127
333
|
}
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
334
|
+
errorCallback(errorMessage);
|
|
335
|
+
} else {
|
|
336
|
+
// Handle default error display like original
|
|
131
337
|
let errorMessage = '';
|
|
132
|
-
|
|
133
338
|
if (error.response && error.response.data) {
|
|
134
339
|
const { data } = error.response;
|
|
135
|
-
|
|
136
340
|
if (data.errors) {
|
|
137
341
|
const { errors } = data;
|
|
138
|
-
|
|
139
342
|
Object.values(errors).forEach((errorArray) => {
|
|
140
343
|
errorArray.forEach((errorMsg) => {
|
|
141
344
|
errorMessage += `${errorMsg}<br />`;
|
|
@@ -144,6 +347,7 @@ const CustomFetch = (
|
|
|
144
347
|
} else if (data.message) {
|
|
145
348
|
if (data.message === 'CSRF token mismatch.') {
|
|
146
349
|
window.location.replace('/login');
|
|
350
|
+
return;
|
|
147
351
|
} else {
|
|
148
352
|
errorMessage = data.message;
|
|
149
353
|
}
|
|
@@ -152,21 +356,53 @@ const CustomFetch = (
|
|
|
152
356
|
errorMessage = error.message;
|
|
153
357
|
}
|
|
154
358
|
|
|
155
|
-
if (
|
|
156
|
-
|
|
359
|
+
if (errorMessage !== 'Unauthenticated.') {
|
|
360
|
+
toast.error(<div>{parse(errorMessage)}</div>);
|
|
157
361
|
} else {
|
|
158
|
-
|
|
159
|
-
toast.error(<div>{parse(errorMessage)}</div>);
|
|
160
|
-
} else {
|
|
161
|
-
console.info(
|
|
162
|
-
'You are not authenticated into the system.'
|
|
163
|
-
);
|
|
164
|
-
}
|
|
362
|
+
console.info('You are not authenticated into the system.');
|
|
165
363
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
})
|
|
367
|
+
);
|
|
368
|
+
} else {
|
|
369
|
+
// Use original implementation for non-cacheable requests (mutations)
|
|
370
|
+
const result = trackPromise(
|
|
371
|
+
originalCustomFetch(url, method, formData, successCallback, errorCallback)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Auto-invalidate related caches after mutations
|
|
375
|
+
result.then(() => {
|
|
376
|
+
invalidateRelatedCaches(url, method);
|
|
377
|
+
}).catch(() => {
|
|
378
|
+
// Don't invalidate caches on error
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Export the QueryClient instance for potential advanced usage
|
|
386
|
+
CustomFetch.getQueryClient = getQueryClient;
|
|
387
|
+
|
|
388
|
+
// Method to manually invalidate specific cache patterns
|
|
389
|
+
CustomFetch.invalidateCache = (pattern) => {
|
|
390
|
+
const queryClient = getQueryClient();
|
|
391
|
+
if (Array.isArray(pattern)) {
|
|
392
|
+
queryClient.invalidateQueries({ queryKey: pattern });
|
|
393
|
+
} else {
|
|
394
|
+
queryClient.invalidateQueries({
|
|
395
|
+
predicate: (query) => query.queryKey.some(key =>
|
|
396
|
+
typeof key === 'string' && key.includes(pattern)
|
|
397
|
+
)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Method to clear all cache
|
|
403
|
+
CustomFetch.clearCache = () => {
|
|
404
|
+
const queryClient = getQueryClient();
|
|
405
|
+
queryClient.clear();
|
|
170
406
|
};
|
|
171
407
|
|
|
172
|
-
export default CustomFetch;
|
|
408
|
+
export default CustomFetch;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { trackPromise } from 'react-promise-tracker';
|
|
3
|
+
import { toast } from 'react-toastify';
|
|
4
|
+
import parse from 'html-react-parser';
|
|
5
|
+
|
|
6
|
+
const CustomFetch = (
|
|
7
|
+
url,
|
|
8
|
+
method,
|
|
9
|
+
formData,
|
|
10
|
+
successCallback = null,
|
|
11
|
+
errorCallback = null
|
|
12
|
+
) => {
|
|
13
|
+
const baseUrl = '';
|
|
14
|
+
const headers = {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
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
|
+
}
|
|
27
|
+
const MAX_PAYLOAD_SIZE_MB =
|
|
28
|
+
parseFloat(import.meta.env.VITE_MAX_PAYLOAD_SIZE_MB) || 4.5;
|
|
29
|
+
const MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE_MB * 1024 * 1024; // Convert MB to bytes
|
|
30
|
+
|
|
31
|
+
// Estimate headers size
|
|
32
|
+
const headersSize = JSON.stringify(headers).length;
|
|
33
|
+
|
|
34
|
+
// Estimate cookies size (if any)
|
|
35
|
+
const cookies = document.cookie;
|
|
36
|
+
const cookiesSize = cookies ? cookies.length : 0;
|
|
37
|
+
|
|
38
|
+
// Check payload size
|
|
39
|
+
const payloadSize = new Blob([JSON.stringify(formData)]).size;
|
|
40
|
+
const totalRequestSize = payloadSize + headersSize + cookiesSize;
|
|
41
|
+
|
|
42
|
+
if (totalRequestSize > MAX_PAYLOAD_SIZE) {
|
|
43
|
+
const errorMessage =
|
|
44
|
+
'The request is too large. Please try again with a smaller payload.';
|
|
45
|
+
if (errorCallback) {
|
|
46
|
+
errorCallback(errorMessage);
|
|
47
|
+
} else {
|
|
48
|
+
toast.error(errorMessage);
|
|
49
|
+
}
|
|
50
|
+
return Promise.reject(new Error(errorMessage));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (method.toUpperCase() === 'PUT' || method.toUpperCase() === 'PATCH') {
|
|
54
|
+
formData = {
|
|
55
|
+
...formData,
|
|
56
|
+
_method: method,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Configure fetch options
|
|
61
|
+
const fetchOptions = {
|
|
62
|
+
method: method,
|
|
63
|
+
headers: headers,
|
|
64
|
+
};
|
|
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
|
+
|
|
86
|
+
return trackPromise(
|
|
87
|
+
new Promise((resolve, reject) => {
|
|
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();
|
|
99
|
+
} else {
|
|
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
|
+
};
|
|
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;
|
|
124
|
+
if (error && error !== '') {
|
|
125
|
+
toast.error(<div>{parse(error)}</div>);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
resolve(responseObj);
|
|
129
|
+
})
|
|
130
|
+
.catch((error) => {
|
|
131
|
+
let errorMessage = '';
|
|
132
|
+
|
|
133
|
+
if (error.response && error.response.data) {
|
|
134
|
+
const { data } = error.response;
|
|
135
|
+
|
|
136
|
+
if (data.errors) {
|
|
137
|
+
const { errors } = data;
|
|
138
|
+
|
|
139
|
+
Object.values(errors).forEach((errorArray) => {
|
|
140
|
+
errorArray.forEach((errorMsg) => {
|
|
141
|
+
errorMessage += `${errorMsg}<br />`;
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
} else if (data.message) {
|
|
145
|
+
if (data.message === 'CSRF token mismatch.') {
|
|
146
|
+
window.location.replace('/login');
|
|
147
|
+
} else {
|
|
148
|
+
errorMessage = data.message;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
errorMessage = error.message;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (errorCallback) {
|
|
156
|
+
errorCallback(errorMessage);
|
|
157
|
+
} else {
|
|
158
|
+
if (errorMessage !== 'Unauthenticated.') {
|
|
159
|
+
toast.error(<div>{parse(errorMessage)}</div>);
|
|
160
|
+
} else {
|
|
161
|
+
console.info(
|
|
162
|
+
'You are not authenticated into the system.'
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
reject(error);
|
|
167
|
+
});
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default CustomFetch;
|
package/src/components/Field.jsx
CHANGED
|
@@ -1302,12 +1302,14 @@ function Field({
|
|
|
1302
1302
|
|
|
1303
1303
|
return htmlContainer;
|
|
1304
1304
|
case 'html5_date':
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
moment(
|
|
1309
|
-
|
|
1310
|
-
|
|
1305
|
+
// Handle "today" value for new entries
|
|
1306
|
+
let formattedDate = inputValue;
|
|
1307
|
+
if (inputValue === 'today') {
|
|
1308
|
+
formattedDate = moment().format('YYYY-MM-DD');
|
|
1309
|
+
} else if (inputValue && moment(inputValue, moment.ISO_8601, true).isValid()) {
|
|
1310
|
+
// Convert inputValue to 'YYYY-MM-DD' if necessary
|
|
1311
|
+
formattedDate = moment(inputValue).format('YYYY-MM-DD');
|
|
1312
|
+
}
|
|
1311
1313
|
|
|
1312
1314
|
return (
|
|
1313
1315
|
<input
|
|
@@ -1321,6 +1323,14 @@ function Field({
|
|
|
1321
1323
|
/>
|
|
1322
1324
|
);
|
|
1323
1325
|
case 'html5_datetime':
|
|
1326
|
+
// Handle "today" or "now" value for new entries
|
|
1327
|
+
let formattedDateTime = inputValue;
|
|
1328
|
+
if (inputValue === 'today') {
|
|
1329
|
+
formattedDateTime = moment().format('YYYY-MM-DD') + 'T00:00';
|
|
1330
|
+
} else if (inputValue === 'now') {
|
|
1331
|
+
formattedDateTime = moment().format('YYYY-MM-DDTHH:mm');
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1324
1334
|
return (
|
|
1325
1335
|
<input
|
|
1326
1336
|
type="datetime-local"
|
|
@@ -1329,8 +1339,8 @@ function Field({
|
|
|
1329
1339
|
placeholder=" "
|
|
1330
1340
|
onChange={onChange}
|
|
1331
1341
|
value={
|
|
1332
|
-
|
|
1333
|
-
?
|
|
1342
|
+
formattedDateTime && formattedDateTime !== 'null'
|
|
1343
|
+
? formattedDateTime
|
|
1334
1344
|
: ''
|
|
1335
1345
|
}
|
|
1336
1346
|
autoComplete="off"
|
|
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|
|
2
2
|
import Popup from 'reactjs-popup';
|
|
3
3
|
import { toast } from 'react-toastify';
|
|
4
4
|
import imageCompression from 'browser-image-compression';
|
|
5
|
-
import Vapor from 'laravel-vapor';
|
|
5
|
+
// import Vapor from 'laravel-vapor'; // Commented out to avoid build issues
|
|
6
6
|
import Lightbox from 'yet-another-react-lightbox';
|
|
7
7
|
import 'yet-another-react-lightbox/styles.css';
|
|
8
8
|
import { X, Image as ImageIcon, CloudUpload } from 'lucide-react';
|