@visns-studio/visns-components 5.6.6 → 5.6.8

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
@@ -59,6 +59,7 @@ VISNS Components is a React-based UI component library that provides a set of re
59
59
  - **Notification Pages** - User notification center
60
60
  - **Sorting Pages** - Content ordering interfaces
61
61
  - **Report Pages** - Custom report building and execution
62
+ - **Audit Logs** - System activity tracking and history
62
63
 
63
64
  ## Installation
64
65
 
@@ -224,6 +225,34 @@ A powerful data table component with sorting, filtering, and pagination.
224
225
  | setConfig | Callback for configuration changes | Function | - |
225
226
  | style | Custom styles | Object | - |
226
227
 
228
+ #### Auto-Refresh Functionality
229
+
230
+ The DataGrid component includes an auto-refresh feature that automatically reloads data at specified intervals. This is useful for displaying real-time or frequently updated data.
231
+
232
+ To enable auto-refresh:
233
+
234
+ ```jsx
235
+ <DataGrid
236
+ ajaxSetting={{
237
+ url: '/api/data',
238
+ method: 'GET',
239
+ params: {},
240
+ autoRefresh: 30, // Refresh every 30 seconds
241
+ // or
242
+ autoRefresh: true, // Use default 30-second interval
243
+ }}
244
+ // Other props...
245
+ />
246
+ ```
247
+
248
+ When auto-refresh is configured:
249
+
250
+ - A checkbox appears in the DataGrid header to enable/disable auto-refresh
251
+ - Auto-refresh is enabled by default when configured
252
+ - The interval can be customized (in seconds) by setting a numeric value
253
+ - Setting `autoRefresh: true` uses the default 30-second interval
254
+ - The component silently refreshes data without showing notifications
255
+
227
256
  ### Form Components
228
257
 
229
258
  #### Form
@@ -336,7 +365,7 @@ File upload component with drag and drop support.
336
365
 
337
366
  #### CustomFetch
338
367
 
339
- Data fetching utility with error handling.
368
+ Data fetching utility with error handling and payload size validation.
340
369
 
341
370
  ```jsx
342
371
  // Using async/await
@@ -358,6 +387,15 @@ CustomFetch(
358
387
  );
359
388
  ```
360
389
 
390
+ The CustomFetch component includes these features:
391
+
392
+ - **Payload Size Validation**: Automatically checks if the request payload exceeds the maximum allowed size (default: 4.5MB, configurable via `VITE_MAX_PAYLOAD_SIZE_MB` environment variable)
393
+ - **Toast Notifications**: Displays error messages using toast notifications
394
+ - **Promise Tracking**: Integrates with react-promise-tracker for loading indicators
395
+ - **HTML Error Parsing**: Properly renders HTML content in error messages
396
+ - **CSRF Protection**: Redirects to login page on CSRF token mismatch
397
+ - **Authentication Handling**: Silently handles unauthenticated errors without showing toast notifications
398
+
361
399
  #### Download
362
400
 
363
401
  File download utility.
@@ -404,6 +442,53 @@ The QrCode component fetches QR code data from the specified URL and displays it
404
442
  }
