@homebound/beam 2.301.0 → 2.302.0

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.
@@ -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
- }, [presentationProps, existingContext]);
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 keptSelectedDataRows = (0, hooks_1.useComputed)(() => tableState.keptSelectedRows, [tableState]);
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 && keptSelectedDataRows.length > 0) {
148
- return (0, sortRows_1.sortRows)(columns, keptSelectedDataRows, sortState, caseSensitive);
147
+ if (sortOn === "client" && sortState && keptDataRows.length > 0) {
148
+ return (0, sortRows_1.sortRows)(columns, keptDataRows, sortState, caseSensitive);
149
149
  }
150
- return keptSelectedDataRows;
151
- }, [columns, sortOn, sortState, caseSensitive, keptSelectedDataRows]);
152
- // Flatten + component-ize the sorted rows.
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
- function makeRowComponent(row, level, isKeptSelectedRow = false, isLastKeptSelectionRow = false) {
155
- return ((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
- sortOn,
168
- hasExpandableHeader,
169
- isKeptSelectedRow,
170
- isLastKeptSelectionRow,
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
- const filteredRowIds = [];
180
- const hasExpandableHeader = rows.some((row) => row.id === utils_1.EXPANDABLE_HEADER);
181
- function visit([row, children], level, visible) {
182
- visible && visibleDataRows.push([row, makeRowComponent(row, level)]);
183
- // This row may be invisible (because it's parent is collapsed), but we still want
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[0].kind === "totals") {
199
- totalsRows.push([row[0], makeRowComponent(row[0], level)]);
200
- return;
183
+ else if (row.kind === "totals") {
184
+ totalsRows.push([row, makeRowComponent(row, level)]);
201
185
  }
202
- if (row[0].kind === "expandableHeader") {
203
- expandableHeaderRows.push([row[0], makeRowComponent(row[0], level)]);
204
- return;
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 a pre-filtered set list
210
- const filteredRows = filterRows(api, columns, maybeSorted, filter);
211
- visitRows(filteredRows, 0, true);
212
- // Check for any selected rows that are not displayed in the table because they don't match the current filter, or are no longer part of the `rows` prop.
213
- // We persist these selected rows and hoist them to the top of the table.
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
- // The "group row" for selected rows that are hidden by filters and add the children
216
- const keptSelectionGroupRow = {
217
- id: utils_1.KEPT_GROUP,
218
- kind: utils_1.KEPT_GROUP,
219
- children: sortedKeptSelections,
220
- initCollapsed: true,
221
- data: undefined,
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
- sortOn,
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
- let tooManyClientSideRows = false;
248
- if (filterMaxRows && visibleDataRows.length > filterMaxRows) {
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.keptSelectedRows.length, [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
- /** * Whether to pin this sort to the first/last of its parent's children.
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, sortOn, columnSizes, level, getCount, api, cellHighlight, omitRowHover, hasExpandableHeader, isKeptSelectedRow, isLastKeptSelectionRow, ...others } = props;
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((_a = style.rowHoverColor) !== null && _a !== void 0 ? _a : Css_1.Palette.LightBlue100).$,
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,51 @@
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
+ }
@@ -0,0 +1,129 @@
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.children && this.inferSelectedState)
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.children && this.inferSelectedState) {
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
+ // Parents don't need keeping, unless they're actually real rows
111
+ !(this.children && this.inferSelectedState) &&
112
+ (!this.isMatched || this.wasRemoved));
113
+ }
114
+ get inferSelectedState() {
115
+ return this.row.inferSelectedState !== false;
116
+ }
117
+ get visibleChildren() {
118
+ var _a, _b, _c;
119
+ // The keptGroup should treat all of its children as visible, as this makes select/unselect all work.
120
+ if (this.row.kind === src_1.KEPT_GROUP)
121
+ return (_a = this.children) !== null && _a !== void 0 ? _a : [];
122
+ return (_c = (_b = this.children) === null || _b === void 0 ? void 0 : _b.filter((c) => c.isMatched === true)) !== null && _c !== void 0 ? _c : [];
123
+ }
124
+ /** Pretty toString. */
125
+ [Symbol.for("nodejs.util.inspect.custom")]() {
126
+ return `RowState ${this.row.kind}-${this.row.id}`;
127
+ }
128
+ }
129
+ 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 { ObservableMap, ObservableSet } from "mobx";
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 either all data rows (non-header, non-totals, etc) that are selected where `row.selectable !== false` */
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
- // This will include rows that are selected, are partially selected, or were at once selected but are now unchecked.
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
- // Provide some defaults to get the sort state to properly work.
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
- // Whenever our `matchedRows` change (i.e. via filtering) then we need to re-derive header and parent rows' selected state.
77
- (0, mobx_1.reaction)(() => [...this.matchedRows.values()].sort(), () => {
78
- const newlyKeptRows = [...this.selectedDataRows.values()].filter((row) => keptSelectionsFilter(row, this.matchedRows));
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
- // When filters are applied, we need to determine if there are any selected rows that are no longer matched.
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
- // ObservableSet doesn't seem to do a `diff` inside `replace` before firing
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 either all data rows (non-header, non-totals, etc) that are selected where `row.selectable !== false` */
285
+ /** Returns selected data rows (non-header, non-totals, etc.), ignoring rows that have `row.selectable !== false`. */
345
286
  get selectedRows() {
346
- return [...this.selectedDataRows.values()].filter((row) => row.selectable !== false && !utils_1.reservedRowKinds.includes(row.kind));
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
- var _a;
351
- // Return the row's selected state if it's been explicitly set. If it is in the selectedDataRows set, then it's selected. Otherwise, it's unchecked.
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
- var _a;
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.keptSelectedRows = this.keptSelectedRows.filter((row) => !ids.includes(row.id));
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.301.0",
3
+ "version": "2.302.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",