@visns-studio/visns-components 6.6.3 → 6.6.4
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
CHANGED
|
@@ -607,6 +607,41 @@ The DataGrid component utilizes a modular column renderer system with 28 special
|
|
|
607
607
|
|
|
608
608
|
All column renderers are exported from `@visns-studio/visns-components` and can be used individually or as part of the DataGrid component.
|
|
609
609
|
|
|
610
|
+
#### `autoGrow` — rows that outgrow the fixed tablet row height
|
|
611
|
+
|
|
612
|
+
In tablet mode (and in `large` density) the DataGrid runs at a **fixed** row
|
|
613
|
+
height from `DENSITY_GRID` — 52px or 60px — because the alternative, letting
|
|
614
|
+
the grid measure every row, depends on a rAF that a background-throttled tab
|
|
615
|
+
never runs, which clipped action buttons after an auto-refresh. The cost is
|
|
616
|
+
that a cell whose content genuinely needs a second or third line (a
|
|
617
|
+
`stageCounter` with enough chips to wrap) is cut off at the row height.
|
|
618
|
+
|
|
619
|
+
`"autoGrow": true` on a column buys those rows back, and only those rows:
|
|
620
|
+
|
|
621
|
+
```jsx
|
|
622
|
+
{
|
|
623
|
+
id: 'headers',
|
|
624
|
+
label: 'Headers',
|
|
625
|
+
type: 'stageCounter',
|
|
626
|
+
autoGrow: true,
|
|
627
|
+
stageConfig: { key: 'headers', stageLabelKey: 'name' }
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
- The cell's content is measured with a `ResizeObserver`; a row whose content
|
|
632
|
+
needs more than the fixed height grows to fit it, and every other row keeps
|
|
633
|
+
the fixed height exactly as before.
|
|
634
|
+
- The added height is the content's natural height plus the cell's **measured**
|
|
635
|
+
vertical padding and borders, so it tracks `--grid-cell-padding` and the
|
|
636
|
+
density in force rather than a constant.
|
|
637
|
+
- **Desktop density is unaffected** (`rowHeight` is `null` there, so rows
|
|
638
|
+
already measure naturally) and a grid with no `autoGrow` column takes exactly
|
|
639
|
+
the code path it did before.
|
|
640
|
+
- Group rows (`groupBy`) keep the default height; `treeGroupBy` parents and
|
|
641
|
+
members grow like any other row.
|
|
642
|
+
- Grown rows stay vertically centred, so neighbouring cells and the action
|
|
643
|
+
buttons line up as they always have.
|
|
644
|
+
|
|
610
645
|
#### Dropdown Column Color Support
|
|
611
646
|
|
|
612
647
|
The dropdown column type now supports automatic background color application based on the selected option's `colour` property. This feature works in both DataGrid and GenericEditableTable components.
|
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.6.
|
|
94
|
+
"version": "6.6.4",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -144,6 +144,7 @@ import {
|
|
|
144
144
|
renderPhoneColumn,
|
|
145
145
|
renderAbnColumn,
|
|
146
146
|
} from './columns/ColumnRenderers.jsx';
|
|
147
|
+
import AutoGrowCell from './columns/AutoGrowCell.jsx';
|
|
147
148
|
import _ from 'lodash';
|
|
148
149
|
|
|
149
150
|
// Enhanced multi-line content support for data cells
|
|
@@ -959,6 +960,197 @@ const DataGrid = forwardRef(
|
|
|
959
960
|
// prevents action buttons from being clipped.
|
|
960
961
|
const { grid } = useDensity();
|
|
961
962
|
|
|
963
|
+
// ---------------------------------------------------------------
|
|
964
|
+
// Per-column autoGrow
|
|
965
|
+
// ---------------------------------------------------------------
|
|
966
|
+
//
|
|
967
|
+
// A column with `"autoGrow": true` lets the ROWS whose content does
|
|
968
|
+
// not fit the fixed height above grow to fit — and only those rows.
|
|
969
|
+
// Nothing here runs unless such a column exists AND the density gives
|
|
970
|
+
// a numeric `grid.rowHeight`; on desktop (`rowHeight === null`) the
|
|
971
|
+
// grid already measures every row, so autoGrow has nothing to add.
|
|
972
|
+
//
|
|
973
|
+
// Why this drives the grid's RowHeightManager instead of passing
|
|
974
|
+
// `rowHeight` as a function: the datagrid's types advertise
|
|
975
|
+
// `rowHeight: number | ((rowIndex) => number) | null`, but in this
|
|
976
|
+
// fork the function form is not implemented. `RowHeightManager`
|
|
977
|
+
// stores it as `rowHeightFn` and never reads it again, and
|
|
978
|
+
// `factory.tsx` derives `naturalRowHeight: typeof props.rowHeight !==
|
|
979
|
+
// 'number'` — so a function rowHeight silently turns on exactly the
|
|
980
|
+
// lazy per-row rAF measurement the fixed height exists to avoid.
|
|
981
|
+
//
|
|
982
|
+
// What IS implemented is the manager's per-index override map: every
|
|
983
|
+
// row's height comes from `rowHeightManager.getRowHeight(index)`
|
|
984
|
+
// (packages/react-virtual-list-pro/src/Row.tsx), which reads that map
|
|
985
|
+
// first and falls back to the fixed height; `setHeights(map)`
|
|
986
|
+
// re-indexes the offsets and emits `index`, which `VirtualList`
|
|
987
|
+
// listens for and answers with a full re-layout. The cells follow the
|
|
988
|
+
// row because Content/Row.tsx passes the row's own height down as the
|
|
989
|
+
// cell's `initialRowHeight` on unexpanded rows, so a grown row's other
|
|
990
|
+
// cells and its action buttons stay vertically centred in the taller
|
|
991
|
+
// box exactly as they are today.
|
|
992
|
+
//
|
|
993
|
+
// Heights are keyed by ROW ID (stable across sorting, filtering and
|
|
994
|
+
// virtualised scrolling) and only converted to the manager's
|
|
995
|
+
// index-keyed map at flush time, from the grid's own computed data —
|
|
996
|
+
// which includes group rows, so indexes stay right under groupBy/tree.
|
|
997
|
+
const autoGrowHeightsRef = useRef(new Map());
|
|
998
|
+
const autoGrowRowIndexRef = useRef(new Map());
|
|
999
|
+
const autoGrowAppliedRef = useRef(null);
|
|
1000
|
+
const autoGrowRafRef = useRef(0);
|
|
1001
|
+
|
|
1002
|
+
const flushAutoGrowHeights = useCallback(() => {
|
|
1003
|
+
autoGrowRafRef.current = 0;
|
|
1004
|
+
|
|
1005
|
+
const gridApi = gridRef.current;
|
|
1006
|
+
const manager = gridApi?.rowHeightManager;
|
|
1007
|
+
const baseRowHeight = grid.rowHeight;
|
|
1008
|
+
|
|
1009
|
+
if (!manager || typeof baseRowHeight !== 'number') {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const heights = autoGrowHeightsRef.current;
|
|
1014
|
+
const next = {};
|
|
1015
|
+
|
|
1016
|
+
// The grid's computed data is the authority on index → row: it is
|
|
1017
|
+
// the array the virtual list indexes into, group rows included.
|
|
1018
|
+
// The `onRenderRow` record is only a fallback for the frame
|
|
1019
|
+
// before `handle` has published the computed props.
|
|
1020
|
+
const data = Array.isArray(gridApi?.data) ? gridApi.data : null;
|
|
1021
|
+
|
|
1022
|
+
if (data) {
|
|
1023
|
+
const present = new Set();
|
|
1024
|
+
|
|
1025
|
+
for (let index = 0; index < data.length; index++) {
|
|
1026
|
+
const row = data[index];
|
|
1027
|
+
|
|
1028
|
+
// Group rows keep the default height — they carry a title,
|
|
1029
|
+
// not a column's content.
|
|
1030
|
+
if (!row || row.__group) {
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const rowId = row.id;
|
|
1035
|
+
|
|
1036
|
+
if (rowId === undefined || rowId === null) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const key = String(rowId);
|
|
1041
|
+
|
|
1042
|
+
present.add(key);
|
|
1043
|
+
|
|
1044
|
+
const measured = heights.get(key);
|
|
1045
|
+
|
|
1046
|
+
if (measured > baseRowHeight) {
|
|
1047
|
+
next[index] = measured;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Rows that left the page cannot be measured again, so drop
|
|
1052
|
+
// their heights rather than letting the map grow forever.
|
|
1053
|
+
heights.forEach((_value, key) => {
|
|
1054
|
+
if (!present.has(key)) {
|
|
1055
|
+
heights.delete(key);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
} else {
|
|
1059
|
+
autoGrowRowIndexRef.current.forEach((key, index) => {
|
|
1060
|
+
const measured = heights.get(key);
|
|
1061
|
+
|
|
1062
|
+
if (measured > baseRowHeight) {
|
|
1063
|
+
next[index] = measured;
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// setHeights re-indexes and emits, and the re-layout re-renders
|
|
1069
|
+
// the rows — which measures them again. Applying an unchanged map
|
|
1070
|
+
// would therefore loop, so compare before writing.
|
|
1071
|
+
const serialised = JSON.stringify(next);
|
|
1072
|
+
|
|
1073
|
+
if (serialised === autoGrowAppliedRef.current) {
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
autoGrowAppliedRef.current = serialised;
|
|
1078
|
+
|
|
1079
|
+
if (typeof manager.setHeights === 'function') {
|
|
1080
|
+
manager.setHeights(next);
|
|
1081
|
+
}
|
|
1082
|
+
}, [grid]);
|
|
1083
|
+
|
|
1084
|
+
// One flush per frame: a page of 100 rows measuring on mount is 100
|
|
1085
|
+
// reports and one re-layout.
|
|
1086
|
+
const scheduleAutoGrowFlush = useCallback(() => {
|
|
1087
|
+
if (autoGrowRafRef.current) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
1092
|
+
flushAutoGrowHeights();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
autoGrowRafRef.current =
|
|
1097
|
+
requestAnimationFrame(flushAutoGrowHeights);
|
|
1098
|
+
}, [flushAutoGrowHeights]);
|
|
1099
|
+
|
|
1100
|
+
/** `AutoGrowCell`'s callback: one row's natural height, in px. */
|
|
1101
|
+
const reportAutoGrowHeight = useCallback(
|
|
1102
|
+
(rowId, height, force) => {
|
|
1103
|
+
if (
|
|
1104
|
+
rowId === undefined ||
|
|
1105
|
+
rowId === null ||
|
|
1106
|
+
!Number.isFinite(height)
|
|
1107
|
+
) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const key = String(rowId);
|
|
1112
|
+
const previous = autoGrowHeightsRef.current.get(key);
|
|
1113
|
+
const changed =
|
|
1114
|
+
previous === undefined || Math.abs(previous - height) > 1;
|
|
1115
|
+
|
|
1116
|
+
if (changed) {
|
|
1117
|
+
autoGrowHeightsRef.current.set(key, height);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// `force` covers the mount/row-changed case: the height may be
|
|
1121
|
+
// identical while the INDEX it has to be applied at is not.
|
|
1122
|
+
if (changed || force) {
|
|
1123
|
+
scheduleAutoGrowFlush();
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
[scheduleAutoGrowFlush]
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
useEffect(
|
|
1130
|
+
() => () => {
|
|
1131
|
+
if (
|
|
1132
|
+
autoGrowRafRef.current &&
|
|
1133
|
+
typeof cancelAnimationFrame !== 'undefined'
|
|
1134
|
+
) {
|
|
1135
|
+
cancelAnimationFrame(autoGrowRafRef.current);
|
|
1136
|
+
autoGrowRafRef.current = 0;
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
[]
|
|
1140
|
+
);
|
|
1141
|
+
|
|
1142
|
+
// Leaving the fixed-height densities hands row heights back to the
|
|
1143
|
+
// grid; anything measured under the old density is meaningless.
|
|
1144
|
+
useEffect(() => {
|
|
1145
|
+
if (typeof grid.rowHeight === 'number') {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
autoGrowHeightsRef.current.clear();
|
|
1150
|
+
autoGrowRowIndexRef.current.clear();
|
|
1151
|
+
autoGrowAppliedRef.current = null;
|
|
1152
|
+
}, [grid]);
|
|
1153
|
+
|
|
962
1154
|
// Effect to handle autoRefresh setting
|
|
963
1155
|
useEffect(() => {
|
|
964
1156
|
if (ajaxSetting && ajaxSetting.autoRefresh !== undefined) {
|
|
@@ -1921,7 +2113,11 @@ const DataGrid = forwardRef(
|
|
|
1921
2113
|
}
|
|
1922
2114
|
|
|
1923
2115
|
// Show confirmation dialog for other actions to prevent accidental clicks
|
|
1924
|
-
const confirmMessage = `Are you sure you want to perform "${
|
|
2116
|
+
const confirmMessage = `Are you sure you want to perform "${
|
|
2117
|
+
iconConfig.label
|
|
2118
|
+
}" action on group "${getGroupDisplayLabel(groupValue)}" (${
|
|
2119
|
+
groupRows.length
|
|
2120
|
+
} rows)?`;
|
|
1925
2121
|
|
|
1926
2122
|
confirmDialog({
|
|
1927
2123
|
title: 'Confirm Group Action',
|
|
@@ -1959,6 +2155,28 @@ const DataGrid = forwardRef(
|
|
|
1959
2155
|
);
|
|
1960
2156
|
};
|
|
1961
2157
|
|
|
2158
|
+
/**
|
|
2159
|
+
* `groupBySetting.labelKey` is display-only: when set, the group row
|
|
2160
|
+
* shows the named field taken from the group's first row instead of
|
|
2161
|
+
* the raw group value (falling back to it when the field is empty).
|
|
2162
|
+
* The group KEY is still `groupValue` — it decides membership,
|
|
2163
|
+
* selection, collapse state, `handleGroupAction`/`shouldShowGroupAction`
|
|
2164
|
+
* and what the backend receives (`groupLabel`).
|
|
2165
|
+
*/
|
|
2166
|
+
const getGroupDisplayLabel = (groupValue) => {
|
|
2167
|
+
const labelKey = ajaxSetting?.groupBySetting?.labelKey;
|
|
2168
|
+
|
|
2169
|
+
if (!labelKey) {
|
|
2170
|
+
return groupValue;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
const label = getGroupRows(groupValue)[0]?.[labelKey];
|
|
2174
|
+
|
|
2175
|
+
return label === null || label === undefined || label === ''
|
|
2176
|
+
? groupValue
|
|
2177
|
+
: label;
|
|
2178
|
+
};
|
|
2179
|
+
|
|
1962
2180
|
/**
|
|
1963
2181
|
* The show-condition grammar shared by group action icons and row
|
|
1964
2182
|
* action icons:
|
|
@@ -2096,7 +2314,9 @@ const DataGrid = forwardRef(
|
|
|
2096
2314
|
iconConfig.fetch.messageFromResponse &&
|
|
2097
2315
|
result?.data?.message
|
|
2098
2316
|
? result.data.message
|
|
2099
|
-
: `${iconConfig.label} - Group: ${
|
|
2317
|
+
: `${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2318
|
+
groupValue
|
|
2319
|
+
)} completed successfully`
|
|
2100
2320
|
);
|
|
2101
2321
|
|
|
2102
2322
|
// Reload grid data if needed
|
|
@@ -2113,7 +2333,9 @@ const DataGrid = forwardRef(
|
|
|
2113
2333
|
} catch (error) {
|
|
2114
2334
|
console.error('Group action fetch error:', error);
|
|
2115
2335
|
toast.error(
|
|
2116
|
-
`${iconConfig.label} - Group: ${
|
|
2336
|
+
`${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2337
|
+
groupValue
|
|
2338
|
+
)} failed: ${error.message}`
|
|
2117
2339
|
);
|
|
2118
2340
|
}
|
|
2119
2341
|
} else if (iconConfig.formModal) {
|
|
@@ -2137,7 +2359,11 @@ const DataGrid = forwardRef(
|
|
|
2137
2359
|
// Fallback to existing switch logic if no fetch config
|
|
2138
2360
|
// Skip toast notification for edit actions
|
|
2139
2361
|
if (iconConfig.id !== 'edit') {
|
|
2140
|
-
toast.info(
|
|
2362
|
+
toast.info(
|
|
2363
|
+
`${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2364
|
+
groupValue
|
|
2365
|
+
)}`
|
|
2366
|
+
);
|
|
2141
2367
|
}
|
|
2142
2368
|
|
|
2143
2369
|
switch (iconConfig.id) {
|
|
@@ -2684,6 +2910,26 @@ const DataGrid = forwardRef(
|
|
|
2684
2910
|
|
|
2685
2911
|
const onRenderRow = useCallback(
|
|
2686
2912
|
(rowProps) => {
|
|
2913
|
+
// Index → row id, the fallback map autoGrow flushes from
|
|
2914
|
+
// before the grid has published its computed data (see the
|
|
2915
|
+
// autoGrow block above). Group rows are skipped: they keep the
|
|
2916
|
+
// default height.
|
|
2917
|
+
const autoGrowIndex =
|
|
2918
|
+
rowProps?.rowIndex ?? rowProps?.realIndex;
|
|
2919
|
+
|
|
2920
|
+
if (
|
|
2921
|
+
typeof autoGrowIndex === 'number' &&
|
|
2922
|
+
rowProps?.data &&
|
|
2923
|
+
!rowProps.data.__group &&
|
|
2924
|
+
rowProps.data.id !== undefined &&
|
|
2925
|
+
rowProps.data.id !== null
|
|
2926
|
+
) {
|
|
2927
|
+
autoGrowRowIndexRef.current.set(
|
|
2928
|
+
autoGrowIndex,
|
|
2929
|
+
String(rowProps.data.id)
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2687
2933
|
// save the original handlers to be called later
|
|
2688
2934
|
const { onClick } = rowProps;
|
|
2689
2935
|
|
|
@@ -4594,16 +4840,91 @@ const DataGrid = forwardRef(
|
|
|
4594
4840
|
};
|
|
4595
4841
|
};
|
|
4596
4842
|
|
|
4843
|
+
/**
|
|
4844
|
+
* `"autoGrow": true` on a column definition.
|
|
4845
|
+
*
|
|
4846
|
+
* Opt-in, and only meaningful where the grid runs at a
|
|
4847
|
+
* fixed row height — on desktop (`grid.rowHeight === null`)
|
|
4848
|
+
* rows already measure naturally, so this returns the
|
|
4849
|
+
* column untouched and the code path below is the one that
|
|
4850
|
+
* has always run.
|
|
4851
|
+
*
|
|
4852
|
+
* Outermost decorator on purpose: it has to measure the
|
|
4853
|
+
* FINAL content of the cell, including the tree indent and
|
|
4854
|
+
* any row-action icons the decorators below add.
|
|
4855
|
+
*/
|
|
4856
|
+
const decorateAutoGrow = (gridColumn, columnDef) => {
|
|
4857
|
+
if (
|
|
4858
|
+
columnDef?.autoGrow !== true ||
|
|
4859
|
+
typeof grid.rowHeight !== 'number' ||
|
|
4860
|
+
!gridColumn
|
|
4861
|
+
) {
|
|
4862
|
+
return gridColumn;
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
const originalRender = gridColumn.render;
|
|
4866
|
+
const existingClassName = gridColumn.className;
|
|
4867
|
+
|
|
4868
|
+
// The class relaxes the nowrap/ellipsis clamp that
|
|
4869
|
+
// global-datagrid.css puts on every non-wrapping cell;
|
|
4870
|
+
// without it the content can never take a second line
|
|
4871
|
+
// and so can never be taller than the row.
|
|
4872
|
+
const withAutoGrowClass = (base) =>
|
|
4873
|
+
[base, 'cell-auto-grow'].filter(Boolean).join(' ');
|
|
4874
|
+
|
|
4875
|
+
return {
|
|
4876
|
+
...gridColumn,
|
|
4877
|
+
className:
|
|
4878
|
+
typeof existingClassName === 'function'
|
|
4879
|
+
? (...args) =>
|
|
4880
|
+
withAutoGrowClass(
|
|
4881
|
+
existingClassName(...args)
|
|
4882
|
+
)
|
|
4883
|
+
: withAutoGrowClass(existingClassName),
|
|
4884
|
+
render: (arg) => {
|
|
4885
|
+
const content = originalRender
|
|
4886
|
+
? originalRender(arg)
|
|
4887
|
+
: arg?.value ?? null;
|
|
4888
|
+
|
|
4889
|
+
const row = arg?.data;
|
|
4890
|
+
|
|
4891
|
+
// Group rows keep the default height, and an
|
|
4892
|
+
// empty cell has nothing to grow for.
|
|
4893
|
+
if (
|
|
4894
|
+
!row ||
|
|
4895
|
+
row.__group ||
|
|
4896
|
+
content === null ||
|
|
4897
|
+
content === undefined ||
|
|
4898
|
+
content === false
|
|
4899
|
+
) {
|
|
4900
|
+
return content;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
return (
|
|
4904
|
+
<AutoGrowCell
|
|
4905
|
+
rowId={row.id}
|
|
4906
|
+
onMeasure={reportAutoGrowHeight}
|
|
4907
|
+
>
|
|
4908
|
+
{content}
|
|
4909
|
+
</AutoGrowCell>
|
|
4910
|
+
);
|
|
4911
|
+
},
|
|
4912
|
+
};
|
|
4913
|
+
};
|
|
4914
|
+
|
|
4597
4915
|
// decorateTreeChildIndent sits INSIDE
|
|
4598
4916
|
// decorateTreeParentActions: the latter draws parent rows
|
|
4599
4917
|
// itself and delegates everything else downward, so the
|
|
4600
4918
|
// member indent has to be the render it delegates to.
|
|
4601
4919
|
const newColumns = columns.map((column) =>
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4920
|
+
decorateAutoGrow(
|
|
4921
|
+
decorateColumnRowActions(
|
|
4922
|
+
decorateTreeParentActions(
|
|
4923
|
+
decorateTreeChildIndent(
|
|
4924
|
+
guardTreeParentRender(
|
|
4925
|
+
renderColumn(column),
|
|
4926
|
+
column
|
|
4927
|
+
),
|
|
4607
4928
|
column
|
|
4608
4929
|
),
|
|
4609
4930
|
column
|
|
@@ -5328,126 +5649,130 @@ const DataGrid = forwardRef(
|
|
|
5328
5649
|
aria-label="Select all items in this group"
|
|
5329
5650
|
/>
|
|
5330
5651
|
)}
|
|
5331
|
-
<
|
|
5332
|
-
className={`group-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5652
|
+
<div
|
|
5653
|
+
className={`group-controls ${styles.groupControls}`}
|
|
5654
|
+
>
|
|
5655
|
+
<button
|
|
5656
|
+
className={`group-expand-btn ${styles.groupExpandBtn}`}
|
|
5657
|
+
onClick={(e) => {
|
|
5658
|
+
e.stopPropagation();
|
|
5659
|
+
if (
|
|
5660
|
+
typeof toggleGroup ===
|
|
5661
|
+
'function'
|
|
5662
|
+
) {
|
|
5663
|
+
toggleGroup();
|
|
5664
|
+
}
|
|
5665
|
+
}}
|
|
5666
|
+
onTouchStart={(e) => {
|
|
5667
|
+
e.stopPropagation();
|
|
5668
|
+
handleTouchStart(
|
|
5669
|
+
e,
|
|
5670
|
+
'group-expand',
|
|
5671
|
+
collapsed
|
|
5672
|
+
? 'Expand group'
|
|
5673
|
+
: 'Collapse group'
|
|
5674
|
+
);
|
|
5675
|
+
}}
|
|
5676
|
+
onTouchEnd={
|
|
5677
|
+
handleTouchEnd
|
|
5340
5678
|
}
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
'group-expand',
|
|
5679
|
+
onTouchMove={
|
|
5680
|
+
handleTouchMove
|
|
5681
|
+
}
|
|
5682
|
+
data-tooltip-id="system-tooltip"
|
|
5683
|
+
data-tooltip-content={
|
|
5347
5684
|
collapsed
|
|
5348
5685
|
? 'Expand group'
|
|
5349
5686
|
: 'Collapse group'
|
|
5350
|
-
);
|
|
5351
|
-
}}
|
|
5352
|
-
onTouchEnd={
|
|
5353
|
-
handleTouchEnd
|
|
5354
|
-
}
|
|
5355
|
-
onTouchMove={
|
|
5356
|
-
handleTouchMove
|
|
5357
|
-
}
|
|
5358
|
-
data-tooltip-id="system-tooltip"
|
|
5359
|
-
data-tooltip-content={
|
|
5360
|
-
collapsed
|
|
5361
|
-
? 'Expand group'
|
|
5362
|
-
: 'Collapse group'
|
|
5363
|
-
}
|
|
5364
|
-
>
|
|
5365
|
-
{collapsed
|
|
5366
|
-
? '+'
|
|
5367
|
-
: '−'}
|
|
5368
|
-
</button>
|
|
5369
|
-
{/* Dynamic group action buttons */}
|
|
5370
|
-
{ajaxSetting?.groupBySetting?.icons?.map(
|
|
5371
|
-
(
|
|
5372
|
-
iconConfig,
|
|
5373
|
-
index
|
|
5374
|
-
) => {
|
|
5375
|
-
// Check if this icon should be shown based on show conditions
|
|
5376
|
-
if (
|
|
5377
|
-
!shouldShowGroupAction(
|
|
5378
|
-
iconConfig,
|
|
5379
|
-
groupValue
|
|
5380
|
-
)
|
|
5381
|
-
) {
|
|
5382
|
-
return null;
|
|
5383
5687
|
}
|
|
5688
|
+
>
|
|
5689
|
+
{collapsed
|
|
5690
|
+
? '+'
|
|
5691
|
+
: '−'}
|
|
5692
|
+
</button>
|
|
5693
|
+
{/* Dynamic group action buttons */}
|
|
5694
|
+
{ajaxSetting?.groupBySetting?.icons?.map(
|
|
5695
|
+
(
|
|
5696
|
+
iconConfig,
|
|
5697
|
+
index
|
|
5698
|
+
) => {
|
|
5699
|
+
// Check if this icon should be shown based on show conditions
|
|
5700
|
+
if (
|
|
5701
|
+
!shouldShowGroupAction(
|
|
5702
|
+
iconConfig,
|
|
5703
|
+
groupValue
|
|
5704
|
+
)
|
|
5705
|
+
) {
|
|
5706
|
+
return null;
|
|
5707
|
+
}
|
|
5384
5708
|
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5709
|
+
const IconComponent =
|
|
5710
|
+
getGroupIconComponent(
|
|
5711
|
+
iconConfig.id
|
|
5712
|
+
);
|
|
5389
5713
|
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
}
|
|
5431
|
-
onTouchMove={
|
|
5432
|
-
handleTouchMove
|
|
5433
|
-
}
|
|
5434
|
-
data-tooltip-id="system-tooltip"
|
|
5435
|
-
data-tooltip-content={
|
|
5436
|
-
iconConfig.label
|
|
5437
|
-
}
|
|
5438
|
-
>
|
|
5439
|
-
<IconComponent
|
|
5440
|
-
strokeWidth={
|
|
5441
|
-
2
|
|
5714
|
+
// Generate different color classes for multiple buttons
|
|
5715
|
+
const colorClasses =
|
|
5716
|
+
[
|
|
5717
|
+
'success',
|
|
5718
|
+
'warning',
|
|
5719
|
+
'info',
|
|
5720
|
+
'purple',
|
|
5721
|
+
'pink',
|
|
5722
|
+
];
|
|
5723
|
+
const colorClass =
|
|
5724
|
+
colorClasses[
|
|
5725
|
+
index %
|
|
5726
|
+
colorClasses.length
|
|
5727
|
+
];
|
|
5728
|
+
|
|
5729
|
+
return (
|
|
5730
|
+
<button
|
|
5731
|
+
key={`group-action-${index}`}
|
|
5732
|
+
className={`group-action-btn group-${iconConfig.id}-btn ${styles.groupActionBtn} ${styles[colorClass]}`}
|
|
5733
|
+
onClick={(
|
|
5734
|
+
e
|
|
5735
|
+
) => {
|
|
5736
|
+
e.stopPropagation();
|
|
5737
|
+
handleGroupAction(
|
|
5738
|
+
groupValue,
|
|
5739
|
+
iconConfig
|
|
5740
|
+
);
|
|
5741
|
+
}}
|
|
5742
|
+
onTouchStart={(
|
|
5743
|
+
e
|
|
5744
|
+
) => {
|
|
5745
|
+
e.stopPropagation();
|
|
5746
|
+
handleTouchStart(
|
|
5747
|
+
e,
|
|
5748
|
+
`group-${iconConfig.id}-${index}`,
|
|
5749
|
+
iconConfig.label
|
|
5750
|
+
);
|
|
5751
|
+
}}
|
|
5752
|
+
onTouchEnd={
|
|
5753
|
+
handleTouchEnd
|
|
5442
5754
|
}
|
|
5443
|
-
|
|
5444
|
-
|
|
5755
|
+
onTouchMove={
|
|
5756
|
+
handleTouchMove
|
|
5445
5757
|
}
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5758
|
+
data-tooltip-id="system-tooltip"
|
|
5759
|
+
data-tooltip-content={
|
|
5760
|
+
iconConfig.label
|
|
5761
|
+
}
|
|
5762
|
+
>
|
|
5763
|
+
<IconComponent
|
|
5764
|
+
strokeWidth={
|
|
5765
|
+
2
|
|
5766
|
+
}
|
|
5767
|
+
size={
|
|
5768
|
+
14
|
|
5769
|
+
}
|
|
5770
|
+
/>
|
|
5771
|
+
</button>
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5774
|
+
) || null}
|
|
5775
|
+
</div>
|
|
5451
5776
|
<div
|
|
5452
5777
|
className={
|
|
5453
5778
|
styles.groupInfo
|
|
@@ -5458,7 +5783,9 @@ const DataGrid = forwardRef(
|
|
|
5458
5783
|
styles.groupValue
|
|
5459
5784
|
}
|
|
5460
5785
|
>
|
|
5461
|
-
{
|
|
5786
|
+
{getGroupDisplayLabel(
|
|
5787
|
+
groupValue
|
|
5788
|
+
)}
|
|
5462
5789
|
{/* Important marker */}
|
|
5463
5790
|
{hasImportantMarker && (
|
|
5464
5791
|
<AlertCircle
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The measuring wrapper behind a column's `"autoGrow": true`.
|
|
5
|
+
*
|
|
6
|
+
* Why it exists
|
|
7
|
+
* -------------
|
|
8
|
+
* In tablet mode (and in `large` density generally) the grid runs with a
|
|
9
|
+
* FIXED `rowHeight` — see the density table in `utils/useDensity.js` and the
|
|
10
|
+
* comment above `rowHeight={grid.rowHeight}` in `DataGrid.jsx`. A fixed height
|
|
11
|
+
* is what keeps action buttons from being clipped after an auto-refresh on an
|
|
12
|
+
* idle tab, because Inovua's alternative (`rowHeight={null}`) measures every
|
|
13
|
+
* row with a lazily-scheduled rAF, which a background-throttled tab never runs.
|
|
14
|
+
*
|
|
15
|
+
* The cost of that trade is that a cell whose content genuinely needs three
|
|
16
|
+
* lines — a `stageCounter` column with enough chips to wrap twice — gets cut
|
|
17
|
+
* off at the row's height. `autoGrow` buys back JUST those rows: this wrapper
|
|
18
|
+
* measures the content's natural height and hands it up, and the grid raises
|
|
19
|
+
* that ONE row to fit. Every other row keeps the fixed height, so the property
|
|
20
|
+
* that made the fixed height worth having (no measurement in the common path)
|
|
21
|
+
* is preserved.
|
|
22
|
+
*
|
|
23
|
+
* How the measurement stays honest
|
|
24
|
+
* --------------------------------
|
|
25
|
+
* The cell itself is height-clamped (`.InovuaReactDataGrid__cell` gets an
|
|
26
|
+
* inline `height` from the row's height, and `global-datagrid.css` gives it
|
|
27
|
+
* `overflow: hidden`), so measuring the CELL would only ever report the clamp
|
|
28
|
+
* back. This `div` is a plain block inside the cell's content wrapper with no
|
|
29
|
+
* height of its own, so its `scrollHeight` is the content's natural height
|
|
30
|
+
* whatever the cell around it is doing.
|
|
31
|
+
*
|
|
32
|
+
* The number reported is a ROW height, not a content height: the owning cell's
|
|
33
|
+
* vertical padding and borders are read off `getComputedStyle` and added, so
|
|
34
|
+
* the figure tracks `--grid-cell-padding` and the density that sets it instead
|
|
35
|
+
* of a constant that would drift the moment either changed.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} props
|
|
38
|
+
* @param {*} props.rowId id of the row this cell belongs to; the
|
|
39
|
+
* key the grid stores the height under.
|
|
40
|
+
* @param {Function} props.onMeasure `(rowId, heightPx, force) => void`
|
|
41
|
+
* @param {import('react').ReactNode} props.children the column's own render.
|
|
42
|
+
*/
|
|
43
|
+
const AutoGrowCell = ({ rowId, onMeasure, children }) => {
|
|
44
|
+
const nodeRef = useRef(null);
|
|
45
|
+
// Last height handed up, so a ResizeObserver that fires on every column
|
|
46
|
+
// resize does not re-report a height nothing has changed about.
|
|
47
|
+
const lastReportedRef = useRef(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const node = nodeRef.current;
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
!node ||
|
|
54
|
+
rowId === undefined ||
|
|
55
|
+
rowId === null ||
|
|
56
|
+
typeof onMeasure !== 'function'
|
|
57
|
+
) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Padding + borders of the cell this content sits in. Measured rather
|
|
63
|
+
* than assumed: `--grid-cell-padding` is density-driven and a caller
|
|
64
|
+
* can override it per grid.
|
|
65
|
+
*/
|
|
66
|
+
const cellChrome = () => {
|
|
67
|
+
if (
|
|
68
|
+
typeof window === 'undefined' ||
|
|
69
|
+
typeof window.getComputedStyle !== 'function' ||
|
|
70
|
+
typeof node.closest !== 'function'
|
|
71
|
+
) {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cell = node.closest('.InovuaReactDataGrid__cell');
|
|
76
|
+
|
|
77
|
+
if (!cell) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cs = window.getComputedStyle(cell);
|
|
82
|
+
const sum =
|
|
83
|
+
(parseFloat(cs.paddingTop) || 0) +
|
|
84
|
+
(parseFloat(cs.paddingBottom) || 0) +
|
|
85
|
+
(parseFloat(cs.borderTopWidth) || 0) +
|
|
86
|
+
(parseFloat(cs.borderBottomWidth) || 0);
|
|
87
|
+
|
|
88
|
+
return Number.isFinite(sum) ? sum : 0;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {boolean} force report even when the height has not moved —
|
|
93
|
+
* used on mount and whenever the row under this cell changes,
|
|
94
|
+
* because the grid keys heights by row index and an unchanged
|
|
95
|
+
* height at a NEW index still has to be re-applied.
|
|
96
|
+
*/
|
|
97
|
+
const report = (force) => {
|
|
98
|
+
if (!nodeRef.current) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const natural = nodeRef.current.scrollHeight || 0;
|
|
103
|
+
|
|
104
|
+
if (!natural) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const height = Math.ceil(natural + cellChrome());
|
|
109
|
+
const last = lastReportedRef.current;
|
|
110
|
+
|
|
111
|
+
if (!force && last !== null && Math.abs(last - height) <= 1) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lastReportedRef.current = height;
|
|
116
|
+
onMeasure(rowId, height, !!force);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
report(true);
|
|
120
|
+
|
|
121
|
+
// No ResizeObserver (jsdom, older Safari): the mount measurement above
|
|
122
|
+
// still applies, the cell just will not follow later reflows.
|
|
123
|
+
if (typeof ResizeObserver === 'undefined') {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const observer = new ResizeObserver(() => report(false));
|
|
128
|
+
|
|
129
|
+
observer.observe(node);
|
|
130
|
+
|
|
131
|
+
return () => observer.disconnect();
|
|
132
|
+
}, [rowId, onMeasure]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div ref={nodeRef} className="autoGrowCell" style={{ width: '100%' }}>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default AutoGrowCell;
|
|
@@ -1011,6 +1011,16 @@
|
|
|
1011
1011
|
position: relative;
|
|
1012
1012
|
}
|
|
1013
1013
|
|
|
1014
|
+
// The expand toggle and the group-action buttons as one block, so a
|
|
1015
|
+
// consuming stylesheet can give the controls a minimum width (lining the
|
|
1016
|
+
// label up with a column) while still letting a row with many buttons push
|
|
1017
|
+
// the label right instead of running underneath it.
|
|
1018
|
+
.groupControls {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
align-items: center;
|
|
1021
|
+
flex: 0 0 auto;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1014
1024
|
.groupHeaderCheckbox {
|
|
1015
1025
|
width: 22px;
|
|
1016
1026
|
height: 22px;
|
|
@@ -110,6 +110,35 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
|
110
110
|
overflow: hidden !important;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/* Auto-grow cells — a column with `"autoGrow": true`.
|
|
114
|
+
|
|
115
|
+
The opposite trade to the tablet word-wrap clamp above: instead of the cell
|
|
116
|
+
clamping its content to the row, the ROW grows to its content (DataGrid
|
|
117
|
+
measures it through AutoGrowCell and raises that one row via the grid's
|
|
118
|
+
RowHeightManager). So the content must be allowed to take a second and third
|
|
119
|
+
line — the nowrap/ellipsis rule that applies to every other non-wrapping
|
|
120
|
+
cell would otherwise make the content one line tall forever and there would
|
|
121
|
+
be nothing to grow for.
|
|
122
|
+
|
|
123
|
+
The CELL keeps `overflow: hidden`: between the frame the content grows in
|
|
124
|
+
and the frame the row grows in, the extra lines are clipped at the row
|
|
125
|
+
boundary rather than spilling over the row below, which is what the grid
|
|
126
|
+
looks like today. The inner wrapper stays `overflow: visible` so nothing
|
|
127
|
+
clips once the row has caught up, and `align-self: center` on the grid's own
|
|
128
|
+
content wrapper keeps the value vertically centred in the taller row, next
|
|
129
|
+
to the neighbouring cells and the action buttons. */
|
|
130
|
+
.InovuaReactDataGrid__cell.cell-auto-grow,
|
|
131
|
+
.InovuaReactDataGrid__cell.cell-auto-grow .InovuaReactDataGrid__cell__content,
|
|
132
|
+
.InovuaReactDataGrid__cell.cell-auto-grow .autoGrowCell {
|
|
133
|
+
white-space: normal !important;
|
|
134
|
+
text-overflow: clip !important;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.InovuaReactDataGrid__cell.cell-auto-grow .InovuaReactDataGrid__cell__content,
|
|
138
|
+
.InovuaReactDataGrid__cell.cell-auto-grow .autoGrowCell {
|
|
139
|
+
overflow: visible !important;
|
|
140
|
+
}
|
|
141
|
+
|
|
113
142
|
/* NOTE: this fork renders header cells as .InovuaReactDataGrid__column-header
|
|
114
143
|
(there is no __header-cell class in its DOM), so header padding is left to
|
|
115
144
|
the module stylesheet; a __header-cell padding rule that used to sit here
|