405
443
  ```
406
444
 
445
+ #### AuditLogs and AuditLog
446
+
447
+ The AuditLogs component displays a table of system activity logs, showing who made changes, what was changed, and when the changes occurred.
448
+
449
+ ```jsx
450
+ <AuditLogs layout="full" />
451
+ ```
452
+
453
+ The AuditLog component displays detailed information about a specific audit log entry.
454
+
455
+ ```jsx
456
+ <AuditLog userProfile={userProfile} />
457
+ ```
458
+
459
+ The AuditLogs component features:
460
+
461
+ - User tracking - Shows which user performed the action
462
+ - Event tracking - Displays the type of action (create, update, delete)
463
+ - Value comparison - Shows old and new values for changed fields
464
+ - IP address logging - Records the IP address of the user
465
+ - Timestamp recording - Shows when the action occurred
466
+ - Formatted display - Presents JSON data in a readable format
467
+
468
+ The component expects audit log data from the API with the following structure:
469
+
470
+ ```json
471
+ {
472
+ "id": 123,
473
+ "user": {
474
+ "id": 1,
475
+ "name": "John Doe"
476
+ },
477
+ "old_values": {
478
+ "field1": "old value",
479
+ "field2": "old value"
480
+ },
481
+ "new_values": {
482
+ "field1": "new value",
483
+ "field2": "new value"
484
+ },
485
+ "formatted_event": "Updating",
486
+ "ip_address": "192.168.1.1",
487
+ "url": "/api/resource/123",
488
+ "created_at": "2023-01-01T12:00:00Z"
489
+ }
490
+ ```
491
+
407
492
  ## Two-Factor Authentication (2FA)
408
493
 
409
494
  ## Client Portal
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.6",
87
+ "version": "5.6.8",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -17,7 +17,6 @@ import numeral from 'numeral';
17
17
  import Popup from 'reactjs-popup';
18
18
  import 'reactjs-popup/dist/index.css';
19
19
  import parse from 'html-react-parser';
20
- import 'react-toggle/style.css';
21
20
  import { saveAs } from 'file-saver';
22
21
  import NumberFilter from '@visns-studio/visns-datagrid-enterprise/NumberFilter';
23
22
  import SelectFilter from '@visns-studio/visns-datagrid-enterprise/SelectFilter';
@@ -386,10 +385,51 @@ const DataGrid = forwardRef(
386
385
  const [localInputValues, setLocalInputValues] = useState({});
387
386
  const localInputValuesRef = useRef({}); // Add this ref to track values
388
387
 
388
+ /** Auto Refresh States */
389
+ const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(false);
390
+ const [refreshInterval, setRefreshInterval] = useState(30); // Default 30 seconds interval
391
+ const refreshTimerRef = useRef(null);
392
+ const autoRefreshInitialized = useRef(false); // Track if we've initialized auto-refresh
393
+
394
+ /** Responsive Modal States */
395
+ const [windowWidth, setWindowWidth] = useState(window.innerWidth);
396
+
389
397
  useEffect(() => {
390
398
  localInputValuesRef.current = localInputValues;
391
399
  }, [localInputValues]);
392
400
 
401
+ // Effect to handle window resize for responsive modals
402
+ useEffect(() => {
403
+ const handleResize = () => {
404
+ setWindowWidth(window.innerWidth);
405
+ };
406
+
407
+ window.addEventListener('resize', handleResize);
408
+
409
+ // Clean up event listener on unmount
410
+ return () => {
411
+ window.removeEventListener('resize', handleResize);
412
+ };
413
+ }, []);
414
+
415
+ // Effect to handle autoRefresh setting
416
+ useEffect(() => {
417
+ if (ajaxSetting && ajaxSetting.autoRefresh !== undefined) {
418
+ // Enable auto-refresh by default when the setting is present
419
+ if (!autoRefreshInitialized.current) {
420
+ setAutoRefreshEnabled(true);
421
+ autoRefreshInitialized.current = true;
422
+ }
423
+
424
+ // Set the refresh interval based on the provided value
425
+ if (typeof ajaxSetting.autoRefresh === 'number') {
426
+ setRefreshInterval(ajaxSetting.autoRefresh);
427
+ } else if (ajaxSetting.autoRefresh === true) {
428
+ setRefreshInterval(30); // Default 30 seconds
429
+ }
430
+ }
431
+ }, [ajaxSetting]);
432
+
393
433
  /** Table Functions */
394
434
  useImperativeHandle(ref, () => ({
395
435
  reload: () => {
@@ -754,8 +794,6 @@ const DataGrid = forwardRef(
754
794
  // Extracting nested value using predefined keys from 's.relation'
755
795
  const nestedValue = getNestedValue(d, s.relation);
756
796
 
757
- console.info(d, s.relation);
758
-
759
797
  // Check if nestedValue is defined and is an array before mapping
760
798
  if (!nestedValue || !Array.isArray(nestedValue)) {
761
799
  console.warn(
@@ -1243,11 +1281,33 @@ const DataGrid = forwardRef(
1243
1281
  className={styles.searchInput}
1244
1282
  />
1245
1283
  </div>
1246
- {!isCreateDisabled && (
1247
- <div className={styles.actionButtonsWrapper}>
1248
- {renderCreateButton()}
1249
- </div>
1250
- )}
1284
+ <div className={styles.actionButtonsWrapper}>
1285
+ {!isCreateDisabled && renderCreateButton()}
1286
+ </div>
1287
+ </div>
1288
+ );
1289
+ };
1290
+
1291
+ const renderAutoRefreshControls = () => {
1292
+ // Only show auto-refresh if the setting is present in ajaxSetting
1293
+ if (!ajaxSetting || ajaxSetting.autoRefresh === undefined) {
1294
+ return null;
1295
+ }
1296
+
1297
+ return (
1298
+ <div className={styles.autoRefreshContainer}>
1299
+ <label className={styles.autoRefreshLabel}>
1300
+ <input
1301
+ type="checkbox"
1302
+ checked={autoRefreshEnabled}
1303
+ onChange={() =>
1304
+ setAutoRefreshEnabled(!autoRefreshEnabled)
1305
+ }
1306
+ className={styles.autoRefreshCheckbox}
1307
+ />
1308
+ Auto Refresh{' '}
1309
+ {refreshInterval && `(${refreshInterval}s)`}
1310
+ </label>
1251
1311
  </div>
1252
1312
  );
1253
1313
  };
@@ -1259,7 +1319,7 @@ const DataGrid = forwardRef(
1259
1319
  if (linkUrl) {
1260
1320
  const baseUrl = window.location.origin;
1261
1321
  const fullUrl = `${baseUrl}${linkUrl}${
1262
- rowProps.data[form.primaryKey]
1322
+ rowProps.data[cellProps?.link?.key || form.primaryKey]
1263
1323
  }`;
1264
1324
 
1265
1325
  menuProps.autoDismiss = true;
@@ -3146,6 +3206,34 @@ const DataGrid = forwardRef(
3146
3206
  };
3147
3207
  }, []);
3148
3208
 
3209
+ // Auto-refresh effect
3210
+ useEffect(() => {
3211
+ // Clear any existing timer
3212
+ if (refreshTimerRef.current) {
3213
+ clearInterval(refreshTimerRef.current);
3214
+ refreshTimerRef.current = null;
3215
+ }
3216
+
3217
+ // Set up new timer if auto-refresh is enabled and the setting is present in ajaxSetting
3218
+ if (
3219
+ autoRefreshEnabled &&
3220
+ ajaxSetting &&
3221
+ ajaxSetting.autoRefresh !== undefined
3222
+ ) {
3223
+ refreshTimerRef.current = setInterval(() => {
3224
+ handleReload();
3225
+ // No toast notification on refresh
3226
+ }, refreshInterval * 1000);
3227
+ }
3228
+
3229
+ // Clean up on unmount
3230
+ return () => {
3231
+ if (refreshTimerRef.current) {
3232
+ clearInterval(refreshTimerRef.current);
3233
+ }
3234
+ };
3235
+ }, [autoRefreshEnabled, refreshInterval, ajaxSetting]);
3236
+
3149
3237
  return (
3150
3238
  <div
3151
3239
  style={{
@@ -3156,12 +3244,15 @@ const DataGrid = forwardRef(
3156
3244
  overflow: 'hidden',
3157
3245
  }}
3158
3246
  >
3159
- {/* Search and action buttons container - separated from the DataGrid */}
3160
- {renderSearch() && (
3161
- <div className={styles.dataGridTopContainer}>
3247
+ {/* Top container with search, action buttons, and auto-refresh */}
3248
+ <div className={styles.dataGridTopContainer}>
3249
+ <div className={styles.dataGridTopLeftContainer}>
3162
3250
  {renderSearch()}
3163
3251
  </div>
3164
- )}
3252
+ <div className={styles.dataGridTopRightContainer}>
3253
+ {renderAutoRefreshControls()}
3254
+ </div>
3255
+ </div>
3165
3256
 
3166
3257
  {/* DataGrid container with its own border */}
3167
3258
  <div className={styles.dataGridContainer}>
@@ -3221,6 +3312,11 @@ const DataGrid = forwardRef(
3221
3312
  open={modalShow}
3222
3313
  onClose={modalClose}
3223
3314
  closeOnDocumentClick={false}
3315
+ contentStyle={{
3316
+ width: windowWidth < 768 ? '95vw' : '80vw',
3317
+ minWidth: windowWidth < 768 ? '95vw' : '600px',
3318
+ maxWidth: '1200px',
3319
+ }}
3224
3320
  nested
3225
3321
  >
3226
3322
  <Form
@@ -3244,7 +3340,11 @@ const DataGrid = forwardRef(
3244
3340
  open={modalGalleryShow}
3245
3341
  onClose={modalGalleryClose}
3246
3342
  closeOnDocumentClick={false}
3247
- contentStyle={{ width: 'auto', maxWidth: '90vw' }}
3343
+ contentStyle={{
3344
+ width: windowWidth < 768 ? '95vw' : '80vw',
3345
+ minWidth: windowWidth < 768 ? '95vw' : '600px',
3346
+ maxWidth: '1200px',
3347
+ }}
3248
3348
  overlayStyle={{ background: 'rgba(0, 0, 0, 0.7)' }}
3249
3349
  >
3250
3350
  <div
@@ -1085,6 +1085,7 @@ function Field({
1085
1085
  ? settings.options
1086
1086
  : options
1087
1087
  }
1088
+ isSearchable={settings.isSearchable || false}
1088
1089
  isCreatable={settings.isCreatable || false}
1089
1090
  dataOptions={dataOptions}
1090
1091
  style={style}
@@ -27,6 +27,7 @@ function MultiSelect({
27
27
  settings,
28
28
  style,
29
29
  isCreatable = false,
30
+ isSearchable = true,
30
31
  creatableConfig = {},
31
32
  }) {
32
33
  const [selectOptions, setSelectOptions] = useState([]);
@@ -142,7 +143,7 @@ function MultiSelect({
142
143
  <SelectComponent
143
144
  closeMenuOnSelect={!multi || settings.limit === 1}
144
145
  isClearable
145
- isSearchable
146
+ isSearchable={isSearchable}
146
147
  isMulti={multi}
147
148
  filterOption={createFilter({ ignoreAccents: false })}
148
149
  components={{ SelectList }}
@@ -28,20 +28,11 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
28
28
  });
29
29
 
30
30
  const renderSsoButton = () => {
31
- let content =
32
- providers && providers.length > 0 ? (
33
- <div className="formItem fwItem lastItem forgotten">
34
- <span
35
- style={{
36
- display: 'block',
37
- textAlign: 'center',
38
- width: '100%',
39
- }}
40
- >
41
- OR
42
- </span>
43
- </div>
44
- ) : null;
31
+ if (!providers || providers.length === 0) {
32
+ return null;
33
+ }
34
+
35
+ let content = <div className={styles.divider}>OR</div>;
45
36
 
46
37
  if (providers) {
47
38
  providers.forEach((provider) => {
@@ -51,13 +42,12 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
51
42
  <>
52
43
  {content}
53
44
  <div
54
- className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
45
+ className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
55
46
  >
56
47
  <button
57
- className={styles.btn}
48
+ className={styles.ssoButton}
58
49
  onClick={(e) => {
59
50
  e.preventDefault();
60
-
61
51
  window.location.href =
62
52
  '/auth/azure';
63
53
  }}
@@ -66,10 +56,11 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
66
56
  src={
67
57
  'https://d16lktya8ojp5z.cloudfront.net/generic/images/microsoft.png'
68
58
  }
69
- width="13px"
70
- height="13px"
71
- />{' '}
72
- Microsoft
59
+ width="18px"
60
+ height="18px"
61
+ alt="Microsoft logo"
62
+ />
63
+ Sign in with Microsoft
73
64
  </button>
74
65
  </div>
75
66
  </>
@@ -182,6 +173,17 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
182
173
  alt="CRM"
183
174
  className={styles.loginlogo}
184
175
  />
176
+ <h2
177
+ style={{
178
+ textAlign: 'center',
179
+ marginBottom: '1.5rem',
180
+ color: '#111827',
181
+ fontWeight: '600',
182
+ fontSize: '1.5rem',
183
+ }}
184
+ >
185
+ Sign in to your account
186
+ </h2>
185
187
  <div
186
188
  className={`${styles.formItem} ${styles.fwItem}`}
187
189
  >
@@ -232,9 +234,10 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
232
234
  >
233
235
  <div className={styles.rememberMeContainer}>
234
236
  <div
235
- className={
236
- styles.rememberMeCheckboxWrapper
237
- }
237
+ style={{
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ }}
238
241
  >
239
242
  <input
240
243
  type="checkbox"
@@ -246,13 +249,28 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
246
249
  styles.rememberMeCheckbox
247
250
  }
248
251
  />
252
+ <label
253
+ htmlFor="rememberMe"
254
+ className={
255
+ styles.rememberMeLabel
256
+ }
257
+ >
258
+ Remember me
259
+ </label>
260
+ </div>
261
+ <div>
262
+ <Link
263
+ to="/reset"
264
+ style={{
265
+ color: 'var(--primary-color, #4f46e5)',
266
+ textDecoration: 'none',
267
+ fontWeight: '500',
268
+ fontSize: '0.875rem',
269
+ }}
270
+ >
271
+ Forgot password?
272
+ </Link>
249
273
  </div>
250
- <label
251
- htmlFor="rememberMe"
252
- className={styles.rememberMeLabel}
253
- >
254
- Remember me
255
- </label>
256
274
  </div>
257
275
  </div>
258
276
  <div
@@ -263,17 +281,11 @@ const Login = ({ logo, providers, setSystemAuth, setUserProfile }) => {
263
281
  type="submit"
264
282
  disabled={isLoading}
265
283
  >
266
- {isLoading ? 'Logging in...' : 'Login'}
284
+ {isLoading
285
+ ? 'Signing in...'
286
+ : 'Sign in'}
267
287
  </button>
268
288
  </div>
269
- <div
270
- className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem} ${styles.forgotten}`}
271
- >
272
- <span>
273
- Forgot your password?{' '}
274
- <Link to="/reset">Click Here</Link>
275
- </span>
276
- </div>
277
289
  {renderSsoButton()}
