@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.0",
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}
@@ -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
- useEffect(() => {
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
  }