@homebound/beam 2.302.2 → 2.304.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.
@@ -99,17 +99,21 @@ function GridTable(props) {
99
99
  api.init(persistCollapse, virtuosoRef, rows);
100
100
  api.setActiveRowId(activeRowId);
101
101
  api.setActiveCellId(activeCellId);
102
+ // Push the initial columns directly into tableState, b/c that is what
103
+ // makes the tests pass, but then further updates we'll do through useEffect
104
+ // to avoid "Cannot update component during render" errors.
105
+ api.tableState.setColumns(columnsWithIds, visibleColumnsStorageKey);
102
106
  return api;
103
107
  }, [props.api]);
104
108
  const style = (0, TableStyles_1.resolveStyles)(maybeStyle);
105
109
  const { tableState } = api;
106
110
  tableState.setRows(rows);
107
- tableState.setColumns(columnsWithIds, visibleColumnsStorageKey);
108
- const columns = (0, hooks_1.useComputed)(() => tableState.columns
109
- .filter((c) => tableState.visibleColumnIds.includes(c.id))
110
- .flatMap((c) => c.expandColumns && tableState.expandedColumnIds.includes(c.id)
111
- ? [c, ...tableState.getExpandedColumns(c)]
112
- : [c]), [tableState]);
111
+ (0, react_1.useEffect)(() => {
112
+ tableState.setColumns(columnsWithIds, visibleColumnsStorageKey);
113
+ }, [tableState, columnsWithIds, visibleColumnsStorageKey]);
114
+ const columns = (0, hooks_1.useComputed)(() => {
115
+ return tableState.visibleColumns;
116
+ }, [tableState]);
113
117
  // Initialize the sort state. This will only happen on the first render.
114
118
  // Once the `TableState.sort` is defined, it will not re-initialize.
115
119
  tableState.initSortState(props.sorting, columns);
