@homebound/beam 2.307.0 → 2.308.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { MutableRefObject } from "react";
2
2
  import { GridTableApi } from "./GridTableApi";
3
3
  import { GridStyle, GridStyleDef, RowStyles } from "./TableStyles";
4
- import { Direction, GridColumn, GridColumnWithId, GridTableXss, InfiniteScroll, Kinded, ParentChildrenTuple, RenderAs } from "./types";
4
+ import { Direction, GridColumn, GridTableXss, InfiniteScroll, Kinded, RenderAs } from "./types";
5
5
  import { GridRowLookup } from "./utils/GridRowLookup";
6
6
  import { Only } from "../../Css";
7
7
  import { GridDataRow } from "./components/Row";
@@ -138,11 +138,3 @@ export interface GridTableProps<R extends Kinded, X> {
138
138
  * https://docs.google.com/document/d/1DFnlkDubK4nG_GLf_hB8yp0flnSNt_3IBh5iOicuaFM/edit#heading=h.9m9cpwgeqfc9
139
139
  */
140
140
  export declare function GridTable<R extends Kinded, X extends Only<GridTableXss, X> = any>(props: GridTableProps<R, X>): import("@emotion/react/jsx-runtime").JSX.Element;
141
- /**
142
- * Filters rows given a client-side text `filter.
143
- *
144
- * Ensures parent rows remain in the list if any children match the filter.
145
- *
146
- * We return a copy of `[Parent, [Child]]` tuples so that we don't modify the `GridDataRow.children`.
147
- */
148
- export declare function filterRows<R extends Kinded>(api: GridTableApi<R>, columns: GridColumnWithId<R>[], rows: GridDataRow<R>[], filter: string | undefined): [string[], ParentChildrenTuple<R>[]];
@@ -26,10 +26,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.filterRows = exports.GridTable = exports.setGridTableDefaults = exports.setDefaultStyle = exports.setRunningInJest = void 0;
29
+ exports.GridTable = exports.setGridTableDefaults = exports.setDefaultStyle = exports.setRunningInJest = void 0;
30
30
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
31
31
  const memoize_one_1 = __importDefault(require("memoize-one"));
32
- const mobx_1 = require("mobx");
33
32
  const react_1 = __importStar(require("react"));
34
33
  const react_virtuoso_1 = require("react-virtuoso");
35
34
  const PresentationContext_1 = require("../PresentationContext");
@@ -38,7 +37,6 @@ const useSetupColumnSizes_1 = require("./hooks/useSetupColumnSizes");
38
37
  const TableStyles_1 = require("./TableStyles");
39
38
  const columns_1 = require("./utils/columns");
40
39
  const GridRowLookup_1 = require("./utils/GridRowLookup");
41
- const sortRows_1 = require("./utils/sortRows");
42
40
  const TableState_1 = require("./utils/TableState");
43
41
  const utils_1 = require("./utils/utils");
44
42
  const Css_1 = require("../../Css");
@@ -117,16 +115,15 @@ function GridTable(props) {
117
115
  // Initialize the sort state. This will only happen on the first render.
118
116
  // Once the `TableState.sort` is defined, it will not re-initialize.
119
117
  tableState.initSortState(props.sorting, columns);
120
- const [sortOn, caseSensitive] = (0, hooks_1.useComputed)(() => {
121
- const { sortConfig } = tableState;
122
- return [sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.on, (sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.on) === "client" ? !!sortConfig.caseSensitive : false];
123
- }, [tableState]);
124
118
  (0, react_1.useEffect)(() => {
125
119
  tableState.activeRowId = activeRowId;
126
120
  }, [tableState, activeRowId]);
127
121
  (0, react_1.useEffect)(() => {
128
122
  tableState.activeCellId = activeCellId;
129
123
  }, [tableState, activeCellId]);
124
+ (0, react_1.useEffect)(() => {
125
+ tableState.setSearch(filter);
126
+ }, [tableState, filter]);
130
127
  // We track render count at the table level, which seems odd (we should be able to track this
131
128
  // internally within each GridRow using a useRef), but we have suspicions that react-virtuoso
132
129
  // (or us) is resetting component state more than necessary, so we track render counts from
@@ -135,111 +132,63 @@ function GridTable(props) {
135
132
  // Our column sizes use either `w` or `expandedWidth`, so see which columns are currently expanded
136
133
  const expandedColumnIds = (0, hooks_1.useComputed)(() => tableState.expandedColumnIds, [tableState]);
137
134
  const columnSizes = (0, useSetupColumnSizes_1.useSetupColumnSizes)(style, columns, resizeTarget !== null && resizeTarget !== void 0 ? resizeTarget : resizeRef, expandedColumnIds);
138
- // Make a single copy of our current collapsed state, so we'll have a single observer.
139
- const collapsedIds = (0, hooks_1.useComputed)(() => tableState.collapsedIds, [tableState]);
140
- const sortState = (0, hooks_1.useComputed)(() => (0, mobx_1.toJS)(tableState.sortState), [tableState]);
141
- const maybeSorted = (0, react_1.useMemo)(() => {
142
- if (sortOn === "client" && sortState) {
143
- // If using client-side sort, the sortState use S = number
144
- return (0, sortRows_1.sortRows)(columns, rows, sortState, caseSensitive);
145
- }
146
- return rows;
147
- }, [columns, rows, sortOn, sortState, caseSensitive]);
148
- const [keptGroupRow, keptDataRows] = (0, hooks_1.useComputed)(() => [tableState.keptRowGroup, tableState.keptRows], [tableState]);
149
- // Sort the `keptSelectedDataRows` separately because the current sorting logic sorts within groups and these "kept" rows are now displayed in a flat list.
150
- // It could also be the case that some of these rows are no longer in the `props.rows` list, and so wouldn't be sorted by the `maybeSorted` logic above.
151
- const sortedKeptSelections = (0, react_1.useMemo)(() => {
152
- if (sortOn === "client" && sortState && keptDataRows.length > 0) {
153
- return (0, sortRows_1.sortRows)(columns, keptDataRows, sortState, caseSensitive);
154
- }
155
- return keptDataRows;
156
- }, [columns, sortOn, sortState, caseSensitive, keptDataRows]);
157
135
  // Flatten, hide-if-filtered, hide-if-collapsed, and component-ize the sorted rows.
158
- let [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds] = (0, react_1.useMemo)(() => {
159
- const hasExpandableHeader = maybeSorted.some((row) => row.id === utils_1.EXPANDABLE_HEADER);
160
- const makeRowComponent = (row, level, isKeptSelectedRow = false, isLastKeptSelectionRow = false) => ((0, jsx_runtime_1.jsx)(Row_1.Row, { ...{
161
- as,
162
- columns,
163
- row,
164
- style,
165
- rowStyles,
166
- columnSizes,
167
- level,
168
- getCount,
169
- api,
170
- cellHighlight: "cellHighlight" in maybeStyle && maybeStyle.cellHighlight === true,
171
- omitRowHover: "rowHover" in maybeStyle && maybeStyle.rowHover === false,
172
- hasExpandableHeader,
173
- isKeptSelectedRow,
174
- isLastKeptSelectionRow,
175
- } }, `${row.kind}-${row.id}`));
136
+ const [tableHeadRows, visibleDataRows, keptSelectedRows, tooManyClientSideRows] = (0, hooks_1.useComputed)(() => {
137
+ const columns = tableState.visibleColumns;
176
138
  // Split out the header rows from the data rows so that we can put an `infoMessage` in between them (if needed).
177
139
  const headerRows = [];
178
140
  const expandableHeaderRows = [];
179
141
  const totalsRows = [];
180
- const visibleDataRows = [];
181
142
  const keptSelectedRows = [];
182
- // Flatten the tuple tree into lists of rows
183
- function visitRows(tuples, level) {
184
- tuples.forEach(([row, children]) => {
185
- if (row.kind === "header") {
186
- headerRows.push([row, makeRowComponent(row, level)]);
187
- }
188
- else if (row.kind === "totals") {
189
- totalsRows.push([row, makeRowComponent(row, level)]);
190
- }
191
- else if (row.kind === "expandableHeader") {
192
- expandableHeaderRows.push([row, makeRowComponent(row, level)]);
193
- }
194
- else {
195
- visibleDataRows.push([row, makeRowComponent(row, level)]);
196
- // tuples has already been client-side filtered, so just check collapsed
197
- if (children.length && !collapsedIds.includes(row.id)) {
198
- visitRows(children, level + 1);
199
- }
200
- }
201
- });
202
- }
203
- // Call `visitRows` with our post-filtered list
204
- const [filteredRowIds, filteredRows] = filterRows(api, columns, maybeSorted, filter);
205
- visitRows(filteredRows, 0);
206
- // Check for any selected rows that are not displayed in the table because they don't
207
- // match the current filter, or are no longer part of the `rows` prop. We persist these
208
- // selected rows and hoist them to the top of the table.
209
- if (sortedKeptSelections.length) {
210
- keptSelectedRows.push([keptGroupRow, makeRowComponent(keptGroupRow, 1)]);
211
- if (!collapsedIds.includes(utils_1.KEPT_GROUP)) {
212
- keptSelectedRows.push(...sortedKeptSelections.map((row, idx) => {
213
- const isLast = idx === sortedKeptSelections.length - 1;
214
- return [row, makeRowComponent(row, 1, true, isLast)];
215
- }));
143
+ let visibleDataRows = [];
144
+ const { visibleRows, keptRows } = tableState;
145
+ const hasExpandableHeader = visibleRows.some((rs) => rs.row.id === utils_1.EXPANDABLE_HEADER);
146
+ // Get the flat list or rows from the header down...
147
+ visibleRows.forEach((rs) => {
148
+ const row = rs.row;
149
+ const tuple = [
150
+ row,
151
+ (0, jsx_runtime_1.jsx)(Row_1.Row, { ...{
152
+ as,
153
+ columns,
154
+ row,
155
+ style,
156
+ rowStyles,
157
+ columnSizes,
158
+ level: rs.level,
159
+ getCount,
160
+ api,
161
+ cellHighlight: "cellHighlight" in maybeStyle && maybeStyle.cellHighlight === true,
162
+ omitRowHover: "rowHover" in maybeStyle && maybeStyle.rowHover === false,
163
+ hasExpandableHeader,
164
+ isKeptSelectedRow: rs.isKept,
165
+ isLastKeptSelectionRow: keptRows[keptRows.length - 1] === rs.row,
166
+ } }, `${row.kind}-${row.id}`),
167
+ ];
168
+ if (row.kind === "header") {
169
+ headerRows.push(tuple);
170
+ }
171
+ else if (row.kind === "expandableHeader") {
172
+ expandableHeaderRows.push(tuple);
173
+ }
174
+ else if (row.kind === "totals") {
175
+ totalsRows.push(tuple);
176
+ }
177
+ else if (rs.isKept || row.kind === utils_1.KEPT_GROUP) {
178
+ keptSelectedRows.push(tuple);
179
+ }
180
+ else {
181
+ visibleDataRows.push(tuple);
216
182
  }
183
+ });
184
+ // Once our header rows are created we can organize them in expected order.
185
+ const tableHeadRows = expandableHeaderRows.concat(headerRows).concat(totalsRows);
186
+ const tooManyClientSideRows = !!filterMaxRows && visibleDataRows.length > filterMaxRows;
187
+ if (tooManyClientSideRows) {
188
+ visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
217
189
  }
218
- return [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds];
219
- }, [
220
- as,
221
- api,
222
- filter,
223
- maybeSorted,
224
- columns,
225
- style,
226
- rowStyles,
227
- maybeStyle,
228
- columnSizes,
229
- collapsedIds,
230
- getCount,
231
- keptGroupRow,
232
- sortedKeptSelections,
233
- ]);
234
- // Once our header rows are created we can organize them in expected order.
235
- const tableHeadRows = expandableHeaderRows.concat(headerRows).concat(totalsRows);
236
- const tooManyClientSideRows = filterMaxRows && visibleDataRows.length > filterMaxRows;
237
- if (tooManyClientSideRows) {
238
- visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
239
- }
240
- (0, react_1.useEffect)(() => {
241
- tableState.setMatchedRows(filteredRowIds);
242
- }, [tableState, filteredRowIds]);
190
+ return [tableHeadRows, visibleDataRows, keptSelectedRows, tooManyClientSideRows];
191
+ }, [as, api, style, rowStyles, maybeStyle, columnSizes, getCount, filterMaxRows]);
243
192
  // Push back to the caller a way to ask us where a row is.
244
193
  const { rowLookup } = props;
245
194
  if (rowLookup) {
@@ -417,48 +366,3 @@ const VirtualRoot = (0, memoize_one_1.default)((gs, _columns, id, xss) => {
417
366
  }, "data-testid": id, children: children }));
418
367
  });
419
368
  });
