@homebound/beam 2.301.0 → 2.302.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/dist/components/PresentationContext.js +3 -1
- package/dist/components/Table/GridTable.d.ts +1 -1
- package/dist/components/Table/GridTable.js +67 -76
- package/dist/components/Table/GridTableApi.js +0 -1
- package/dist/components/Table/components/KeptGroupRow.js +1 -1
- package/dist/components/Table/components/Row.d.ts +4 -4
- package/dist/components/Table/components/Row.js +4 -3
- package/dist/components/Table/utils/RowState.d.ts +63 -0
- package/dist/components/Table/utils/RowState.js +142 -0
- package/dist/components/Table/utils/RowStates.d.ts +24 -0
- package/dist/components/Table/utils/RowStates.js +108 -0
- package/dist/components/Table/utils/TableState.d.ts +9 -9
- package/dist/components/Table/utils/TableState.js +28 -216
- package/dist/components/Table/utils/utils.d.ts +1 -0
- package/dist/components/Table/utils/utils.js +1 -0
- package/package.json +1 -1
|
@@ -11,7 +11,9 @@ function PresentationProvider(props) {
|
|
|
11
11
|
const context = (0, react_1.useMemo)(() => {
|
|
12
12
|
const fieldProps = { ...existingContext.fieldProps, ...presentationProps.fieldProps };
|
|
13
13
|
return { ...existingContext, ...presentationProps, fieldProps };
|
|
14
|
-
},
|
|
14
|
+
},
|
|
15
|
+
// Isn't this `presentationProps` always a new instance due to the `...` above?
|
|
16
|
+
[presentationProps, existingContext]);
|
|
15
17
|
return (0, jsx_runtime_1.jsx)(exports.PresentationContext.Provider, { value: context, children: children });
|
|
16
18
|
}
|
|
17
19
|
exports.PresentationProvider = PresentationProvider;
|
|
@@ -145,4 +145,4 @@ export declare function GridTable<R extends Kinded, X extends Only<GridTableXss,
|
|
|
145
145
|
*
|
|
146
146
|
* We return a copy of `[Parent, [Child]]` tuples so that we don't modify the `GridDataRow.children`.
|
|
147
147
|
*/
|
|
148
|
-
export declare function filterRows<R extends Kinded>(api: GridTableApi<R>, columns: GridColumnWithId<R>[], rows: GridDataRow<R>[], filter: string | undefined): ParentChildrenTuple<R>[];
|
|
148
|
+
export declare function filterRows<R extends Kinded>(api: GridTableApi<R>, columns: GridColumnWithId<R>[], rows: GridDataRow<R>[], filter: string | undefined): [string[], ParentChildrenTuple<R>[]];
|
|
@@ -140,92 +140,75 @@ function GridTable(props) {
|
|
|
140
140
|
}
|
|
141
141
|
return rows;
|
|
142
142
|
}, [columns, rows, sortOn, sortState, caseSensitive]);
|
|
143
|
-
const
|
|
143
|
+
const [keptGroupRow, keptDataRows] = (0, hooks_1.useComputed)(() => [tableState.keptRowGroup, tableState.keptRows], [tableState]);
|
|
144
144
|
// Sort the `keptSelectedDataRows` separately because the current sorting logic sorts within groups and these "kept" rows are now displayed in a flat list.
|
|
145
145
|
// It could also be the case that some of these rows are no longer in the `props.rows` list, and so wouldn't be sorted by the `maybeSorted` logic above.
|
|
146
146
|
const sortedKeptSelections = (0, react_1.useMemo)(() => {
|
|
147
|
-
if (sortOn === "client" && sortState &&
|
|
148
|
-
return (0, sortRows_1.sortRows)(columns,
|
|
147
|
+
if (sortOn === "client" && sortState && keptDataRows.length > 0) {
|
|
148
|
+
return (0, sortRows_1.sortRows)(columns, keptDataRows, sortState, caseSensitive);
|
|
149
149
|
}
|
|
150
|
-
return
|
|
151
|
-
}, [columns, sortOn, sortState, caseSensitive,
|
|
152
|
-
// Flatten
|
|
150
|
+
return keptDataRows;
|
|
151
|
+
}, [columns, sortOn, sortState, caseSensitive, keptDataRows]);
|
|
152
|
+
// Flatten, hide-if-filtered, hide-if-collapsed, and component-ize the sorted rows.
|
|
153
153
|
let [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds] = (0, react_1.useMemo)(() => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} }, `${row.kind}-${row.id}`));
|
|
172
|
-
}
|
|
154
|
+
const hasExpandableHeader = maybeSorted.some((row) => row.id === utils_1.EXPANDABLE_HEADER);
|
|
155
|
+
const makeRowComponent = (row, level, isKeptSelectedRow = false, isLastKeptSelectionRow = false) => ((0, jsx_runtime_1.jsx)(Row_1.Row, { ...{
|
|
156
|
+
as,
|
|
157
|
+
columns,
|
|
158
|
+
row,
|
|
159
|
+
style,
|
|
160
|
+
rowStyles,
|
|
161
|
+
columnSizes,
|
|
162
|
+
level,
|
|
163
|
+
getCount,
|
|
164
|
+
api,
|
|
165
|
+
cellHighlight: "cellHighlight" in maybeStyle && maybeStyle.cellHighlight === true,
|
|
166
|
+
omitRowHover: "rowHover" in maybeStyle && maybeStyle.rowHover === false,
|
|
167
|
+
hasExpandableHeader,
|
|
168
|
+
isKeptSelectedRow,
|
|
169
|
+
isLastKeptSelectionRow,
|
|
170
|
+
} }, `${row.kind}-${row.id}`));
|
|
173
171
|
// Split out the header rows from the data rows so that we can put an `infoMessage` in between them (if needed).
|
|
174
172
|
const headerRows = [];
|
|
175
173
|
const expandableHeaderRows = [];
|
|
176
174
|
const totalsRows = [];
|
|
177
175
|
const visibleDataRows = [];
|
|
178
176
|
const keptSelectedRows = [];
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// to consider it matched if it or it's parent matched a filter.
|
|
185
|
-
filteredRowIds.push(row.id);
|
|
186
|
-
if (children.length) {
|
|
187
|
-
// Consider "isCollapsed" as true if the parent wasn't visible.
|
|
188
|
-
const isCollapsed = !visible || collapsedIds.includes(row.id);
|
|
189
|
-
visitRows(children, level + 1, !isCollapsed);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function visitRows(rows, level, visible) {
|
|
193
|
-
rows.forEach((row, i) => {
|
|
194
|
-
if (row[0].kind === "header") {
|
|
195
|
-
headerRows.push([row[0], makeRowComponent(row[0], level)]);
|
|
196
|
-
return;
|
|
177
|
+
// Flatten the tuple tree into lists of rows
|
|
178
|
+
function visitRows(tuples, level) {
|
|
179
|
+
tuples.forEach(([row, children]) => {
|
|
180
|
+
if (row.kind === "header") {
|
|
181
|
+
headerRows.push([row, makeRowComponent(row, level)]);
|
|
197
182
|
}
|
|
198
|
-
if (row
|
|
199
|
-
totalsRows.push([row
|
|
200
|
-
return;
|
|
183
|
+
else if (row.kind === "totals") {
|
|
184
|
+
totalsRows.push([row, makeRowComponent(row, level)]);
|
|
201
185
|
}
|
|
202
|
-
if (row
|
|
203
|
-
expandableHeaderRows.push([row
|
|
204
|
-
|
|
186
|
+
else if (row.kind === "expandableHeader") {
|
|
187
|
+
expandableHeaderRows.push([row, makeRowComponent(row, level)]);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
visibleDataRows.push([row, makeRowComponent(row, level)]);
|
|
191
|
+
// tuples has already been client-side filtered, so just check collapsed
|
|
192
|
+
if (children.length && !collapsedIds.includes(row.id)) {
|
|
193
|
+
visitRows(children, level + 1);
|
|
194
|
+
}
|
|
205
195
|
}
|
|
206
|
-
visit(row, level, visible);
|
|
207
196
|
});
|
|
208
197
|
}
|
|
209
|
-
// Call `visitRows` with our
|
|
210
|
-
const filteredRows = filterRows(api, columns, maybeSorted, filter);
|
|
211
|
-
visitRows(filteredRows, 0
|
|
212
|
-
// Check for any selected rows that are not displayed in the table because they don't
|
|
213
|
-
//
|
|
198
|
+
// Call `visitRows` with our post-filtered list
|
|
199
|
+
const [filteredRowIds, filteredRows] = filterRows(api, columns, maybeSorted, filter);
|
|
200
|
+
visitRows(filteredRows, 0);
|
|
201
|
+
// Check for any selected rows that are not displayed in the table because they don't
|
|
202
|
+
// match the current filter, or are no longer part of the `rows` prop. We persist these
|
|
203
|
+
// selected rows and hoist them to the top of the table.
|
|
214
204
|
if (sortedKeptSelections.length) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
};
|
|
223
|
-
keptSelectedRows.push([keptSelectionGroupRow, makeRowComponent(keptSelectionGroupRow, 1)], ...(collapsedIds.includes(utils_1.KEPT_GROUP)
|
|
224
|
-
? []
|
|
225
|
-
: (sortedKeptSelections.map((row, idx) => [
|
|
226
|
-
row,
|
|
227
|
-
makeRowComponent(row, 1, true, idx === sortedKeptSelections.length - 1),
|
|
228
|
-
]))));
|
|
205
|
+
keptSelectedRows.push([keptGroupRow, makeRowComponent(keptGroupRow, 1)]);
|
|
206
|
+
if (!collapsedIds.includes(utils_1.KEPT_GROUP)) {
|
|
207
|
+
keptSelectedRows.push(...sortedKeptSelections.map((row, idx) => {
|
|
208
|
+
const isLast = idx === sortedKeptSelections.length - 1;
|
|
209
|
+
return [row, makeRowComponent(row, 1, true, isLast)];
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
229
212
|
}
|
|
230
213
|
return [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds];
|
|
231
214
|
}, [
|
|
@@ -236,17 +219,17 @@ function GridTable(props) {
|
|
|
236
219
|
columns,
|
|
237
220
|
style,
|
|
238
221
|
rowStyles,
|
|
239
|
-
|
|
222
|
+
maybeStyle,
|
|
240
223
|
columnSizes,
|
|
241
224
|
collapsedIds,
|
|
242
225
|
getCount,
|
|
226
|
+
keptGroupRow,
|
|
243
227
|
sortedKeptSelections,
|
|
244
228
|
]);
|
|
245
229
|
// Once our header rows are created we can organize them in expected order.
|
|
246
230
|
const tableHeadRows = expandableHeaderRows.concat(headerRows).concat(totalsRows);
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
tooManyClientSideRows = true;
|
|
231
|
+
const tooManyClientSideRows = filterMaxRows && visibleDataRows.length > filterMaxRows;
|
|
232
|
+
if (tooManyClientSideRows) {
|
|
250
233
|
visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
|
|
251
234
|
}
|
|
252
235
|
tableState.setMatchedRows(filteredRowIds);
|
|
@@ -256,6 +239,7 @@ function GridTable(props) {
|
|
|
256
239
|
// Refs are cheap to assign to, so we don't bother doing this in a useEffect
|
|
257
240
|
rowLookup.current = (0, GridRowLookup_1.createRowLookup)(columns, visibleDataRows, virtuosoRef);
|
|
258
241
|
}
|
|
242
|
+
// TODO: Replace setRowCount with clients observing TableState via the API
|
|
259
243
|
(0, react_1.useEffect)(() => {
|
|
260
244
|
setRowCount && (visibleDataRows === null || visibleDataRows === void 0 ? void 0 : visibleDataRows.length) !== undefined && setRowCount(visibleDataRows.length);
|
|
261
245
|
}, [visibleDataRows === null || visibleDataRows === void 0 ? void 0 : visibleDataRows.length, setRowCount]);
|
|
@@ -434,26 +418,33 @@ const VirtualRoot = (0, memoize_one_1.default)((gs, _columns, id, xss) => {
|
|
|
434
418
|
* We return a copy of `[Parent, [Child]]` tuples so that we don't modify the `GridDataRow.children`.
|
|
435
419
|
*/
|
|
436
420
|
function filterRows(api, columns, rows, filter) {
|
|
421
|
+
// Make a flat list of ids, in addition to the tuple tree
|
|
422
|
+
const filteredRowIds = [];
|
|
423
|
+
// Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
|
|
424
|
+
const filters = (filter && filter.split(/ +/)) || [];
|
|
437
425
|
// Make a functions to do recursion
|
|
438
426
|
function acceptAll(acc, row) {
|
|
439
427
|
var _a, _b;
|
|
428
|
+
filteredRowIds.push(row.id);
|
|
440
429
|
return acc.concat([[row, (_b = (_a = row.children) === null || _a === void 0 ? void 0 : _a.reduce(acceptAll, [])) !== null && _b !== void 0 ? _b : []]]);
|
|
441
430
|
}
|
|
442
431
|
function filterFn(acc, row) {
|
|
443
432
|
var _a, _b, _c, _d;
|
|
444
|
-
// Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
|
|
445
|
-
const filters = (filter && filter.split(/ +/)) || [];
|
|
446
433
|
const matches = utils_1.reservedRowKinds.includes(row.kind) ||
|
|
447
434
|
filters.length === 0 ||
|
|
448
435
|
filters.every((f) => columns.map((c) => (0, utils_1.applyRowFn)(c, row, api, 0, false)).some((maybeContent) => (0, utils_1.matchesFilter)(maybeContent, f)));
|
|
449
436
|
if (matches) {
|
|
437
|
+
filteredRowIds.push(row.id);
|
|
438
|
+
// A matched parent means show all it's children
|
|
450
439
|
return acc.concat([[row, (_b = (_a = row.children) === null || _a === void 0 ? void 0 : _a.reduce(acceptAll, [])) !== null && _b !== void 0 ? _b : []]]);
|
|
451
440
|
}
|
|
452
441
|
else {
|
|
442
|
+
// An unmatched parent but with matched children means show the parent
|
|
453
443
|
const matchedChildren = (_d = (_c = row.children) === null || _c === void 0 ? void 0 : _c.reduce(filterFn, [])) !== null && _d !== void 0 ? _d : [];
|
|
454
444
|
if (matchedChildren.length > 0 ||
|
|
455
445
|
typeof row.pin === "string" ||
|
|
456
446
|
(row.pin !== undefined && row.pin.filter !== true)) {
|
|
447
|
+
filteredRowIds.push(row.id);
|
|
457
448
|
return acc.concat([[row, matchedChildren]]);
|
|
458
449
|
}
|
|
459
450
|
else {
|
|
@@ -461,6 +452,6 @@ function filterRows(api, columns, rows, filter) {
|
|
|
461
452
|
}
|
|
462
453
|
}
|
|
463
454
|
}
|
|
464
|
-
return rows.reduce(filterFn, []);
|
|
455
|
+
return [filteredRowIds, rows.reduce(filterFn, [])];
|
|
465
456
|
}
|
|
466
457
|
exports.filterRows = filterRows;
|
|
@@ -32,7 +32,6 @@ class GridTableApiImpl {
|
|
|
32
32
|
/** Called once by the GridTable when it takes ownership of this api instance. */
|
|
33
33
|
init(persistCollapse, virtuosoRef, rows) {
|
|
34
34
|
this.tableState.loadCollapse(persistCollapse, rows);
|
|
35
|
-
this.tableState.loadSelected(rows);
|
|
36
35
|
this.virtuosoRef = virtuosoRef;
|
|
37
36
|
}
|
|
38
37
|
scrollToIndex(index) {
|
|
@@ -12,7 +12,7 @@ function KeptGroupRow(props) {
|
|
|
12
12
|
const { as, columnSizes, style, row, colSpan } = props;
|
|
13
13
|
const CellTag = as === "table" ? "td" : "div";
|
|
14
14
|
const { tableState } = (0, react_1.useContext)(Table_1.TableStateContext);
|
|
15
|
-
const numHiddenSelectedRows = (0, hooks_1.useComputed)(() => tableState.
|
|
15
|
+
const numHiddenSelectedRows = (0, hooks_1.useComputed)(() => tableState.keptRows.length, [tableState]);
|
|
16
16
|
return ((0, jsx_runtime_1.jsx)(CellTag, { css: {
|
|
17
17
|
...style.cellCss,
|
|
18
18
|
...style.keptGroupRowCss,
|
|
@@ -2,7 +2,6 @@ import { ReactElement } from "react";
|
|
|
2
2
|
import { GridTableApi } from "../GridTableApi";
|
|
3
3
|
import { GridStyle, RowStyles } from "../TableStyles";
|
|
4
4
|
import { DiscriminateUnion, GridColumnWithId, IfAny, Kinded, Pin, RenderAs } from "../types";
|
|
5
|
-
import { SortOn } from "../utils/TableState";
|
|
6
5
|
import { AnyObject } from "../../../types";
|
|
7
6
|
interface RowProps<R extends Kinded> {
|
|
8
7
|
as: RenderAs;
|
|
@@ -10,7 +9,6 @@ interface RowProps<R extends Kinded> {
|
|
|
10
9
|
row: GridDataRow<R>;
|
|
11
10
|
style: GridStyle;
|
|
12
11
|
rowStyles: RowStyles<R> | undefined;
|
|
13
|
-
sortOn: SortOn;
|
|
14
12
|
columnSizes: string[];
|
|
15
13
|
level: number;
|
|
16
14
|
getCount: (id: string) => object;
|
|
@@ -55,11 +53,13 @@ export type GridDataRow<R extends Kinded> = {
|
|
|
55
53
|
id: string;
|
|
56
54
|
/** A list of parent/grand-parent ids for collapsing parent/child rows. */
|
|
57
55
|
children?: GridDataRow<R>[];
|
|
58
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Whether to pin this sort to the first/last of its parent's children.
|
|
59
58
|
*
|
|
60
59
|
* By default, pinned rows are always shown/not filtered out, however providing
|
|
61
60
|
* the pin `filter: true` property will allow pinned rows to be hidden
|
|
62
|
-
* while filtering
|
|
61
|
+
* while filtering.
|
|
62
|
+
*/
|
|
63
63
|
pin?: "first" | "last" | Pin;
|
|
64
64
|
data: unknown;
|
|
65
65
|
/** Whether to have the row collapsed (children not visible) on initial load. This will be ignore in subsequent re-renders of the table */
|
|
@@ -38,9 +38,10 @@ const utils_2 = require("../../../utils");
|
|
|
38
38
|
const shallowEqual_1 = require("../../../utils/shallowEqual");
|
|
39
39
|
// We extract Row to its own mini-component primarily so we can React.memo'ize it.
|
|
40
40
|
function RowImpl(props) {
|
|
41
|
-
var _a;
|
|
42
|
-
const { as, columns, row, style, rowStyles,
|
|
41
|
+
var _a, _b;
|
|
42
|
+
const { as, columns, row, style, rowStyles, columnSizes, level, getCount, api, cellHighlight, omitRowHover, hasExpandableHeader, isKeptSelectedRow, isLastKeptSelectionRow, ...others } = props;
|
|
43
43
|
const { tableState } = (0, react_1.useContext)(TableState_1.TableStateContext);
|
|
44
|
+
const sortOn = (_a = tableState.sortConfig) === null || _a === void 0 ? void 0 : _a.on;
|
|
44
45
|
const rowId = `${row.kind}_${row.id}`;
|
|
45
46
|
const isActive = (0, hooks_1.useComputed)(() => tableState.activeRowId === rowId, [rowId, tableState]);
|
|
46
47
|
// We treat the "header" and "totals" kind as special for "good defaults" styling
|
|
@@ -57,7 +58,7 @@ function RowImpl(props) {
|
|
|
57
58
|
// Optionally include the row hover styles, by default they should be turned on.
|
|
58
59
|
...(showRowHoverColor && {
|
|
59
60
|
// Even though backgroundColor is set on the cellCss, the hover target is the row.
|
|
60
|
-
"&:hover > *": Css_1.Css.bgColor((
|
|
61
|
+
"&:hover > *": Css_1.Css.bgColor((_b = style.rowHoverColor) !== null && _b !== void 0 ? _b : Css_1.Palette.LightBlue100).$,
|
|
61
62
|
}),
|
|
62
63
|
// For virtual tables use `display: flex` to keep all cells on the same row. For each cell in the row use `flexNone` to ensure they stay their defined widths
|
|
63
64
|
...(as === "table" ? {} : Css_1.Css.relative.df.fg1.fs1.addIn("&>*", Css_1.Css.flexNone.$).$),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { GridDataRow, SelectedState } from "../../..";
|
|
2
|
+
/**
|
|
3
|
+
* A reactive/observable state of each GridDataRow's current behavior.
|
|
4
|
+
*
|
|
5
|
+
* We set up the RowStates in a tree, just like GridDataRow, to make business logic
|
|
6
|
+
* that uses parent/children easier to write, i.e. selected-ness and collapsed-ness.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RowState {
|
|
9
|
+
/** Our row, only ref observed, so we don't crawl into GraphQL fragments. */
|
|
10
|
+
row: GridDataRow<any>;
|
|
11
|
+
/** Our parent RowState, or the `header` RowState if we're a top-level row. */
|
|
12
|
+
parent: RowState | undefined;
|
|
13
|
+
/** Our children row states, as of the latest `props.rows`, without any filtering applied. */
|
|
14
|
+
children: RowState[] | undefined;
|
|
15
|
+
/** Whether we match a client-side filter; true if no filter is in place. */
|
|
16
|
+
isMatched: boolean;
|
|
17
|
+
/** Whether we are *directly* selected. */
|
|
18
|
+
selected: boolean;
|
|
19
|
+
/** Whether our `row` had been in `props.rows`, but was removed, i.e. probably by server-side filters. */
|
|
20
|
+
wasRemoved: boolean;
|
|
21
|
+
constructor(parent: RowState | undefined, row: GridDataRow<any>);
|
|
22
|
+
/**
|
|
23
|
+
* Whether we are currently selected, for `GridTableApi.getSelectedRows`.
|
|
24
|
+
*
|
|
25
|
+
* Note that we don't use "I'm selected || my parent is selected" logic here, because whether a child is selected
|
|
26
|
+
* is actually based on whether it was _visible at the time the parent was selected_. So, we can't just assume
|
|
27
|
+
* "a parent being selected means the child is also selected", and instead parents have to push selected-ness down
|
|
28
|
+
* to their visible children explicitly.
|
|
29
|
+
*/
|
|
30
|
+
get isSelected(): boolean;
|
|
31
|
+
/** The UI state for checked/unchecked + "partially checked" for parents. */
|
|
32
|
+
get selectedState(): SelectedState;
|
|
33
|
+
/**
|
|
34
|
+
* A special SelectedState that "sees through"/ignores inferSelectedState, so the header works.
|
|
35
|
+
*
|
|
36
|
+
* I.e. a row might have `inferSelectedState: false`, so is showing unchecked, but the header
|
|
37
|
+
* wants to show partial-ness whenever any given child is selected.
|
|
38
|
+
*/
|
|
39
|
+
get selectedStateForHeader(): SelectedState;
|
|
40
|
+
/**
|
|
41
|
+
* Called to explicitly select/unselect this row.
|
|
42
|
+
*
|
|
43
|
+
* This could be either because the user clicked directly on us, or because we're a visible
|
|
44
|
+
* child of a selected parent row.
|
|
45
|
+
*/
|
|
46
|
+
select(selected: boolean): void;
|
|
47
|
+
/** Whether this is a selected-but-filtered-out row that we should hoist to the top. */
|
|
48
|
+
get isKept(): boolean;
|
|
49
|
+
private get inferSelectedState();
|
|
50
|
+
private get visibleChildren();
|
|
51
|
+
/**
|
|
52
|
+
* Returns whether this row should act like a parent.
|
|
53
|
+
*
|
|
54
|
+
* This means "has children" and "does not have inferSelectedState: false"
|
|
55
|
+
* set. I.e. `inferSelectedState: false` allows a parent with children to
|
|
56
|
+
* still act as its own selectable identity.
|
|
57
|
+
*
|
|
58
|
+
* We also check `children.length > 0`, because sometimes pages will calc a
|
|
59
|
+
* row's children as `children = someList.map(...)`, and if the list is empty,
|
|
60
|
+
* they want the row to be selectable.
|
|
61
|
+
*/
|
|
62
|
+
private get isParent();
|
|
63
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RowState = void 0;
|
|
4
|
+
const mobx_1 = require("mobx");
|
|
5
|
+
const src_1 = require("../../..");
|
|
6
|
+
/**
|
|
7
|
+
* A reactive/observable state of each GridDataRow's current behavior.
|
|
8
|
+
*
|
|
9
|
+
* We set up the RowStates in a tree, just like GridDataRow, to make business logic
|
|
10
|
+
* that uses parent/children easier to write, i.e. selected-ness and collapsed-ness.
|
|
11
|
+
*/
|
|
12
|
+
class RowState {
|
|
13
|
+
// ...eventually...
|
|
14
|
+
// isDirectlyMatched = accept filters in the constructor and do match here
|
|
15
|
+
// isEffectiveMatched = isDirectlyMatched || hasMatchedChildren
|
|
16
|
+
constructor(parent, row) {
|
|
17
|
+
/** Our children row states, as of the latest `props.rows`, without any filtering applied. */
|
|
18
|
+
this.children = undefined;
|
|
19
|
+
/** Whether we match a client-side filter; true if no filter is in place. */
|
|
20
|
+
this.isMatched = true;
|
|
21
|
+
/** Whether we are *directly* selected. */
|
|
22
|
+
this.selected = false;
|
|
23
|
+
/** Whether our `row` had been in `props.rows`, but was removed, i.e. probably by server-side filters. */
|
|
24
|
+
this.wasRemoved = false;
|
|
25
|
+
this.parent = parent;
|
|
26
|
+
this.row = row;
|
|
27
|
+
this.selected = !!row.initSelected;
|
|
28
|
+
(0, mobx_1.makeAutoObservable)(this, { row: mobx_1.observable.ref });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether we are currently selected, for `GridTableApi.getSelectedRows`.
|
|
32
|
+
*
|
|
33
|
+
* Note that we don't use "I'm selected || my parent is selected" logic here, because whether a child is selected
|
|
34
|
+
* is actually based on whether it was _visible at the time the parent was selected_. So, we can't just assume
|
|
35
|
+
* "a parent being selected means the child is also selected", and instead parents have to push selected-ness down
|
|
36
|
+
* to their visible children explicitly.
|
|
37
|
+
*/
|
|
38
|
+
get isSelected() {
|
|
39
|
+
// We consider group rows selected if all of their children are selected.
|
|
40
|
+
if (this.isParent)
|
|
41
|
+
return this.selectedState === "checked";
|
|
42
|
+
return this.selected;
|
|
43
|
+
}
|
|
44
|
+
/** The UI state for checked/unchecked + "partially checked" for parents. */
|
|
45
|
+
get selectedState() {
|
|
46
|
+
// Parent `selectedState` is special b/c it does not directly depend on the parent's own selected-ness,
|
|
47
|
+
// but instead depends on the current visible children. I.e. a parent might be "selected", but then the
|
|
48
|
+
// client-side filter changes, a child reappears, and we need to transition to partial-ness.
|
|
49
|
+
if (this.isParent) {
|
|
50
|
+
// Use visibleChildren b/c if filters are hiding some of our children, we still want to show fully selected
|
|
51
|
+
const children = this.visibleChildren;
|
|
52
|
+
const allChecked = children.every((child) => child.selectedState === "checked");
|
|
53
|
+
const allUnchecked = children.every((child) => child.selectedState === "unchecked");
|
|
54
|
+
return children.length === 0 ? "unchecked" : allChecked ? "checked" : allUnchecked ? "unchecked" : "partial";
|
|
55
|
+
}
|
|
56
|
+
return this.selected ? "checked" : "unchecked";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A special SelectedState that "sees through"/ignores inferSelectedState, so the header works.
|
|
60
|
+
*
|
|
61
|
+
* I.e. a row might have `inferSelectedState: false`, so is showing unchecked, but the header
|
|
62
|
+
* wants to show partial-ness whenever any given child is selected.
|
|
63
|
+
*/
|
|
64
|
+
get selectedStateForHeader() {
|
|
65
|
+
if (this.children) {
|
|
66
|
+
const children = this.visibleChildren;
|
|
67
|
+
const allChecked = children.every((child) => child.selectedStateForHeader === "checked");
|
|
68
|
+
const allUnchecked = children.every((child) => child.selectedStateForHeader === "unchecked");
|
|
69
|
+
// For the header purposes, if this is a selectable-row (i.e. not inferSelectedState) make sure
|
|
70
|
+
// both the row itself & all children are "all checked" or "not all checked", otherwise consider
|
|
71
|
+
// ourselves partially selected.
|
|
72
|
+
if ((allUnchecked || children.length === 0) && (this.inferSelectedState || !this.selected)) {
|
|
73
|
+
return "unchecked";
|
|
74
|
+
}
|
|
75
|
+
else if (allChecked && (this.inferSelectedState || this.selected)) {
|
|
76
|
+
return "checked";
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return "partial";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return this.selected ? "checked" : "unchecked";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Called to explicitly select/unselect this row.
|
|
86
|
+
*
|
|
87
|
+
* This could be either because the user clicked directly on us, or because we're a visible
|
|
88
|
+
* child of a selected parent row.
|
|
89
|
+
*/
|
|
90
|
+
select(selected) {
|
|
91
|
+
if (this.row.selectable === false)
|
|
92
|
+
return;
|
|
93
|
+
this.selected = selected;
|
|
94
|
+
// We don't check inferSelectedState here, b/c even if the parent is considered selectable
|
|
95
|
+
// on its own, we still push down selected-ness to our visible children.
|
|
96
|
+
if (this.children) {
|
|
97
|
+
for (const child of this.visibleChildren) {
|
|
98
|
+
child.select(selected);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Whether this is a selected-but-filtered-out row that we should hoist to the top. */
|
|
103
|
+
get isKept() {
|
|
104
|
+
// this row is "kept" if it is selected, and:
|
|
105
|
+
// - it is not matched (hidden by filter) (being hidden by collapse is okay)
|
|
106
|
+
// - or it has (probably) been server-side filtered
|
|
107
|
+
return (this.selected &&
|
|
108
|
+
// Headers, totals, etc., do not need keeping
|
|
109
|
+
!src_1.reservedRowKinds.includes(this.row.kind) &&
|
|
110
|
+
!this.isParent &&
|
|
111
|
+
(!this.isMatched || this.wasRemoved));
|
|
112
|
+
}
|
|
113
|
+
get inferSelectedState() {
|
|
114
|
+
return this.row.inferSelectedState !== false;
|
|
115
|
+
}
|
|
116
|
+
get visibleChildren() {
|
|
117
|
+
var _a, _b, _c;
|
|
118
|
+
// The keptGroup should treat all of its children as visible, as this makes select/unselect all work.
|
|
119
|
+
if (this.row.kind === src_1.KEPT_GROUP)
|
|
120
|
+
return (_a = this.children) !== null && _a !== void 0 ? _a : [];
|
|
121
|
+
return (_c = (_b = this.children) === null || _b === void 0 ? void 0 : _b.filter((c) => c.isMatched === true)) !== null && _c !== void 0 ? _c : [];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Returns whether this row should act like a parent.
|
|
125
|
+
*
|
|
126
|
+
* This means "has children" and "does not have inferSelectedState: false"
|
|
127
|
+
* set. I.e. `inferSelectedState: false` allows a parent with children to
|
|
128
|
+
* still act as its own selectable identity.
|
|
129
|
+
*
|
|
130
|
+
* We also check `children.length > 0`, because sometimes pages will calc a
|
|
131
|
+
* row's children as `children = someList.map(...)`, and if the list is empty,
|
|
132
|
+
* they want the row to be selectable.
|
|
133
|
+
*/
|
|
134
|
+
get isParent() {
|
|
135
|
+
return !!this.children && this.children.length > 0 && this.inferSelectedState;
|
|
136
|
+
}
|
|
137
|
+
/** Pretty toString. */
|
|
138
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
139
|
+
return `RowState ${this.row.kind}-${this.row.id}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.RowState = RowState;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { GridDataRow } from "../../..";
|
|
2
|
+
import { RowState } from "./RowState";
|
|
3
|
+
/**
|
|
4
|
+
* Manages our tree of observable RowStates that manage each GridDataRow's behavior.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RowStates {
|
|
7
|
+
private map;
|
|
8
|
+
keptGroupRow: RowState;
|
|
9
|
+
/** Returns a flat list of all of our RowStates. */
|
|
10
|
+
get allStates(): RowState[];
|
|
11
|
+
/** Returns the `RowState` for the given `id`. We should probably require `kind`. */
|
|
12
|
+
get(id: string): RowState;
|
|
13
|
+
/**
|
|
14
|
+
* Merge a new set of `rows` prop into our state.
|
|
15
|
+
*
|
|
16
|
+
* Any missing rows are marked as `wasRemoved` so we can consider them "kept" if they're also selected.
|
|
17
|
+
*/
|
|
18
|
+
setRows(rows: GridDataRow<any>[]): void;
|
|
19
|
+
/** Fully delete `ids`, so they don't show up in kept rows anymore. */
|
|
20
|
+
delete(ids: string[]): void;
|
|
21
|
+
setMatchedRows(ids: string[]): void;
|
|
22
|
+
/** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
|
|
23
|
+
get keptRows(): RowState[];
|
|
24
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RowStates = void 0;
|
|
4
|
+
const mobx_1 = require("mobx");
|
|
5
|
+
const src_1 = require("../../..");
|
|
6
|
+
const RowState_1 = require("./RowState");
|
|
7
|
+
/**
|
|
8
|
+
* Manages our tree of observable RowStates that manage each GridDataRow's behavior.
|
|
9
|
+
*/
|
|
10
|
+
class RowStates {
|
|
11
|
+
constructor() {
|
|
12
|
+
// A flat map of all row id -> RowState
|
|
13
|
+
this.map = new mobx_1.ObservableMap();
|
|
14
|
+
// Pre-create our keptGroupRow for if/when we need it.
|
|
15
|
+
this.keptGroupRow = creatKeptGroupRow();
|
|
16
|
+
}
|
|
17
|
+
/** Returns a flat list of all of our RowStates. */
|
|
18
|
+
get allStates() {
|
|
19
|
+
return [...this.map.values()];
|
|
20
|
+
}
|
|
21
|
+
/** Returns the `RowState` for the given `id`. We should probably require `kind`. */
|
|
22
|
+
get(id) {
|
|
23
|
+
const rs = this.map.get(id);
|
|
24
|
+
if (!rs)
|
|
25
|
+
throw new Error(`No RowState for ${id}`);
|
|
26
|
+
return rs;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Merge a new set of `rows` prop into our state.
|
|
30
|
+
*
|
|
31
|
+
* Any missing rows are marked as `wasRemoved` so we can consider them "kept" if they're also selected.
|
|
32
|
+
*/
|
|
33
|
+
setRows(rows) {
|
|
34
|
+
const existing = new Set(this.map.values());
|
|
35
|
+
const map = this.map;
|
|
36
|
+
function addRowAndChildren(parent, row) {
|
|
37
|
+
var _a;
|
|
38
|
+
// This should really be kind+id, but a lot of our lookups just use id
|
|
39
|
+
const key = row.id;
|
|
40
|
+
let state = map.get(key);
|
|
41
|
+
if (!state) {
|
|
42
|
+
state = new RowState_1.RowState(parent, row);
|
|
43
|
+
map.set(key, state);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
state.parent = parent;
|
|
47
|
+
state.row = row;
|
|
48
|
+
state.wasRemoved = false;
|
|
49
|
+
existing.delete(state);
|
|
50
|
+
}
|
|
51
|
+
state.children = (_a = row.children) === null || _a === void 0 ? void 0 : _a.map((child) => addRowAndChildren(state, child));
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
// Probe for the header row, so we can create it as a root RowState, even
|
|
55
|
+
// though we don't require the user to model their GridDataRows that way.
|
|
56
|
+
const header = rows.find((r) => r.kind === src_1.HEADER);
|
|
57
|
+
const headerState = header ? addRowAndChildren(undefined, header) : undefined;
|
|
58
|
+
// Add the top-level rows
|
|
59
|
+
const children = rows.filter((row) => row !== header).map((row) => addRowAndChildren(headerState, row));
|
|
60
|
+
// And attach them to the header for select-all/etc. to work
|
|
61
|
+
if (headerState) {
|
|
62
|
+
headerState.children = children.filter((rs) => !src_1.reservedRowKinds.includes(rs.row.kind));
|
|
63
|
+
}
|
|
64
|
+
// Then mark any remaining as removed
|
|
65
|
+
for (const state of existing) {
|
|
66
|
+
state.wasRemoved = true;
|
|
67
|
+
}
|
|
68
|
+
const keptRows = this.keptRows;
|
|
69
|
+
if (keptRows.length > 0) {
|
|
70
|
+
// Stitch the current keptRows into the placeholder keptGroupRow
|
|
71
|
+
keptRows.forEach((rs) => (rs.parent = this.keptGroupRow));
|
|
72
|
+
this.keptGroupRow.children = keptRows;
|
|
73
|
+
this.keptGroupRow.row.children = keptRows.map((rs) => rs.row);
|
|
74
|
+
// And then stitch the keptGroupRow itself into the root header, so that the kept rows
|
|
75
|
+
// are treated as just another child for the header's select/unselect all to work.
|
|
76
|
+
if (headerState) {
|
|
77
|
+
this.keptGroupRow.parent = headerState;
|
|
78
|
+
headerState.children.unshift(this.keptGroupRow);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Fully delete `ids`, so they don't show up in kept rows anymore. */
|
|
83
|
+
delete(ids) {
|
|
84
|
+
var _a;
|
|
85
|
+
for (const id of ids) {
|
|
86
|
+
const rs = this.map.get(id);
|
|
87
|
+
if (rs && rs.parent) {
|
|
88
|
+
rs.parent.children = (_a = rs.parent.children) === null || _a === void 0 ? void 0 : _a.filter((o) => o !== rs);
|
|
89
|
+
}
|
|
90
|
+
this.map.delete(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
setMatchedRows(ids) {
|
|
94
|
+
for (const rs of this.allStates) {
|
|
95
|
+
rs.isMatched = ids.includes(rs.row.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
|
|
99
|
+
get keptRows() {
|
|
100
|
+
return this.allStates.filter((rs) => rs.isKept);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.RowStates = RowStates;
|
|
104
|
+
function creatKeptGroupRow() {
|
|
105
|
+
// The "group row" for selected rows that are hidden by filters and add the children
|
|
106
|
+
const keptGroupRow = { id: src_1.KEPT_GROUP, kind: src_1.KEPT_GROUP, initCollapsed: true, data: undefined };
|
|
107
|
+
return new RowState_1.RowState(undefined, keptGroupRow);
|
|
108
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObservableSet } from "mobx";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { GridDataRow } from "../components/Row";
|
|
4
4
|
import { GridSortConfig } from "../GridTable";
|
|
@@ -22,11 +22,8 @@ export type SelectedState = "checked" | "unchecked" | "partial";
|
|
|
22
22
|
export declare class TableState {
|
|
23
23
|
private readonly collapsedRows;
|
|
24
24
|
private persistCollapse;
|
|
25
|
-
private readonly rowSelectedState;
|
|
26
|
-
selectedDataRows: ObservableMap<string, GridDataRow<any>>;
|
|
27
|
-
private matchedRows;
|
|
28
|
-
keptSelectedRows: GridDataRow<any>[];
|
|
29
25
|
rows: GridDataRow<any>[];
|
|
26
|
+
private readonly rowStates;
|
|
30
27
|
activeRowId: string | undefined;
|
|
31
28
|
activeCellId: string | undefined;
|
|
32
29
|
sortConfig: GridSortConfig | undefined;
|
|
@@ -43,7 +40,6 @@ export declare class TableState {
|
|
|
43
40
|
*/
|
|
44
41
|
constructor();
|
|
45
42
|
loadCollapse(persistCollapse: string | undefined, rows: GridDataRow<any>[]): void;
|
|
46
|
-
loadSelected(rows: GridDataRow<any>[]): void;
|
|
47
43
|
initSortState(sortConfig: GridSortConfig | undefined, columns: GridColumnWithId<any>[]): void;
|
|
48
44
|
setSortKey(clickedColumnId: string): void;
|
|
49
45
|
get sortState(): SortState | undefined;
|
|
@@ -59,17 +55,20 @@ export declare class TableState {
|
|
|
59
55
|
get visibleColumnIds(): string[];
|
|
60
56
|
get expandedColumnIds(): string[];
|
|
61
57
|
toggleExpandedColumn(columnId: string): void;
|
|
58
|
+
/** Called when GridTable has re-calced the rows that pass the client-side filter, or all rows. */
|
|
62
59
|
setMatchedRows(rowIds: string[]): void;
|
|
63
|
-
/** Returns
|
|
60
|
+
/** Returns selected data rows (non-header, non-totals, etc.), ignoring rows that have `row.selectable !== false`. */
|
|
64
61
|
get selectedRows(): GridDataRow<any>[];
|
|
62
|
+
/** Returns kept group row, with the latest kept children, if any. */
|
|
63
|
+
get keptRowGroup(): GridDataRow<any>;
|
|
64
|
+
/** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
|
|
65
|
+
get keptRows(): GridDataRow<any>[];
|
|
65
66
|
getSelected(id: string): SelectedState;
|
|
66
67
|
selectRow(id: string, selected: boolean): void;
|
|
67
68
|
get collapsedIds(): string[];
|
|
68
69
|
isCollapsed(id: string): boolean;
|
|
69
70
|
toggleCollapsed(id: string): void;
|
|
70
71
|
deleteRows(ids: string[]): void;
|
|
71
|
-
private getMatchedChildrenStates;
|
|
72
|
-
private setNestedSelectedStates;
|
|
73
72
|
}
|
|
74
73
|
/** Provides a context for rows to access their table's `TableState`. */
|
|
75
74
|
export declare const TableStateContext: React.Context<{
|
|
@@ -82,6 +81,7 @@ type ColumnSort = {
|
|
|
82
81
|
};
|
|
83
82
|
export type SortState = {
|
|
84
83
|
current?: ColumnSort;
|
|
84
|
+
/** The persistent sort is always applied first, i.e. for schedules, probably. */
|
|
85
85
|
persistent?: ColumnSort;
|
|
86
86
|
};
|
|
87
87
|
export type SortOn = "client" | "server" | undefined;
|
|
@@ -7,8 +7,8 @@ exports.deriveSortState = exports.TableStateContext = exports.TableState = void
|
|
|
7
7
|
const change_case_1 = require("change-case");
|
|
8
8
|
const mobx_1 = require("mobx");
|
|
9
9
|
const react_1 = __importDefault(require("react"));
|
|
10
|
+
const RowStates_1 = require("./RowStates");
|
|
10
11
|
const utils_1 = require("./utils");
|
|
11
|
-
const visitor_1 = require("./visitor");
|
|
12
12
|
const utils_2 = require("../../../utils");
|
|
13
13
|
const columns_1 = require("./columns");
|
|
14
14
|
/**
|
|
@@ -33,24 +33,15 @@ class TableState {
|
|
|
33
33
|
constructor() {
|
|
34
34
|
// A set of just row ids, i.e. not row.kind+row.id
|
|
35
35
|
this.collapsedRows = new mobx_1.ObservableSet([]);
|
|
36
|
-
//
|
|
37
|
-
// Rows within this map may be in `this.rows` or removed from `this.rows`.
|
|
38
|
-
this.rowSelectedState = new mobx_1.ObservableMap();
|
|
39
|
-
// Keep a copy of all the selected GridDataRows - Do not include custom row kinds such as `header`, `totals`, etc.
|
|
40
|
-
// This allows us to keep track of rows that were selected, but may no longer be defined in `GridTable.rows`
|
|
41
|
-
// This will be used to determine which rows are considered "kept" rows when filtering is applied or `GridTableProp.rows` changes
|
|
42
|
-
this.selectedDataRows = new mobx_1.ObservableMap();
|
|
43
|
-
// Set of just row ids. Keeps track of which rows match the filter. Used to filter rows from `selectedIds`
|
|
44
|
-
this.matchedRows = new mobx_1.ObservableSet();
|
|
45
|
-
// All fully selected data rows that do not match the applied filter, if any. Will not include group/parent rows unless they `inferSelectedState: false`
|
|
46
|
-
this.keptSelectedRows = [];
|
|
47
|
-
// The current list of rows, basically a useRef.current. Not reactive.
|
|
36
|
+
// The current list of rows, basically a useRef.current. Only shallow reactive.
|
|
48
37
|
this.rows = [];
|
|
38
|
+
this.rowStates = new RowStates_1.RowStates();
|
|
49
39
|
// Keeps track of the 'active' row, formatted `${row.kind}_${row.id}`
|
|
50
40
|
this.activeRowId = undefined;
|
|
51
41
|
// Keeps track of the 'active' cell, formatted `${row.kind}_${row.id}_${column.name}`
|
|
52
42
|
this.activeCellId = undefined;
|
|
53
|
-
//
|
|
43
|
+
// Tracks the active sort column(s), so GridTable or SortHeaders can reactively
|
|
44
|
+
// re-render (for GridTable, only if client-side sorting)
|
|
54
45
|
this.sort = {};
|
|
55
46
|
// Non-reactive list of our columns
|
|
56
47
|
this.columns = [];
|
|
@@ -73,35 +64,12 @@ class TableState {
|
|
|
73
64
|
// Do not observe columns, expect this to be a non-reactive value for us to base our reactive values off of.
|
|
74
65
|
columns: false,
|
|
75
66
|
});
|
|
76
|
-
//
|
|
77
|
-
(0, mobx_1.reaction)(() => [...this.
|
|
78
|
-
|
|
79
|
-
// If the kept rows went from empty to not empty, then introduce the SELECTED_GROUP row as collapsed
|
|
80
|
-
if (newlyKeptRows.length > 0 && this.keptSelectedRows.length === 0) {
|
|
67
|
+
// If the kept rows went from empty to not empty, then introduce the SELECTED_GROUP row as collapsed
|
|
68
|
+
(0, mobx_1.reaction)(() => [...this.keptRows.values()], (curr, prev) => {
|
|
69
|
+
if (prev.length === 0 && curr.length > 0) {
|
|
81
70
|
this.collapsedRows.add(utils_1.KEPT_GROUP);
|
|
82
71
|
}
|
|
83
|
-
|
|
84
|
-
if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
|
|
85
|
-
this.keptSelectedRows = newlyKeptRows;
|
|
86
|
-
}
|
|
87
|
-
// Re-derive the selected state for both the header and parent rows
|
|
88
|
-
const map = new Map();
|
|
89
|
-
map.set("header", deriveParentSelected([...this.rows, ...this.keptSelectedRows].flatMap((row) => this.setNestedSelectedStates(row, map))));
|
|
90
|
-
// Merge the changes back into the selected rows state
|
|
91
|
-
this.rowSelectedState.merge(map);
|
|
92
|
-
// Update the selectedDataRows to include those that are fully selected based on the filter changes
|
|
93
|
-
[...this.rowSelectedState.entries()].forEach(([id, state]) => {
|
|
94
|
-
if (this.selectedDataRows.has(id) && state !== "checked") {
|
|
95
|
-
this.selectedDataRows.delete(id);
|
|
96
|
-
}
|
|
97
|
-
else if (!this.selectedDataRows.has(id) && state === "checked") {
|
|
98
|
-
const row = this.rows.find((row) => row.id === id);
|
|
99
|
-
if (row) {
|
|
100
|
-
this.selectedDataRows.set(id, row);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}, { equals: mobx_1.comparer.shallow });
|
|
72
|
+
});
|
|
105
73
|
}
|
|
106
74
|
loadCollapse(persistCollapse, rows) {
|
|
107
75
|
this.persistCollapse = persistCollapse;
|
|
@@ -117,24 +85,6 @@ class TableState {
|
|
|
117
85
|
}
|
|
118
86
|
}
|
|
119
87
|
}
|
|
120
|
-
loadSelected(rows) {
|
|
121
|
-
const selectedRows = [];
|
|
122
|
-
(0, visitor_1.visit)(rows, (row) => row.initSelected && selectedRows.push(row));
|
|
123
|
-
const selectedStateMap = new Map();
|
|
124
|
-
const selectedRowMap = new Map();
|
|
125
|
-
selectedRows.forEach((row) => {
|
|
126
|
-
selectedStateMap.set(row.id, "checked");
|
|
127
|
-
selectedRowMap.set(row.id, row);
|
|
128
|
-
});
|
|
129
|
-
this.rowSelectedState.merge(selectedStateMap);
|
|
130
|
-
this.selectedDataRows.merge(selectedRowMap);
|
|
131
|
-
// Determine if we need to initially display the kept selected group
|
|
132
|
-
const newlyKeptRows = selectedRows.filter((row) => keptSelectionsFilter(row, this.matchedRows));
|
|
133
|
-
if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
|
|
134
|
-
this.collapsedRows.add(utils_1.KEPT_GROUP);
|
|
135
|
-
this.keptSelectedRows = newlyKeptRows;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
88
|
initSortState(sortConfig, columns) {
|
|
139
89
|
var _a, _b;
|
|
140
90
|
if (this.sortConfig) {
|
|
@@ -191,6 +141,7 @@ class TableState {
|
|
|
191
141
|
setRows(rows) {
|
|
192
142
|
// If the set of rows are different
|
|
193
143
|
if (rows !== this.rows) {
|
|
144
|
+
this.rowStates.setRows(rows);
|
|
194
145
|
const currentCollapsedIds = this.collapsedIds;
|
|
195
146
|
// Create a list of the (maybe) new rows that should be initially collapsed
|
|
196
147
|
const maybeNewCollapsedRowIds = flattenRows(rows)
|
|
@@ -218,10 +169,6 @@ class TableState {
|
|
|
218
169
|
}
|
|
219
170
|
// Finally replace our existing list of rows
|
|
220
171
|
this.rows = rows;
|
|
221
|
-
// Update the selected rows map to include the rows' updated data
|
|
222
|
-
const selectedRows = new Map();
|
|
223
|
-
(0, visitor_1.visit)(this.rows, (row) => this.selectedDataRows.has(row.id) && selectedRows.set(row.id, row));
|
|
224
|
-
this.selectedDataRows.merge(selectedRows);
|
|
225
172
|
}
|
|
226
173
|
setColumns(columns, visibleColumnsStorageKey) {
|
|
227
174
|
const isInitial = !this.columns || this.columns.length === 0;
|
|
@@ -331,106 +278,31 @@ class TableState {
|
|
|
331
278
|
sessionStorage.setItem(getColumnStorageKey(this.persistCollapse), JSON.stringify(this.expandedColumnIds));
|
|
332
279
|
}
|
|
333
280
|
}
|
|
281
|
+
/** Called when GridTable has re-calced the rows that pass the client-side filter, or all rows. */
|
|
334
282
|
setMatchedRows(rowIds) {
|
|
335
|
-
|
|
336
|
-
// observers/reactions that watch it, which can lead to render loops with the
|
|
337
|
-
// application page is observing `GridTableApi.getSelectedRows`, and merely
|
|
338
|
-
// the act of rendering GridTable (w/o row changes) causes it's `useComputed`
|
|
339
|
-
// to be triggered.
|
|
340
|
-
if (!mobx_1.comparer.shallow(rowIds, [...this.matchedRows.values()])) {
|
|
341
|
-
this.matchedRows.replace(rowIds);
|
|
342
|
-
}
|
|
283
|
+
this.rowStates.setMatchedRows(rowIds);
|
|
343
284
|
}
|
|
344
|
-
/** Returns
|
|
285
|
+
/** Returns selected data rows (non-header, non-totals, etc.), ignoring rows that have `row.selectable !== false`. */
|
|
345
286
|
get selectedRows() {
|
|
346
|
-
return
|
|
287
|
+
return this.rowStates.allStates
|
|
288
|
+
.filter((rs) => rs.isSelected && !utils_1.reservedRowKinds.includes(rs.row.kind))
|
|
289
|
+
.map((rs) => rs.row);
|
|
290
|
+
}
|
|
291
|
+
/** Returns kept group row, with the latest kept children, if any. */
|
|
292
|
+
get keptRowGroup() {
|
|
293
|
+
return this.rowStates.keptGroupRow.row;
|
|
294
|
+
}
|
|
295
|
+
/** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
|
|
296
|
+
get keptRows() {
|
|
297
|
+
return this.rowStates.keptRows.map((rs) => rs.row);
|
|
347
298
|
}
|
|
348
299
|
// Should be called in an Observer/useComputed to trigger re-renders
|
|
349
300
|
getSelected(id) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return (_a = this.rowSelectedState.get(id)) !== null && _a !== void 0 ? _a : (this.selectedDataRows.has(id) ? "checked" : "unchecked");
|
|
301
|
+
const rs = this.rowStates.get(id);
|
|
302
|
+
return id === utils_1.HEADER ? rs.selectedStateForHeader : rs.selectedState;
|
|
353
303
|
}
|
|
354
304
|
selectRow(id, selected) {
|
|
355
|
-
|
|
356
|
-
if (id === "header") {
|
|
357
|
-
// Select/unselect all has special behavior
|
|
358
|
-
if (selected) {
|
|
359
|
-
// Just mash the header + all rows + children as selected
|
|
360
|
-
const selectedStateMap = new Map();
|
|
361
|
-
const selectedRowMap = new Map();
|
|
362
|
-
selectedStateMap.set("header", "checked");
|
|
363
|
-
(0, visitor_1.visit)(this.rows, (row) => {
|
|
364
|
-
if (!utils_1.reservedRowKinds.includes(row.kind) && this.matchedRows.has(row.id)) {
|
|
365
|
-
selectedStateMap.set(row.id, "checked");
|
|
366
|
-
selectedRowMap.set(row.id, row);
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
this.rowSelectedState.replace(selectedStateMap);
|
|
370
|
-
// Use `merge` to ensure we don't lose any row that were selected, but no longer in `this.rows` (i.e. due to server-side filtering)
|
|
371
|
-
this.selectedDataRows.merge(selectedRowMap);
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
// Similarly "unmash" all rows + children.
|
|
375
|
-
this.rowSelectedState.clear();
|
|
376
|
-
this.selectedDataRows.clear();
|
|
377
|
-
this.keptSelectedRows = [];
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
// This is the regular/non-header behavior to just add/remove the individual row id,
|
|
382
|
-
// plus percolate the change down-to-child + up-to-parents.
|
|
383
|
-
// Find the clicked on row
|
|
384
|
-
const curr = (_a = findRow(this.rows, id)) !== null && _a !== void 0 ? _a : (this.selectedDataRows.has(id) ? { row: this.selectedDataRows.get(id), parents: [] } : undefined);
|
|
385
|
-
if (!curr) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
// Everything here & down is deterministically on/off
|
|
389
|
-
const selectedStateMap = new Map();
|
|
390
|
-
(0, visitor_1.visit)([curr.row], (row) => {
|
|
391
|
-
// The `visit` method walks through the selected row and all of its children, if any.
|
|
392
|
-
// Depending on whether we are determining the clicked row's state or its children, then we handle updating the selection differently.
|
|
393
|
-
// We can tell if we are determining a child row's selected state by checking against the row selected `id` and the row we're currently evaluating `row.id`.
|
|
394
|
-
const isClickedRow = row.id === id;
|
|
395
|
-
// Only update the selected states if we're updating the clicked row, or if we are checking a child row, then the row must match the filter.
|
|
396
|
-
// Meaning, rows that are filtered out and displayed in the "selectedGroup" can only change their selection state by interacting with them directly.
|
|
397
|
-
if (isClickedRow || this.matchedRows.has(row.id)) {
|
|
398
|
-
selectedStateMap.set(row.id, selected ? "checked" : "unchecked");
|
|
399
|
-
if (selected) {
|
|
400
|
-
this.selectedDataRows.set(row.id, row);
|
|
401
|
-
}
|
|
402
|
-
else {
|
|
403
|
-
this.selectedDataRows.delete(row.id);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
// Now walk up the parents and see if they are now-all-checked/now-all-unchecked/some-of-each
|
|
408
|
-
for (const parent of [...curr.parents].reverse()) {
|
|
409
|
-
// Only derive selected state of the parent row if `inferSelectedState` is not `false`
|
|
410
|
-
if (parent.children && parent.inferSelectedState !== false) {
|
|
411
|
-
const selectedState = deriveParentSelected(this.getMatchedChildrenStates(parent.children, selectedStateMap));
|
|
412
|
-
if (selectedState === "checked") {
|
|
413
|
-
this.selectedDataRows.set(parent.id, parent);
|
|
414
|
-
}
|
|
415
|
-
else {
|
|
416
|
-
this.selectedDataRows.delete(parent.id);
|
|
417
|
-
}
|
|
418
|
-
selectedStateMap.set(parent.id, selectedState);
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// And do the header + top-level "children" as a final one-off
|
|
422
|
-
selectedStateMap.set("header", deriveParentSelected(this.getMatchedChildrenStates([...this.rows, ...this.keptSelectedRows], selectedStateMap)));
|
|
423
|
-
// And merge the new selected state map into the existing one
|
|
424
|
-
this.rowSelectedState.merge(selectedStateMap);
|
|
425
|
-
// Lastly, we need to update the `keptSelectedRows` if the row was deselected.
|
|
426
|
-
// (If selected === true, then it's not possible for the row to be in `keptSelectedRows` as you can only select rows that match the filter)
|
|
427
|
-
if (!selected) {
|
|
428
|
-
const newlyKeptRows = [...this.selectedDataRows.values()].filter((row) => keptSelectionsFilter(row, this.matchedRows));
|
|
429
|
-
if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
|
|
430
|
-
this.keptSelectedRows = newlyKeptRows;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
305
|
+
this.rowStates.get(id).select(selected);
|
|
434
306
|
}
|
|
435
307
|
get collapsedIds() {
|
|
436
308
|
return [...this.collapsedRows.values()];
|
|
@@ -494,46 +366,13 @@ class TableState {
|
|
|
494
366
|
}
|
|
495
367
|
deleteRows(ids) {
|
|
496
368
|
this.rows = this.rows.filter((row) => !ids.includes(row.id));
|
|
497
|
-
this.
|
|
369
|
+
this.rowStates.delete(ids);
|
|
498
370
|
ids.forEach((id) => {
|
|
499
|
-
this.selectedDataRows.delete(id);
|
|
500
|
-
this.rowSelectedState.delete(id);
|
|
501
371
|
this.collapsedRows.delete(id);
|
|
502
|
-
this.matchedRows.delete(id);
|
|
503
372
|
});
|
|
504
373
|
}
|
|
505
|
-
getMatchedChildrenStates(children, map) {
|
|
506
|
-
const respectedChildren = children.flatMap(getChildrenForDerivingSelectState);
|
|
507
|
-
// When determining the children selected states to base the parent's state from, then only base this off of rows that match the filter or are in the "hidden selected" group (via the `filter` below)
|
|
508
|
-
return respectedChildren
|
|
509
|
-
.filter((row) => row.id !== "header" && (this.matchedRows.has(row.id) || this.selectedDataRows.has(row.id)))
|
|
510
|
-
.map((row) => map.get(row.id) || this.getSelected(row.id));
|
|
511
|
-
}
|
|
512
|
-
// Recursively traverse through rows to determine selected state of parent rows based on children
|
|
513
|
-
// Returns the selected states for the immediately children (if any) of the row passed in
|
|
514
|
-
setNestedSelectedStates(row, map) {
|
|
515
|
-
if (this.matchedRows.has(row.id) || this.selectedDataRows.has(row.id)) {
|
|
516
|
-
// do not derive selected state if there are no children, or if `inferSelectedState` is set to false
|
|
517
|
-
if (!row.children || row.inferSelectedState === false) {
|
|
518
|
-
return [this.getSelected(row.id)];
|
|
519
|
-
}
|
|
520
|
-
const childrenSelectedStates = row.children.flatMap((rc) => this.setNestedSelectedStates(rc, map));
|
|
521
|
-
const parentState = deriveParentSelected(childrenSelectedStates);
|
|
522
|
-
map.set(row.id, parentState);
|
|
523
|
-
return [parentState];
|
|
524
|
-
}
|
|
525
|
-
return [];
|
|
526
|
-
}
|
|
527
374
|
}
|
|
528
375
|
exports.TableState = TableState;
|
|
529
|
-
/** Returns the child rows needed for deriving the selected state of a parent/group row */
|
|
530
|
-
function getChildrenForDerivingSelectState(row) {
|
|
531
|
-
// Only look deeper if the parent row does not infer its selected state
|
|
532
|
-
if (row.children && row.inferSelectedState === false) {
|
|
533
|
-
return [row, ...row.children.flatMap(getChildrenForDerivingSelectState)];
|
|
534
|
-
}
|
|
535
|
-
return [row];
|
|
536
|
-
}
|
|
537
376
|
/** Provides a context for rows to access their table's `TableState`. */
|
|
538
377
|
exports.TableStateContext = react_1.default.createContext({
|
|
539
378
|
get tableState() {
|
|
@@ -559,27 +398,6 @@ function readOrSetLocalVisibleColumnState(columns, storageKey) {
|
|
|
559
398
|
sessionStorage.setItem(storageKey, JSON.stringify(visibleColumnIds));
|
|
560
399
|
return visibleColumnIds;
|
|
561
400
|
}
|
|
562
|
-
/** Finds a row by id, and returns it + any parents. */
|
|
563
|
-
function findRow(rows, id) {
|
|
564
|
-
// This is technically an array of "maybe FoundRow"
|
|
565
|
-
const todo = rows.map((row) => ({ row, parents: [] }));
|
|
566
|
-
while (todo.length > 0) {
|
|
567
|
-
const curr = todo.pop();
|
|
568
|
-
if (curr.row.id === id) {
|
|
569
|
-
return curr;
|
|
570
|
-
}
|
|
571
|
-
else if (curr.row.children) {
|
|
572
|
-
// Search our children and pass along us as the parent
|
|
573
|
-
todo.push(...curr.row.children.map((child) => ({ row: child, parents: [...curr.parents, curr.row] })));
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
return undefined;
|
|
577
|
-
}
|
|
578
|
-
function deriveParentSelected(children) {
|
|
579
|
-
const allChecked = children.every((child) => child === "checked");
|
|
580
|
-
const allUnchecked = children.every((child) => child === "unchecked");
|
|
581
|
-
return children.length === 0 ? "unchecked" : allChecked ? "checked" : allUnchecked ? "unchecked" : "partial";
|
|
582
|
-
}
|
|
583
401
|
function getCollapsedIdsFromRows(rows) {
|
|
584
402
|
return rows.reduce((acc, r) => {
|
|
585
403
|
if (r.initCollapsed) {
|
|
@@ -627,9 +445,3 @@ function deriveSortState(currentSortState, clickedKey, initialSortState) {
|
|
|
627
445
|
return initialSortState;
|
|
628
446
|
}
|
|
629
447
|
exports.deriveSortState = deriveSortState;
|
|
630
|
-
function keptSelectionsFilter(row, matchedRows) {
|
|
631
|
-
return (!matchedRows.has(row.id) &&
|
|
632
|
-
row.selectable !== false &&
|
|
633
|
-
!utils_1.reservedRowKinds.includes(row.kind) &&
|
|
634
|
-
(!row.children || row.inferSelectedState === false));
|
|
635
|
-
}
|
|
@@ -22,6 +22,7 @@ export declare function maybeApplyFunction<T>(row: T, maybeFn: Properties | ((ro
|
|
|
22
22
|
export declare function matchesFilter(maybeContent: ReactNode | GridCellContent, filter: string): boolean;
|
|
23
23
|
export declare const HEADER = "header";
|
|
24
24
|
export declare const TOTALS = "totals";
|
|
25
|
+
/** Tables expandable columns get an extra header. */
|
|
25
26
|
export declare const EXPANDABLE_HEADER = "expandableHeader";
|
|
26
27
|
export declare const KEPT_GROUP = "keptGroup";
|
|
27
28
|
export declare const reservedRowKinds: string[];
|
|
@@ -156,6 +156,7 @@ function matchesFilter(maybeContent, filter) {
|
|
|
156
156
|
exports.matchesFilter = matchesFilter;
|
|
157
157
|
exports.HEADER = "header";
|
|
158
158
|
exports.TOTALS = "totals";
|
|
159
|
+
/** Tables expandable columns get an extra header. */
|
|
159
160
|
exports.EXPANDABLE_HEADER = "expandableHeader";
|
|
160
161
|
exports.KEPT_GROUP = "keptGroup";
|
|
161
162
|
exports.reservedRowKinds = [exports.HEADER, exports.TOTALS, exports.EXPANDABLE_HEADER, exports.KEPT_GROUP];
|