@@ -128,6 +132,7 @@ function GridTable(props) {
128
132
  // (or us) is resetting component state more than necessary, so we track render counts from
129
133
  // here instead.
130
134
  const { getCount } = (0, useRenderCount_1.useRenderCount)();
135
+ // Our column sizes use either `w` or `expandedWidth`, so see which columns are currently expanded
131
136
  const expandedColumnIds = (0, hooks_1.useComputed)(() => tableState.expandedColumnIds, [tableState]);
132
137
  const columnSizes = (0, useSetupColumnSizes_1.useSetupColumnSizes)(style, columns, resizeTarget !== null && resizeTarget !== void 0 ? resizeTarget : resizeRef, expandedColumnIds);
133
138
  // Make a single copy of our current collapsed state, so we'll have a single observer.
@@ -22,28 +22,30 @@ export declare function useGridTableApi<R extends Kinded>(): GridTableApi<R>;
22
22
  /** Provides an imperative API for an application page to interact with the table. */
23
23
  export type GridTableApi<R extends Kinded> = {
24
24
  /** Scrolls row `index` into view; only supported with `as=virtual` and after a `useEffect`. */
25
- scrollToIndex: (index: number) => void;
25
+ scrollToIndex(index: number): void;
26
26
  /** Returns the ids of currently-selected rows. */
27
27
  getSelectedRowIds(): string[];
28
28
  getSelectedRowIds<K extends R["kind"]>(kind: K): string[];
29
29
  /** Returns the currently-selected rows. */
30
30
  getSelectedRows(): GridDataRow<R>[];
31
+ /** Returns the currently-selected rows of the given `kind`. */
31
32
  getSelectedRows<K extends R["kind"]>(kind: K): GridDataRow<DiscriminateUnion<R, "kind", K>>[];
32
- /** Deselects all rows */
33
+ /** Set selected state of a row by id. */
34
+ selectRow(id: string, selected?: boolean): void;
35
+ /** De-selects all selected rows. */
33
36
  clearSelections(): void;
37
+ /** Whether a row is currently collapsed. */
38
+ isCollapsedRow(id: string): boolean;
39
+ /** Toggle collapse state of a row by id. */
40
+ toggleCollapsedRow(id: string): void;
34
41
  /** Sets the internal state of 'activeRowId' */
35
- setActiveRowId: (id: string | undefined) => void;
42
+ setActiveRowId(id: string | undefined): void;
36
43
  /** Sets the internal state of 'activeCellId' */
37
- setActiveCellId: (id: string | undefined) => void;
38
- /** Set selected state of a row by id */
39
- selectRow: (id: string, selected?: boolean) => void;
40
- /** Deletes a row from the table */
41
- deleteRows: (ids: string[]) => void;
42
- /** Toggle collapse state of a row by id */
43
- toggleCollapsedRow: (id: string) => void;
44
- isCollapsedRow: (id: string) => boolean;
45
- setVisibleColumns: (ids: string[]) => void;
46
- getVisibleColumnIds: () => string[];
44
+ setActiveCellId(id: string | undefined): void;
45
+ /** Deletes a row from the table, i.e. so it's not detected as kept. */
46
+ deleteRows(ids: string[]): void;
47
+ getVisibleColumnIds(): string[];
48
+ setVisibleColumns(ids: string[]): void;
47
49
  };
48
50
  export declare class GridTableApiImpl<R extends Kinded> implements GridTableApi<R> {
49
51
  readonly tableState: TableState;
@@ -31,7 +31,9 @@ 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
+ // Technically this drives both row-collapse and column-expanded
35
+ if (persistCollapse)
36
+ this.tableState.loadCollapse(persistCollapse);
35
37
  this.virtuosoRef = virtuosoRef;
36
38
  }
37
39
  scrollToIndex(index) {
@@ -24,22 +24,23 @@ function EditColumnsButton(props) {
24
24
  : (0, OverlayTrigger_1.isIconButton)(trigger)
25
25
  ? trigger.icon
26
26
  : trigger.name);
27
- const { options } = (0, react_1.useMemo)(() => {
28
- return columns.reduce((acc, column) => {
29
- // Only include options that can be hidden and have the `name` property defined.
30
- if (!column.canHide)
31
- return acc;
32
- if (!column.name || column.name.length === 0 || !column.id || column.id.length === 0) {
33
- console.warn("Column is missing 'name' and/or 'id' property required by the Edit Columns button", column);
34
- return acc;
35
- }
36
- // Add current column as an option
37
- return { ...acc, options: acc.options.concat({ label: column.name, value: column.id }) };
38
- }, { options: [] });
39
- }, [columns]);
27
+ const options = (0, react_1.useMemo)(() => columns
28
+ // Only include options that can be hidden
29
+ .filter((column) => column.canHide)
30
+ // And have the `name` property defined
31
+ .filter((column) => {
32
+ if (!column.name || column.name.length === 0 || !column.id || column.id.length === 0) {
33
+ console.warn("Column is missing 'name' and/or 'id' property required by the Edit Columns button", column);
34
+ return false;
35
+ }
36
+ return true;
37
+ })
38
+ .map((column) => ({ label: column.name, value: column.id })), [columns]);
40
39
  const selectedValues = (0, hooks_1.useComputed)(() => api.getVisibleColumnIds(), [api]);
41
- const setSelectedValues = (0, react_1.useCallback)((values) => {
42
- api.setVisibleColumns(columns.filter((column) => (column.canHide ? values.includes(column.id) : true)).map((c) => c.id));
40
+ const setSelectedValues = (0, react_1.useCallback)((ids) => {
41
+ // Doesn't `options` already filter us to non-hidden/valid-id columns? I.e. could we just do:
42
+ // api.setVisibleColumns(ids);
43
+ api.setVisibleColumns(columns.filter((column) => (column.canHide ? ids.includes(column.id) : true)).map((c) => c.id));
43
44
  }, [columns, api]);
44
45
  return ((0, jsx_runtime_1.jsx)(OverlayTrigger_1.OverlayTrigger, { ...props, menuTriggerProps: menuTriggerProps, state: state, buttonRef: buttonRef, ...tid, children: (0, jsx_runtime_1.jsxs)("div", { css: {
45
46
  ...Css_1.Css.bgWhite.py5.px3.maxwPx(380).bshBasic.$,
@@ -22,7 +22,7 @@ function ExpandableHeader(props) {
22
22
  return ((0, jsx_runtime_1.jsxs)("button", { ...hoverProps, css: Css_1.Css.df.xsMd.aic.jcsb.gap2.px1.hPx(32).mxPx(-8).w("calc(100% + 16px)").br4.lightBlue700.if(isHovered).bgGray100.$, onClick: async () => {
23
23
  if ((0, utils_2.isFunction)(column.expandColumns)) {
24
24
  setIsLoading(true);
25
- await tableState.loadExpandedColumns(column);
25
+ await tableState.loadExpandedColumns(column.id);
26
26
  setIsLoading(false);
27
27
  }
28
28
  // manually calling this as loadExpandedColumns does not toggle
@@ -78,11 +78,11 @@ function RowImpl(props) {
78
78
  let minStickyLeftOffset = 0;
79
79
  let expandColumnHidden = false;
80
80
  return ((0, jsx_runtime_1.jsx)(RowTag, { css: rowCss, ...others, "data-gridrow": true, ...getCount(row.id), children: isKeptGroupRow ? ((0, jsx_runtime_1.jsx)(KeptGroupRow_1.KeptGroupRow, { as: as, style: style, columnSizes: columnSizes, row: row, colSpan: columns.length })) : (columns.map((column, columnIndex) => {
81
- var _a, _b, _c, _d, _e;
81
+ var _a, _b, _c, _d;
82
82
  // If the expandable column was hidden, then we need to look at the previous column to format the `expandHeader` and 'header' kinds correctly.
83
83
  const maybeExpandedColumn = expandColumnHidden ? columns[columnIndex - 1] : column;
84
84
  // Figure out if this column should be considered 'expanded' or not. If the column is hidden on expand, then we need to look at the previous column to see if it's expanded.
85
- const isExpanded = tableState.expandedColumnIds.includes(maybeExpandedColumn.id);
85
+ const isExpanded = tableState.isExpandedColumn(maybeExpandedColumn.id);
86
86
  // If the column is hidden on expand, we don't want to render it. We'll flag that it was hidden, so on the next column we can render this column's "expandHeader" property.
87
87
  if (column.hideOnExpand && isExpanded) {
88
88
  expandColumnHidden = true;
@@ -90,9 +90,9 @@ function RowImpl(props) {
90
90
  }
91
91
  // Need to keep track of the expanded columns so we can add borders as expected for the header rows
92
92
  const numExpandedColumns = isExpanded
93
- ? ((_a = tableState.getExpandedColumns(maybeExpandedColumn)) === null || _a === void 0 ? void 0 : _a.length)
93
+ ? tableState.numberOfExpandedChildren(maybeExpandedColumn.id)
94
94
  ? // Subtract 1 if the column is hidden on expand, since we're not rendering it.
95
- tableState.getExpandedColumns(maybeExpandedColumn).length - (maybeExpandedColumn.hideOnExpand ? 1 : 0)
95
+ tableState.numberOfExpandedChildren(maybeExpandedColumn.id) - (maybeExpandedColumn.hideOnExpand ? 1 : 0)
96
96
  : 0
97
97
  : 0;
98
98
  // If we're rendering the Expandable Header row, then we might need to render the previous column's `expandHeader` property in the case where the column is hidden on expand.
@@ -145,7 +145,7 @@ function RowImpl(props) {
145
145
  column.expandedWidth !== undefined;
146
146
  const content = (0, utils_1.toContent)(maybeContent, isHeader, canSortColumn, sortOn === "client", style, as, alignment, column, isExpandableHeader, isExpandable, minStickyLeftOffset, isKeptSelectedRow);
147
147
  (0, sortRows_1.ensureClientSideSortValueIsSortable)(sortOn, isHeader || isTotals || isExpandableHeader, column, columnIndex, maybeContent);
148
- const maybeSticky = (_b = (((0, utils_1.isGridCellContent)(maybeContent) && maybeContent.sticky) || column.sticky)) !== null && _b !== void 0 ? _b : undefined;
148
+ const maybeSticky = (_a = (((0, utils_1.isGridCellContent)(maybeContent) && maybeContent.sticky) || column.sticky)) !== null && _a !== void 0 ? _a : undefined;
149
149
  const maybeStickyColumnStyles = maybeSticky && columnSizes
150
150
  ? {
151
151
  ...Css_1.Css.sticky.z(utils_1.zIndices.stickyColumns).bgWhite.$,
@@ -199,13 +199,13 @@ function RowImpl(props) {
199
199
  currentExpandedColumnCount === 0 &&
200
200
  Css_1.Css.boxShadow(`inset -1px -1px 0 ${Css_1.Palette.Gray200}`).$),
201
201
  // Or level-specific styling
202
- ...(!isHeader && !isTotals && !isExpandableHeader && !!style.levels && ((_c = style.levels[level]) === null || _c === void 0 ? void 0 : _c.cellCss)),
202
+ ...(!isHeader && !isTotals && !isExpandableHeader && !!style.levels && ((_b = style.levels[level]) === null || _b === void 0 ? void 0 : _b.cellCss)),
203
203
  // Level specific styling for the first content column
204
- ...(applyFirstContentColumnStyles && !!style.levels && ((_d = style.levels[level]) === null || _d === void 0 ? void 0 : _d.firstContentColumn)),
204
+ ...(applyFirstContentColumnStyles && !!style.levels && ((_c = style.levels[level]) === null || _c === void 0 ? void 0 : _c.firstContentColumn)),
205
205
  // The specific cell's css (if any from GridCellContent)
206
206
  ...rowStyleCellCss,
207
207
  // Apply active row styling for non-nested card styles.
208
- ...(isActive ? Css_1.Css.bgColor((_e = style.activeBgColor) !== null && _e !== void 0 ? _e : Css_1.Palette.LightBlue50).$ : {}),
208
+ ...(isActive ? Css_1.Css.bgColor((_d = style.activeBgColor) !== null && _d !== void 0 ? _d : Css_1.Palette.LightBlue50).$ : {}),
209
209
  // Add any cell specific style overrides
210
210
  ...((0, utils_1.isGridCellContent)(maybeContent) && maybeContent.typeScale ? Css_1.Css[maybeContent.typeScale].$ : {}),
211
211
  // And any cell specific css
@@ -85,6 +85,12 @@ export type GridColumn<R extends Kinded> = {
85
85
  /** Determines whether this column should be hidden when expanded (only the 'expandColumns' would show) */
86
86
  hideOnExpand?: boolean;
87
87
  };
88
+ /**
89
+ * Adds an `id` to `GridColumn`, for use in storage/APIs.
90
+ *
91
+ * Ideally we'd require this on `GridColumn` itself, but that would be
92
+ * a large breaking change for a lot of tables that don't need column ids.
93
+ */
88
94
  export type GridColumnWithId<R extends Kinded> = GridColumn<R> & {
89
95
  id: string;
90
96
  expandColumns?: GridColumnWithId<R>[] | (() => Promise<GridColumn<R>[]>);
@@ -102,6 +108,6 @@ export type IfAny<T, Y, N> = 0 extends 1 & T ? Y : N;
102
108
  export type InfiniteScroll = {
103
109
  /** will be called when the user scrolls to the end of the list with the last item index as an argument. */
104
110
  onEndReached: (index: number) => void;
105
- /** The number of pixels from the bottom of the list to eagerly trigger `onEndReached`. The default is is 500px. */
111
+ /** The number of pixels from the bottom of the list to eagerly trigger `onEndReached`. The default is 500px. */
106
112
  endOffsetPx?: number;
107
113
  };
@@ -0,0 +1,24 @@
1
+ import { GridColumnWithId } from "../../..";
2
+ import { ColumnStates } from "./ColumnStates";
3
+ import { ColumnStorage } from "./ColumnStorage";
4
+ /**
5
+ * A reactive/observable wrapper around each GridColumn.
6
+ *
7
+ * This is primarily for tracking visible/expanded columns for tables
8
+ * that use the expandable columns feature.
9
+ */
10
+ export declare class ColumnState {
11
+ private states;
12
+ column: GridColumnWithId<any>;
13
+ children: ColumnState[] | undefined;
14
+ private visible;
15
+ private expanded;
16
+ constructor(states: ColumnStates, storage: ColumnStorage, column: GridColumnWithId<any>);
17
+ setVisible(visible: boolean): void;
18
+ get isExpanded(): boolean;
19
+ toggleExpanded(): void;
20
+ /** Calls the `column.expandColumns` function, if set, and adds the resulting columns. */
21
+ doExpand(force?: boolean): Promise<void>;
22
+ /** Returns this column, if visible, and its children, if expanded. */
23
+ get maybeSelfAndChildren(): ColumnState[];
24
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ColumnState = void 0;
4
+ const mobx_1 = require("mobx");
5
+ const src_1 = require("../../..");
6
+ const index_1 = require("../../../utils/index");
7
+ /**
8
+ * A reactive/observable wrapper around each GridColumn.
9
+ *
10
+ * This is primarily for tracking visible/expanded columns for tables
11
+ * that use the expandable columns feature.
12
+ */
13
+ class ColumnState {
14
+ constructor(states, storage, column) {
15
+ var _a, _b, _c;
16
+ this.states = states;
17
+ this.children = undefined;
18
+ this.visible = true;
19
+ this.expanded = false;
20
+ this.column = column;
21
+ // If the user sets `canHide: true`, we default to hidden unless they set `initVisible: true`
22
+ this.visible = (_a = storage.wasVisible(column.id)) !== null && _a !== void 0 ? _a : (column.canHide ? (_b = column.initVisible) !== null && _b !== void 0 ? _b : false : true);
23
+ if (this.visible && ((_c = storage.wasExpanded(column.id)) !== null && _c !== void 0 ? _c : column.initExpanded)) {
24
+ this.expanded = true;
25
+ this.doExpand();
26
+ }
27
+ (0, mobx_1.makeAutoObservable)(this, { column: mobx_1.observable.ref });
28
+ }
29
+ setVisible(visible) {
30
+ const wasVisible = this.visible;
31
+ this.visible = visible;
32
+ // If an expandable header is becoming visible for the 1st time, expand it
33
+ if (!wasVisible && visible && this.column.initExpanded && this.children === undefined) {
34
+ this.expanded = true;
35
+ this.doExpand();
36
+ }
37
+ }
38
+ get isExpanded() {
39
+ return this.expanded;
40
+ }
41
+ toggleExpanded() {
42
+ const wasExpanded = this.expanded;
43
+ this.expanded = !this.expanded;
44
+ // The first time we expand, fetch our children. Note that ExpandableHeader
45
+ // technically pre-loads our children, so it can show a spinner while loading,
46
+ // and only after loading is complete, tell our column to expand.
47
+ if (!wasExpanded)
48
+ this.doExpand();
49
+ }
50
+ /** Calls the `column.expandColumns` function, if set, and adds the resulting columns. */
51
+ async doExpand(force = false) {
52
+ const { expandColumns } = this.column;
53
+ // If we've already got the children, don't re-expand unless forced (i.e. props.columns changed)
54
+ if (this.children !== undefined && !force)
55
+ return;
56
+ if ((0, index_1.isFunction)(expandColumns)) {
57
+ const ecs = await expandColumns();
58
+ this.children = (0, src_1.assignDefaultColumnIds)(ecs).map((ec) => this.states.addColumn(ec));
59
+ }
60
+ else if (expandColumns) {
61
+ this.children = expandColumns.map((ec) => this.states.addColumn(ec));
62
+ }
63
+ }
64
+ /** Returns this column, if visible, and its children, if expanded. */
65
+ get maybeSelfAndChildren() {
66
+ if (!this.visible) {
67
+ return [];
68
+ }
69
+ else if (this.expanded && this.children) {
70
+ // Maybe do the `hideOnExpand` thing here? Seems cute, but the Row rendering still
71
+ // needs to do the "look back to the prior column for the expandableHeader cell" logic.
72
+ // if (this.column.hideOnExpand) {
73
+ // return this.children.flatMap((c) => c.selfAndMaybeChildren);
74
+ // }
75
+ return [this, ...this.children.flatMap((c) => c.maybeSelfAndChildren)];
76
+ }
77
+ else {
78
+ return [this];
79
+ }
80
+ }
81
+ }
82
+ exports.ColumnState = ColumnState;
@@ -0,0 +1,29 @@
1
+ import { GridColumnWithId } from "../../..";
2
+ import { ColumnState } from "./ColumnState";
3
+ /** A reactive/observable wrapper around our columns. */
4
+ export declare class ColumnStates {
5
+ private columns;
6
+ private map;
7
+ private storage;
8
+ constructor();
9
+ /**
10
+ * Updates our internal columns states when `props.columns` changes.
11
+ *
12
+ * We handle sessionStorage here b/c we allow the user to either provide their own
13
+ * storage key, or calc the storage key based on the currently-visible columns.
14
+ * So like you expand a column, and new columns show up, but we'll remember they
15
+ * were hidden last time you looked at this specific expansion of columns.
16
+ */
17
+ setColumns(columns: GridColumnWithId<any>[], visibleColumnsStorageKey: string | undefined): void;
18
+ /** Adds a column to our state, i.e. maybe a dynamically loaded column. */
19
+ addColumn(column: GridColumnWithId<any>): ColumnState;
20
+ /** Returns the `ColumnState` for the given `id`. */
21
+ get(id: string): ColumnState;
22
+ /** Returns all currently-expanded columns. */
23
+ get expandedColumns(): ColumnState[];
24
+ /** Returns a flat list of all visible columns. */
25
+ get allVisibleColumns(): ColumnState[];
26
+ setVisibleColumns(ids: string[]): void;
27
+ loadExpanded(storageKey: string): void;
28
+ loadVisible(storageKey: string): void;
29
+ }
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ColumnStates = void 0;
4
+ const change_case_1 = require("change-case");
5
+ const mobx_1 = require("mobx");
6
+ const ColumnState_1 = require("./ColumnState");
7
+ const ColumnStorage_1 = require("./ColumnStorage");
8
+ /** A reactive/observable wrapper around our columns. */
9
+ class ColumnStates {
10
+ constructor() {
11
+ // The top-level list of columns
12
+ this.columns = [];
13
+ this.map = new Map();
14
+ this.storage = new ColumnStorage_1.ColumnStorage(this);
15
+ (0, mobx_1.makeAutoObservable)(this);
16
+ }
17
+ /**
18
+ * Updates our internal columns states when `props.columns` changes.
19
+ *
20
+ * We handle sessionStorage here b/c we allow the user to either provide their own
21
+ * storage key, or calc the storage key based on the currently-visible columns.
22
+ * So like you expand a column, and new columns show up, but we'll remember they
23
+ * were hidden last time you looked at this specific expansion of columns.
24
+ */
25
+ setColumns(columns, visibleColumnsStorageKey) {
26
+ if (columns.some((c) => c.canHide)) {
27
+ // We optionally auto-calc visible columns based on the currently-_potentially_-visible columns
28
+ visibleColumnsStorageKey !== null && visibleColumnsStorageKey !== void 0 ? visibleColumnsStorageKey : (visibleColumnsStorageKey = (0, change_case_1.camelCase)(columns.map((c) => c.id).join()));
29
+ this.loadVisible(visibleColumnsStorageKey);
30
+ }
31
+ this.columns = columns.map((c) => this.addColumn(c));
32
+ // After the very first non-zero `setColumns`, we disconnect from sessionStorage
33
+ if (columns.length > 0)
34
+ this.storage.done();
35
+ }
36
+ /** Adds a column to our state, i.e. maybe a dynamically loaded column. */
37
+ addColumn(column) {
38
+ const existing = this.map.get(column.id);
39
+ if (!existing) {
40
+ const cs = new ColumnState_1.ColumnState(this, this.storage, column);
41
+ this.map.set(column.id, cs);
42
+ return cs;
43
+ }
44
+ else {
45
+ existing.column = column;
46
+ // Any time a column is re-added (i.e. props.columns changed), re-expand it
47
+ if (existing.isExpanded)
48
+ existing.doExpand(true);
49
+ return existing;
50
+ }
51
+ }
52
+ /** Returns the `ColumnState` for the given `id`. */
53
+ get(id) {
54
+ const cs = this.map.get(id);
55
+ if (!cs)
56
+ throw new Error(`No ColumnState for ${id}`);
57
+ return cs;
58
+ }
59
+ /** Returns all currently-expanded columns. */
60
+ get expandedColumns() {
61
+ return this.columns.filter((cs) => cs.isExpanded);
62
+ }
63
+ /** Returns a flat list of all visible columns. */
64
+ get allVisibleColumns() {
65
+ return this.columns.flatMap((cs) => cs.maybeSelfAndChildren);
66
+ }
67
+ setVisibleColumns(ids) {
68
+ for (const cs of this.map.values()) {
69
+ cs.setVisible(ids.includes(cs.column.id));
70
+ }
71
+ }
72
+ loadExpanded(storageKey) {
73
+ this.storage.loadExpanded(storageKey);
74
+ }
75
+ loadVisible(storageKey) {
76
+ this.storage.loadVisible(storageKey);
77
+ }
78
+ }
79
+ exports.ColumnStates = ColumnStates;
@@ -0,0 +1,13 @@
1
+ import { ColumnStates } from "./ColumnStates";
2
+ /** Loads/saves the column state from sessionStorage. */
3
+ export declare class ColumnStorage {
4
+ private states;
5
+ private expandedIds;
6
+ private visibleIds;
7
+ constructor(states: ColumnStates);
8
+ loadExpanded(persistCollapse: string): void;
9
+ loadVisible(storageKey: string): void;
10
+ wasExpanded(id: string): boolean | undefined;
11
+ wasVisible(id: string): boolean | undefined;
12
+ done(): void;
13
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ColumnStorage = void 0;
4
+ const mobx_1 = require("mobx");
5
+ const utils_1 = require("./utils");
6
+ /** Loads/saves the column state from sessionStorage. */
7
+ class ColumnStorage {
8
+ constructor(states) {
9
+ this.states = states;
10
+ }
11
+ loadExpanded(persistCollapse) {
12
+ const key = `expandedColumn_${persistCollapse}`;
13
+ this.expandedIds = (0, utils_1.loadArrayOrUndefined)(key);
14
+ (0, mobx_1.reaction)(() => this.states.expandedColumns.map((cs) => cs.column.id), (columnIds) => sessionStorage.setItem(key, JSON.stringify(columnIds)));
15
+ }
16
+ loadVisible(storageKey) {
17
+ this.visibleIds = (0, utils_1.loadArrayOrUndefined)(storageKey);
18
+ // Unlike the others, where we only store the value on change, we immediately
19
+ // store this value (but I'm not sure why...), hence using `autorun`.
20
+ (0, mobx_1.autorun)(() => {
21
+ const columnIds = this.states.allVisibleColumns.map((cs) => cs.column.id);
22
+ sessionStorage.setItem(storageKey, JSON.stringify(columnIds));
23
+ });
24
+ }
25
+ wasExpanded(id) {
26
+ var _a;
27
+ return (_a = this.expandedIds) === null || _a === void 0 ? void 0 : _a.includes(id);
28
+ }
29
+ wasVisible(id) {
30
+ var _a;
31
+ return (_a = this.visibleIds) === null || _a === void 0 ? void 0 : _a.includes(id);
32
+ }
33
+ done() {
34
+ this.expandedIds = undefined;
35
+ }
36
+ }
37
+ exports.ColumnStorage = ColumnStorage;
@@ -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.