278
290
  </div>
279
291
  </Reveal>
@@ -53,11 +53,17 @@ const Reset = ({ logo }) => {
53
53
  alt="CRM"
54
54
  className={styles.loginlogo}
55
55
  />
56
- <div
57
- className={`${styles.formItem} ${styles.fwItem}`}
56
+ <h2
57
+ style={{
58
+ textAlign: 'center',
59
+ marginBottom: '1.5rem',
60
+ color: '#111827',
61
+ fontWeight: '600',
62
+ fontSize: '1.5rem',
63
+ }}
58
64
  >
59
- <h1>Forgot your password?</h1>
60
- </div>
65
+ Forgot your password?
66
+ </h2>
61
67
  <div
62
68
  className={`${styles.formItem} ${styles.fwItem}`}
63
69
  >
@@ -89,7 +95,7 @@ const Reset = ({ logo }) => {
89
95
  type="submit"
90
96
  tabIndex="2"
91
97
  >
92
- Reset Password
98
+ Send Reset Link
93
99
  </button>
94
100
  </div>
95
101
  </div>
@@ -107,11 +107,17 @@ const Verify = ({ logo }) => {
107
107
  alt="CRM"
108
108
  className={styles.loginlogo}
109
109
  />
110
- <div
111
- className={`${styles.formItem} ${styles.fwItem}`}
110
+ <h2
111
+ style={{
112
+ textAlign: 'center',
113
+ marginBottom: '1.5rem',
114
+ color: '#111827',
115
+ fontWeight: '600',
116
+ fontSize: '1.5rem',
117
+ }}
112
118
  >
113
- <h1>Reset your password</h1>
114
- </div>
119
+ Reset your password
120
+ </h2>
115
121
  <div
116
122
  className={`${styles.formItem} ${styles.fwItem}`}
117
123
  >
@@ -154,7 +160,7 @@ const Verify = ({ logo }) => {
154
160
  type="submit"
155
161
  tabIndex="3"
156
162
  >
157
- Reset
163
+ Set New Password
158
164
  </button>
159
165
  </div>
160
166
  </div>