@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.
- package/dist/components/Table/GridTable.js +3 -1
- package/dist/components/Table/GridTableApi.js +2 -1
- package/dist/components/Table/utils/RowState.d.ts +27 -6
- package/dist/components/Table/utils/RowState.js +41 -7
- package/dist/components/Table/utils/RowStates.d.ts +13 -2
- package/dist/components/Table/utils/RowStates.js +73 -29
- package/dist/components/Table/utils/RowStorage.d.ts +27 -0
- package/dist/components/Table/utils/RowStorage.js +44 -0
- package/dist/components/Table/utils/TableState.d.ts +2 -3
- package/dist/components/Table/utils/TableState.js +9 -118
- package/package.json +1 -1
|
@@ -232,7 +232,9 @@ function GridTable(props) {
|
|
|
232
232
|
if (tooManyClientSideRows) {
|
|
233
233
|
visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
|
|
234
234
|
}
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
constructor(parent: RowState | undefined, row: GridDataRow<any>);
|
|
18
|
+
/** Whether we are collapsed. */
|
|
19
|
+
collapsed: boolean;
|
|
22
20
|
/**
|
|
23
|
-
* Whether
|
|
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(
|
|
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
|
|
24
|
-
this.
|
|
25
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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 (
|
|
62
|
-
|
|
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.
|
|
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 (
|
|
77
|
-
this.keptGroupRow
|
|
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
|
|
88
|
-
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
68
|
+
this.rowStates.get(utils_1.KEPT_GROUP).collapsed = true;
|
|
71
69
|
}
|
|
72
70
|
});
|
|
73
71
|
}
|
|
74
|
-
loadCollapse(persistCollapse
|
|
72
|
+
loadCollapse(persistCollapse) {
|
|
75
73
|
this.persistCollapse = persistCollapse;
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
275
|
+
return this.rowStates.get(id).collapsed;
|
|
313
276
|
}
|
|
314
277
|
toggleCollapsed(id) {
|
|
315
|
-
|
|
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
|
}
|