@visns-studio/visns-components 5.4.9 → 5.4.11

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 CHANGED
@@ -102,6 +102,7 @@ The main component that handles authentication and routing.
102
102
  navigation={navigationConfig} // Navigation configuration
103
103
  routeConfig={routeConfig} // Route configuration
104
104
  themeType="primary" // Theme type: 'primary', 'secondary', etc.
105
+ enforce2FA={false} // Whether to enforce 2FA (default: false)
105
106
  />
106
107
  ```
107
108
 
@@ -341,6 +342,31 @@ Toast notification component.
341
342
  4. User enters the verification code
342
343
  5. If the code is valid, the user is authenticated and redirected to the main application
343
344
 
345
+ ### Enforcing 2FA
346
+
347
+ You can enforce 2FA for all users by setting the `enforce2FA` prop to `true` on the GenericAuth component:
348
+
349
+ ```jsx
350
+ <GenericAuth
351
+ // ... other props
352
+ enforce2FA={true}
353
+ />
354
+ ```
355
+
356
+ When 2FA enforcement is enabled:
357
+
358
+ 1. Users without 2FA enabled will see a non-dismissible dialog prompting them to set up 2FA
359
+ 2. The dialog will redirect them to the profile page with the 2FA tab open when they click the button
360
+ 3. Users cannot dismiss or skip this dialog, ensuring they set up 2FA
361
+ 4. The dialog uses an error icon to indicate the requirement
362
+ 5. The dialog will appear on every page load and route change until 2FA is set up
363
+ 6. The dialog will not appear when the user is already on the profile page with the 2FA tab open
364
+
365
+ When 2FA enforcement is disabled (default):
366
+
367
+ 1. No alert is shown to users without 2FA
368
+ 2. Users can still enable 2FA voluntarily through their profile page
369
+
344
370
  ### API Integration for 2FA
345
371
 
346
372
  #### Login Endpoint Response (when 2FA is required)
package/package.json CHANGED
@@ -15,6 +15,7 @@
15
15
  "array-move": "^4.0.0",
16
16
  "awesome-debounce-promise": "^2.1.0",
17
17
  "axios": "^1.8.4",
18
+ "browser-image-compression": "^2.0.2",
18
19
  "dayjs": "^1.11.13",
19
20
  "fabric": "^6.6.2",
20
21
  "file-saver": "^2.0.5",
@@ -53,6 +54,7 @@
53
54
  "reactjs-popup": "^2.0.6",
54
55
  "style-loader": "^4.0.0",
55
56
  "swapy": "^1.0.5",
57
+ "sweetalert2": "^11.17.2",
56
58
  "truncate": "^3.0.0",
57
59
  "uuid": "^10.0.0",
58
60
  "validator": "^13.15.0",
@@ -79,7 +81,7 @@
79
81
  "react-dom": "^17.0.0 || ^18.0.0"
80
82
  },
81
83
  "name": "@visns-studio/visns-components",
82
- "version": "5.4.9",
84
+ "version": "5.4.11",
83
85
  "description": "Various packages to assist in the development of our Custom Applications.",
84
86
  "main": "src/index.js",
85
87
  "files": [
@@ -100,5 +102,5 @@
100
102
  "url": "https://github.com/visnsstudio/visns-components/issues"
101
103
  },
102
104
  "homepage": "https://github.com/visnsstudio/visns-components#readme",
103
- "packageManager": "yarn@4.4.0"
105
+ "packageManager": "yarn@4.7.0"
104
106
  }
@@ -6,6 +6,7 @@ import { CircleX, File, TrashCan } from 'akar-icons';
6
6
  import { toast } from 'react-toastify';
7
7
  import { arrayMoveImmutable } from 'array-move';
8
8
  import Popup from 'reactjs-popup';
9
+ import imageCompression from 'browser-image-compression';
9
10
 
10
11
  import 'react-confirm-alert/src/react-confirm-alert.css';
11
12
 
@@ -11,6 +11,7 @@ import _ from 'lodash';
11
11
  import ReactDataGrid from '@inovua/reactdatagrid-community';
12
12
  import { CircleX } from 'akar-icons';
13
13
  import { confirmAlert } from 'react-confirm-alert';
14
+ import imageCompression from 'browser-image-compression';
14
15
 
15
16
  import '@inovua/reactdatagrid-community/index.css';
16
17
  import 'react-confirm-alert/src/react-confirm-alert.css';
@@ -22,6 +23,49 @@ import Table from './DataGrid';
22
23
 
23
24
  import styles from './styles/Form.module.scss';
24
25
 
26
+ // Helper function to check if a file is an image and optimize it if needed
27
+ const optimizeImageIfNeeded = async (file) => {
28
+ // Check if the file is an image
29
+ const isImage = file.type.startsWith('image/');
30
+
31
+ if (isImage) {
32
+ // Image compression options
33
+ const options = {
34
+ maxSizeMB: 1, // Max file size in MB
35
+ maxWidthOrHeight: 1920, // Max width or height in pixels
36
+ useWebWorker: true,
37
+ fileType: file.type,
38
+ };
39
+
40
+ try {
41
+ // Show optimization toast
42
+ const optimizingToastId = toast.info(`Optimizing ${file.name}...`, {
43
+ autoClose: false,
44
+ });
45
+
46
+ // Compress the image
47
+ const compressedFile = await imageCompression(file, options);
48
+
49
+ // Update toast
50
+ toast.update(optimizingToastId, {
51
+ render: `${file.name} optimized successfully!`,
52
+ type: 'success',
53
+ autoClose: 1000,
54
+ });
55
+
56
+ // Return the compressed file
57
+ return compressedFile;
58
+ } catch (error) {
59
+ console.error('Error optimizing image:', error);
60
+ // If optimization fails, return the original file
61
+ return file;
62
+ }
63
+ }
64
+
65
+ // If not an image, return the original file
66
+ return file;
67
+ };
68
+
25
69
  function Form({
26
70
  ajaxSetting,
27
71
  api,
@@ -701,6 +745,10 @@ function Form({
701
745
  if (Array.isArray(formData[fileKey])) {
702
746
  const uploadPromises = formData[fileKey].map(
703
747
  async (file) => {
748
+ // Optimize image if it's an image file
749
+ const optimizedFile =
750
+ await optimizeImageIfNeeded(file);
751
+
704
752
  const toastId = toast.info(
705
753
  `Uploading ${file.name}`,
706
754
  {
@@ -709,14 +757,19 @@ function Form({
709
757
  }
710
758
  );
711
759
 
712
- const fileRes = await Vapor.store(file, {
713
- progress: (progress) => {
714
- setUploadProgress(progress * 100);
715
- toast.update(toastId, {
716
- progress,
717
- });
718
- },
719
- });
760
+ const fileRes = await Vapor.store(
761
+ optimizedFile,
762
+ {
763
+ progress: (progress) => {
764
+ setUploadProgress(
765
+ progress * 100
766
+ );
767
+ toast.update(toastId, {
768
+ progress,
769
+ });
770
+ },
771
+ }
772
+ );
720
773
 
721
774
  toast.update(toastId, {
722
775
  render: `${file.name} uploaded successfully!`,
@@ -729,7 +782,7 @@ function Form({
729
782
  key: fileRes.key,
730
783
  bucket: fileRes.bucket,
731
784
  filename: file.name,
732
- filesize: file.size,
785
+ filesize: optimizedFile.size,
733
786
  extension: fileRes.extension,
734
787
  file_relationship: fileKey,
735
788
  fileable_field:
@@ -742,6 +795,11 @@ function Form({
742
795
  uploadPromises
743
796
  );
744
797
  } else {
798
+ // Optimize image if it's an image file
799
+ const optimizedFile = await optimizeImageIfNeeded(
800
+ formData[fileKey]
801
+ );
802
+
745
803
  const toastId = toast.info(
746
804
  `Uploading ${formData[fileKey].name}`,
747
805
  {
@@ -750,17 +808,14 @@ function Form({
750
808
  }
751
809
  );
752
810
 
753
- const fileRes = await Vapor.store(
754
- formData[fileKey],
755
- {
756
- progress: (progress) => {
757
- setUploadProgress(progress * 100);
758
- toast.update(toastId, {
759
- progress,
760
- });
761
- },
762
- }
763
- );
811
+ const fileRes = await Vapor.store(optimizedFile, {
812
+ progress: (progress) => {
813
+ setUploadProgress(progress * 100);
814
+ toast.update(toastId, {
815
+ progress,
816
+ });
817
+ },
818
+ });
764
819
 
765
820
  toast.update(toastId, {
766
821
  render: `${formData[fileKey].name} uploaded successfully!`,
@@ -772,7 +827,7 @@ function Form({
772
827
  formData['key'] = fileRes.key;
773
828
  formData['bucket'] = fileRes.bucket;
774
829
  formData['filename'] = formData[fileKey].name;
775
- formData['filesize'] = formData[fileKey].size;
830
+ formData['filesize'] = optimizedFile.size;
776
831
  formData['extension'] = fileRes.extension;
777
832
  formData['file_relationship'] = fileKey;
778
833
  formData['fileable_field'] =
@@ -930,8 +985,9 @@ function Form({
930
985
  method,
931
986
  payload = {};
932
987
 
933
- if (ajaxSetting?.where) {
934
- const getDataId = () => {
988
+ // Helper function to get dataId from ajaxSetting
989
+ const getDataId = () => {
990
+ if (ajaxSetting?.where) {
935
991
  for (const item of ajaxSetting.where) {
936
992
  if (
937
993
  item.hasOwnProperty('urlParam') &&
@@ -940,9 +996,9 @@ function Form({
940
996
  return item.value;
941
997
  }
942
998
  }
943
- return 0;
944
- };
945
- }
999
+ }
1000
+ return 0;
1001
+ };
946
1002
 
947
1003
  if (
948
1004
  formSettings.hasOwnProperty('json') &&
@@ -2,9 +2,11 @@
2
2
  import '../../styles/global.css';
3
3
 
4
4
  import React, { useState, useEffect, useRef, useCallback, memo } from 'react';
5
+ import { useLocation } from 'react-router-dom';
5
6
  import PasswordStrengthBar from 'react-password-strength-bar';
6
7
  import { toast } from 'react-toastify';
7
8
  import SignaturePad from 'react-signature-pad-wrapper';
9
+ import Swal from 'sweetalert2';
8
10
  import CustomFetch from '../Fetch';
9
11
  import TableFilter from '../TableFilter';
10
12
  import styles from './styles/Profile.module.scss';
@@ -60,6 +62,7 @@ const FormField = memo(
60
62
  );
61
63
 
62
64
  const Profile = ({ userProfile, setUserProfile }) => {
65
+ const location = useLocation();
63
66
  const signatureRef = useRef(null);
64
67
  const [isLoading, setIsLoading] = useState(false);
65
68
  const [profile, setProfile] = useState({
@@ -296,6 +299,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
296
299
 
297
300
  // Function to enable 2FA with verification code
298
301
  const enableTwoFactor = useCallback(async () => {
302
+ let toastId;
299
303
  try {
300
304
  if (!verificationCode.trim()) {
301
305
  toast.error('Please enter the verification code');
@@ -303,9 +307,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
303
307
  }
304
308
 
305
309
  setIsLoading(true);
306
- const toastId = toast.loading(
307
- 'Enabling two-factor authentication...'
308
- );
310
+ toastId = toast.loading('Enabling two-factor authentication...');
309
311
 
310
312
  const res = await CustomFetch(
311
313
  `/ajax/user/two-factor-auth/confirm`,
@@ -320,6 +322,12 @@ const Profile = ({ userProfile, setUserProfile }) => {
320
322
  setShowTwoFactorSetup(false);
321
323
  setVerificationCode('');
322
324
 
325
+ // Update the parent component's userProfile state to reflect 2FA is now enabled
326
+ setUserProfile((prevState) => ({
327
+ ...prevState,
328
+ two_factor_confirmed_at: new Date().toISOString(), // Add confirmation timestamp
329
+ }));
330
+
323
331
  toast.update(toastId, {
324
332
  render: 'Two-factor authentication enabled successfully',
325
333
  type: 'success',
@@ -340,22 +348,42 @@ const Profile = ({ userProfile, setUserProfile }) => {
340
348
  }
341
349
  } catch (err) {
342
350
  console.error('Error enabling 2FA:', err);
343
- toast.error(
344
- 'Failed to enable two-factor authentication. Please try again.'
345
- );
351
+
352
+ // Update the loading toast if it exists, otherwise create a new error toast
353
+ if (toastId) {
354
+ toast.update(toastId, {
355
+ render: 'Failed to enable two-factor authentication. Please try again.',
356
+ type: 'error',
357
+ isLoading: false,
358
+ autoClose: 3000,
359
+ closeButton: true,
360
+ });
361
+ } else {
362
+ toast.error(
363
+ 'Failed to enable two-factor authentication. Please try again.'
364
+ );
365
+ }
346
366
  } finally {
347
367
  setIsLoading(false);
348
368
  }
349
- }, [verificationCode]);
369
+ }, [verificationCode, setUserProfile]);
350
370
 
351
371
  // Function to disable 2FA
352
372
  const disableTwoFactor = useCallback(async () => {
353
373
  try {
354
- if (
355
- !window.confirm(
356
- 'Are you sure you want to disable two-factor authentication? This will make your account less secure.'
357
- )
358
- ) {
374
+ // Use SweetAlert2 instead of window.confirm
375
+ const result = await Swal.fire({
376
+ title: 'Disable Two-Factor Authentication',
377
+ text: 'Are you sure you want to disable two-factor authentication? This will make your account less secure.',
378
+ icon: 'warning',
379
+ showCancelButton: true,
380
+ confirmButtonColor: 'var(--primary-color)',
381
+ cancelButtonColor: 'var(--secondary-color)',
382
+ confirmButtonText: 'Yes, disable it',
383
+ cancelButtonText: 'No, keep it enabled',
384
+ });
385
+
386
+ if (!result.isConfirmed) {
359
387
  return;
360
388
  }
361
389
 
@@ -373,6 +401,12 @@ const Profile = ({ userProfile, setUserProfile }) => {
373
401
  if (res.data) {
374
402
  setTwoFactorEnabled(res.data.two_factor_enabled || false);
375
403
 
404
+ // Update the parent component's userProfile state to reflect 2FA is now disabled
405
+ setUserProfile((prevState) => ({
406
+ ...prevState,
407
+ two_factor_confirmed_at: null, // Remove confirmation timestamp
408
+ }));
409
+
376
410
  toast.update(toastId, {
377
411
  render: 'Two-factor authentication disabled successfully',
378
412
  type: 'success',
@@ -389,7 +423,7 @@ const Profile = ({ userProfile, setUserProfile }) => {
389
423
  } finally {
390
424
  setIsLoading(false);
391
425
  }
392
- }, []);
426
+ }, [setUserProfile]);
393
427
 
394
428
  // Function to copy text to clipboard
395
429
  const copyToClipboard = useCallback((text) => {
@@ -932,7 +966,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
932
966
  } else {
933
967
  setTwoFactorEnabled(false);
934
968
  }
935
- }, [userProfile.name, userProfile.email, userProfile.signature]);
969
+
970
+ // Check URL query parameters for tab selection
971
+ const queryParams = new URLSearchParams(location.search);
972
+ const tabParam = queryParams.get('tab');
973
+
974
+ if (tabParam === '2fa') {
975
+ // Update filters to show 2FA tab
976
+ setFilters((prevFilters) =>
977
+ prevFilters.map((filter) => ({
978
+ ...filter,
979
+ show: filter.id === '2fa',
980
+ class: filter.id === '2fa' ? 'subactive' : '',
981
+ }))
982
+ );
983
+ }
984
+ }, [
985
+ userProfile.name,
986
+ userProfile.email,
987
+ userProfile.signature,
988
+ location.search,
989
+ ]);
936
990
 
937
991
  return (
938
992
  <div>
@@ -42,6 +42,7 @@ const GenericAuth = ({
42
42
  navigation,
43
43
  routeConfig,
44
44
  themeType = 'primary',
45
+ enforce2FA = false, // Default to not enforcing 2FA
45
46
  }) => {
46
47
  const navigate = useNavigate();
47
48
  const location = useLocation();
@@ -143,6 +144,7 @@ const GenericAuth = ({
143
144
  setSystemAuth={setIsAuthenticated}
144
145
  themeType={themeType}
145
146
  userProfile={userProfile}
147
+ enforce2FA={enforce2FA}
146
148
  />
147
149
  }
148
150
  />
@@ -14,6 +14,7 @@ import truncate from 'truncate';
14
14
  import { CopyToClipboard } from 'react-copy-to-clipboard';
15
15
  import { saveAs } from 'file-saver';
16
16
  import { toast } from 'react-toastify';
17
+ import imageCompression from 'browser-image-compression';
17
18
  import {
18
19
  CircleCheck,
19
20
  Copy,
@@ -43,6 +44,49 @@ import styles from './styles/GenericDetail.module.scss';
43
44
  const localizer = momentLocalizer(moment);
44
45
  const DnDCalendar = withDragAndDrop(Calendar);
45
46
 
47
+ // Helper function to check if a file is an image and optimize it if needed
48
+ const optimizeImageIfNeeded = async (file) => {
49
+ // Check if the file is an image
50
+ const isImage = file.type.startsWith('image/');
51
+
52
+ if (isImage) {
53
+ // Image compression options
54
+ const options = {
55
+ maxSizeMB: 1, // Max file size in MB
56
+ maxWidthOrHeight: 1920, // Max width or height in pixels
57
+ useWebWorker: true,
58
+ fileType: file.type,
59
+ };
60
+
61
+ try {
62
+ // Show optimization toast
63
+ const optimizingToastId = toast.info(`Optimizing ${file.name}...`, {
64
+ autoClose: false,
65
+ });
66
+
67
+ // Compress the image
68
+ const compressedFile = await imageCompression(file, options);
69
+
70
+ // Update toast
71
+ toast.update(optimizingToastId, {
72
+ render: `${file.name} optimized successfully!`,
73
+ type: 'success',
74
+ autoClose: 1000,
75
+ });
76
+
77
+ // Return the compressed file
78
+ return compressedFile;
79
+ } catch (error) {
80
+ console.error('Error optimizing image:', error);
81
+ // If optimization fails, return the original file
82
+ return file;
83
+ }
84
+ }
85
+
86
+ // If not an image, return the original file
87
+ return file;
88
+ };
89
+
46
90
  function GenericDetail({
47
91
  extraUrlParam,
48
92
  layout = 'full',
@@ -1275,63 +1319,157 @@ function GenericDetail({
1275
1319
  </div>
1276
1320
  <Dropzone
1277
1321
  onDrop={(acceptedFiles) => {
1278
- const uploadFile = (file) => {
1279
- Vapor.store(file, {
1322
+ const uploadFile = async (file) => {
1323
+ // Optimize image if it's an image file
1324
+ const optimizedFile =
1325
+ await optimizeImageIfNeeded(
1326
+ file
1327
+ );
1328
+
1329
+ // Show upload toast
1330
+ const uploadToastId =
1331
+ toast.info(
1332
+ `Uploading ${file.name}...`,
1333
+ {
1334
+ autoClose: false,
1335
+ }
1336
+ );
1337
+
1338
+ Vapor.store(optimizedFile, {
1280
1339
  progress: (progress) => {
1281
1340
  setLoadingProgress(
1282
1341
  progress * 100
1283
1342
  );
1284
1343
  },
1285
- }).then((response) => {
1286
- setLoadingProgress(0);
1287
- const requestBody = {
1288
- uuid: response.uuid,
1289
- key: response.key,
1290
- bucket: response.bucket,
1291
- filename: file.name,
1292
- file_name: file.name,
1293
- filesize: file.size,
1294
- file_size: file.size,
1295
- extension:
1296
- response.extension,
1297
- file_extension:
1298
- response.extension,
1299
- file_relationship:
1300
- activeTabConfig.file_relationship,
1301
- };
1344
+ })
1345
+ .then((response) => {
1346
+ setLoadingProgress(0);
1347
+
1348
+ // Update upload toast
1349
+ toast.update(
1350
+ uploadToastId,
1351
+ {
1352
+ render: `Processing ${file.name}...`,
1353
+ type: 'info',
1354
+ autoClose: false,
1355
+ }
1356
+ );
1302
1357
 
1303
- CustomFetch(
1304
- `${activeTabConfig.url}/${routeParams[urlParam]}`,
1305
- 'PUT',
1306
- requestBody,
1307
- (res) => {
1308
- if (
1309
- activeTabConfig.file_relationship &&
1310
- res.data[
1311
- activeTabConfig
1312
- .file_relationship
1313
- ]
1314
- ) {
1315
- setFiles(
1358
+ const requestBody = {
1359
+ uuid: response.uuid,
1360
+ key: response.key,
1361
+ bucket: response.bucket,
1362
+ filename: file.name,
1363
+ file_name:
1364
+ file.name,
1365
+ filesize:
1366
+ optimizedFile.size,
1367
+ file_size:
1368
+ optimizedFile.size,
1369
+ extension:
1370
+ response.extension,
1371
+ file_extension:
1372
+ response.extension,
1373
+ file_relationship:
1374
+ activeTabConfig.file_relationship,
1375
+ };
1376
+
1377
+ CustomFetch(
1378
+ `${activeTabConfig.url}/${routeParams[urlParam]}`,
1379
+ 'PUT',
1380
+ requestBody,
1381
+ (res) => {
1382
+ // Check if there's an error in the response
1383
+ if (
1384
+ res.error &&
1385
+ res.error !==
1386
+ ''
1387
+ ) {
1388
+ toast.update(
1389
+ uploadToastId,
1390
+ {
1391
+ render: `Error: ${res.error}`,
1392
+ type: 'error',
1393
+ autoClose: 5000,
1394
+ }
1395
+ );
1396
+ return;
1397
+ }
1398
+ if (
1399
+ activeTabConfig.file_relationship &&
1316
1400
  res.data[
1317
1401
  activeTabConfig
1318
1402
  .file_relationship
1319
1403
  ]
1320
- );
1321
- } else {
1322
- if (
1323
- res?.data
1324
- ?.files
1325
1404
  ) {
1326
1405
  setFiles(
1406
+ res
1407
+ .data[
1408
+ activeTabConfig
1409
+ .file_relationship
1410
+ ]
1411
+ );
1412
+ // Update the main data state with the new data
1413
+ setData(
1327
1414
  res.data
1328
- .files
1329
1415
  );
1416
+
1417
+ // Update the upload toast
1418
+ toast.update(
1419
+ uploadToastId,
1420
+ {
1421
+ render: `${file.name} uploaded successfully!`,
1422
+ type: 'success',
1423
+ autoClose: 3000,
1424
+ }
1425
+ );
1426
+ } else {
1427
+ if (
1428
+ res
1429
+ ?.data
1430
+ ?.files
1431
+ ) {
1432
+ setFiles(
1433
+ res
1434
+ .data
1435
+ .files
1436
+ );
1437
+ // Update the main data state with the new data
1438
+ setData(
1439
+ res.data
1440
+ );
1441
+
1442
+ // Update the upload toast
1443
+ toast.update(
1444
+ uploadToastId,
1445
+ {
1446
+ render: `${file.name} uploaded successfully!`,
1447
+ type: 'success',
1448
+ autoClose: 3000,
1449
+ }
1450
+ );
1451
+ }
1330
1452
  }
1331
1453
  }
1332
- }
1333
- );
1334
- });
1454
+ );
1455
+ })
1456
+ .catch((error) => {
1457
+ console.error(
1458
+ 'Error uploading file:',
1459
+ error
1460
+ );
1461
+ toast.update(
1462
+ uploadToastId,
1463
+ {
1464
+ render: `Error uploading file: ${
1465
+ error.message ||
1466
+ 'Unknown error'
1467
+ }`,
1468
+ type: 'error',
1469
+ autoClose: 5000,
1470
+ }
1471
+ );
1472
+ });
1335
1473
  };
1336
1474
 
1337
1475
  acceptedFiles.forEach((a) => {
@@ -206,6 +206,17 @@ function GenericIndex({
206
206
  rowsSelectedRef.current = rowsSelected;
207
207
  }, [rowsSelected]);
208
208
 
209
+ const handleCustomAction = async () => {
210
+ try {
211
+ const { data, method, message, url } = setting?.functions
212
+ ?.customAction
213
+ ? setting.functions.customAction
214
+ : config.form.functions.customAction;
215
+ } catch (err) {
216
+ console.error(err);
217
+ }
218
+ };
219
+
209
220
  const handleCheckboxUpdate = useCallback(async () => {
210
221
  try {
211
222
  const { data, method, message, url } = setting?.functions
@@ -548,6 +559,17 @@ function GenericIndex({
548
559
  </button>
549
560
  )}
550
561
 
562
+ {setting.functions?.customAction && (
563
+ <button className={styles.btn} onClick={handleCustomAction}>
564
+ {setting.functions.customAction.label || 'Delete'}
565
+ </button>
566
+ )}
567
+ {config?.form?.functions?.customAction && (
568
+ <button className={styles.btn} onClick={handleCustomAction}>
569
+ {config.form.functions.customAction.label || 'Delete'}
570
+ </button>
571
+ )}
572
+
551
573
  {setting.export?.label && (
552
574
  <button className={styles.btn} onClick={handleExport}>
553
575
  {setting.export.label}
@@ -1,7 +1,14 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useState } from 'react';
4
- import { Routes, Route, Navigate } from 'react-router-dom';
3
+ import React, { useState, useEffect, useCallback } from 'react';
4
+ import {
5
+ Routes,
6
+ Route,
7
+ Navigate,
8
+ useNavigate,
9
+ useLocation,
10
+ } from 'react-router-dom';
11
+ import Swal from 'sweetalert2';
5
12
 
6
13
  import Call from '../Call';
7
14
  import Loader from '../Loader';
@@ -19,7 +26,10 @@ const GenericMain = ({
19
26
  setUserProfile,
20
27
  themeType,
21
28
  userProfile,
29
+ enforce2FA = false, // Default to not enforcing 2FA
22
30
  }) => {
31
+ const navigate = useNavigate();
32
+ const location = useLocation();
23
33
  const [incomingCallData, setIncomingCallData] = useState({});
24
34
 
25
35
  // Function to extract route parameters
@@ -29,6 +39,51 @@ const GenericMain = ({
29
39
  };
30
40
 
31
41
  // Recursive function to render routes
42
+ // Function to show 2FA enforcement alert
43
+ const show2FAEnforcementAlert = useCallback(() => {
44
+ // Only show the alert if:
45
+ // 1. User is authenticated (userProfile exists and has an ID)
46
+ // 2. 2FA is not enabled (two_factor_confirmed_at is null or undefined)
47
+ // 3. enforce2FA is true (only show alert when 2FA is enforced)
48
+ // 4. Not already on the profile page with 2FA tab
49
+ const isOnProfilePage =
50
+ location.pathname === '/profile' &&
51
+ location.search.includes('tab=2fa');
52
+
53
+ if (
54
+ enforce2FA && // Only show alert when 2FA is enforced
55
+ userProfile &&
56
+ userProfile.id &&
57
+ !userProfile.two_factor_confirmed_at &&
58
+ !isOnProfilePage // Don't show alert if already on profile page with 2FA tab
59
+ ) {
60
+ // Configure SweetAlert2 options for enforced 2FA
61
+ const swalOptions = {
62
+ title: 'Two-Factor Authentication Required',
63
+ text: 'Two-factor authentication is required for this system. You must set up 2FA to continue.',
64
+ icon: 'error',
65
+ confirmButtonColor: 'var(--primary-color)',
66
+ confirmButtonText: 'Set up 2FA now',
67
+ allowOutsideClick: false,
68
+ allowEscapeKey: false,
69
+ allowEnterKey: false,
70
+ };
71
+
72
+ // Show SweetAlert2 dialog
73
+ Swal.fire(swalOptions).then((result) => {
74
+ // Only navigate to profile page when the user clicks the button
75
+ if (result.isConfirmed) {
76
+ navigate('/profile?tab=2fa');
77
+ }
78
+ });
79
+ }
80
+ }, [userProfile, navigate, enforce2FA, location.pathname, location.search]);
81
+
82
+ // Check if 2FA is enabled whenever the route changes
83
+ useEffect(() => {
84
+ show2FAEnforcementAlert();
85
+ }, [show2FAEnforcementAlert, location]);
86
+
32
87
  const renderRoutes = (routes) => {
33
88
  return routes.map((route, index) => {
34
89
  let elementProps = {