@visns-studio/visns-components 5.6.5 → 5.6.7

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
@@ -16,17 +16,17 @@
16
16
  "akar-icons": "^1.9.31",
17
17
  "array-move": "^4.0.0",
18
18
  "awesome-debounce-promise": "^2.1.0",
19
- "axios": "^1.8.4",
19
+ "axios": "^1.9.0",
20
20
  "browser-image-compression": "^2.0.2",
21
21
  "dayjs": "^1.11.13",
22
22
  "fabric": "^6.6.4",
23
23
  "file-saver": "^2.0.5",
24
- "framer-motion": "^12.7.4",
24
+ "framer-motion": "^12.9.2",
25
25
  "html-react-parser": "^5.2.3",
26
26
  "lodash": "^4.17.21",
27
27
  "lodash.debounce": "^4.0.8",
28
28
  "moment": "^2.30.1",
29
- "motion": "^12.7.4",
29
+ "motion": "^12.9.2",
30
30
  "numeral": "^2.0.6",
31
31
  "pluralize": "^8.0.0",
32
32
  "qrcode.react": "^4.2.0",
@@ -48,7 +48,7 @@
48
48
  "react-signature-pad-wrapper": "^4.1.0",
49
49
  "react-slugify": "^4.0.1",
50
50
  "react-sortable-hoc": "^2.0.0",
51
- "react-to-print": "^3.0.6",
51
+ "react-to-print": "^3.1.0",
52
52
  "react-toastify": "^11.0.5",
53
53
  "react-toggle": "^4.1.3",
54
54
  "react-tooltip": "^5.28.1",
@@ -62,7 +62,7 @@
62
62
  "validator": "^13.15.0",
63
63
  "vite": "^5.4.18",
64
64
  "yarn": "^1.22.22",
65
- "yet-another-react-lightbox": "^3.22.0"
65
+ "yet-another-react-lightbox": "^3.23.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@babel/core": "^7.26.10",
@@ -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.5",
87
+ "version": "5.6.7",
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 }}
@@ -1,5 +1,24 @@
1
1
  import '../../styles/global.css';
2
2
 
3
+ /**
4
+ * GenericDetail Component
5
+ *
6
+ * This component supports two editing modes:
7
+ * 1. Modal editing (default) - Opens a modal with the form when the edit button is clicked
8
+ * 2. In-place editing - Makes the existing interface editable when the edit button is clicked
9
+ *
10
+ * To enable in-place editing, add the `inPlaceEdit: true` property to the form object in the tab configuration.
11
+ * Example:
12
+ * {
13
+ * id: 'overview',
14
+ * type: 'overview',
15
+ * form: {
16
+ * inPlaceEdit: true,
17
+ * // other form properties
18
+ * }
19
+ * }
20
+ */
21
+
3
22
  import React, { useEffect, useRef, useState } from 'react';
4
23
  import { Link, useParams } from 'react-router-dom';
5
24
  import { Calendar, momentLocalizer } from 'react-big-calendar';
