@visns-studio/visns-components 6.6.2 → 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 +35 -0
- package/package.json +1 -1
- package/src/components/DataGrid.jsx +459 -122
- package/src/components/Form.jsx +74 -19
- package/src/components/MultiSelect.jsx +8 -60
- package/src/components/columns/AutoGrowCell.jsx +141 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/global-datagrid.css +29 -0
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
|
|
@@ -711,7 +712,9 @@ const DataGrid = forwardRef(
|
|
|
711
712
|
}
|
|
712
713
|
|
|
713
714
|
if (j - i > 1) {
|
|
714
|
-
const children = rows
|
|
715
|
+
const children = rows
|
|
716
|
+
.slice(i, j)
|
|
717
|
+
.map((child) => ({ ...child }));
|
|
715
718
|
const parent = {
|
|
716
719
|
__treeParent: true,
|
|
717
720
|
id: `tree-group-${value}`,
|
|
@@ -957,6 +960,197 @@ const DataGrid = forwardRef(
|
|
|
957
960
|
// prevents action buttons from being clipped.
|
|
958
961
|
const { grid } = useDensity();
|
|
959
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
|
+
|
|
960
1154
|
// Effect to handle autoRefresh setting
|
|
961
1155
|
useEffect(() => {
|
|
962
1156
|
if (ajaxSetting && ajaxSetting.autoRefresh !== undefined) {
|
|
@@ -1919,7 +2113,11 @@ const DataGrid = forwardRef(
|
|
|
1919
2113
|
}
|
|
1920
2114
|
|
|
1921
2115
|
// Show confirmation dialog for other actions to prevent accidental clicks
|
|
1922
|
-
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)?`;
|
|
1923
2121
|
|
|
1924
2122
|
confirmDialog({
|
|
1925
2123
|
title: 'Confirm Group Action',
|
|
@@ -1957,6 +2155,28 @@ const DataGrid = forwardRef(
|
|
|
1957
2155
|
);
|
|
1958
2156
|
};
|
|
1959
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
|
+
|
|
1960
2180
|
/**
|
|
1961
2181
|
* The show-condition grammar shared by group action icons and row
|
|
1962
2182
|
* action icons:
|
|
@@ -2094,7 +2314,9 @@ const DataGrid = forwardRef(
|
|
|
2094
2314
|
iconConfig.fetch.messageFromResponse &&
|
|
2095
2315
|
result?.data?.message
|
|
2096
2316
|
? result.data.message
|
|
2097
|
-
: `${iconConfig.label} - Group: ${
|
|
2317
|
+
: `${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2318
|
+
groupValue
|
|
2319
|
+
)} completed successfully`
|
|
2098
2320
|
);
|
|
2099
2321
|
|
|
2100
2322
|
// Reload grid data if needed
|
|
@@ -2111,7 +2333,9 @@ const DataGrid = forwardRef(
|
|
|
2111
2333
|
} catch (error) {
|
|
2112
2334
|
console.error('Group action fetch error:', error);
|
|
2113
2335
|
toast.error(
|
|
2114
|
-
`${iconConfig.label} - Group: ${
|
|
2336
|
+
`${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2337
|
+
groupValue
|
|
2338
|
+
)} failed: ${error.message}`
|
|
2115
2339
|
);
|
|
2116
2340
|
}
|
|
2117
2341
|
} else if (iconConfig.formModal) {
|
|
@@ -2135,7 +2359,11 @@ const DataGrid = forwardRef(
|
|
|
2135
2359
|
// Fallback to existing switch logic if no fetch config
|
|
2136
2360
|
// Skip toast notification for edit actions
|
|
2137
2361
|
if (iconConfig.id !== 'edit') {
|
|
2138
|
-
toast.info(
|
|
2362
|
+
toast.info(
|
|
2363
|
+
`${iconConfig.label} - Group: ${getGroupDisplayLabel(
|
|
2364
|
+
groupValue
|
|
2365
|
+
)}`
|
|
2366
|
+
);
|
|
2139
2367
|
}
|
|
2140
2368
|
|
|
2141
2369
|
switch (iconConfig.id) {
|
|
@@ -2511,6 +2739,14 @@ const DataGrid = forwardRef(
|
|
|
2511
2739
|
timestamp: new Date().toISOString(),
|
|
2512
2740
|
});
|
|
2513
2741
|
|
|
2742
|
+
// Synthetic tree parent rows are group summaries with a
|
|
2743
|
+
// fabricated string id — they must never open the update modal
|
|
2744
|
+
// or follow links. Their own controls (chevron, group action
|
|
2745
|
+
// icons) stopPropagation before this handler fires.
|
|
2746
|
+
if (rowProps.data?.__treeParent) {
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2514
2750
|
const cellElement = event.target.closest(
|
|
2515
2751
|
'.InovuaReactDataGrid__cell'
|
|
2516
2752
|
);
|
|
@@ -2674,6 +2910,26 @@ const DataGrid = forwardRef(
|
|
|
2674
2910
|
|
|
2675
2911
|
const onRenderRow = useCallback(
|
|
2676
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
|
+
|
|
2677
2933
|
// save the original handlers to be called later
|
|
2678
2934
|
const { onClick } = rowProps;
|
|
2679
2935
|
|
|
@@ -4584,16 +4840,91 @@ const DataGrid = forwardRef(
|
|
|
4584
4840
|
};
|
|
4585
4841
|
};
|
|
4586
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
|
+
|
|
4587
4915
|
// decorateTreeChildIndent sits INSIDE
|
|
4588
4916
|
// decorateTreeParentActions: the latter draws parent rows
|
|
4589
4917
|
// itself and delegates everything else downward, so the
|
|
4590
4918
|
// member indent has to be the render it delegates to.
|
|
4591
4919
|
const newColumns = columns.map((column) =>
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4920
|
+
decorateAutoGrow(
|
|
4921
|
+
decorateColumnRowActions(
|
|
4922
|
+
decorateTreeParentActions(
|
|
4923
|
+
decorateTreeChildIndent(
|
|
4924
|
+
guardTreeParentRender(
|
|
4925
|
+
renderColumn(column),
|
|
4926
|
+
column
|
|
4927
|
+
),
|
|
4597
4928
|
column
|
|
4598
4929
|
),
|
|
4599
4930
|
column
|
|
@@ -5318,126 +5649,130 @@ const DataGrid = forwardRef(
|
|
|
5318
5649
|
aria-label="Select all items in this group"
|
|
5319
5650
|
/>
|
|
5320
5651
|
)}
|
|
5321
|
-
<
|
|
5322
|
-
className={`group-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
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
|
|
5330
5678
|
}
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
'group-expand',
|
|
5679
|
+
onTouchMove={
|
|
5680
|
+
handleTouchMove
|
|
5681
|
+
}
|
|
5682
|
+
data-tooltip-id="system-tooltip"
|
|
5683
|
+
data-tooltip-content={
|
|
5337
5684
|
collapsed
|
|
5338
5685
|
? 'Expand group'
|
|
5339
5686
|
: 'Collapse group'
|
|
5340
|
-
);
|
|
5341
|
-
}}
|
|
5342
|
-
onTouchEnd={
|
|
5343
|
-
handleTouchEnd
|
|
5344
|
-
}
|
|
5345
|
-
onTouchMove={
|
|
5346
|
-
handleTouchMove
|
|
5347
|
-
}
|
|
5348
|
-
data-tooltip-id="system-tooltip"
|
|
5349
|
-
data-tooltip-content={
|
|
5350
|
-
collapsed
|
|
5351
|
-
? 'Expand group'
|
|
5352
|
-
: 'Collapse group'
|
|
5353
|
-
}
|
|
5354
|
-
>
|
|
5355
|
-
{collapsed
|
|
5356
|
-
? '+'
|
|
5357
|
-
: '−'}
|
|
5358
|
-
</button>
|
|
5359
|
-
{/* Dynamic group action buttons */}
|
|
5360
|
-
{ajaxSetting?.groupBySetting?.icons?.map(
|
|
5361
|
-
(
|
|
5362
|
-
iconConfig,
|
|
5363
|
-
index
|
|
5364
|
-
) => {
|
|
5365
|
-
// Check if this icon should be shown based on show conditions
|
|
5366
|
-
if (
|
|
5367
|
-
!shouldShowGroupAction(
|
|
5368
|
-
iconConfig,
|
|
5369
|
-
groupValue
|
|
5370
|
-
)
|
|
5371
|
-
) {
|
|
5372
|
-
return null;
|
|
5373
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
|
+
}
|
|
5374
5708
|
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5709
|
+
const IconComponent =
|
|
5710
|
+
getGroupIconComponent(
|
|
5711
|
+
iconConfig.id
|
|
5712
|
+
);
|
|
5379
5713
|
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
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
|
-
onTouchMove={
|
|
5422
|
-
handleTouchMove
|
|
5423
|
-
}
|
|
5424
|
-
data-tooltip-id="system-tooltip"
|
|
5425
|
-
data-tooltip-content={
|
|
5426
|
-
iconConfig.label
|
|
5427
|
-
}
|
|
5428
|
-
>
|
|
5429
|
-
<IconComponent
|
|
5430
|
-
strokeWidth={
|
|
5431
|
-
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
|
|
5432
5754
|
}
|
|
5433
|
-
|
|
5434
|
-
|
|
5755
|
+
onTouchMove={
|
|
5756
|
+
handleTouchMove
|
|
5435
5757
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
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>
|
|
5441
5776
|
<div
|
|
5442
5777
|
className={
|
|
5443
5778
|
styles.groupInfo
|
|
@@ -5448,7 +5783,9 @@ const DataGrid = forwardRef(
|
|
|
5448
5783
|
styles.groupValue
|
|
5449
5784
|
}
|
|
5450
5785
|
>
|
|
5451
|
-
{
|
|
5786
|
+
{getGroupDisplayLabel(
|
|
5787
|
+
groupValue
|
|
5788
|
+
)}
|
|
5452
5789
|
{/* Important marker */}
|
|
5453
5790
|
{hasImportantMarker && (
|
|
5454
5791
|
<AlertCircle
|
package/src/components/Form.jsx
CHANGED
|
@@ -1310,6 +1310,26 @@ function Form({
|
|
|
1310
1310
|
}
|
|
1311
1311
|
}
|
|
1312
1312
|
|
|
1313
|
+
// A form that never fetched its record (see the columnId > 0
|
|
1314
|
+
// gate on the record GET) has no primary key — submitting
|
|
1315
|
+
// would PUT/DELETE against `${url}/undefined`. Abort loudly
|
|
1316
|
+
// instead.
|
|
1317
|
+
if (
|
|
1318
|
+
['update', 'delete'].includes(formType) &&
|
|
1319
|
+
!bulkEditMode &&
|
|
1320
|
+
!formSettings.json &&
|
|
1321
|
+
!formSettings.customUrl
|
|
1322
|
+
) {
|
|
1323
|
+
const pk = formData?.[formSettings.primaryKey];
|
|
1324
|
+
|
|
1325
|
+
if (pk == null || pk === '' || Number.isNaN(Number(pk))) {
|
|
1326
|
+
toast.error(
|
|
1327
|
+
'This record could not be loaded, so it cannot be saved. Please close the form and reopen it from its row.'
|
|
1328
|
+
);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1313
1333
|
let fields = formSettings.fields;
|
|
1314
1334
|
let _inputClass = inputClass;
|
|
1315
1335
|
let validation = '';
|
|
@@ -2199,32 +2219,67 @@ function Form({
|
|
|
2199
2219
|
fetchTable(createdItem);
|
|
2200
2220
|
}
|
|
2201
2221
|
} else {
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2222
|
+
// Without a record id the follow-up
|
|
2223
|
+
// would hit `${url}/undefined`, so
|
|
2224
|
+
// skip the request and report it
|
|
2225
|
+
// the same way a failed one is
|
|
2226
|
+
const saveAnotherId =
|
|
2227
|
+
formData?.[saveAnother.dataKey];
|
|
2228
|
+
|
|
2229
|
+
if (
|
|
2230
|
+
saveAnotherId == null ||
|
|
2231
|
+
saveAnotherId === '' ||
|
|
2232
|
+
Number.isNaN(
|
|
2233
|
+
Number(saveAnotherId)
|
|
2234
|
+
)
|
|
2235
|
+
) {
|
|
2236
|
+
saveAnotherFailed = true;
|
|
2237
|
+
toast.error(
|
|
2238
|
+
'Unable to run the follow-up action: the record id is missing.'
|
|
2210
2239
|
);
|
|
2211
2240
|
|
|
2212
|
-
if (resSaveAnother.data.error === '') {
|
|
2213
|
-
toast.success(saveAnother.message);
|
|
2214
2241
|
if (fetchTable) {
|
|
2215
2242
|
const createdItem =
|
|
2216
|
-
res.data.data ||
|
|
2243
|
+
res.data.data ||
|
|
2244
|
+
res.data;
|
|
2217
2245
|
fetchTable(createdItem);
|
|
2218
2246
|
}
|
|
2219
2247
|
} else {
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2248
|
+
const saveAnotherUrl = `${
|
|
2249
|
+
saveAnother.url
|
|
2250
|
+
}/${saveAnotherId}`;
|
|
2251
|
+
const resSaveAnother =
|
|
2252
|
+
await CustomFetch(
|
|
2253
|
+
saveAnotherUrl,
|
|
2254
|
+
saveAnother.method,
|
|
2255
|
+
processedData
|
|
2256
|
+
);
|
|
2257
|
+
|
|
2258
|
+
if (
|
|
2259
|
+
resSaveAnother.data
|
|
2260
|
+
.error === ''
|
|
2261
|
+
) {
|
|
2262
|
+
toast.success(
|
|
2263
|
+
saveAnother.message
|
|
2264
|
+
);
|
|
2265
|
+
if (fetchTable) {
|
|
2266
|
+
const createdItem =
|
|
2267
|
+
res.data.data ||
|
|
2268
|
+
res.data;
|
|
2269
|
+
fetchTable(createdItem);
|
|
2270
|
+
}
|
|
2271
|
+
} else {
|
|
2272
|
+
saveAnotherFailed = true;
|
|
2273
|
+
toast.error(
|
|
2274
|
+
resSaveAnother.data
|
|
2275
|
+
.error
|
|
2276
|
+
);
|
|
2277
|
+
if (fetchTable) {
|
|
2278
|
+
const createdItem =
|
|
2279
|
+
res.data.data ||
|
|
2280
|
+
res.data;
|
|
2281
|
+
fetchTable(createdItem);
|
|
2282
|
+
}
|
|
2228
2283
|
}
|
|
2229
2284
|
}
|
|
2230
2285
|
}
|
|
@@ -36,7 +36,7 @@ const CustomOption = (props) => {
|
|
|
36
36
|
|
|
37
37
|
// Custom MultiValue component to display description for selected values
|
|
38
38
|
const CustomMultiValue = (props) => {
|
|
39
|
-
const { children, data,
|
|
39
|
+
const { children, data, settings, ...rest } = props;
|
|
40
40
|
|
|
41
41
|
return (
|
|
42
42
|
<components.MultiValue {...rest} data={data}>
|
|
@@ -62,61 +62,18 @@ const CustomMultiValue = (props) => {
|
|
|
62
62
|
);
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
// Custom MultiValueRemove
|
|
65
|
+
// Custom MultiValueRemove: fixed values cannot be deselected — render no
|
|
66
|
+
// × at all (the module SCSS forces `display` on the remove button with
|
|
67
|
+
// !important, so hiding it from the styles function is not enough).
|
|
68
|
+
// Removal itself is react-select's own behaviour: CustomMultiValue
|
|
69
|
+
// forwards removeProps, so innerProps already carries working
|
|
70
|
+
// onClick/onTouchEnd handlers.
|
|
66
71
|
const CustomMultiValueRemove = (props) => {
|
|
67
|
-
// Fixed values cannot be deselected: render no × at all — the module
|
|
68
|
-
// SCSS forces `display` on the remove button with !important, so
|
|
69
|
-
// hiding it from the styles function is not enough.
|
|
70
72
|
if (props.data?.isFixed) {
|
|
71
73
|
return null;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
e.stopPropagation();
|
|
76
|
-
|
|
77
|
-
// Try to find the data by looking at the DOM structure
|
|
78
|
-
const removeButton = e.currentTarget;
|
|
79
|
-
const multiValueContainer = removeButton.closest('.visns-select__multi-value');
|
|
80
|
-
|
|
81
|
-
if (multiValueContainer) {
|
|
82
|
-
// Get the index of this multi-value item
|
|
83
|
-
const allMultiValues = removeButton.closest('.visns-select__value-container').querySelectorAll('.visns-select__multi-value');
|
|
84
|
-
const index = Array.from(allMultiValues).indexOf(multiValueContainer);
|
|
85
|
-
|
|
86
|
-
console.log('MultiSelect: Found index:', index);
|
|
87
|
-
|
|
88
|
-
// Get current value and remove the item at this index
|
|
89
|
-
const currentValue = props.selectProps?.value || [];
|
|
90
|
-
console.log('MultiSelect: Current value:', currentValue);
|
|
91
|
-
|
|
92
|
-
if (index >= 0 && index < currentValue.length) {
|
|
93
|
-
const itemToRemove = currentValue[index];
|
|
94
|
-
const newValue = currentValue.filter((_, i) => i !== index);
|
|
95
|
-
|
|
96
|
-
console.log('MultiSelect: Removing item at index', index, ':', itemToRemove);
|
|
97
|
-
console.log('MultiSelect: New value:', newValue);
|
|
98
|
-
|
|
99
|
-
// Call onChange with the filtered value and remove action
|
|
100
|
-
if (props.selectProps?.onChange) {
|
|
101
|
-
props.selectProps.onChange(newValue, {
|
|
102
|
-
action: 'remove-value',
|
|
103
|
-
removedValue: itemToRemove
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<components.MultiValueRemove
|
|
112
|
-
{...props}
|
|
113
|
-
innerProps={{
|
|
114
|
-
...props.innerProps,
|
|
115
|
-
onClick: handleRemove,
|
|
116
|
-
onMouseDown: (e) => e.stopPropagation()
|
|
117
|
-
}}
|
|
118
|
-
/>
|
|
119
|
-
);
|
|
76
|
+
return <components.MultiValueRemove {...props} />;
|
|
120
77
|
};
|
|
121
78
|
|
|
122
79
|
// Custom NoOptionsMessage component to guide users about creation capability
|
|
@@ -333,15 +290,6 @@ function MultiSelect({
|
|
|
333
290
|
menuPlacement={menuPlacement}
|
|
334
291
|
onMenuOpen={handleMenuOpen}
|
|
335
292
|
onChange={(inputValue, action) => {
|
|
336
|
-
// Debug logging for delete actions
|
|
337
|
-
if (action.action === 'remove-value') {
|
|
338
|
-
console.log('MultiSelect: Individual item delete:', {
|
|
339
|
-
removedValue: action.removedValue,
|
|
340
|
-
newValue: inputValue,
|
|
341
|
-
action: action
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
293
|
// Values seeded with isFixed represent memberships this
|
|
346
294
|
// field is not allowed to take away (e.g. a tag's existing
|
|
347
295
|
// headers in an add-only group modal): ignore their removal
|
|
@@ -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
|