@visns-studio/visns-components 5.16.3 → 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.
|
|
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) {
|
|
@@ -921,6 +947,36 @@ const DataGrid = forwardRef(
|
|
|
921
947
|
[selected, ajaxSetting]
|
|
922
948
|
);
|
|
923
949
|
|
|
950
|
+
const isGroupPartiallySelected = useCallback(
|
|
951
|
+
(groupValue) => {
|
|
952
|
+
if (!dataRef.current || !ajaxSetting?.groupBy) {
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const groupField = ajaxSetting.groupBy[0];
|
|
957
|
+
const validRows = dataRef.current.filter(
|
|
958
|
+
(row) =>
|
|
959
|
+
row &&
|
|
960
|
+
row[groupField] === groupValue &&
|
|
961
|
+
row.id !== undefined &&
|
|
962
|
+
row.id !== null
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
if (validRows.length === 0) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const selectedCount = validRows.filter(
|
|
970
|
+
(row) =>
|
|
971
|
+
selected[row.id] &&
|
|
972
|
+
!selected[row.id].__group
|
|
973
|
+
).length;
|
|
974
|
+
|
|
975
|
+
return selectedCount > 0 && selectedCount < validRows.length;
|
|
976
|
+
},
|
|
977
|
+
[selected, ajaxSetting]
|
|
978
|
+
);
|
|
979
|
+
|
|
924
980
|
const findUpdatedFilter = (filters, currentValues) => {
|
|
925
981
|
for (const filter of filters) {
|
|
926
982
|
const matchingCurrentValue = currentValues.find(
|
|
@@ -1398,11 +1454,21 @@ const DataGrid = forwardRef(
|
|
|
1398
1454
|
// Check if all show conditions are met
|
|
1399
1455
|
return iconConfig.show.every((condition) => {
|
|
1400
1456
|
const { id, value } = condition;
|
|
1457
|
+
const hasValueProp = condition.hasOwnProperty('value');
|
|
1401
1458
|
|
|
1402
1459
|
// Check if any row in the group matches the condition
|
|
1403
1460
|
return groupRows.some((row) => {
|
|
1404
1461
|
const rowValue = row[id];
|
|
1405
1462
|
|
|
1463
|
+
// No value prop → field must be truthy (set/non-empty)
|
|
1464
|
+
if (!hasValueProp) {
|
|
1465
|
+
return (
|
|
1466
|
+
rowValue !== null &&
|
|
1467
|
+
rowValue !== undefined &&
|
|
1468
|
+
rowValue !== ''
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1406
1472
|
// Handle null value condition
|
|
1407
1473
|
if (value === null) {
|
|
1408
1474
|
return (
|
|
@@ -1574,6 +1640,35 @@ const DataGrid = forwardRef(
|
|
|
1574
1640
|
const sortBy = id || null;
|
|
1575
1641
|
const sort = dir === 1 ? 'asc' : dir === -1 ? 'desc' : null;
|
|
1576
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
|
+
|
|
1577
1672
|
return {
|
|
1578
1673
|
...prevState,
|
|
1579
1674
|
ajaxSetting: {
|
|
@@ -1586,6 +1681,51 @@ const DataGrid = forwardRef(
|
|
|
1586
1681
|
}
|
|
1587
1682
|
};
|
|
1588
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
|
+
|
|
1589
1729
|
/**
|
|
1590
1730
|
* DEBUG: Row Click Analysis
|
|
1591
1731
|
*
|
|
@@ -3532,6 +3672,7 @@ const DataGrid = forwardRef(
|
|
|
3532
3672
|
|
|
3533
3673
|
{/* DataGrid container with its own border */}
|
|
3534
3674
|
<div className={styles.dataGridContainer}>
|
|
3675
|
+
|
|
3535
3676
|
{limit && limit > 0 ? (
|
|
3536
3677
|
<ReactDataGrid
|
|
3537
3678
|
key={`datagrid-${ajaxSetting?.url || ''}-${
|
|
@@ -3754,6 +3895,37 @@ const DataGrid = forwardRef(
|
|
|
3754
3895
|
styles.groupActionContainer
|
|
3755
3896
|
}
|
|
3756
3897
|
>
|
|
3898
|
+
{tableSetting?.checkboxColumn && (
|
|
3899
|
+
<input
|
|
3900
|
+
type="checkbox"
|
|
3901
|
+
className={
|
|
3902
|
+
styles.groupHeaderCheckbox
|
|
3903
|
+
}
|
|
3904
|
+
checked={isGroupSelected(
|
|
3905
|
+
groupValue
|
|
3906
|
+
)}
|
|
3907
|
+
ref={(el) => {
|
|
3908
|
+
if (el) {
|
|
3909
|
+
el.indeterminate =
|
|
3910
|
+
isGroupPartiallySelected(
|
|
3911
|
+
groupValue
|
|
3912
|
+
);
|
|
3913
|
+
}
|
|
3914
|
+
}}
|
|
3915
|
+
onClick={(e) =>
|
|
3916
|
+
e.stopPropagation()
|
|
3917
|
+
}
|
|
3918
|
+
onChange={(e) => {
|
|
3919
|
+
e.stopPropagation();
|
|
3920
|
+
handleGroupSelectionChange(
|
|
3921
|
+
groupValue,
|
|
3922
|
+
e.target
|
|
3923
|
+
.checked
|
|
3924
|
+
);
|
|
3925
|
+
}}
|
|
3926
|
+
aria-label="Select all items in this group"
|
|
3927
|
+
/>
|
|
3928
|
+
)}
|
|
3757
3929
|
<button
|
|
3758
3930
|
className={`group-expand-btn ${styles.groupExpandBtn}`}
|
|
3759
3931
|
onClick={(e) => {
|
|
@@ -4059,7 +4231,7 @@ const DataGrid = forwardRef(
|
|
|
4059
4231
|
}
|
|
4060
4232
|
headerProps={headerProps}
|
|
4061
4233
|
idProperty="id"
|
|
4062
|
-
minRowHeight={32}
|
|
4234
|
+
minRowHeight={isTabletMode ? 52 : 32}
|
|
4063
4235
|
onFilterValueChange={(fv) => {
|
|
4064
4236
|
handleFilterChange(fv, filterValue);
|
|
4065
4237
|
}}
|
|
@@ -4071,7 +4243,7 @@ const DataGrid = forwardRef(
|
|
|
4071
4243
|
renderRowContextMenu={renderRowContextMenu}
|
|
4072
4244
|
renderColumnContextMenu={renderColumnContextMenu}
|
|
4073
4245
|
rowClassName="vs-datagrid--row"
|
|
4074
|
-
rowHeight={null}
|
|
4246
|
+
rowHeight={isTabletMode ? 52 : null}
|
|
4075
4247
|
rowStyle={getRowStyle}
|
|
4076
4248
|
selected={selected}
|
|
4077
4249
|
showZebraRows={true}
|
package/src/components/Form.jsx
CHANGED
|
@@ -2229,6 +2229,62 @@ function Form({
|
|
|
2229
2229
|
}
|
|
2230
2230
|
}, [tableData.dataSource]);
|
|
2231
2231
|
|
|
2232
|
+
// Auto-submit on mount when formSettings.autoSubmit is true.
|
|
2233
|
+
// Used for "report" tabs that should show their results immediately instead of
|
|
2234
|
+
// requiring the user to click "View Report" with an empty filter form.
|
|
2235
|
+
// We trigger an actual click on the rendered Save button so the path is
|
|
2236
|
+
// identical to the user clicking it manually.
|
|
2237
|
+
const autoSubmittedRef = useRef(false);
|
|
2238
|
+
useEffect(() => {
|
|
2239
|
+
if (formSettings?.autoSubmit && !autoSubmittedRef.current) {
|
|
2240
|
+
autoSubmittedRef.current = true;
|
|
2241
|
+
const timer = setTimeout(() => {
|
|
2242
|
+
const saveBtn = document.querySelector('[data-type="save"]');
|
|
2243
|
+
if (saveBtn) {
|
|
2244
|
+
saveBtn.click();
|
|
2245
|
+
} else {
|
|
2246
|
+
handleSubmit({
|
|
2247
|
+
preventDefault: () => {},
|
|
2248
|
+
target: { dataset: { type: 'save' } },
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}, 150);
|
|
2252
|
+
return () => clearTimeout(timer);
|
|
2253
|
+
}
|
|
2254
|
+
}, [formSettings?.autoSubmit]);
|
|
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
|
+
|
|
2232
2288
|
useEffect(() => {
|
|
2233
2289
|
formSettings.fields.forEach((field) => {
|
|
2234
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';
|
|
@@ -241,6 +241,23 @@ function GenericIndex({
|
|
|
241
241
|
const [customActionFormData, setCustomActionFormData] = useState({});
|
|
242
242
|
const [customActionConfig, setCustomActionConfig] = useState(null);
|
|
243
243
|
const { windowHeight, windowWidth } = useWindowDimensions();
|
|
244
|
+
|
|
245
|
+
// Toggle a body-level "tablet-mode" class when this view opts in via
|
|
246
|
+
// setting.tabletMode. Lets consumer projects style the whole UI
|
|
247
|
+
// (including app-level chrome) for tablet use without touching this lib.
|
|
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(() => {
|
|
252
|
+
if (!setting?.tabletMode) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
document.body.classList.add('tablet-mode');
|
|
256
|
+
return () => {
|
|
257
|
+
document.body.classList.remove('tablet-mode');
|
|
258
|
+
};
|
|
259
|
+
}, [setting?.tabletMode]);
|
|
260
|
+
|
|
244
261
|
const { config, setConfig, subnav, setSubnav } = useConfig(
|
|
245
262
|
setting,
|
|
246
263
|
setting.tabs,
|
|
@@ -879,6 +879,21 @@
|
|
|
879
879
|
position: relative;
|
|
880
880
|
}
|
|
881
881
|
|
|
882
|
+
.groupHeaderCheckbox {
|
|
883
|
+
width: 22px;
|
|
884
|
+
height: 22px;
|
|
885
|
+
margin-right: 12px;
|
|
886
|
+
cursor: pointer;
|
|
887
|
+
accent-color: var(--primary-color, #3b82f6);
|
|
888
|
+
flex-shrink: 0;
|
|
889
|
+
border-radius: 4px;
|
|
890
|
+
transition: transform 0.15s ease;
|
|
891
|
+
|
|
892
|
+
&:hover {
|
|
893
|
+
transform: scale(1.08);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
882
897
|
.groupExpandBtn {
|
|
883
898
|
background: var(--primary-color, #3b82f6);
|
|
884
899
|
border: none;
|
|
@@ -1200,3 +1215,5 @@
|
|
|
1200
1215
|
transform: scale(1.2);
|
|
1201
1216
|
}
|
|
1202
1217
|
}
|
|
1218
|
+
|
|
1219
|
+
|