@@ -1546,7 +1565,6 @@ function GenericDetail({
1546
1565
  src={
1547
1566
  imageSource
1548
1567
  }
1549
- width="300"
1550
1568
  alt={
1551
1569
  a.file_name
1552
1570
  }
@@ -1571,6 +1589,17 @@ function GenericDetail({
1571
1589
  styles.delete
1572
1590
  }
1573
1591
  />
1592
+ {a.file_name && (
1593
+ <div
1594
+ className={
1595
+ styles.filename
1596
+ }
1597
+ >
1598
+ {
1599
+ a.file_name
1600
+ }
1601
+ </div>
1602
+ )}
1574
1603
  </div>
1575
1604
  </li>
1576
1605
  )
@@ -1586,21 +1615,29 @@ function GenericDetail({
1586
1615
  }
1587
1616
  >
1588
1617
  {files.map((a, b) => (
1589
- <li
1590
- onClick={() => {
1591
- downloadFile(
1592
- a
1593
- );
1594
- }}
1595
- key={'tf-' + b}
1596
- >
1597
- <File
1598
- strokeWidth={
1599
- 2
1600
- }
1601
- size={18}
1602
- />
1603
- {a.file_name}
1618
+ <li key={'tf-' + b}>
1619
+ <div
1620
+ className="file-info"
1621
+ onClick={() => {
1622
+ downloadFile(
1623
+ a
1624
+ );
1625
+ }}
1626
+ >
1627
+ <File
1628
+ strokeWidth={
1629
+ 2
1630
+ }
1631
+ size={
1632
+ 20
1633
+ }
1634
+ />
1635
+ <div className="filename">
1636
+ {
1637
+ a.file_name
1638
+ }
1639
+ </div>
1640
+ </div>
1604
1641
  <button
1605
1642
  onClick={(
1606
1643
  event
@@ -1611,6 +1648,7 @@ function GenericDetail({
1611
1648
  a.file_name
1612
1649
  );
1613
1650
  }}
1651
+ title="Delete file"
1614
1652
  >
1615
1653
  <TrashCan
1616
1654
  strokeWidth={
@@ -1833,104 +1871,225 @@ function GenericDetail({
1833
1871
  case 'overview':
1834
1872
  return (
1835
1873
  <>
1836
- <div className={styles.gridtxt}>
1837
- {activeTabConfig.sections.map(
1838
- (section, sectionIndex) => (
1839
- <React.Fragment
1840
- key={`section-${activeTabConfig.id}-${sectionIndex}`}
1841
- >
1842
- <div
1843
- className={
1844
- styles.gridtxt__header
1845
- }
1874
+ {/* Show the form in-place when editing is active */}
1875
+ {inPlaceEditing &&
1876
+ editingTabId === activeTabConfig.id &&
1877
+ activeTabConfig.form ? (
1878
+ <div
1879
+ className={`${styles.gridtxt} inPlaceEdit`}
1880
+ >
1881
+ <div
1882
+ className={
1883
+ styles.gridtxt__header
1884
+ }
1885
+ >
1886
+ <span>
1887
+ {activeTabConfig.title ||
1888
+ 'Edit'}
1889
+ </span>
1890
+ </div>
1891
+ <Form
1892
+ key={`form-inline-${activeTabConfig.id}`}
1893
+ columnId={routeParams[urlParam]}
1894
+ formSettings={
1895
+ activeTabConfig.form
1896
+ }
1897
+ formType="update"
1898
+ paramValue={
1899
+ routeParams[urlParam] ||
1900
+ null
1901
+ }
1902
+ style={
1903
+ userProfile?.settings
1904
+ ?.style || {}
1905
+ }
1906
+ updateForm={setFormData}
1907
+ type="inline"
1908
+ userProfile={userProfile}
1909
+ fetchTable={() => {
1910
+ handleReload();
1911
+ setInPlaceEditing(false);
1912
+ setEditingTabId(null);
1913
+ }}
1914
+ />
1915
+ </div>
1916
+ ) : (
1917
+ // Show the regular overview when not editing
1918
+ <div className={styles.gridtxt}>
1919
+ {activeTabConfig.sections.map(
1920
+ (section, sectionIndex) => (
1921
+ <React.Fragment
1922
+ key={`section-${activeTabConfig.id}-${sectionIndex}`}
1846
1923
  >
1847
- <span>
1848
- {section.title}
1849
- </span>
1850
- </div>
1851
- {section.content &&
1852
- section.content.length >
1853
- 0 && (
1854
- <ul
1855
- className={
1856
- styles.customer__overview
1857
- }
1858
- >
1859
- {section.content.map(
1860
- (
1861
- item,
1862
- itemKey
1863
- ) => (
1864
- <li
1865
- key={`li-${item.id}-${itemKey}`}
1866
- className={
1867
- item.size ===
1868
- 'full'
1869
- ? `${styles['fw-grid-item']} ${styles.notecolor}`
1870
- : null
1871
- }
1872
- >
1873
- <strong>
1874
- {item.label
1875
- ? `${item.label}:`
1876
- : null}
1877
- </strong>{' '}
1878
- {item
1879
- .href
1880
- ?.url &&
1881
- item
1882
- .href
1883
- ?.key ? (
1884
- <Link
1885
- to={`${
1886
- item
1887
- .href
1888
- .url
1889
- }${
1890
- data[
1924
+ <div
1925
+ className={
1926
+ styles.gridtxt__header
1927
+ }
1928
+ >
1929
+ <span>
1930
+ {section.title}
1931
+ </span>
1932
+ </div>
1933
+ {section.content &&
1934
+ section.content
1935
+ .length > 0 && (
1936
+ <ul
1937
+ className={
1938
+ styles.customer__overview
1939
+ }
1940
+ >
1941
+ {section.content.map(
1942
+ (
1943
+ item,
1944
+ itemKey
1945
+ ) => (
1946
+ <li
1947
+ key={`li-${item.id}-${itemKey}`}
1948
+ className={
1949
+ item.size ===
1950
+ 'full'
1951
+ ? `${styles['fw-grid-item']} ${styles.notecolor}`
1952
+ : null
1953
+ }
1954
+ >
1955
+ <strong>
1956
+ {item.label
1957
+ ? `${item.label}:`
1958
+ : null}
1959
+ </strong>{' '}
1960
+ {item
1961
+ .href
1962
+ ?.url &&
1963
+ item
1964
+ .href
1965
+ ?.key ? (
1966
+ <Link
1967
+ to={`${
1891
1968
  item
1892
1969
  .href
1893
- .key
1894
- ]
1895
- }`}
1896
- >
1897
- {renderValue(
1898
- item,
1899
- data
1900
- )}
1901
- </Link>
1902
- ) : (
1903
- <>
1904
- {renderValue(
1905
- item,
1906
- data
1907
- )}
1908
- </>
1909
- )}
1910
- </li>
1911
- )
1912
- )}
1913
- </ul>
1914
- )}
1915
- </React.Fragment>
1916
- )
1917
- )}
1918
- </div>
1970
+ .url
1971
+ }${
1972
+ data[
1973
+ item
1974
+ .href
1975
+ .key
1976
+ ]
1977
+ }`}
1978
+ >
1979
+ {renderValue(
1980
+ item,
1981
+ data
1982
+ )}
1983
+ </Link>
1984
+ ) : (
1985
+ <>
1986
+ {renderValue(
1987
+ item,
1988
+ data
1989
+ )}
1990
+ </>
1991
+ )}
1992
+ </li>
1993
+ )
1994
+ )}
1995
+ </ul>
1996
+ )}
1997
+ </React.Fragment>
1998
+ )
1999
+ )}
2000
+ </div>
2001
+ )}
1919
2002
 
1920
2003
  {activeTabConfig.form && (
1921
2004
  <div className={styles.polActions}>
1922
- {/* Always render the "Edit" button if activeTabConfig.form exists */}
1923
- <button
1924
- className={styles.btn}
1925
- onClick={() =>
1926
- modalOpen(
1927
- 'update',
1928
- routeParams[urlParam]
1929
- )
1930
- }
1931
- >
1932
- Edit
1933
- </button>
2005
+ {/* Render the "Edit" button if activeTabConfig.form exists */}
2006
+ {!inPlaceEditing ? (
2007
+ <button
2008
+ className={styles.btn}
2009
+ onClick={() => {
2010
+ if (
2011
+ activeTabConfig.form
2012
+ .inPlaceEdit
2013
+ ) {
2014
+ startInPlaceEditing(
2015
+ activeTabConfig.id
2016
+ );
2017
+ } else {
2018
+ modalOpen(
2019
+ 'update',
2020
+ routeParams[
2021
+ urlParam
2022
+ ]
2023
+ );
2024
+ }
2025
+ }}
2026
+ >
2027
+ Edit
2028
+ </button>
2029
+ ) : (
2030
+ editingTabId ===
2031
+ activeTabConfig.id && (
2032
+ <>
2033
+ <button
2034
+ className={
2035
+ styles.btn
2036
+ }
2037
+ onClick={
2038
+ cancelInPlaceEditing
2039
+ }
2040
+ style={{
2041
+ backgroundColor:
2042
+ '#fff0f0',
2043
+ color: '#d32f2f',
2044
+ border: '1px solid #ffcdd2',
2045
+ }}
2046
+ >
2047
+ Cancel
2048
+ </button>
2049
+ <button
2050
+ className={
2051
+ styles.btn
2052
+ }
2053
+ onClick={() => {
2054
+ // Find the form's save button and click it
2055
+ const saveButton =
2056
+ document.querySelector(
2057
+ `.inPlaceEdit .${styles.saveBtn}`
2058
+ );
2059
+ if (
2060
+ saveButton
2061
+ ) {
2062
+ saveButton.click();
2063
+ } else {
2064
+ // Fallback to any button with data-type="save"
2065
+ const fallbackButton =
2066
+ document.querySelector(
2067
+ `.inPlaceEdit button[data-type="save"]`
2068
+ );
2069
+ if (
2070
+ fallbackButton
2071
+ ) {
2072
+ fallbackButton.click();
2073
+ }
2074
+ }
2075
+
2076
+ // Reset the editing state
2077
+ setInPlaceEditing(
2078
+ false
2079
+ );
2080
+ setEditingTabId(
2081
+ null
2082
+ );
2083
+
2084
+ // Reload data
2085
+ handleReload();
2086
+ }}
2087
+ >
2088
+ Save
2089
+ </button>
2090
+ </>
2091
+ )
2092
+ )}
1934
2093
 
1935
2094
  {/* Check if sections contain "ocrTemplate" type and render the "Save" button under the same polActions */}
1936
2095
  {activeTabConfig.sections &&
@@ -2545,6 +2704,8 @@ function GenericDetail({
2545
2704
  const [formType, setFormType] = useState('');
2546
2705
  const [formId, setFormId] = useState(0);
2547
2706
  const [modalShow, setModalShow] = useState(false);
2707
+ const [inPlaceEditing, setInPlaceEditing] = useState(false);
2708
+ const [editingTabId, setEditingTabId] = useState(null);
2548
2709
 
2549
2710
  /** Modal Functions */
2550
2711
  const modalOpen = (formType, formId) => {
@@ -2557,6 +2718,19 @@ function GenericDetail({
2557
2718
  setModalShow(false);
2558
2719
  };
2559
2720
 
2721
+ /** In-place Editing Functions */
2722
+ const startInPlaceEditing = (tabId) => {
2723
+ setInPlaceEditing(true);
2724
+ setEditingTabId(tabId);
2725
+ setFormType('update');
2726
+ setFormId(routeParams[urlParam]);
2727
+ };
2728
+
2729
+ const cancelInPlaceEditing = () => {
2730
+ setInPlaceEditing(false);
2731
+ setEditingTabId(null);
2732
+ };
2733
+
2560
2734
  return (
2561
2735
  <>
2562
2736
  <div className={styles.grid}>
@@ -285,76 +285,129 @@
285
285
 
286
286
  .dropzone__files {
287
287
  list-style: none;
288
- padding: 0;
288
+ padding: 16px 0;
289
289
  margin: 0;
290
290
  box-sizing: border-box;
291
+ display: grid;
292
+ grid-template-columns: repeat(2, 1fr); /* 2 columns by default */
293
+ gap: 16px;
294
+
295
+ /* Responsive grid layout */
296
+ @media (max-width: 768px) {
297
+ grid-template-columns: repeat(1, 1fr); /* 1 column on small screens */
298
+ }
291
299
 
292
300
  li {
293
- width: 100%;
294
- display: block;
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: space-between;
295
304
  background: rgba(var(--primary-rgb), 0.05);
296
305
  color: var(--primary-color);
297
- border-radius: 3px;
298
- transition: background 0.25s cubic-bezier(0.25, 0.8, 0.25, 1);
299
- outline: none;
306
+ border-radius: 6px;
307
+ transition: all 0.2s ease;
300
308
  cursor: pointer;
301
- border-radius: 3px;
302
- padding: 0.5rem;
309
+ padding: 12px 16px;
303
310
  box-sizing: border-box;
304
311
  position: relative;
305
312
  border: 1px solid rgba(var(--primary-rgb), 0.15);
306
- line-height: 1;
307
- font-size: 0.85rem;
308
- margin-bottom: 5px;
313
+ line-height: 1.4;
314
+ font-size: 0.9rem;
315
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
316
+
317
+ &:hover {
318
+ transform: translateY(-2px);
319
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
320
+ background: rgba(var(--primary-rgb), 0.08);
321
+ }
309
322
 
310
- svg {
311
- position: relative;
312
- top: -2px;
313
- color: rgba(var(--secondary-rgb), 1);
314
- float: left;
315
- stroke-width: 1;
316
- line-height: 1;
317
- margin-right: 5px;
323
+ .file-info {
324
+ display: flex;
325
+ align-items: center;
326
+ flex: 1;
327
+ overflow: hidden;
328
+
329
+ svg {
330
+ color: var(--secondary-color);
331
+ flex-shrink: 0;
332
+ margin-right: 12px;
333
+ }
334
+
335
+ .filename {
336
+ white-space: nowrap;
337
+ overflow: hidden;
338
+ text-overflow: ellipsis;
339
+ }
318
340
  }
319
341
 
320
342
  button {
321
- position: relative;
322
- float: right;
323
343
  background: none;
324
344
  outline: none;
325
345
  border: none;
326
- z-index: 200;
346
+ padding: 6px;
347
+ margin-left: 8px;
348
+ border-radius: 4px;
327
349
  cursor: pointer;
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ transition: background-color 0.2s ease;
354
+
355
+ &:hover {
356
+ background-color: rgba(var(--primary-rgb), 0.1);
357
+ }
328
358
 
329
359
  svg {
330
- font-size: 1em;
331
360
  color: var(--primary-color);
332
- line-height: 1;
333
361
  }
334
362
  }
335
363
  }
336
-
337
- li:hover {
338
- background: rgba(var(--secondary-color-rgb, 0.65));
339
- }
340
364
  }
341
365
 
342
366
  .sortlist {
343
- display: flex;
344
- flex-wrap: wrap;
345
- gap: 20px;
346
- padding: 0;
367
+ display: grid;
368
+ grid-template-columns: repeat(4, 1fr); /* 4 columns by default */
369
+ gap: 16px;
370
+ padding: 16px 0;
347
371
  list-style: none;
348
372
 
373
+ /* Responsive grid layout */
374
+ @media (max-width: 1200px) {
375
+ grid-template-columns: repeat(3, 1fr); /* 3 columns on medium screens */
376
+ }
377
+
378
+ @media (max-width: 768px) {
379
+ grid-template-columns: repeat(2, 1fr); /* 2 columns on small screens */
380
+ }
381
+
382
+ @media (max-width: 480px) {
383
+ grid-template-columns: repeat(
384
+ 1,
385
+ 1fr
386
+ ); /* 1 column on very small screens */
387
+ }
388
+
349
389
  li {
350
390
  position: relative;
391
+ aspect-ratio: 4/3;
392
+ border-radius: 8px;
393
+ overflow: hidden;
394
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
395
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
396
+ cursor: pointer;
397
+
398
+ &:hover {
399
+ transform: translateY(-2px);
400
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
401
+ }
351
402
 
352
403
  .sortimg {
353
- width: 300px;
354
- height: 300px;
404
+ width: 100%;
405
+ height: 100%;
355
406
  border-radius: 8px;
356
407
  overflow: hidden;
357
408
  position: relative;
409
+ display: flex;
410
+ flex-direction: column;
358
411
 
359
412
  img {
360
413
  width: 100%;
@@ -376,6 +429,21 @@
376
429
  background: rgba(255, 255, 255, 1);
377
430
  }
378
431
  }
432
+
433
+ .filename {
434
+ position: absolute;
435
+ bottom: 0;
436
+ left: 0;
437
+ right: 0;
438
+ padding: 8px;
439
+ background-color: rgba(0, 0, 0, 0.6);
440
+ color: white;
441
+ font-size: 0.8rem;
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ z-index: 1;
446
+ }
379
447
  }
380
448
  }
381
449
  }
@@ -1,7 +1,7 @@
1
1
  .header {
2
2
  background: var(--primary-color);
3
3
  color: var(--primary-color);
4
- position: relative;
4
+ position: sticky;
5
5
  top: 0;
6
6
  width: 100%;
7
7
  z-index: 995;
@@ -11,7 +11,7 @@
11
11
  .headerAlternate {
12
12
  background: var(--bg-color);
13
13
  color: var(--paragraph-color);
14
- position: relative;
14
+ position: sticky;
15
15
  top: 0;
16
16
  width: 100%;
17
17
  z-index: 995;
@@ -596,7 +596,22 @@
596
596
 
597
597
  /* DataGrid top container and search container styles */
598
598
  .dataGridTopContainer {
599
+ display: flex;
600
+ justify-content: space-between;
601
+ align-items: center;
599
602
  margin-bottom: 8px;
603
+ padding: 0 5px;
604
+ }
605
+
606
+ .dataGridTopLeftContainer {
607
+ flex: 1;
608
+ }
609
+
610
+ .dataGridTopRightContainer {
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: flex-end;
614
+ padding-right: 10px;
600
615
  }
601
616
 
602
617
  .searchContainer {
@@ -651,12 +666,44 @@
651
666
  justify-content: flex-end;
652
667
  }
653
668
 
669
+ /* Auto-refresh controls */
670
+ .autoRefreshContainer {
671
+ display: inline-flex;
672
+ align-items: center;
673
+ padding: 0 15px;
674
+ height: 38px;
675
+ background-color: #f9fafb;
676
+ border: 1px solid #d1d5db;
677
+ border-radius: var(--br, 4px);
678
+ margin-left: 10px;
679
+ margin-bottom: 0;
680
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
681
+ }
682
+
683
+ .autoRefreshLabel {
684
+ display: flex;
685
+ align-items: center;
686
+ gap: 8px;
687
+ font-size: 0.9rem;
688
+ color: var(--secondary-color, #1f2937);
689
+ white-space: nowrap;
690
+ cursor: pointer;
691
+ font-weight: normal;
692
+ }
693
+
694
+ .autoRefreshCheckbox {
695
+ margin: 0;
696
+ cursor: pointer;
697
+ width: 16px;
698
+ height: 16px;
699
+ }
700
+
654
701
  /* Container for when only the action button is shown (search disabled) */
655
702
  .actionButtonsContainer {
656
703
  display: flex;
657
704
  justify-content: flex-end;
658
705
  width: 100%;
659
- margin-bottom: 8px; /* Reduced spacing to match when searchbar is present */
706
+ margin-bottom: 0; /* No bottom margin to align with auto-refresh */
660
707
  }
661
708
 
662
709
  .actionButtons {
@@ -318,6 +318,7 @@ input[type='file'] {
318
318
  bottom: 15px;
319
319
  right: 20px;
320
320
  width: auto;
321
+ z-index: 100;
321
322
  }
322
323
 
323
324
  .polActions button {
@@ -327,6 +328,53 @@ input[type='file'] {
327
328
  margin: 0 0.15em;
328
329
  }
329
330
 
331
+ /* For in-place editing mode */
332
+ :global(.inPlaceEdit) {
333
+ background-color: rgba(245, 247, 250, 0.7);
334
+ border-radius: 5px;
335
+ padding: 10px;
336
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
337
+ border: 1px solid rgba(0, 0, 0, 0.1);
338
+ margin-bottom: 20px;
339
+ }
340
+
341
+ :global(.inPlaceEdit) [class*='gridtxt__header'] {
342
+ background-color: rgba(0, 0, 0, 0.03);
343
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
344
+ padding: 10px 15px;
345
+ margin-bottom: 15px;
346
+ border-radius: 4px 4px 0 0;
347
+ }
348
+
349
+ :global(.inPlaceEdit) .polActions {
350
+ position: static;
351
+ display: flex;
352
+ justify-content: flex-end;
353
+ align-items: center;
354
+ margin-top: 15px;
355
+ margin-bottom: 10px;
356
+ padding: 0 15px;
357
+ width: 100%;
358
+ gap: 10px;
359
+ }
360
+
361
+ :global(.inPlaceEdit) .polActions button {
362
+ margin: 0;
363
+ min-width: 120px;
364
+ border-radius: 4px;
365
+ }
366
+
367
+ /* Style for cancel button hover state */
368
+ :global(.inPlaceEdit) button[style*='color: #d32f2f']:hover {
369
+ background-color: #ffebee !important;
370
+ border-color: #ef9a9a !important;
371
+ }
372
+
373
+ /* Hide the save button at the bottom of the form when in in-place edit mode */
374
+ :global(.inPlaceEdit) .formcontainer > .polActions {
375
+ display: none;
376
+ }
377
+
330
378
  .buttonContainer {
331
379
  display: flex;
332
380
  justify-content: center;
@@ -618,6 +618,9 @@
618
618
  -webkit-overflow-scrolling: touch;
619
619
  scrollbar-width: none; /* Firefox */
620
620
  -ms-overflow-style: none; /* IE and Edge */
621
+ position: sticky;
622
+ top: 0;
623
+ z-index: 995;
621
624
 
622
625
  /* Hide scrollbar for Chrome/Safari/Opera */
623
626
  &::-webkit-scrollbar {
@@ -805,6 +808,12 @@
805
808
 
806
809
  .content {
807
810
  margin-left: 0;
811
+
812
+ .content-header {
813
+ position: sticky;
814
+ top: 0;
815
+ z-index: 995;
816
+ }
808
817
  }
809
818
 
810
819
  .popup-content {