@homebound/beam 2.302.1 → 2.303.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.
@@ -232,7 +232,9 @@ function GridTable(props) {
232
232
  if (tooManyClientSideRows) {
233
233
  visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
234
234
  }
235
- tableState.setMatchedRows(filteredRowIds);
235
+ (0, react_1.useEffect)(() => {
236
+ tableState.setMatchedRows(filteredRowIds);
237
+ }, [tableState, filteredRowIds]);
236
238
  // Push back to the caller a way to ask us where a row is.
237
239
  const { rowLookup } = props;
238
240
  if (rowLookup) {
@@ -31,7 +31,8 @@ class GridTableApiImpl {
31
31
  }
32
32
  /** Called once by the GridTable when it takes ownership of this api instance. */
33
33
  init(persistCollapse, virtuosoRef, rows) {
34
- this.tableState.loadCollapse(persistCollapse, rows);
34
+ if (persistCollapse)
35
+ this.tableState.loadCollapse(persistCollapse);
35
36
  this.virtuosoRef = virtuosoRef;
36
37
  }
37
38
  scrollToIndex(index) {
@@ -1,4 +1,5 @@
1
1
  import { GridDataRow, SelectedState } from "../../..";
2
+ import { RowStates } from "./RowStates";
2
3
  /**
3
4
  * A reactive/observable state of each GridDataRow's current behavior.
4
5
  *
@@ -8,19 +9,36 @@ import { GridDataRow, SelectedState } from "../../..";
8
9
  export declare class RowState {
9
10
  /** Our row, only ref observed, so we don't crawl into GraphQL fragments. */
10
11
  row: GridDataRow<any>;
11
- /** Our parent RowState, or the `header` RowState if we're a top-level row. */
12
- parent: RowState | undefined;
13
12
  /** Our children row states, as of the latest `props.rows`, without any filtering applied. */
14
13
  children: RowState[] | undefined;
15
14
  /** Whether we match a client-side filter; true if no filter is in place. */
16
15
  isMatched: boolean;
17
16
  /** Whether we are *directly* selected. */
18
17
  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>);
18
+ /** Whether we are collapsed. */
19
+ collapsed: boolean;
22
20
  /**
23
- * Whether we are currently selected, for `GridTableApi.getSelectedRows`.
21
+ * Whether our `row` had been in `props.rows`, but then removed _while being
22
+ * selected_, i.e. potentially by server-side filters.
23
+ *
24
+ * We have had a large foot-gun where users "select a row", change the filters,
25
+ * the row disappears (filtered out), and the user clicks "Go!", but the table
26
+ * thinks their previously-selected row is gone (b/c it's not in view), and
27
+ * then the row is inappropriately deleted/unassociated/etc. (b/c in the user's
28
+ * head, it is "still selected").
29
+ *
30
+ * To avoid this, we by default keep selected rows, as "kept rows", to make
31
+ * extra sure the user wants them to go away.
32
+ *
33
+ * Soft-deleted rows are rows that were removed from `props.rows` (i.e. we
34
+ * suspect are just hidden by a changed server-side-filter), and hard-deleted
35
+ * rows are rows the page called `api.deleteRow` and confirmed it should be
36
+ * actively removed.
37
+ */
38
+ removed: false | "soft" | "hard";
39
+ constructor(states: RowStates, row: GridDataRow<any>);
40
+ /**
41
+ * Whether we are effectively selected, for `GridTableApi.getSelectedRows`.
24
42
  *
25
43
  * Note that we don't use "I'm selected || my parent is selected" logic here, because whether a child is selected
26
44
  * is actually based on whether it was _visible at the time the parent was selected_. So, we can't just assume
@@ -44,6 +62,9 @@ export declare class RowState {
44
62
  * child of a selected parent row.
45
63
  */
46
64
  select(selected: boolean): void;
65
+ /** Marks the row as removed from `props.rows`, to potentially become kept. */
66
+ markRemoved(): void;
67
+ toggleCollapsed(): void;
47
68
  /** Whether this is a selected-but-filtered-out row that we should hoist to the top. */
48
69
  get isKept(): boolean;
49
70
  private get inferSelectedState();
@@ -13,22 +13,42 @@ class RowState {
13
13
  // ...eventually...
14
14
  // isDirectlyMatched = accept filters in the constructor and do match here
15
15
  // isEffectiveMatched = isDirectlyMatched || hasMatchedChildren
16
- constructor(parent, row) {
16
+ constructor(states, row) {
17
+ var _a;
17
18
  /** Our children row states, as of the latest `props.rows`, without any filtering applied. */
18
19
  this.children = undefined;
19
20
  /** Whether we match a client-side filter; true if no filter is in place. */
20
21
  this.isMatched = true;
21
22
  /** Whether we are *directly* selected. */
22
23
  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;
24
+ /** Whether we are collapsed. */
25
+ this.collapsed = false;
26
+ /**
27
+ * Whether our `row` had been in `props.rows`, but then removed _while being
28
+ * selected_, i.e. potentially by server-side filters.
29
+ *
30
+ * We have had a large foot-gun where users "select a row", change the filters,
31
+ * the row disappears (filtered out), and the user clicks "Go!", but the table
32
+ * thinks their previously-selected row is gone (b/c it's not in view), and
33
+ * then the row is inappropriately deleted/unassociated/etc. (b/c in the user's
34
+ * head, it is "still selected").
35
+ *
36
+ * To avoid this, we by default keep selected rows, as "kept rows", to make
37
+ * extra sure the user wants them to go away.
38
+ *
39
+ * Soft-deleted rows are rows that were removed from `props.rows` (i.e. we
40
+ * suspect are just hidden by a changed server-side-filter), and hard-deleted
41
+ * rows are rows the page called `api.deleteRow` and confirmed it should be
42
+ * actively removed.
43
+ */
44
+ this.removed = false;
26
45
  this.row = row;
27
46
  this.selected = !!row.initSelected;
47
+ this.collapsed = (_a = states.storage.wasCollapsed(row.id)) !== null && _a !== void 0 ? _a : !!row.initCollapsed;
28
48
  (0, mobx_1.makeAutoObservable)(this, { row: mobx_1.observable.ref });
29
49
  }
30
50
  /**
31
- * Whether we are currently selected, for `GridTableApi.getSelectedRows`.
51
+ * Whether we are effectively selected, for `GridTableApi.getSelectedRows`.
32
52
  *
33
53
  * Note that we don't use "I'm selected || my parent is selected" logic here, because whether a child is selected
34
54
  * is actually based on whether it was _visible at the time the parent was selected_. So, we can't just assume
@@ -99,6 +119,16 @@ class RowState {
99
119
  }
100
120
  }
101
121
  }
122
+ /** Marks the row as removed from `props.rows`, to potentially become kept. */
123
+ markRemoved() {
124
+ // The kept group is never in `props.rows`, so ignore asks to delete it
125
+ if (this.row.kind === src_1.KEPT_GROUP)
126
+ return;
127
+ this.removed = this.selected && this.removed !== "hard" ? "soft" : "hard";
128
+ }
129
+ toggleCollapsed() {
130
+ this.collapsed = !this.collapsed;
131
+ }
102
132
  /** Whether this is a selected-but-filtered-out row that we should hoist to the top. */
103
133
  get isKept() {
104
134
  // this row is "kept" if it is selected, and:
@@ -108,7 +138,7 @@ class RowState {
108
138
  // Headers, totals, etc., do not need keeping
109
139
  !src_1.reservedRowKinds.includes(this.row.kind) &&
110
140
  !this.isParent &&
111
- (!this.isMatched || this.wasRemoved));
141
+ (!this.isMatched || this.removed === "soft"));
112
142
  }
113
143
  get inferSelectedState() {
114
144
  return this.row.inferSelectedState !== false;
@@ -118,7 +148,11 @@ class RowState {
118
148
  // The keptGroup should treat all of its children as visible, as this makes select/unselect all work.
119
149
  if (this.row.kind === src_1.KEPT_GROUP)
120
150
  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 : [];
151
+ // Ignore hard-deleted rows, i.e. from `api.deleteRows`; in theory any hard-deleted
152
+ // rows should be removed from `this.children` anyway, by a change to `props.rows`,
153
+ // but just in case the user calls _only_ `api.deleteRows`, and expects the row to
154
+ // go away, go ahead and filter them out here.
155
+ return (_c = (_b = this.children) === null || _b === void 0 ? void 0 : _b.filter((c) => c.isMatched === true && c.removed !== "hard")) !== null && _c !== void 0 ? _c : [];
122
156
  }
123
157
  /**
124
158
  * Returns whether this row should act like a parent.
@@ -1,11 +1,17 @@
1
- import { GridDataRow } from "../../..";
1
+ import { GridDataRow } from "../components/Row";
2
2
  import { RowState } from "./RowState";
3
+ import { RowStorage } from "./RowStorage";
3
4
  /**
4
5
  * Manages our tree of observable RowStates that manage each GridDataRow's behavior.
5
6
  */
6
7
  export declare class RowStates {
7
8
  private map;
8
- keptGroupRow: RowState;
9
+ storage: RowStorage;
10
+ private keptGroupRow;
11
+ private header;
12
+ /** The first level of rows, i.e. not the header (or kept group), but the totals + top-level children. */
13
+ private topRows;
14
+ constructor();
9
15
  /** Returns a flat list of all of our RowStates. */
10
16
  get allStates(): RowState[];
11
17
  /** Returns the `RowState` for the given `id`. We should probably require `kind`. */
@@ -18,7 +24,12 @@ export declare class RowStates {
18
24
  setRows(rows: GridDataRow<any>[]): void;
19
25
  /** Fully delete `ids`, so they don't show up in kept rows anymore. */
20
26
  delete(ids: string[]): void;
27
+ /** Implements special collapse behavior, which is just the header's collapse/uncollapse. */
28
+ toggleCollapsed(id: string): void;
21
29
  setMatchedRows(ids: string[]): void;
22
30
  /** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
23
31
  get keptRows(): RowState[];
32
+ get collapsedRows(): RowState[];
33
+ /** Create our synthetic "group row" for kept rows, that users never pass in, but we self-inject as needed. */
34
+ private creatKeptGroupRow;
24
35
  }
@@ -2,8 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RowStates = void 0;
4
4
  const mobx_1 = require("mobx");
5
- const src_1 = require("../../..");
6
5
  const RowState_1 = require("./RowState");
6
+ const RowStorage_1 = require("./RowStorage");
7
+ const utils_1 = require("./utils");
7
8
  /**
8
9
  * Manages our tree of observable RowStates that manage each GridDataRow's behavior.
9
10
  */
@@ -11,8 +12,13 @@ class RowStates {
11
12
  constructor() {
12
13
  // A flat map of all row id -> RowState
13
14
  this.map = new mobx_1.ObservableMap();
15
+ this.storage = new RowStorage_1.RowStorage(this);
14
16
  // Pre-create our keptGroupRow for if/when we need it.
15
- this.keptGroupRow = creatKeptGroupRow();
17
+ this.keptGroupRow = this.creatKeptGroupRow();
18
+ this.header = undefined;
19
+ /** The first level of rows, i.e. not the header (or kept group), but the totals + top-level children. */
20
+ this.topRows = [];
21
+ this.map.set(this.keptGroupRow.row.id, this.keptGroupRow);
16
22
  }
17
23
  /** Returns a flat list of all of our RowStates. */
18
24
  get allStates() {
@@ -32,77 +38,115 @@ class RowStates {
32
38
  */
33
39
  setRows(rows) {
34
40
  const existing = new Set(this.map.values());
41
+ const states = this;
35
42
  const map = this.map;
36
- function addRowAndChildren(parent, row) {
43
+ function addRowAndChildren(row) {
37
44
  var _a;
38
45
  // This should really be kind+id, but a lot of our lookups just use id
39
46
  const key = row.id;
40
47
  let state = map.get(key);
41
48
  if (!state) {
42
- state = new RowState_1.RowState(parent, row);
49
+ state = new RowState_1.RowState(states, row);
43
50
  map.set(key, state);
44
51
  }
45
52
  else {
46
- state.parent = parent;
47
53
  state.row = row;
48
- state.wasRemoved = false;
54
+ state.removed = false;
49
55
  existing.delete(state);
50
56
  }
51
- state.children = (_a = row.children) === null || _a === void 0 ? void 0 : _a.map((child) => addRowAndChildren(state, child));
57
+ state.children = (_a = row.children) === null || _a === void 0 ? void 0 : _a.map((child) => addRowAndChildren(child));
52
58
  return state;
53
59
  }
54
60
  // Probe for the header row, so we can create it as a root RowState, even
55
61
  // 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;
62
+ const headerRow = rows.find((r) => r.kind === utils_1.HEADER);
63
+ this.header = headerRow ? addRowAndChildren(headerRow) : undefined;
58
64
  // Add the top-level rows
59
- const children = rows.filter((row) => row !== header).map((row) => addRowAndChildren(headerState, row));
65
+ this.topRows = rows.filter((row) => row !== headerRow).map((row) => addRowAndChildren(row));
60
66
  // 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));
67
+ if (this.header) {
68
+ this.header.children = this.topRows.filter((rs) => !utils_1.reservedRowKinds.includes(rs.row.kind));
63
69
  }
64
70
  // Then mark any remaining as removed
65
- for (const state of existing) {
66
- state.wasRemoved = true;
67
- }
71
+ for (const state of existing)
72
+ state.markRemoved();
68
73
  const keptRows = this.keptRows;
69
74
  if (keptRows.length > 0) {
70
75
  // Stitch the current keptRows into the placeholder keptGroupRow
71
- keptRows.forEach((rs) => (rs.parent = this.keptGroupRow));
72
76
  this.keptGroupRow.children = keptRows;
73
77
  this.keptGroupRow.row.children = keptRows.map((rs) => rs.row);
74
78
  // And then stitch the keptGroupRow itself into the root header, so that the kept rows
75
79
  // 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);
80
+ if (this.header) {
81
+ this.header.children.unshift(this.keptGroupRow);
79
82
  }
80
83
  }
84
+ // After the first load of real data, we detach collapse state, to respect
85
+ // any incoming initCollapsed.
86
+ if (this.topRows.some((rs) => !utils_1.reservedRowKinds.includes(rs.row.kind))) {
87
+ this.storage.done();
88
+ }
81
89
  }
82
90
  /** Fully delete `ids`, so they don't show up in kept rows anymore. */
83
91
  delete(ids) {
84
- var _a;
85
92
  for (const id of ids) {
86
93
  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
- }
94
+ if (rs)
95
+ rs.removed = "hard";
90
96
  this.map.delete(id);
91
97
  }
92
98
  }
99
+ /** Implements special collapse behavior, which is just the header's collapse/uncollapse. */
100
+ toggleCollapsed(id) {
101
+ const rs = this.get(id);
102
+ if (rs === this.header) {
103
+ if (rs.collapsed) {
104
+ // The header being "opened" opens everything
105
+ for (const rs of this.allStates)
106
+ rs.collapsed = false;
107
+ }
108
+ else {
109
+ // The header being "closed" marks all non-leaf rows as collapsed,
110
+ // so that when the user re-opens them, they open a level at a time.
111
+ for (const rs of this.allStates) {
112
+ if (rs.children)
113
+ rs.collapsed = true;
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ rs.toggleCollapsed();
119
+ // The header might still be collapsed, even though the user has opened all the top-level rows
120
+ if (this.topRows.every((rs) => !rs.children || !rs.collapsed) && this.header) {
121
+ this.header.collapsed = false;
122
+ }
123
+ // Alternatively, if the user has collapsed all top-level rows, then collapse the header as well.
124
+ if (this.topRows.every((rs) => !rs.children || rs.collapsed) && this.header) {
125
+ this.header.collapsed = true;
126
+ }
127
+ }
128
+ }
93
129
  setMatchedRows(ids) {
94
130
  for (const rs of this.allStates) {
95
- rs.isMatched = ids.includes(rs.row.id);
131
+ // Don't mark headers, kept rows, etc. as unmatched, b/c they will always be visible,
132
+ // i.e. the kept group row, if we've included it, is always matched.
133
+ if (!utils_1.reservedRowKinds.includes(rs.row.kind)) {
134
+ rs.isMatched = ids.includes(rs.row.id);
135
+ }
96
136
  }
97
137
  }
98
138
  /** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
99
139
  get keptRows() {
100
140
  return this.allStates.filter((rs) => rs.isKept);
101
141
  }
142
+ get collapsedRows() {
143
+ return this.allStates.filter((rs) => rs.collapsed);
144
+ }
145
+ /** Create our synthetic "group row" for kept rows, that users never pass in, but we self-inject as needed. */
146
+ creatKeptGroupRow() {
147
+ // The "group row" for selected rows that are hidden by filters and add the children
148
+ const keptGroupRow = { id: utils_1.KEPT_GROUP, kind: utils_1.KEPT_GROUP, initCollapsed: true, data: undefined };
149
+ return new RowState_1.RowState(this, keptGroupRow);
150
+ }
102
151
  }
103
152
  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
- }
@@ -0,0 +1,27 @@
1
+ import { RowStates } from "./RowStates";
2
+ /**
3
+ * Manages loading/saving our currently-collapsed rows to session storage.
4
+ *
5
+ * This is useful for pages that the user has to go in-out/out-of a lot, and
6
+ * want it to restore, as much as possible, like their previous visit. Granted,
7
+ * we try to superdrawer most of these experiences to avoid the user having to
8
+ * jump off-page.
9
+ *
10
+ * Unlike most of our other states, this is not directly reactive/an observable,
11
+ * although we do reactive to collapsedRows changing to persist the new state.
12
+ */
13
+ export declare class RowStorage {
14
+ private states;
15
+ private historicalIds;
16
+ constructor(states: RowStates);
17
+ load(persistCollapse: string): void;
18
+ /** Once the first real-data load is done, we ignore historical ids so that we prefer any new data's `initCollapsed`. */
19
+ done(): void;
20
+ /**
21
+ * Returns if this row had been collapsed.
22
+ *
23
+ * Technically we return `undefined` if a) there is no persisted state for this row, or b) we are
24
+ * past the first real-data load, and thus should prefer new incoming rows' `initCollapsed` flag.
25
+ */
26
+ wasCollapsed(id: string): boolean | undefined;
27
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RowStorage = void 0;
4
+ const mobx_1 = require("mobx");
5
+ /**
6
+ * Manages loading/saving our currently-collapsed rows to session storage.
7
+ *
8
+ * This is useful for pages that the user has to go in-out/out-of a lot, and
9
+ * want it to restore, as much as possible, like their previous visit. Granted,
10
+ * we try to superdrawer most of these experiences to avoid the user having to
11
+ * jump off-page.
12
+ *
13
+ * Unlike most of our other states, this is not directly reactive/an observable,
14
+ * although we do reactive to collapsedRows changing to persist the new state.
15
+ */
16
+ class RowStorage {
17
+ constructor(states) {
18
+ this.states = states;
19
+ }
20
+ load(persistCollapse) {
21
+ // Load what our previously collapsed rows were
22
+ const ids = sessionStorage.getItem(persistCollapse);
23
+ if (ids) {
24
+ this.historicalIds = JSON.parse(ids);
25
+ }
26
+ // And store new collapsed rows going forward
27
+ (0, mobx_1.reaction)(() => [...this.states.collapsedRows.map((rs) => rs.row.id)], (rowIds) => sessionStorage.setItem(persistCollapse, JSON.stringify(rowIds)));
28
+ }
29
+ /** Once the first real-data load is done, we ignore historical ids so that we prefer any new data's `initCollapsed`. */
30
+ done() {
31
+ this.historicalIds = undefined;
32
+ }
33
+ /**
34
+ * Returns if this row had been collapsed.
35
+ *
36
+ * Technically we return `undefined` if a) there is no persisted state for this row, or b) we are
37
+ * past the first real-data load, and thus should prefer new incoming rows' `initCollapsed` flag.
38
+ */
39
+ wasCollapsed(id) {
40
+ var _a;
41
+ return (_a = this.historicalIds) === null || _a === void 0 ? void 0 : _a.includes(id);
42
+ }
43
+ }
44
+ exports.RowStorage = RowStorage;
@@ -20,9 +20,8 @@ export type SelectedState = "checked" | "unchecked" | "partial";
20
20
  * changes.
21
21
  */
22
22
  export declare class TableState {
23
- private readonly collapsedRows;
24
23
  private persistCollapse;
25
- rows: GridDataRow<any>[];
24
+ private rows;
26
25
  private readonly rowStates;
27
26
  activeRowId: string | undefined;
28
27
  activeCellId: string | undefined;
@@ -39,7 +38,7 @@ export declare class TableState {
39
38
  * Creates the `RowState` for a given `GridTable`.
40
39
  */
41
40
  constructor();
42
- loadCollapse(persistCollapse: string | undefined, rows: GridDataRow<any>[]): void;
41
+ loadCollapse(persistCollapse: string): void;
43
42
  initSortState(sortConfig: GridSortConfig | undefined, columns: GridColumnWithId<any>[]): void;
44
43
  setSortKey(clickedColumnId: string): void;
45
44
  get sortState(): SortState | undefined;
@@ -31,8 +31,6 @@ class TableState {
31
31
  * Creates the `RowState` for a given `GridTable`.
32
32
  */
33
33
  constructor() {
34
- // A set of just row ids, i.e. not row.kind+row.id
35
- this.collapsedRows = new mobx_1.ObservableSet([]);
36
34
  // The current list of rows, basically a useRef.current. Only shallow reactive.
37
35
  this.rows = [];
38
36
  this.rowStates = new RowStates_1.RowStates();
@@ -67,23 +65,13 @@ class TableState {
67
65
  // If the kept rows went from empty to not empty, then introduce the SELECTED_GROUP row as collapsed
68
66
  (0, mobx_1.reaction)(() => [...this.keptRows.values()], (curr, prev) => {
69
67
  if (prev.length === 0 && curr.length > 0) {
70
- this.collapsedRows.add(utils_1.KEPT_GROUP);
68
+ this.rowStates.get(utils_1.KEPT_GROUP).collapsed = true;
71
69
  }
72
70
  });
73
71
  }
74
- loadCollapse(persistCollapse, rows) {
72
+ loadCollapse(persistCollapse) {
75
73
  this.persistCollapse = persistCollapse;
76
- const sessionStorageIds = persistCollapse ? sessionStorage.getItem(persistCollapse) : null;
77
- // Initialize with our collapsed rows based on what is in sessionStorage. Otherwise check if any rows have been defined as collapsed
78
- const collapsedGridRowIds = sessionStorageIds ? JSON.parse(sessionStorageIds) : getCollapsedIdsFromRows(rows);
79
- // If we have some initial rows to collapse, then set the internal prop
80
- if (collapsedGridRowIds.length > 0) {
81
- this.collapsedRows.replace(collapsedGridRowIds);
82
- // If `persistCollapse` is set, but sessionStorageIds was not defined, then add them now.
83
- if (this.persistCollapse && !sessionStorageIds) {
84
- sessionStorage.setItem(this.persistCollapse, JSON.stringify(collapsedGridRowIds));
85
- }
86
- }
74
+ this.rowStates.storage.load(persistCollapse);
87
75
  }
88
76
  initSortState(sortConfig, columns) {
89
77
  var _a, _b;
@@ -139,36 +127,10 @@ class TableState {
139
127
  }
140
128
  // Updates the list of rows and regenerates the collapsedRows property if needed.
141
129
  setRows(rows) {
142
- // If the set of rows are different
143
130
  if (rows !== this.rows) {
144
131
  this.rowStates.setRows(rows);
145
- const currentCollapsedIds = this.collapsedIds;
146
- // Create a list of the (maybe) new rows that should be initially collapsed
147
- const maybeNewCollapsedRowIds = flattenRows(rows)
148
- .filter((r) => r.initCollapsed)
149
- .map((r) => r.id);
150
- // Check against local storage for collapsed state only if this is the first render of "data" (non-header or totals) rows.
151
- const checkLocalStorage = this.persistCollapse && !this.rows.some((r) => r.kind !== "totals" && r.kind !== "header");
152
- // If the list of collapsed rows are different, then determine which are net-new rows and should be added to the newCollapsedIds array
153
- if (currentCollapsedIds.length !== maybeNewCollapsedRowIds.length ||
154
- !currentCollapsedIds.every((id) => maybeNewCollapsedRowIds.includes(id))) {
155
- // Flatten out the existing rows to make checking for new rows easier
156
- const flattenedExistingIds = flattenRows(this.rows).map((r) => r.id);
157
- const newCollapsedIds = maybeNewCollapsedRowIds.filter((maybeNewRowId) => !flattenedExistingIds.includes(maybeNewRowId) &&
158
- // Using `!` on `this.persistCollapse!` as `checkLocalStorage` ensures this.persistCollapse is truthy
159
- (!checkLocalStorage || readCollapsedRowStorage(this.persistCollapse).includes(maybeNewRowId)));
160
- // If there are new rows that should be collapsed then update the collapsedRows arrays
161
- if (newCollapsedIds.length > 0) {
162
- this.collapsedRows.replace(currentCollapsedIds.concat(newCollapsedIds));
163
- // Also update our persistCollapse if set
164
- if (this.persistCollapse) {
165
- sessionStorage.setItem(this.persistCollapse, JSON.stringify(currentCollapsedIds.concat(newCollapsedIds)));
166
- }
167
- }
168
- }
132
+ this.rows = rows;
169
133
  }
170
- // Finally replace our existing list of rows
171
- this.rows = rows;
172
134
  }
173
135
  setColumns(columns, visibleColumnsStorageKey) {
174
136
  const isInitial = !this.columns || this.columns.length === 0;
@@ -290,7 +252,7 @@ class TableState {
290
252
  }
291
253
  /** Returns kept group row, with the latest kept children, if any. */
292
254
  get keptRowGroup() {
293
- return this.rowStates.keptGroupRow.row;
255
+ return this.rowStates.get(utils_1.KEPT_GROUP).row;
294
256
  }
295
257
  /** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
296
258
  get keptRows() {
@@ -299,77 +261,25 @@ class TableState {
299
261
  // Should be called in an Observer/useComputed to trigger re-renders
300
262
  getSelected(id) {
301
263
  const rs = this.rowStates.get(id);
264
+ // The header has special behavior to "see through" selectable parents
302
265
  return id === utils_1.HEADER ? rs.selectedStateForHeader : rs.selectedState;
303
266
  }
304
267
  selectRow(id, selected) {
305
268
  this.rowStates.get(id).select(selected);
306
269
  }
307
270
  get collapsedIds() {
308
- return [...this.collapsedRows.values()];
271
+ return this.rowStates.collapsedRows.map((rs) => rs.row.id);
309
272
  }
310
273
  // Should be called in an Observer/useComputed to trigger re-renders
311
274
  isCollapsed(id) {
312
- return this.collapsedRows.has(id);
275
+ return this.rowStates.get(id).collapsed;
313
276
  }
314
277
  toggleCollapsed(id) {
315
- const collapsedIds = [...this.collapsedRows.values()];
316
- // We have different behavior when going from expand/collapse all.
317
- if (id === "header") {
318
- const isAllCollapsed = collapsedIds.includes("header");
319
- if (isAllCollapsed) {
320
- // Expand all means keep `collapsedIds` empty
321
- collapsedIds.splice(0, collapsedIds.length);
322
- }
323
- else {
324
- // Otherwise push `header` and `selectedGroup` to the list as a hint that we're in the collapsed-all state
325
- collapsedIds.push(utils_1.HEADER, utils_1.KEPT_GROUP);
326
- // Find all non-leaf rows so that toggling "all collapsed" -> "all not collapsed" opens
327
- // the parent rows of any level.
328
- const parentIds = new Set();
329
- const todo = [...this.rows];
330
- while (todo.length > 0) {
331
- const r = todo.pop();
332
- if (r.children) {
333
- parentIds.add(r.id);
334
- todo.push(...r.children);
335
- }
336
- }
337
- // And then mark all parent rows as collapsed.
338
- collapsedIds.push(...parentIds);
339
- }
340
- }
341
- else {
342
- // This is the regular/non-header behavior to just add/remove the individual row id
343
- const i = collapsedIds.indexOf(id);
344
- if (i === -1) {
345
- collapsedIds.push(id);
346
- }
347
- else {
348
- collapsedIds.splice(i, 1);
349
- }
350
- // TODO: Need to handle the kept selected group row.
351
- // If all rows have been expanded individually, but the 'header' was collapsed, then remove the header from the collapsedIds so it reverts to the expanded state
352
- if (collapsedIds.length === 1 && collapsedIds[0] === "header") {
353
- collapsedIds.splice(0, 1);
354
- }
355
- else {
356
- // If every top level child has been collapsed, then push "header" into the array to be considered collapsed as well.
357
- if (this.rows.every((maybeParent) => (maybeParent.children ? collapsedIds.includes(maybeParent.id) : true))) {
358
- collapsedIds.push("header");
359
- }
360
- }
361
- }
362
- this.collapsedRows.replace(collapsedIds);
363
- if (this.persistCollapse) {
364
- sessionStorage.setItem(this.persistCollapse, JSON.stringify(collapsedIds));
365
- }
278
+ this.rowStates.toggleCollapsed(id);
366
279
  }
367
280
  deleteRows(ids) {
368
281
  this.rows = this.rows.filter((row) => !ids.includes(row.id));
369
282
  this.rowStates.delete(ids);
370
- ids.forEach((id) => {
371
- this.collapsedRows.delete(id);
372
- });
373
283
  }
374
284
  }
375
285
  exports.TableState = TableState;
@@ -380,10 +290,6 @@ exports.TableStateContext = react_1.default.createContext({
380
290
  throw new Error("No TableStateContext provider");
381
291
  },
382
292
  });
383
- function readCollapsedRowStorage(persistCollapse) {
384
- const collapsedGridRowIds = sessionStorage.getItem(persistCollapse);
385
- return collapsedGridRowIds ? JSON.parse(collapsedGridRowIds) : [];
386
- }
387
293
  function readExpandedColumnsStorage(persistCollapse) {
388
294
  const expandedGridColumnIds = sessionStorage.getItem(getColumnStorageKey(persistCollapse));
389
295
  return expandedGridColumnIds ? JSON.parse(expandedGridColumnIds) : [];
@@ -398,21 +304,6 @@ function readOrSetLocalVisibleColumnState(columns, storageKey) {
398
304
  sessionStorage.setItem(storageKey, JSON.stringify(visibleColumnIds));
399
305
  return visibleColumnIds;
400
306
  }
401
- function getCollapsedIdsFromRows(rows) {
402
- return rows.reduce((acc, r) => {
403
- if (r.initCollapsed) {
404
- acc.push(r.id);
405
- }
406
- if (r.children) {
407
- acc.push(...getCollapsedIdsFromRows(r.children));
408
- }
409
- return acc;
410
- }, []);
411
- }
412
- function flattenRows(rows) {
413
- const childRows = rows.flatMap((r) => (r.children ? flattenRows(r.children) : []));
414
- return [...rows, ...childRows];
415
- }
416
307
  function getColumnStorageKey(storageKey) {
417
308
  return `expandedColumn_${storageKey}`;
418
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.302.1",
3
+ "version": "2.303.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",