@visns-studio/visns-components 5.18.0 → 5.18.1
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.18.
|
|
97
|
+
"version": "5.18.1",
|
|
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
|
*
|
|
@@ -4131,7 +4231,7 @@ const DataGrid = forwardRef(
|
|
|
4131
4231
|
}
|
|
4132
4232
|
headerProps={headerProps}
|
|
4133
4233
|
idProperty="id"
|
|
4134
|
-
minRowHeight={32}
|
|
4234
|
+
minRowHeight={isTabletMode ? 52 : 32}
|
|
4135
4235
|
onFilterValueChange={(fv) => {
|
|
4136
4236
|
handleFilterChange(fv, filterValue);
|
|
4137
4237
|
}}
|
|
@@ -4143,7 +4243,7 @@ const DataGrid = forwardRef(
|
|
|
4143
4243
|
renderRowContextMenu={renderRowContextMenu}
|
|
4144
4244
|
renderColumnContextMenu={renderColumnContextMenu}
|
|
4145
4245
|
rowClassName="vs-datagrid--row"
|
|
4146
|
-
rowHeight={null}
|
|
4246
|
+
rowHeight={isTabletMode ? 52 : null}
|
|
4147
4247
|
rowStyle={getRowStyle}
|
|
4148
4248
|
selected={selected}
|
|
4149
4249
|
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
|
}
|