420
- /**
421
- * Filters rows given a client-side text `filter.
422
- *
423
- * Ensures parent rows remain in the list if any children match the filter.
424
- *
425
- * We return a copy of `[Parent, [Child]]` tuples so that we don't modify the `GridDataRow.children`.
426
- */
427
- function filterRows(api, columns, rows, filter) {
428
- // Make a flat list of ids, in addition to the tuple tree
429
- const filteredRowIds = [];
430
- // Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
431
- const filters = (filter && filter.split(/ +/)) || [];
432
- // Make a functions to do recursion
433
- function acceptAll(acc, row) {
434
- var _a, _b;
435
- filteredRowIds.push(row.id);
436
- return acc.concat([[row, (_b = (_a = row.children) === null || _a === void 0 ? void 0 : _a.reduce(acceptAll, [])) !== null && _b !== void 0 ? _b : []]]);
437
- }
438
- function filterFn(acc, row) {
439
- var _a, _b, _c, _d;
440
- const matches = utils_1.reservedRowKinds.includes(row.kind) ||
441
- filters.length === 0 ||
442
- filters.every((f) => columns.map((c) => (0, utils_1.applyRowFn)(c, row, api, 0, false)).some((maybeContent) => (0, utils_1.matchesFilter)(maybeContent, f)));
443
- if (matches) {
444
- filteredRowIds.push(row.id);
445
- // A matched parent means show all it's children
446
- return acc.concat([[row, (_b = (_a = row.children) === null || _a === void 0 ? void 0 : _a.reduce(acceptAll, [])) !== null && _b !== void 0 ? _b : []]]);
447
- }
448
- else {
449
- // An unmatched parent but with matched children means show the parent
450
- const matchedChildren = (_d = (_c = row.children) === null || _c === void 0 ? void 0 : _c.reduce(filterFn, [])) !== null && _d !== void 0 ? _d : [];
451
- if (matchedChildren.length > 0 ||
452
- typeof row.pin === "string" ||
453
- (row.pin !== undefined && row.pin.filter !== true)) {
454
- filteredRowIds.push(row.id);
455
- return acc.concat([[row, matchedChildren]]);
456
- }
457
- else {
458
- return acc;
459
- }
460
- }
461
- }
462
- return [filteredRowIds, rows.reduce(filterFn, [])];
463
- }
464
- exports.filterRows = filterRows;
@@ -26,7 +26,7 @@ exports.useGridTableApi = useGridTableApi;
26
26
  class GridTableApiImpl {
27
27
  constructor() {
28
28
  // This is public to GridTable but not exported outside of Beam
29
- this.tableState = new TableState_1.TableState();
29
+ this.tableState = new TableState_1.TableState(this);
30
30
  this.virtuosoRef = { current: null };
31
31
  }
32
32
  /** Called once by the GridTable when it takes ownership of this api instance. */
@@ -1,4 +1,4 @@
1
- import { GridColumnWithId } from "../../..";
1
+ import { GridColumnWithId } from "../types";
2
2
  import { ColumnStates } from "./ColumnStates";
3
3
  import { ColumnStorage } from "./ColumnStorage";
4
4
  /**
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ColumnState = void 0;
4
4
  const mobx_1 = require("mobx");
5
- const src_1 = require("../../..");
5
+ const columns_1 = require("./columns");
6
6
  const index_1 = require("../../../utils/index");
7
7
  /**
8
8
  * A reactive/observable wrapper around each GridColumn.
@@ -24,7 +24,7 @@ class ColumnState {
24
24
  this.expanded = true;
25
25
  this.doExpand();
26
26
  }
27
- (0, mobx_1.makeAutoObservable)(this, { column: mobx_1.observable.ref });
27
+ (0, mobx_1.makeAutoObservable)(this, { column: mobx_1.observable.ref }, { name: `ColumnState@${column.id}` });
28
28
  }
29
29
  setVisible(visible) {
30
30
  const wasVisible = this.visible;
@@ -55,7 +55,7 @@ class ColumnState {
55
55
  return;
56
56
  if ((0, index_1.isFunction)(expandColumns)) {
57
57
  const ecs = await expandColumns();
58
- this.children = (0, src_1.assignDefaultColumnIds)(ecs).map((ec) => this.states.addColumn(ec));
58
+ this.children = (0, columns_1.assignDefaultColumnIds)(ecs).map((ec) => this.states.addColumn(ec));
59
59
  }
60
60
  else if (expandColumns) {
61
61
  this.children = expandColumns.map((ec) => this.states.addColumn(ec));
@@ -9,12 +9,11 @@ import { SelectedState } from "./TableState";
9
9
  */
10
10
  export declare class RowState {
11
11
  private states;
12
+ parent: RowState | undefined;
12
13
  /** Our row, only ref observed, so we don't crawl into GraphQL fragments. */
13
14
  row: GridDataRow<any>;
14
15
  /** Our children row states, as of the latest `props.rows`, without any filtering applied. */
15
16
  children: RowState[] | undefined;
16
- /** Whether we match a client-side filter; true if no filter is in place. */
17
- isMatched: boolean;
18
17
  /** Whether we are *directly* selected. */
19
18
  selected: boolean;
20
19
  /** Whether we are collapsed. */
@@ -38,7 +37,14 @@ export declare class RowState {
38
37
  * actively removed.
39
38
  */
40
39
  removed: false | "soft" | "hard";
41
- constructor(states: RowStates, row: GridDataRow<any>);
40
+ constructor(states: RowStates, parent: RowState | undefined, row: GridDataRow<any>);
41
+ /**
42
+ * Whether we match a client-side filter; true if no filter is in place.
43
+ *
44
+ * We should try and keep this based solely on "does/does not match the filter",
45
+ * and do any overrides for things like pinning/kept rows/etc. elsewhere.
46
+ */
47
+ get isMatched(): boolean;
42
48
  /**
43
49
  * Whether we are effectively selected, for `GridTableApi.getSelectedRows`.
44
50
  *
@@ -69,8 +75,13 @@ export declare class RowState {
69
75
  toggleCollapsed(): void;
70
76
  /** Whether this is a selected-but-filtered-out row that we should hoist to the top. */
71
77
  get isKept(): boolean;
78
+ get level(): number;
72
79
  private get inferSelectedState();
80
+ /** Returns this row and, if we're not collapsed, our children. */
81
+ get selfAndMaybeChildren(): RowState[];
73
82
  private get visibleChildren();
83
+ /** The `visibleChildren`, but with the current sort config applied. */
84
+ private get visibleSortedChildren();
74
85
  /**
75
86
  * Returns whether this row should act like a parent.
76
87
  *
@@ -83,4 +94,11 @@ export declare class RowState {
83
94
  * they want the row to be selectable.
84
95
  */
85
96
  private get isParent();
97
+ private get isPinned();
98
+ get isReservedKind(): boolean;
99
+ /** A dedicated method to "looking down" recursively, to avoid loops in `isMatched`. */
100
+ private get hasDirectlyMatchedChildren();
101
+ /** A dedicated method to "looking up" recursively, to avoid loops in `isMatched`. */
102
+ private get hasDirectlyMatchedParent();
103
+ private get isDirectlyMatched();
86
104
  }
@@ -10,16 +10,12 @@ const utils_1 = require("./utils");
10
10
  * that uses parent/children easier to write, i.e. selected-ness and collapsed-ness.
11
11
  */
12
12
  class RowState {
13
- // ...eventually...
14
- // isDirectlyMatched = accept filters in the constructor and do match here
15
- // isEffectiveMatched = isDirectlyMatched || hasMatchedChildren
16
- constructor(states, row) {
13
+ constructor(states, parent, row) {
17
14
  var _a;
18
15
  this.states = states;
16
+ this.parent = parent;
19
17
  /** Our children row states, as of the latest `props.rows`, without any filtering applied. */
20
18
  this.children = undefined;
21
- /** Whether we match a client-side filter; true if no filter is in place. */
22
- this.isMatched = true;
23
19
  /** Whether we are *directly* selected. */
24
20
  this.selected = false;
25
21
  /** Whether we are collapsed. */
@@ -46,7 +42,20 @@ class RowState {
46
42
  this.row = row;
47
43
  this.selected = !!row.initSelected;
48
44
  this.collapsed = (_a = states.storage.wasCollapsed(row.id)) !== null && _a !== void 0 ? _a : !!row.initCollapsed;
49
- (0, mobx_1.makeAutoObservable)(this, { row: mobx_1.observable.ref });
45
+ (0, mobx_1.makeAutoObservable)(this, { row: mobx_1.observable.ref }, { name: `RowState@${row.id}` });
46
+ }
47
+ /**
48
+ * Whether we match a client-side filter; true if no filter is in place.
49
+ *
50
+ * We should try and keep this based solely on "does/does not match the filter",
51
+ * and do any overrides for things like pinning/kept rows/etc. elsewhere.
52
+ */
53
+ get isMatched() {
54
+ return (this.isDirectlyMatched ||
55
+ // A matched parent means show all it's children
56
+ this.hasDirectlyMatchedParent ||
57
+ // An unmatched parent but with matched children means show the parent
58
+ this.hasDirectlyMatchedChildren);
50
59
  }
51
60
  /**
52
61
  * Whether we are effectively selected, for `GridTableApi.getSelectedRows`.
@@ -135,23 +144,47 @@ class RowState {
135
144
  // - or it has (probably) been server-side filtered
136
145
  return (this.selected &&
137
146
  // Headers, totals, etc., do not need keeping
138
- !utils_1.reservedRowKinds.includes(this.row.kind) &&
147
+ !this.isReservedKind &&
139
148
  !this.isParent &&
140
149
  (!this.isMatched || this.removed === "soft"));
141
150
  }
151
+ get level() {
152
+ // Make the header level -1, so the top-level rows are level 0
153
+ return !this.parent ? -1 : this.parent.level + 1;
154
+ }
142
155
  get inferSelectedState() {
143
156
  return this.row.inferSelectedState !== false;
144
157
  }
158
+ /** Returns this row and, if we're not collapsed, our children. */
159
+ get selfAndMaybeChildren() {
160
+ // The header always returns all children/top rows, even if collapsed
161
+ if (this.children && (!this.collapsed || this.row.kind === utils_1.HEADER)) {
162
+ return [this, ...this.visibleSortedChildren.flatMap((c) => c.selfAndMaybeChildren)];
163
+ }
164
+ else {
165
+ return [this];
166
+ }
167
+ }
145
168
  get visibleChildren() {
146
169
  var _a, _b;
147
170
  // The keptGroup is special and its children are the dynamically kept rows
148
171
  if (this.row.kind === utils_1.KEPT_GROUP)
149
172
  return this.states.keptRows;
150
- // Ignore hard-deleted rows, i.e. from `api.deleteRows`; in theory any hard-deleted
151
- // rows should be removed from `this.children` anyway, by a change to `props.rows`,
152
- // but just in case the user calls _only_ `api.deleteRows`, and expects the row to
153
- // go away, go ahead and filter them out here.
154
- return (_b = (_a = this.children) === null || _a === void 0 ? void 0 : _a.filter((c) => c.isMatched === true && c.removed !== "hard")) !== null && _b !== void 0 ? _b : [];
173
+ return ((_b = (_a = this.children) === null || _a === void 0 ? void 0 : _a.filter((rs) =>
174
+ // Reserved rows are always visible, even though they're not considered matched.
175
+ // ...except for the kept group: its `isMatched` will become true whenever it has
176
+ // any kept row children, as they will cause its hasDirectlyMatchedChildren to be true.
177
+ (rs.isReservedKind && rs.row.kind !== utils_1.KEPT_GROUP) || rs.isMatched || rs.isPinned)) !== null && _b !== void 0 ? _b : []);
178
+ }
179
+ /** The `visibleChildren`, but with the current sort config applied. */
180
+ get visibleSortedChildren() {
181
+ let rows = this.visibleChildren;
182
+ const { sortFn } = this.states.table;
183
+ // We need to make a copy for mobx to see the sort as a change, and also to not mutate
184
+ // the original/unsorted array if we need to revert to the original sort order.
185
+ if (sortFn)
186
+ rows = [...rows].sort(sortFn);
187
+ return rows;
155
188
  }
156
189
  /**
157
190
  * Returns whether this row should act like a parent.
@@ -167,9 +200,43 @@ class RowState {
167
200
  get isParent() {
168
201
  return !!this.children && this.children.length > 0 && this.inferSelectedState;
169
202
  }
170
- /** Pretty toString. */
203
+ get isPinned() {
204
+ return typeof this.row.pin === "string" || (!!this.row.pin && this.row.pin.filter !== true);
205
+ }
206
+ get isReservedKind() {
207
+ return utils_1.reservedRowKinds.includes(this.row.kind);
208
+ }
209
+ /** A dedicated method to "looking down" recursively, to avoid loops in `isMatched`. */
210
+ get hasDirectlyMatchedChildren() {
211
+ // The keptGroup is special and its children are the dynamically kept rows
212
+ if (this.row.kind === utils_1.KEPT_GROUP)
213
+ return this.states.keptRows.length > 0;
214
+ return !!this.children && this.children.some((c) => c.isDirectlyMatched || c.hasDirectlyMatchedChildren);
215
+ }
216
+ /** A dedicated method to "looking up" recursively, to avoid loops in `isMatched`. */
217
+ get hasDirectlyMatchedParent() {
218
+ return !!this.parent && (this.parent.isDirectlyMatched || this.parent.hasDirectlyMatchedParent);
219
+ }
220
+ get isDirectlyMatched() {
221
+ // Reserved rows like the header can never be directly matched, and treating them
222
+ // as matched currently throws off the header's select all/etc. behavior
223
+ if (this.isReservedKind)
224
+ return false;
225
+ // Ignore hard-deleted rows, i.e. from `api.deleteRows`; in theory any hard-deleted
226
+ // rows should be removed from `this.children` anyway, by a change to `props.rows`,
227
+ // but just in case the user calls _only_ `api.deleteRows`, and expects the row to
228
+ // go away, go ahead and filter them out here.
229
+ if (this.removed === "hard")
230
+ return false;
231
+ // Reacts to either search state or visibleColumns state changing
232
+ const { visibleColumns, api, search } = this.states.table;
233
+ return search.every((term) => visibleColumns
234
+ .map((c) => (0, utils_1.applyRowFn)(c, this.row, api, 0, false))
235
+ .some((maybeContent) => (0, utils_1.matchesFilter)(maybeContent, term)));
236
+ }
237
+ /** Used by node when doing `console.log(rs)`. */
171
238
  [Symbol.for("nodejs.util.inspect.custom")]() {
172
- return `RowState ${this.row.kind}-${this.row.id}`;
239
+ return `RowState@${this.row.id}`;
173
240
  }
174
241
  }
175
242
  exports.RowState = RowState;
@@ -1,17 +1,19 @@
1
1
  import { GridDataRow } from "../components/Row";
2
2
  import { RowState } from "./RowState";
3
3
  import { RowStorage } from "./RowStorage";
4
+ import { TableState } from "./TableState";
4
5
  /**
5
6
  * Manages our tree of observable RowStates that manage each GridDataRow's behavior.
6
7
  */
7
8
  export declare class RowStates {
8
9
  private map;
9
- storage: RowStorage;
10
+ readonly table: TableState;
11
+ readonly storage: RowStorage;
12
+ private readonly header;
10
13
  private keptGroupRow;
11
- private header;
12
14
  /** The first level of rows, i.e. not the header (or kept group), but the totals + top-level children. */
13
15
  private topRows;
14
- constructor();
16
+ constructor(table: TableState);
15
17
  /** Returns a flat list of all of our RowStates. */
16
18
  get allStates(): RowState[];
17
19
  /** Returns the `RowState` for the given `id`. We should probably require `kind`. */
@@ -26,10 +28,11 @@ export declare class RowStates {
26
28
  delete(ids: string[]): void;
27
29
  /** Implements special collapse behavior, which is just the header's collapse/uncollapse. */
28
30
  toggleCollapsed(id: string): void;
29
- setMatchedRows(ids: string[]): void;
31
+ get visibleRows(): RowState[];
30
32
  /** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
31
33
  get keptRows(): RowState[];
32
34
  get collapsedRows(): RowState[];
35
+ private createHeaderRow;
33
36
  /** Create our synthetic "group row" for kept rows, that users never pass in, but we self-inject as needed. */
34
37
  private createKeptGroupRow;
35
38
  }
@@ -9,15 +9,19 @@ const utils_1 = require("./utils");
9
9
  * Manages our tree of observable RowStates that manage each GridDataRow's behavior.
10
10
  */
11
11
  class RowStates {
12
- constructor() {
12
+ constructor(table) {
13
13
  // A flat map of all row id -> RowState
14
14
  this.map = new mobx_1.ObservableMap();
15
15
  this.storage = new RowStorage_1.RowStorage(this);
16
+ // Pre-create the header to drive select-all/etc. behavior, even if the user
17
+ // doesn't pass an explicit `header` GridDataRow in `rows.props`
18
+ this.header = this.createHeaderRow();
16
19
  // Pre-create our keptGroupRow for if/when we need it.
17
- this.keptGroupRow = this.createKeptGroupRow();
18
- this.header = undefined;
20
+ this.keptGroupRow = this.createKeptGroupRow(this.header);
19
21
  /** The first level of rows, i.e. not the header (or kept group), but the totals + top-level children. */
20
22
  this.topRows = [];
23
+ this.table = table;
24
+ this.map.set(this.header.row.id, this.header);
21
25
  this.map.set(this.keptGroupRow.row.id, this.keptGroupRow);
22
26
  }
23
27
  /** Returns a flat list of all of our RowStates. */
@@ -37,43 +41,50 @@ class RowStates {
37
41
  * Any missing rows are marked as `wasRemoved` so we can consider them "kept" if they're also selected.
38
42
  */
39
43
  setRows(rows) {
40
- const existing = new Set(this.map.values());
41
44
  const states = this;
42
45
  const map = this.map;
43
- function addRowAndChildren(row) {
46
+ // Keep track of ids as we add them, to detect duplicates
47
+ const seenIds = new Set();
48
+ // Keep track of existing rows, so we can mark any that are missing as removed
49
+ const maybeKept = new Set(this.map.values());
50
+ function addRowAndChildren(parent, row) {
44
51
  var _a;
45
- // This should really be kind+id, but a lot of our lookups just use id
52
+ // This should really be kind+id, but nearly all of our existing API uses just ids,
53
+ // b/c we assume our ids are tagged/unique across parent/child kinds anyway. So go
54
+ // ahead and enforce "row.id must be unique across kinds" b/c pragmatically that is
55
+ // baked into the current API signatures.
46
56
  const key = row.id;
57
+ if (seenIds.has(key)) {
58
+ throw new Error(`Duplicate row id ${key}`);
59
+ }
60
+ else {
61
+ seenIds.add(key);
62
+ }
47
63
  let state = map.get(key);
48
64
  if (!state) {
49
- state = new RowState_1.RowState(states, row);
65
+ state = new RowState_1.RowState(states, parent, row);
50
66
  map.set(key, state);
51
67
  }
52
68
  else {
69
+ state.parent = parent;
53
70
  state.row = row;
54
71
  state.removed = false;
55
- existing.delete(state);
72
+ maybeKept.delete(state);
56
73
  }
57
- state.children = (_a = row.children) === null || _a === void 0 ? void 0 : _a.map((child) => addRowAndChildren(child));
74
+ state.children = (_a = row.children) === null || _a === void 0 ? void 0 : _a.map((child) => addRowAndChildren(state, child));
58
75
  return state;
59
76
  }
60
77
  // Probe for the header row, so we can create it as a root RowState, even
61
78
  // though we don't require the user to model their GridDataRows that way.
62
79
  const headerRow = rows.find((r) => r.kind === utils_1.HEADER);
63
- this.header = headerRow ? addRowAndChildren(headerRow) : undefined;
80
+ this.header.row = headerRow || missingHeader;
64
81
  // Add the top-level rows
65
- this.topRows = rows.filter((row) => row !== headerRow).map((row) => addRowAndChildren(row));
66
- // And attach them to the header for select-all/etc. to work
67
- if (this.header) {
68
- this.header.children = [
69
- // Always add the keptGroupRow, and we'll use keptGroupRow.isMatched=true/false to keep it
70
- // from messing up "header is all selected" if its hidden/when there are no kept rows.
71
- this.keptGroupRow,
72
- ...this.topRows.filter((rs) => !utils_1.reservedRowKinds.includes(rs.row.kind)),
73
- ];
74
- }
82
+ this.topRows = rows.filter((row) => row !== headerRow).map((row) => addRowAndChildren(this.header, row));
83
+ // Always add the keptGroupRow, and we'll use keptGroupRow.isMatched=true/false to keep it
84
+ // from messing up "header is all selected" if its hidden/when there are no kept rows.
85
+ this.header.children = [this.keptGroupRow, ...this.topRows];
75
86
  // Then mark any remaining as removed
76
- for (const state of existing)
87
+ for (const state of maybeKept)
77
88
  state.markRemoved();
78
89
  // After the first load of real data, we detach collapse state, to respect
79
90
  // any incoming initCollapsed.
@@ -110,29 +121,24 @@ class RowStates {
110
121
  }
111
122
  else {
112
123
  rs.toggleCollapsed();
113
- // The header might still be collapsed, even though the user has opened all the top-level rows
114
- if (this.topRows.every((rs) => !rs.children || !rs.collapsed) && this.header) {
115
- this.header.collapsed = false;
116
- }
117
- // Alternatively, if the user has collapsed all top-level rows, then collapse the header as well.
118
- if (this.topRows.every((rs) => !rs.children || rs.collapsed) && this.header) {
119
- this.header.collapsed = true;
124
+ // Ignore the kept group
125
+ const topGroupRows = this.topRows.filter((rs) => !rs.isReservedKind && rs.children);
126
+ if (topGroupRows.length > 0) {
127
+ // The header might still be collapsed, even though the user has opened all the top-level rows
128
+ if (topGroupRows.every((rs) => !rs.collapsed))
129
+ this.header.collapsed = false;
130
+ // Alternatively, if the user has collapsed all top-level rows, then collapse the header as well.
131
+ if (topGroupRows.every((rs) => rs.collapsed))
132
+ this.header.collapsed = true;
120
133
  }
121
134
  }
122
135
  }
123
- setMatchedRows(ids) {
124
- for (const rs of this.allStates) {
125
- // Don't mark headers, kept rows, etc. as unmatched, b/c they will always be visible,
126
- // i.e. the kept group row, if we've included it, is always matched.
127
- if (!utils_1.reservedRowKinds.includes(rs.row.kind)) {
128
- rs.isMatched = ids.includes(rs.row.id);
129
- }
136
+ get visibleRows() {
137
+ const rows = this.header.selfAndMaybeChildren;
138
+ if (this.header.row === missingHeader) {
139
+ rows.splice(0, 1);
130
140
  }
131
- // We cheat a little and pretend the "kept group matches the filter" not based on it itself
132
- // literally matching the filter, or its children matching the filter (which is typically
133
- // how the filter logic works), but if it just has any child rows at all (which actually means
134
- // its children did _not_ match the filter, but are kept).
135
- this.keptGroupRow.isMatched = this.keptRows.length > 0;
141
+ return rows;
136
142
  }
137
143
  /** Returns kept rows, i.e. those that were user-selected but then client-side or server-side filtered. */
138
144
  get keptRows() {
@@ -141,8 +147,12 @@ class RowStates {
141
147
  get collapsedRows() {
142
148
  return this.allStates.filter((rs) => rs.collapsed);
143
149
  }
150
+ createHeaderRow() {
151
+ // We'll switch the rs.row from the `missingHeader` to the real header from the props.rows later
152
+ return new RowState_1.RowState(this, undefined, missingHeader);
153
+ }
144
154
  /** Create our synthetic "group row" for kept rows, that users never pass in, but we self-inject as needed. */
145
- createKeptGroupRow() {
155
+ createKeptGroupRow(header) {
146
156
  // The "group row" for selected rows that are hidden by filters and add the children
147
157
  const keptGroupRow = {
148
158
  id: utils_1.KEPT_GROUP,
@@ -151,11 +161,13 @@ class RowStates {
151
161
  selectable: false,
152
162
  data: undefined,
153
163
  children: [],
164
+ pin: { at: "first", filter: true },
154
165
  };
155
- const rs = new RowState_1.RowState(this, keptGroupRow);
166
+ const rs = new RowState_1.RowState(this, header, keptGroupRow);
156
167
  // Make the RowState behave like a parent, even though we calc its visibleChildren.
157
168
  rs.children = [];
158
169
  return rs;
159
170
  }
160
171
  }
161
172
  exports.RowStates = RowStates;
173
+ const missingHeader = { kind: "header", id: "header", data: undefined };
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RowStorage = void 0;
4
4
  const mobx_1 = require("mobx");
5
- const src_1 = require("../../..");
5
+ const utils_1 = require("./utils");
6
6
  /**
7
7
  * Manages loading/saving our currently-collapsed rows to session storage.
8
8
  *
@@ -20,7 +20,7 @@ class RowStorage {
20
20
  }
21
21
  load(persistCollapse) {
22
22
  // Load what our previously collapsed rows were
23
- this.historicalIds = (0, src_1.loadArrayOrUndefined)(persistCollapse);
23
+ this.historicalIds = (0, utils_1.loadArrayOrUndefined)(persistCollapse);
24
24
  // And store new collapsed rows going forward
25
25
  (0, mobx_1.reaction)(() => this.states.collapsedRows.map((rs) => rs.row.id), (rowIds) => sessionStorage.setItem(persistCollapse, JSON.stringify(rowIds)));
26
26
  }
@@ -1,7 +1,9 @@
1
1
  import React from "react";
2
2
  import { GridDataRow } from "../components/Row";
3
3
  import { GridSortConfig } from "../GridTable";
4
+ import { GridTableApi } from "../GridTableApi";
4
5
  import { Direction, GridColumnWithId } from "../types";
6
+ import { RowState } from "./RowState";
5
7
  export type SelectedState = "checked" | "unchecked" | "partial";
6
8
  /**
7
9
  * Stores the collapsed & selected state of rows.
@@ -22,10 +24,13 @@ export declare class TableState {
22
24
  private persistCollapse;
23
25
  private rows;
24
26
  columns: GridColumnWithId<any>[];
27
+ readonly api: GridTableApi<any>;
25
28
  private readonly rowStates;
26
29
  private readonly columnStates;
27
30
  activeRowId: string | undefined;
28
31
  activeCellId: string | undefined;
32
+ /** Stores the current client-side type-ahead search/filter. */
33
+ search: string[];
29
34
  sortConfig: GridSortConfig | undefined;
30
35
  sort: SortState;
31
36
  private initialSortState;
@@ -33,13 +38,17 @@ export declare class TableState {
33
38
  /**
34
39
  * Creates the `RowState` for a given `GridTable`.
35
40
  */
36
- constructor();
41
+ constructor(api: GridTableApi<any>);
37
42
  loadCollapse(persistCollapse: string): void;
38
43
  initSortState(sortConfig: GridSortConfig | undefined, columns: GridColumnWithId<any>[]): void;
39
44
  setSortKey(clickedColumnId: string): void;
40
45
  get sortState(): SortState | undefined;
46
+ /** Returns a client-side sort function, if applicable. */
47
+ get sortFn(): ((a: RowState, b: RowState) => number) | undefined;
41
48
  setRows(rows: GridDataRow<any>[]): void;
42
49
  setColumns(columns: GridColumnWithId<any>[], visibleColumnsStorageKey: string | undefined): void;
50
+ setSearch(search: string | undefined): void;
51
+ get visibleRows(): RowState[];
43
52
  /** Returns visible columns, i.e. those that are visible + any expanded children. */
44
53
  get visibleColumns(): GridColumnWithId<any>[];
45
54
  /** Implements GridTableApi.visibleColumnIds. */
@@ -50,8 +59,6 @@ export declare class TableState {
50
59
  toggleExpandedColumn(columnId: string): void;
51
60
  numberOfExpandedChildren(columnId: string): number;
52
61
  loadExpandedColumns(columnId: string): Promise<void>;
53
- /** Called when GridTable has re-calced the rows that pass the client-side filter, or all rows. */
54
- setMatchedRows(rowIds: string[]): void;
55
62
  /** Returns selected data rows (non-header, non-totals, etc.), ignoring rows that have `row.selectable !== false`. */
56
63
  get selectedRows(): GridDataRow<any>[];
57
64
  /** Returns kept group row, with the latest kept children, if any. */
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.deriveSortState = exports.TableStateContext = exports.TableState = void 0;
7
7
  const mobx_1 = require("mobx");
8
8
  const react_1 = __importDefault(require("react"));
9
+ const components_1 = require("../..");
9
10
  const ColumnStates_1 = require("./ColumnStates");
10
11
  const RowStates_1 = require("./RowStates");
11
12
  const utils_1 = require("./utils");
@@ -28,26 +29,30 @@ class TableState {
28
29
  /**
29
30
  * Creates the `RowState` for a given `GridTable`.
30
31
  */
31
- constructor() {
32
+ constructor(api) {
32
33
  // The current list of rows, basically a useRef.current. Only shallow reactive.
33
34
  this.rows = [];
34
35
  // The current list of columns, basically a useRef.current. Only ref reactive.
35
36
  this.columns = [];
36
- this.rowStates = new RowStates_1.RowStates();
37
+ this.rowStates = new RowStates_1.RowStates(this);
37
38
  this.columnStates = new ColumnStates_1.ColumnStates();
38
39
  // Keeps track of the 'active' row, formatted `${row.kind}_${row.id}`
39
40
  this.activeRowId = undefined;
40
41
  // Keeps track of the 'active' cell, formatted `${row.kind}_${row.id}_${column.name}`
41
42
  this.activeCellId = undefined;
43
+ /** Stores the current client-side type-ahead search/filter. */
44
+ this.search = [];
42
45
  // Tracks the active sort column(s), so GridTable or SortHeaders can reactively
43
46
  // re-render (for GridTable, only if client-side sorting)
44
47
  this.sort = {};
48
+ this.api = api;
45
49
  // Make ourselves an observable so that mobx will do caching of .collapseIds so
46
50
  // that it'll be a stable identity for GridTable to useMemo against.
47
51
  (0, mobx_1.makeAutoObservable)(this, {
48
52
  // We use `ref`s so that observables can watch the immutable data change w/o deeply proxy-ifying Apollo fragments
49
53
  rows: mobx_1.observable.ref,
50
54
  columns: mobx_1.observable.ref,
55
+ search: mobx_1.observable.ref,
51
56
  });
52
57
  // If the kept rows went from empty to not empty, then introduce the SELECTED_GROUP row as collapsed
53
58
  (0, mobx_1.reaction)(() => [...this.keptRows.values()], (curr, prev) => {
@@ -113,6 +118,15 @@ class TableState {
113
118
  get sortState() {
114
119
  return this.sort.current ? this.sort : undefined;
115
120
  }
121
+ /** Returns a client-side sort function, if applicable. */
122
+ get sortFn() {
123
+ const { sortState, sortConfig, visibleColumns } = this;
124
+ if (!sortState || (sortConfig === null || sortConfig === void 0 ? void 0 : sortConfig.on) !== "client")
125
+ return undefined;
126
+ // sortRows.ts wants to sort based on the GridDataRow, so make a small `rowStateFn` adapter
127
+ const dataRowFn = (0, components_1.sortFn)(visibleColumns, sortState, !!sortConfig.caseSensitive);
128
+ return (a, b) => dataRowFn(a.row, b.row);
129
+ }
116
130
  // Updates the list of rows and regenerates the collapsedRows property if needed.
117
131
  setRows(rows) {
118
132
  if (rows !== this.rows) {
@@ -126,6 +140,13 @@ class TableState {
126
140
  this.columns = columns;
127
141
  }
128
142
  }
143
+ setSearch(search) {
144
+ // Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
145
+ this.search = (search && search.split(/ +/)) || [];
146
+ }
147
+ get visibleRows() {
148
+ return this.rowStates.visibleRows;
149
+ }
129
150
  /** Returns visible columns, i.e. those that are visible + any expanded children. */
130
151
  get visibleColumns() {
131
152
  return this.columnStates.allVisibleColumns.map((cs) => cs.column);
@@ -154,10 +175,6 @@ class TableState {
154
175
  loadExpandedColumns(columnId) {
155
176
  return this.columnStates.get(columnId).doExpand();
156
177
  }
157
- /** Called when GridTable has re-calced the rows that pass the client-side filter, or all rows. */
158
- setMatchedRows(rowIds) {
159
- this.rowStates.setMatchedRows(rowIds);
160
- }
161
178
  /** Returns selected data rows (non-header, non-totals, etc.), ignoring rows that have `row.selectable !== false`. */
162
179
  get selectedRows() {
163
180
  return this.rowStates.allStates
@@ -4,4 +4,6 @@ import { GridDataRow } from "../components/Row";
4
4
  import { GridColumnWithId, Kinded } from "../types";
5
5
  import { SortOn, SortState } from "./TableState";
6
6
  export declare function sortRows<R extends Kinded>(columns: GridColumnWithId<R>[], rows: GridDataRow<R>[], sortState: SortState, caseSensitive: boolean): GridDataRow<R>[];
7
+ /** Creates a comparator for two GridDataRows based on the current sortState. */
8
+ export declare function sortFn<R extends Kinded>(columns: GridColumnWithId<R>[], sortState: SortState, caseSensitive: boolean): (a: GridDataRow<R>, b: GridDataRow<R>) => number;
7
9
  export declare function ensureClientSideSortValueIsSortable(sortOn: SortOn, isHeader: boolean, column: GridColumnWithId<any>, idx: number, maybeContent: ReactNode | GridCellContent): void;
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureClientSideSortValueIsSortable = exports.sortRows = void 0;
3
+ exports.ensureClientSideSortValueIsSortable = exports.sortFn = exports.sortRows = void 0;
4
4
  const utils_1 = require("./utils");
5
5
  // Returns a shallow copy of the `rows` parameter sorted based on `sortState`
6
+ // We really only use this for tests; in production the RowState.visibleSortedChildren uses the sortFn
6
7
  function sortRows(columns, rows, sortState, caseSensitive) {
7
- const sorted = sortBatch(columns, rows, sortState, caseSensitive);
8
+ const fn = sortFn(columns, sortState, caseSensitive);
9
+ // Sort this level first
10
+ const sorted = [...rows].sort(fn);
8
11
  // Recursively sort child rows
9
12
  sorted.forEach((row, i) => {
10
13
  if (row.children) {
@@ -14,7 +17,8 @@ function sortRows(columns, rows, sortState, caseSensitive) {
14
17
  return sorted;
15
18
  }
16
19
  exports.sortRows = sortRows;
17
- function sortBatch(columns, batch, sortState, caseSensitive) {
20
+ /** Creates a comparator for two GridDataRows based on the current sortState. */
21
+ function sortFn(columns, sortState, caseSensitive) {
18
22
  // When client-side sort, the sort value is the column index
19
23
  const { current, persistent } = sortState !== null && sortState !== void 0 ? sortState : {};
20
24
  const { columnId, direction } = current !== null && current !== void 0 ? current : {};
@@ -23,8 +27,7 @@ function sortBatch(columns, batch, sortState, caseSensitive) {
23
27
  const invert = direction === "DESC";
24
28
  const primaryInvert = persistentSortDirection === "DESC";
25
29
  const primaryColumn = persistentSortColumnId && columns.find((c) => c.id === persistentSortColumnId);
26
- // Make a shallow copy for sorting to avoid mutating the original list
27
- return [...batch].sort((a, b) => {
30
+ return (a, b) => {
28
31
  if (a.pin || b.pin) {
29
32
  // If both rows are pinned, we don't sort within them, because by pinning the page is taking
30
33
  // explicit ownership over the order of the rows (and we also don't support "levels of pins",
@@ -39,13 +42,14 @@ function sortBatch(columns, batch, sortState, caseSensitive) {
39
42
  // When primary key exist sort that priority first
40
43
  const primaryCompare = compare(primaryColumn, a, b, primaryInvert, caseSensitive);
41
44
  // if both rows are not primary sort equivalent
42
- if (primaryCompare !== 0) {
45
+ if (primaryCompare !== 0)
43
46
  return primaryCompare;
44
- }
47
+ // Fall through to the secondary sort
45
48
  }
46
49
  return column ? compare(column, a, b, invert, caseSensitive) : 0;
47
- });
50
+ };
48
51
  }
52
+ exports.sortFn = sortFn;
49
53
  function getPin(pin) {
50
54
  return typeof pin === "string" ? pin : pin === null || pin === void 0 ? void 0 : pin.at;
51
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.307.0",
3
+ "version": "2.308.1",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",