@visns-studio/visns-components 5.18.0 → 5.19.0
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/package.json
CHANGED
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
95
95
|
},
|
|
96
96
|
"name": "@visns-studio/visns-components",
|
|
97
|
-
"version": "5.
|
|
97
|
+
"version": "5.19.0",
|
|
98
98
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
99
99
|
"main": "src/index.js",
|
|
100
100
|
"files": [
|
|
@@ -618,6 +618,32 @@ const DataGrid = forwardRef(
|
|
|
618
618
|
};
|
|
619
619
|
}, []);
|
|
620
620
|
|
|
621
|
+
// Track body.tablet-mode so we can fix rowHeight in tablet views.
|
|
622
|
+
// With rowHeight={null} Inovua measures each row's content, which is
|
|
623
|
+
// unreliable after auto-refresh on an idle tab (background-throttled
|
|
624
|
+
// rAF/timers). A fixed height in tablet-mode bypasses measurement
|
|
625
|
+
// entirely and prevents action buttons from being clipped.
|
|
626
|
+
const [isTabletMode, setIsTabletMode] = useState(() =>
|
|
627
|
+
typeof document !== 'undefined' &&
|
|
628
|
+
document.body.classList.contains('tablet-mode')
|
|
629
|
+
);
|
|
630
|
+
useEffect(() => {
|
|
631
|
+
if (typeof document === 'undefined') {
|
|
632
|
+
return undefined;
|
|
633
|
+
}
|
|
634
|
+
const sync = () =>
|
|
635
|
+
setIsTabletMode(
|
|
636
|
+
document.body.classList.contains('tablet-mode')
|
|
637
|
+
);
|
|
638
|
+
sync();
|
|
639
|
+
const observer = new MutationObserver(sync);
|
|
640
|
+
observer.observe(document.body, {
|
|
641
|
+
attributes: true,
|
|
642
|
+
attributeFilter: ['class'],
|
|
643
|
+
});
|
|
644
|
+
return () => observer.disconnect();
|
|
645
|
+
}, []);
|
|
646
|
+
|
|
621
647
|
// Effect to handle autoRefresh setting
|
|
622
648
|
useEffect(() => {
|
|
623
649
|
if (ajaxSetting && ajaxSetting.autoRefresh !== undefined) {
|
|
@@ -1614,6 +1640,35 @@ const DataGrid = forwardRef(
|
|
|
1614
1640
|
const sortBy = id || null;
|
|
1615
1641
|
const sort = dir === 1 ? 'asc' : dir === -1 ? 'desc' : null;
|
|
1616
1642
|
|
|
1643
|
+
// Mirror sort to the URL when opted in via ajaxSetting.syncSortToUrl.
|
|
1644
|
+
// Uses replaceState so sort changes don't pollute back-button history.
|
|
1645
|
+
// Router-library-agnostic — works regardless of whether the consumer
|
|
1646
|
+
// uses react-router, next router, or vanilla history.
|
|
1647
|
+
if (prevState.ajaxSetting?.syncSortToUrl && typeof window !== 'undefined') {
|
|
1648
|
+
try {
|
|
1649
|
+
const params = new URLSearchParams(window.location.search);
|
|
1650
|
+
if (sortBy && sort) {
|
|
1651
|
+
params.set('sortBy', sortBy);
|
|
1652
|
+
params.set('sort', sort);
|
|
1653
|
+
} else {
|
|
1654
|
+
params.delete('sortBy');
|
|
1655
|
+
params.delete('sort');
|
|
1656
|
+
}
|
|
1657
|
+
const qs = params.toString();
|
|
1658
|
+
const url =
|
|
1659
|
+
window.location.pathname +
|
|
1660
|
+
(qs ? '?' + qs : '') +
|
|
1661
|
+
window.location.hash;
|
|
1662
|
+
window.history.replaceState(
|
|
1663
|
+
window.history.state,
|
|
1664
|
+
'',
|
|
1665
|
+
url
|
|
1666
|
+
);
|
|
1667
|
+
} catch (e) {
|
|
1668
|
+
// URL sync is best-effort; never block sort behaviour
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1617
1672
|
return {
|
|
1618
1673
|
...prevState,
|
|
1619
1674
|
ajaxSetting: {
|
|
@@ -1626,6 +1681,51 @@ const DataGrid = forwardRef(
|
|
|
1626
1681
|
}
|
|
1627
1682
|
};
|
|
1628
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* Hydrate sort state from URL on first mount when ajaxSetting.syncSortToUrl
|
|
1686
|
+
* is true. Trade-off: the initial fetch may use the default (config) sort
|
|
1687
|
+
* before this effect re-fires the fetch with URL-derived sort. For typical
|
|
1688
|
+
* paginated admin tables that one-extra-fetch is acceptable; the user sees
|
|
1689
|
+
* a brief flash of default-sorted data, then it resorts.
|
|
1690
|
+
*/
|
|
1691
|
+
const urlSortHydratedRef = useRef(false);
|
|
1692
|
+
useEffect(() => {
|
|
1693
|
+
if (
|
|
1694
|
+
!ajaxSetting?.syncSortToUrl ||
|
|
1695
|
+
urlSortHydratedRef.current ||
|
|
1696
|
+
typeof window === 'undefined'
|
|
1697
|
+
) {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
urlSortHydratedRef.current = true;
|
|
1701
|
+
let urlSortBy = null;
|
|
1702
|
+
let urlSort = null;
|
|
1703
|
+
try {
|
|
1704
|
+
const params = new URLSearchParams(window.location.search);
|
|
1705
|
+
urlSortBy = params.get('sortBy');
|
|
1706
|
+
urlSort = params.get('sort');
|
|
1707
|
+
} catch (e) {
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (!urlSortBy || (urlSort !== 'asc' && urlSort !== 'desc')) return;
|
|
1711
|
+
if (
|
|
1712
|
+
urlSortBy === ajaxSetting.sortBy &&
|
|
1713
|
+
urlSort === ajaxSetting.sort
|
|
1714
|
+
) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
if (setConfig) {
|
|
1718
|
+
setConfig((prev) => ({
|
|
1719
|
+
...prev,
|
|
1720
|
+
ajaxSetting: {
|
|
1721
|
+
...prev.ajaxSetting,
|
|
1722
|
+
sortBy: urlSortBy,
|
|
1723
|
+
sort: urlSort,
|
|
1724
|
+
},
|
|
1725
|
+
}));
|
|
1726
|
+
}
|
|
1727
|
+
}, [ajaxSetting?.syncSortToUrl]);
|
|
1728
|
+
|
|
1629
1729
|
/**
|
|
1630
1730
|
* DEBUG: Row Click Analysis
|
|
1631
1731
|
*
|
|
@@ -2700,6 +2800,27 @@ const DataGrid = forwardRef(
|
|
|
2700
2800
|
return { ...acc, ...result };
|
|
2701
2801
|
}, {});
|
|
2702
2802
|
|
|
2803
|
+
// Settings that an 'action' column renders inline render there
|
|
2804
|
+
// instead of the trailing Action column (avoids duplication).
|
|
2805
|
+
const actionColumnSettingIds = new Set(
|
|
2806
|
+
(columns || [])
|
|
2807
|
+
.filter(
|
|
2808
|
+
(c) =>
|
|
2809
|
+
c.type === 'action' &&
|
|
2810
|
+
Array.isArray(c.settings)
|
|
2811
|
+
)
|
|
2812
|
+
.flatMap((c) => c.settings)
|
|
2813
|
+
);
|
|
2814
|
+
|
|
2815
|
+
// Width to fit N action icons: tablet-mode icons have a larger
|
|
2816
|
+
// tap target/margin (~44px) than the default (~34px), plus cell
|
|
2817
|
+
// padding. Used by both action columns and the trailing column.
|
|
2818
|
+
const actionColumnWidthFor = (count) =>
|
|
2819
|
+
Math.max(
|
|
2820
|
+
100,
|
|
2821
|
+
count * (isTabletMode ? 44 : 34) + 16
|
|
2822
|
+
);
|
|
2823
|
+
|
|
2703
2824
|
const renderColumn = (column) => {
|
|
2704
2825
|
let childValue;
|
|
2705
2826
|
let columnId;
|
|
@@ -2851,6 +2972,37 @@ const DataGrid = forwardRef(
|
|
|
2851
2972
|
}
|
|
2852
2973
|
|
|
2853
2974
|
switch (column.type) {
|
|
2975
|
+
case 'action': {
|
|
2976
|
+
const actionSettings = (column.settings || [])
|
|
2977
|
+
.map((id) =>
|
|
2978
|
+
memoizedSettings.find(
|
|
2979
|
+
(s) => s.id === id
|
|
2980
|
+
)
|
|
2981
|
+
)
|
|
2982
|
+
.filter(Boolean);
|
|
2983
|
+
const width = actionColumnWidthFor(
|
|
2984
|
+
actionSettings.length
|
|
2985
|
+
);
|
|
2986
|
+
return {
|
|
2987
|
+
...commonProps,
|
|
2988
|
+
name: Array.isArray(column.id)
|
|
2989
|
+
? column.id.join('_')
|
|
2990
|
+
: column.id,
|
|
2991
|
+
header: column.label ?? '',
|
|
2992
|
+
sortable: false,
|
|
2993
|
+
defaultFlex: undefined,
|
|
2994
|
+
defaultWidth: width,
|
|
2995
|
+
minWidth: width,
|
|
2996
|
+
textAlign: 'center',
|
|
2997
|
+
render: ({ data }) => (
|
|
2998
|
+
<div className={styles.tdactions}>
|
|
2999
|
+
{actionSettings.map((setting) =>
|
|
3000
|
+
renderSetting(setting, data)
|
|
3001
|
+
)}
|
|
3002
|
+
</div>
|
|
3003
|
+
),
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
2854
3006
|
case 'address':
|
|
2855
3007
|
return renderAddressColumn({
|
|
2856
3008
|
column,
|
|
@@ -3183,37 +3335,42 @@ const DataGrid = forwardRef(
|
|
|
3183
3335
|
|
|
3184
3336
|
const newColumns = columns.map(renderColumn);
|
|
3185
3337
|
|
|
3186
|
-
//
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3338
|
+
// Count settings that could render in the trailing Action
|
|
3339
|
+
// column: role-permitted AND not claimed by an 'action' column.
|
|
3340
|
+
// Per-row conditions (active/condition/hide) may hide more at
|
|
3341
|
+
// runtime, but the column must fit the maximum case.
|
|
3342
|
+
const trailingSettings = memoizedSettings.filter(
|
|
3343
|
+
(setting) => {
|
|
3344
|
+
if (actionColumnSettingIds.has(setting.id)) {
|
|
3345
|
+
return false;
|
|
3346
|
+
}
|
|
3193
3347
|
if (
|
|
3194
3348
|
setting.roles &&
|
|
3195
3349
|
Array.isArray(setting.roles) &&
|
|
3196
3350
|
setting.roles.length > 0
|
|
3197
3351
|
) {
|
|
3198
|
-
|
|
3199
|
-
(
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
)
|
|
3352
|
+
return setting.roles.some((requiredRole) =>
|
|
3353
|
+
userProfile?.roles?.some(
|
|
3354
|
+
(userRole) =>
|
|
3355
|
+
userRole.name === requiredRole
|
|
3356
|
+
)
|
|
3204
3357
|
);
|
|
3205
|
-
return hasRequiredRole;
|
|
3206
3358
|
}
|
|
3207
3359
|
// If no roles specified, setting is visible
|
|
3208
3360
|
return true;
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3361
|
+
}
|
|
3362
|
+
);
|
|
3363
|
+
|
|
3364
|
+
if (trailingSettings.length > 0) {
|
|
3365
|
+
const actionColumnWidth = actionColumnWidthFor(
|
|
3366
|
+
trailingSettings.length
|
|
3367
|
+
);
|
|
3211
3368
|
|
|
3212
|
-
if (hasVisibleSettings()) {
|
|
3213
3369
|
newColumns.push({
|
|
3214
3370
|
name: 'setting',
|
|
3215
3371
|
header: 'Action',
|
|
3216
|
-
defaultWidth:
|
|
3372
|
+
defaultWidth: actionColumnWidth,
|
|
3373
|
+
minWidth: actionColumnWidth,
|
|
3217
3374
|
textAlign: 'center',
|
|
3218
3375
|
textVerticalAlign:
|
|
3219
3376
|
style && style.text_vertical_align
|
|
@@ -3222,9 +3379,16 @@ const DataGrid = forwardRef(
|
|
|
3222
3379
|
render: ({ data }) => {
|
|
3223
3380
|
return (
|
|
3224
3381
|
<div className={styles.tdactions}>
|
|
3225
|
-
{memoizedSettings
|
|
3226
|
-
|
|
3227
|
-
|
|
3382
|
+
{memoizedSettings
|
|
3383
|
+
.filter(
|
|
3384
|
+
(setting) =>
|
|
3385
|
+
!actionColumnSettingIds.has(
|
|
3386
|
+
setting.id
|
|
3387
|
+
)
|
|
3388
|
+
)
|
|
3389
|
+
.map((setting) =>
|
|
3390
|
+
renderSetting(setting, data)
|
|
3391
|
+
)}
|
|
3228
3392
|
</div>
|
|
3229
3393
|
);
|
|
3230
3394
|
},
|
|
@@ -3305,7 +3469,7 @@ const DataGrid = forwardRef(
|
|
|
3305
3469
|
.catch((error) => {
|
|
3306
3470
|
console.error('Error in fetching dropdown data: ', error);
|
|
3307
3471
|
});
|
|
3308
|
-
}, [columns, filterDataSource, memoizedSettings]);
|
|
3472
|
+
}, [columns, filterDataSource, memoizedSettings, isTabletMode]);
|
|
3309
3473
|
|
|
3310
3474
|
// Force re-render when filterDataSource updates to ensure dropdown filters get their data
|
|
3311
3475
|
const [renderKey, setRenderKey] = useState(0);
|
|
@@ -4131,7 +4295,7 @@ const DataGrid = forwardRef(
|
|
|
4131
4295
|
}
|
|
4132
4296
|
headerProps={headerProps}
|
|
4133
4297
|
idProperty="id"
|
|
4134
|
-
minRowHeight={32}
|
|
4298
|
+
minRowHeight={isTabletMode ? 52 : 32}
|
|
4135
4299
|
onFilterValueChange={(fv) => {
|
|
4136
4300
|
handleFilterChange(fv, filterValue);
|
|
4137
4301
|
}}
|
|
@@ -4143,7 +4307,7 @@ const DataGrid = forwardRef(
|
|
|
4143
4307
|
renderRowContextMenu={renderRowContextMenu}
|
|
4144
4308
|
renderColumnContextMenu={renderColumnContextMenu}
|
|
4145
4309
|
rowClassName="vs-datagrid--row"
|
|
4146
|
-
rowHeight={null}
|
|
4310
|
+
rowHeight={isTabletMode ? 52 : null}
|
|
4147
4311
|
rowStyle={getRowStyle}
|
|
4148
4312
|
selected={selected}
|
|
4149
4313
|
showZebraRows={true}
|
package/src/components/Form.jsx
CHANGED
|
@@ -2253,6 +2253,38 @@ function Form({
|
|
|
2253
2253
|
}
|
|
2254
2254
|
}, [formSettings?.autoSubmit]);
|
|
2255
2255
|
|
|
2256
|
+
// Auto-focus a named field on mount. Pass the field id as
|
|
2257
|
+
// formSettings.autoFocus, e.g. {"autoFocus": "prefix"}. We wait one
|
|
2258
|
+
// tick for the modal animation to finish so the input is actually
|
|
2259
|
+
// mounted and the browser will honour focus(). Falls back to the
|
|
2260
|
+
// first non-hidden text-ish input if no name match is found.
|
|
2261
|
+
useEffect(() => {
|
|
2262
|
+
if (!formSettings?.autoFocus) return;
|
|
2263
|
+
const target = formSettings.autoFocus;
|
|
2264
|
+
const timer = setTimeout(() => {
|
|
2265
|
+
let el = null;
|
|
2266
|
+
if (typeof target === 'string' && target) {
|
|
2267
|
+
el = document.querySelector(`[name="${target}"]`);
|
|
2268
|
+
}
|
|
2269
|
+
if (!el) {
|
|
2270
|
+
el = document.querySelector(
|
|
2271
|
+
'form input:not([type="hidden"]):not([disabled]), form textarea:not([disabled]), form select:not([disabled])'
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
if (el && typeof el.focus === 'function') {
|
|
2275
|
+
el.focus();
|
|
2276
|
+
if (typeof el.select === 'function') {
|
|
2277
|
+
try {
|
|
2278
|
+
el.select();
|
|
2279
|
+
} catch (e) {
|
|
2280
|
+
// some input types throw on select(); ignore
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}, 150);
|
|
2285
|
+
return () => clearTimeout(timer);
|
|
2286
|
+
}, [formSettings?.autoFocus]);
|
|
2287
|
+
|
|
2256
2288
|
useEffect(() => {
|
|
2257
2289
|
formSettings.fields.forEach((field) => {
|
|
2258
2290
|
// showIfEmpty: only show field when its own value is empty/null/undefined/[]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '../styles/global.css';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react';
|
|
4
4
|
import { useParams, Outlet } from 'react-router-dom';
|
|
5
5
|
import { toast } from 'react-toastify';
|
|
6
6
|
import { saveAs } from 'file-saver';
|
|
@@ -245,7 +245,10 @@ function GenericIndex({
|
|
|
245
245
|
// Toggle a body-level "tablet-mode" class when this view opts in via
|
|
246
246
|
// setting.tabletMode. Lets consumer projects style the whole UI
|
|
247
247
|
// (including app-level chrome) for tablet use without touching this lib.
|
|
248
|
-
|
|
248
|
+
// useInsertionEffect fires before child useLayoutEffects, so the class
|
|
249
|
+
// lands before the DataGrid measures row heights — otherwise rows can
|
|
250
|
+
// be sized for the non-tablet CSS and clip larger tablet-mode buttons.
|
|
251
|
+
useInsertionEffect(() => {
|
|
249
252
|
if (!setting?.tabletMode) {
|
|
250
253
|
return undefined;
|
|
251
254
|
}
|