@visns-studio/visns-components 5.6.6 → 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 +86 -1
- package/package.json +1 -1
- package/src/components/crm/DataGrid.jsx +114 -14
- package/src/components/crm/Field.jsx +1 -0
- package/src/components/crm/MultiSelect.jsx +2 -1
- package/src/components/crm/generic/GenericDetail.jsx +35 -16
- package/src/components/crm/generic/styles/GenericDetail.module.scss +102 -34
- package/src/components/crm/generic/styles/GenericMain.module.scss +2 -2
- package/src/components/crm/styles/DataGrid.module.scss +48 -1
- package/src/components/crm/styles/Navigation.module.scss +9 -0
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.
|
|
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
|
-
{
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
-
{/*
|
|
3160
|
-
{
|
|
3161
|
-
<div className={styles.
|
|
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={{
|
|
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
|
|
@@ -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 }}
|
|
@@ -1565,7 +1565,6 @@ function GenericDetail({
|
|
|
1565
1565
|
src={
|
|
1566
1566
|
imageSource
|
|
1567
1567
|
}
|
|
1568
|
-
width="300"
|
|
1569
1568
|
alt={
|
|
1570
1569
|
a.file_name
|
|
1571
1570
|
}
|
|
@@ -1590,6 +1589,17 @@ function GenericDetail({
|
|
|
1590
1589
|
styles.delete
|
|
1591
1590
|
}
|
|
1592
1591
|
/>
|
|
1592
|
+
{a.file_name && (
|
|
1593
|
+
<div
|
|
1594
|
+
className={
|
|
1595
|
+
styles.filename
|
|
1596
|
+
}
|
|
1597
|
+
>
|
|
1598
|
+
{
|
|
1599
|
+
a.file_name
|
|
1600
|
+
}
|
|
1601
|
+
</div>
|
|
1602
|
+
)}
|
|
1593
1603
|
</div>
|
|
1594
1604
|
</li>
|
|
1595
1605
|
)
|
|
@@ -1605,21 +1615,29 @@ function GenericDetail({
|
|
|
1605
1615
|
}
|
|
1606
1616
|
>
|
|
1607
1617
|
{files.map((a, b) => (
|
|
1608
|
-
<li
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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>
|
|
1623
1641
|
<button
|
|
1624
1642
|
onClick={(
|
|
1625
1643
|
event
|
|
@@ -1630,6 +1648,7 @@ function GenericDetail({
|
|
|
1630
1648
|
a.file_name
|
|
1631
1649
|
);
|
|
1632
1650
|
}}
|
|
1651
|
+
title="Delete file"
|
|
1633
1652
|
>
|
|
1634
1653
|
<TrashCan
|
|
1635
1654
|
strokeWidth={
|
|
@@ -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
|
-
|
|
294
|
-
|
|
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:
|
|
298
|
-
transition:
|
|
299
|
-
outline: none;
|
|
306
|
+
border-radius: 6px;
|
|
307
|
+
transition: all 0.2s ease;
|
|
300
308
|
cursor: pointer;
|
|
301
|
-
|
|
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.
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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:
|
|
344
|
-
|
|
345
|
-
gap:
|
|
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:
|
|
354
|
-
height:
|
|
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:
|
|
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:
|
|
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:
|
|
706
|
+
margin-bottom: 0; /* No bottom margin to align with auto-refresh */
|
|
660
707
|
}
|
|
661
708
|
|
|
662
709
|
.actionButtons {
|
|
@@ -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 {
|