@homebound/beam 2.292.0 → 2.294.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.
@@ -132,6 +132,10 @@ export interface GridTableProps<R extends Kinded, X> {
132
132
  * In a "kind" of cute way, headers are not modeled specially, i.e. they are just another
133
133
  * row `kind` along with the data rows. (Admittedly, out of pragmatism, we do apply some
134
134
  * special styling to the row that uses `kind: "header"`.)
135
+ *
136
+ * For some rationale of our current/historical rendering approaches, see the following doc:
137
+ *
138
+ * https://docs.google.com/document/d/1DFnlkDubK4nG_GLf_hB8yp0flnSNt_3IBh5iOicuaFM/edit#heading=h.9m9cpwgeqfc9
135
139
  */
136
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;
137
141
  /**
@@ -80,6 +80,10 @@ exports.setGridTableDefaults = setGridTableDefaults;
80
80
  * In a "kind" of cute way, headers are not modeled specially, i.e. they are just another
81
81
  * row `kind` along with the data rows. (Admittedly, out of pragmatism, we do apply some
82
82
  * special styling to the row that uses `kind: "header"`.)
83
+ *
84
+ * For some rationale of our current/historical rendering approaches, see the following doc:
85
+ *
86
+ * https://docs.google.com/document/d/1DFnlkDubK4nG_GLf_hB8yp0flnSNt_3IBh5iOicuaFM/edit#heading=h.9m9cpwgeqfc9
83
87
  */
84
88
  function GridTable(props) {
85
89
  var _a, _b, _c;
@@ -136,9 +140,18 @@ function GridTable(props) {
136
140
  }
137
141
  return rows;
138
142
  }, [columns, rows, sortOn, sortState, caseSensitive]);
143
+ const keptSelectedDataRows = (0, hooks_1.useComputed)(() => tableState.keptSelectedRows, [tableState]);
144
+ // Sort the `keptSelectedDataRows` separately because the current sorting logic sorts within groups and these "kept" rows are now displayed in a flat list.
145
+ // It could also be the case that some of these rows are no longer in the `props.rows` list, and so wouldn't be sorted by the `maybeSorted` logic above.
146
+ const sortedKeptSelections = (0, react_1.useMemo)(() => {
147
+ if (sortOn === "client" && sortState && keptSelectedDataRows.length > 0) {
148
+ return (0, sortRows_1.sortRows)(columns, keptSelectedDataRows, sortState, caseSensitive);
149
+ }
150
+ return keptSelectedDataRows;
151
+ }, [columns, sortOn, sortState, caseSensitive, keptSelectedDataRows]);
139
152
  // Flatten + component-ize the sorted rows.
140
- let [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, filteredRowIds] = (0, react_1.useMemo)(() => {
141
- function makeRowComponent(row, level) {
153
+ let [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds] = (0, react_1.useMemo)(() => {
154
+ function makeRowComponent(row, level, isKeptSelectedRow = false, isLastKeptSelectionRow = false) {
142
155
  return ((0, jsx_runtime_1.jsx)(Row_1.Row, { ...{
143
156
  as,
144
157
  columns,
@@ -153,6 +166,8 @@ function GridTable(props) {
153
166
  omitRowHover: "rowHover" in maybeStyle && maybeStyle.rowHover === false,
154
167
  sortOn,
155
168
  hasExpandableHeader,
169
+ isKeptSelectedRow,
170
+ isLastKeptSelectionRow,
156
171
  } }, `${row.kind}-${row.id}`));
157
172
  }
158
173
  // Split out the header rows from the data rows so that we can put an `infoMessage` in between them (if needed).
@@ -160,8 +175,8 @@ function GridTable(props) {
160
175
  const expandableHeaderRows = [];
161
176
  const totalsRows = [];
162
177
  const visibleDataRows = [];
178
+ const keptSelectedRows = [];
163
179
  const filteredRowIds = [];
164
- const hasTotalsRow = rows.some((row) => row.id === utils_1.TOTALS);
165
180
  const hasExpandableHeader = rows.some((row) => row.id === utils_1.EXPANDABLE_HEADER);
166
181
  function visit([row, children], level, visible) {
167
182
  visible && visibleDataRows.push([row, makeRowComponent(row, level)]);
@@ -194,14 +209,45 @@ function GridTable(props) {
194
209
  // Call `visitRows` with our a pre-filtered set list
195
210
  const filteredRows = filterRows(api, columns, maybeSorted, filter);
196
211
  visitRows(filteredRows, 0, true);
197
- return [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, filteredRowIds];
198
- }, [as, api, filter, maybeSorted, columns, style, rowStyles, sortOn, columnSizes, collapsedIds, getCount]);
212
+ // Check for any selected rows that are not displayed in the table because they don't match the current filter, or are no longer part of the `rows` prop.
213
+ // We persist these selected rows and hoist them to the top of the table.
214
+ if (sortedKeptSelections.length) {
215
+ // The "group row" for selected rows that are hidden by filters and add the children
216
+ const keptSelectionGroupRow = {
217
+ id: utils_1.KEPT_GROUP,
218
+ kind: utils_1.KEPT_GROUP,
219
+ children: sortedKeptSelections,
220
+ initCollapsed: true,
221
+ data: undefined,
222
+ };
223
+ keptSelectedRows.push([keptSelectionGroupRow, makeRowComponent(keptSelectionGroupRow, 1)], ...(collapsedIds.includes(utils_1.KEPT_GROUP)
224
+ ? []
225
+ : (sortedKeptSelections.map((row, idx) => [
226
+ row,
227
+ makeRowComponent(row, 1, true, idx === sortedKeptSelections.length - 1),
228
+ ]))));
229
+ }
230
+ return [headerRows, visibleDataRows, totalsRows, expandableHeaderRows, keptSelectedRows, filteredRowIds];
231
+ }, [
232
+ as,
233
+ api,
234
+ filter,
235
+ maybeSorted,
236
+ columns,
237
+ style,
238
+ rowStyles,
239
+ sortOn,
240
+ columnSizes,
241
+ collapsedIds,
242
+ getCount,
243
+ sortedKeptSelections,
244
+ ]);
199
245
  // Once our header rows are created we can organize them in expected order.
200
246
  const tableHeadRows = expandableHeaderRows.concat(headerRows).concat(totalsRows);
201
247
  let tooManyClientSideRows = false;
202
248
  if (filterMaxRows && visibleDataRows.length > filterMaxRows) {
203
249
  tooManyClientSideRows = true;
204
- visibleDataRows = visibleDataRows.slice(0, filterMaxRows);
250
+ visibleDataRows = visibleDataRows.slice(0, filterMaxRows + keptSelectedRows.length);
205
251
  }
206
252
  tableState.setMatchedRows(filteredRowIds);
207
253
  // Push back to the caller a way to ask us where a row is.
@@ -232,7 +278,7 @@ function GridTable(props) {
232
278
  // behave semantically the same as `as=div` did for its tests.
233
279
  const _as = as === "virtual" && runningInJest ? "div" : as;
234
280
  const rowStateContext = (0, react_1.useMemo)(() => ({ tableState: tableState }), [tableState]);
235
- return ((0, jsx_runtime_1.jsx)(TableState_1.TableStateContext.Provider, { value: rowStateContext, children: (0, jsx_runtime_1.jsxs)(PresentationContext_1.PresentationProvider, { fieldProps: fieldProps, wrap: (_c = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _c === void 0 ? void 0 : _c.wrap, children: [(0, jsx_runtime_1.jsx)("div", { ref: resizeRef, css: Css_1.Css.w100.if(as === "virtual").w("calc(100% - 20px)").$ }), renders[_as](style, id, columns, visibleDataRows, firstRowMessage, stickyHeader, xss, virtuosoRef, tableHeadRows, stickyOffset, infiniteScroll)] }) }));
281
+ return ((0, jsx_runtime_1.jsx)(TableState_1.TableStateContext.Provider, { value: rowStateContext, children: (0, jsx_runtime_1.jsxs)(PresentationContext_1.PresentationProvider, { fieldProps: fieldProps, wrap: (_c = style === null || style === void 0 ? void 0 : style.presentationSettings) === null || _c === void 0 ? void 0 : _c.wrap, children: [(0, jsx_runtime_1.jsx)("div", { ref: resizeRef, css: Css_1.Css.w100.if(as === "virtual").w("calc(100% - 20px)").$ }), renders[_as](style, id, columns, visibleDataRows, keptSelectedRows, firstRowMessage, stickyHeader, xss, virtuosoRef, tableHeadRows, stickyOffset, infiniteScroll)] }) }));
236
282
  }
237
283
  exports.GridTable = GridTable;
238
284
  // Determine which HTML element to use to build the GridTable
@@ -242,7 +288,7 @@ const renders = {
242
288
  virtual: renderVirtual,
243
289
  };
244
290
  /** Renders table using divs with flexbox rows, which is the default render */
245
- function renderDiv(style, id, columns, visibleDataRows, firstRowMessage, stickyHeader, xss, _virtuosoRef, tableHeadRows, stickyOffset, _infiniteScroll) {
291
+ function renderDiv(style, id, columns, visibleDataRows, keptSelectedRows, firstRowMessage, stickyHeader, xss, _virtuosoRef, tableHeadRows, stickyOffset, _infiniteScroll) {
246
292
  return ((0, jsx_runtime_1.jsxs)("div", { css: {
247
293
  // Use `fit-content` to ensure the width of the table takes up the full width of its content.
248
294
  // Otherwise, the table's width would be that of its container, which may not be as wide as the table itself.
@@ -259,10 +305,10 @@ function renderDiv(style, id, columns, visibleDataRows, firstRowMessage, stickyH
259
305
  ...(style.betweenRowsCss ? Css_1.Css.addIn(`& > div > *`, style.betweenRowsCss).$ : {}),
260
306
  ...(style.firstNonHeaderRowCss ? Css_1.Css.addIn(`& > div:first-of-type > *`, style.firstNonHeaderRowCss).$ : {}),
261
307
  ...(style.lastRowCss && Css_1.Css.addIn("& > div:last-of-type", style.lastRowCss).$),
262
- }, children: [firstRowMessage && ((0, jsx_runtime_1.jsx)("div", { css: { ...style.firstRowMessageCss }, "data-gridrow": true, children: firstRowMessage })), visibleDataRows.map(([, node]) => node)] })] }));
308
+ }, children: [keptSelectedRows.map(([, node]) => node), firstRowMessage && ((0, jsx_runtime_1.jsx)("div", { css: { ...style.firstRowMessageCss }, "data-gridrow": true, children: firstRowMessage })), visibleDataRows.map(([, node]) => node)] })] }));
263
309
  }
264
310
  /** Renders as a table, primarily/solely for good print support. */
265
- function renderTable(style, id, columns, visibleDataRows, firstRowMessage, stickyHeader, xss, _virtuosoRef, tableHeadRows, stickyOffset, _infiniteScroll) {
311
+ function renderTable(style, id, columns, visibleDataRows, keptSelectedRows, firstRowMessage, stickyHeader, xss, _virtuosoRef, tableHeadRows, stickyOffset, _infiniteScroll) {
266
312
  return ((0, jsx_runtime_1.jsxs)("table", { css: {
267
313
  ...Css_1.Css.w100.add("borderCollapse", "separate").add("borderSpacing", "0").$,
268
314
  ...Css_1.Css.addIn("& > tbody > tr > * ", style.betweenRowsCss || {})
@@ -273,7 +319,7 @@ function renderTable(style, id, columns, visibleDataRows, firstRowMessage, stick
273
319
  ...style.rootCss,
274
320
  ...(style.minWidthPx ? Css_1.Css.mwPx(style.minWidthPx).$ : {}),
275
321
  ...xss,
276
- }, "data-testid": id, children: [(0, jsx_runtime_1.jsx)("thead", { css: Css_1.Css.if(stickyHeader).sticky.topPx(stickyOffset).z(utils_1.zIndices.stickyHeader).$, children: tableHeadRows.map(([, node]) => node) }), (0, jsx_runtime_1.jsxs)("tbody", { children: [firstRowMessage && ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: columns.length, css: { ...style.firstRowMessageCss }, children: firstRowMessage }) })), visibleDataRows.map(([, node]) => node)] })] }));
322
+ }, "data-testid": id, children: [(0, jsx_runtime_1.jsx)("thead", { css: Css_1.Css.if(stickyHeader).sticky.topPx(stickyOffset).z(utils_1.zIndices.stickyHeader).$, children: tableHeadRows.map(([, node]) => node) }), (0, jsx_runtime_1.jsxs)("tbody", { children: [keptSelectedRows.map(([, node]) => node), firstRowMessage && ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: columns.length, css: { ...style.firstRowMessageCss }, children: firstRowMessage }) })), visibleDataRows.map(([, node]) => node)] })] }));
277
323
  }
278
324
  /**
279
325
  * Uses react-virtuoso to render rows virtually.
@@ -295,7 +341,7 @@ function renderTable(style, id, columns, visibleDataRows, firstRowMessage, stick
295
341
  * [2]: https://github.com/tannerlinsley/react-virtual/issues/85
296
342
  * [3]: https://github.com/tannerlinsley/react-virtual/issues/108
297
343
  */
298
- function renderVirtual(style, id, columns, visibleDataRows, firstRowMessage, stickyHeader, xss, virtuosoRef, tableHeadRows, _stickyOffset, infiniteScroll) {
344
+ function renderVirtual(style, id, columns, visibleDataRows, keptSelectedRows, firstRowMessage, stickyHeader, xss, virtuosoRef, tableHeadRows, _stickyOffset, infiniteScroll) {
299
345
  var _a;
300
346
  // eslint-disable-next-line react-hooks/rules-of-hooks
301
347
  const { footerStyle, listStyle } = (0, react_1.useMemo)(() => {
@@ -310,13 +356,19 @@ function renderVirtual(style, id, columns, visibleDataRows, firstRowMessage, sti
310
356
  Footer: () => (0, jsx_runtime_1.jsx)("div", { css: footerStyle }),
311
357
  },
312
358
  // Pin/sticky both the header row(s) + firstRowMessage to the top
313
- topItemCount: (stickyHeader ? tableHeadRows.length : 0) + (firstRowMessage ? 1 : 0), itemContent: (index) => {
314
- // Since we have 2 arrays of rows: `tableHeadRows`, and `filteredRow` we must determine which one to render.
359
+ topItemCount: stickyHeader ? tableHeadRows.length : 0, itemContent: (index) => {
360
+ // Since we have 3 arrays of rows: `tableHeadRows` and `visibleDataRows` and `keptSelectedRows` we must determine which one to render.
315
361
  if (index < tableHeadRows.length) {
316
362
  return tableHeadRows[index][1];
317
363
  }
318
364
  // Reset index
319
365
  index -= tableHeadRows.length;
366
+ // Show keptSelectedRows if there are any
367
+ if (index < keptSelectedRows.length) {
368
+ return keptSelectedRows[index][1];
369
+ }
370
+ // Reset index
371
+ index -= keptSelectedRows.length;
320
372
  // Show firstRowMessage as the first `filteredRow`
321
373
  if (firstRowMessage) {
322
374
  if (index === 0) {
@@ -326,9 +378,9 @@ function renderVirtual(style, id, columns, visibleDataRows, firstRowMessage, sti
326
378
  // first `filteredRow`
327
379
  index--;
328
380
  }
329
- // Lastly render `filteredRow`
381
+ // Lastly render the table body rows
330
382
  return visibleDataRows[index][1];
331
- }, totalCount: tableHeadRows.length + (firstRowMessage ? 1 : 0) + visibleDataRows.length, ...(infiniteScroll
383
+ }, totalCount: tableHeadRows.length + (firstRowMessage ? 1 : 0) + visibleDataRows.length + keptSelectedRows.length, ...(infiniteScroll
332
384
  ? {
333
385
  increaseViewportBy: {
334
386
  bottom: (_a = infiniteScroll.endOffsetPx) !== null && _a !== void 0 ? _a : 500,
@@ -16,7 +16,7 @@ import { TableState } from "./utils/TableState";
16
16
  * This is very similar to a `useRef`, except that the parent function has
17
17
  * immediate access to `api` and can use it for `useComputed`, instead of
18
18
  * having to wait for `ref.current` to be set after the child `GridTable`
19
- * has ran.
19
+ * has run.
20
20
  */
21
21
  export declare function useGridTableApi<R extends Kinded>(): GridTableApi<R>;
22
22
  /** Provides an imperative API for an application page to interact with the table. */
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GridTableApiImpl = exports.useGridTableApi = void 0;
4
4
  const react_1 = require("react");
5
5
  const TableState_1 = require("./utils/TableState");
6
- const visitor_1 = require("./utils/visitor");
7
6
  /**
8
7
  * Creates an `api` handle to drive a `GridTable`.
9
8
  *
@@ -17,7 +16,7 @@ const visitor_1 = require("./utils/visitor");
17
16
  * This is very similar to a `useRef`, except that the parent function has
18
17
  * immediate access to `api` and can use it for `useComputed`, instead of
19
18
  * having to wait for `ref.current` to be set after the child `GridTable`
20
- * has ran.
19
+ * has run.
21
20
  */
22
21
  function useGridTableApi() {
23
22
  return (0, react_1.useMemo)(() => new GridTableApiImpl(), []);
@@ -42,16 +41,9 @@ class GridTableApiImpl {
42
41
  getSelectedRowIds(kind) {
43
42
  return this.getSelectedRows(kind).map((row) => row.id);
44
43
  }
45
- // The any is not great, but getting the overload to handle the optional kind is annoying
44
+ // The `any` is not great, but getting the overload to handle the optional kind is annoying
46
45
  getSelectedRows(kind) {
47
- const ids = this.tableState.selectedIds;
48
- const selected = [];
49
- (0, visitor_1.visit)(this.tableState.rows, (row) => {
50
- if (row.selectable !== false && ids.includes(row.id) && (!kind || row.kind === kind)) {
51
- selected.push(row);
52
- }
53
- });
54
- return selected;
46
+ return this.tableState.selectedRows.filter((row) => !kind || row.kind === kind);
55
47
  }
56
48
  clearSelections(id) {
57
49
  this.tableState.selectRow("header", false);
@@ -50,6 +50,10 @@ export interface GridStyle {
50
50
  }>;
51
51
  /** Allows for customization of the background color used to denote an "active" row */
52
52
  activeBgColor?: Palette;
53
+ /** Defines styles for the group row which holds the selected rows that have been filtered out */
54
+ keptGroupRowCss?: Properties;
55
+ /** Defines styles for the last row `keptGroup` to provide separation from the rest of the table */
56
+ keptLastRowCss?: Properties;
53
57
  }
54
58
  export interface GridStyleDef {
55
59
  /** Changes the height of the rows when `rowHeight: fixed` to provide more space between rows for input fields. */
@@ -62,6 +62,8 @@ function memoizedTableStyles() {
62
62
  presentationSettings: { borderless: true, typeScale: "xs", wrap: rowHeight === "flexible" },
63
63
  levels: grouped ? groupedLevels : defaultLevels,
64
64
  rowHoverColor: Css_1.Palette.LightBlue100,
65
+ keptGroupRowCss: Css_1.Css.bgYellow100.gray900.xsMd.$,
66
+ keptLastRowCss: Css_1.Css.boxShadow("inset 0px -14px 8px -11px rgba(63,63,63,.18)").$,
65
67
  };
66
68
  }
67
69
  return cache[key];
@@ -12,9 +12,11 @@ function CollapseToggle(props) {
12
12
  const isCollapsed = (0, hooks_1.useComputed)(() => tableState.isCollapsed(row.id), [tableState]);
13
13
  const iconKey = isCollapsed ? "chevronRight" : "chevronDown";
14
14
  const headerIconKey = isCollapsed ? "chevronsRight" : "chevronsDown";
15
- // If we're not a header, only render a toggle if we have child rows to actually collapse
16
- const isHeader = row.kind === "header";
17
- if (!isHeader && (!row.children || row.children.length === 0)) {
15
+ // If we're not a header and not the selected group row, only render a toggle if we have child rows to actually collapse
16
+ const isHeader = row.kind === index_1.HEADER;
17
+ const isKeptGroup = row.kind === index_1.KEPT_GROUP;
18
+ const hasChildren = row.children ? row.children.length > 0 : false;
19
+ if (!isHeader && !isKeptGroup && !hasChildren) {
18
20
  return null;
19
21
  }
20
22
  return ((0, jsx_runtime_1.jsx)(index_1.IconButton, { onClick: () => tableState.toggleCollapsed(row.id), icon: isHeader ? headerIconKey : iconKey, compact: compact }));
@@ -0,0 +1,10 @@
1
+ import { GridDataRow, GridStyle, RenderAs } from "..";
2
+ interface KeptGroupRowProps {
3
+ as: RenderAs;
4
+ columnSizes: string[];
5
+ style: GridStyle;
6
+ row: GridDataRow<any>;
7
+ colSpan: number;
8
+ }
9
+ export declare function KeptGroupRow(props: KeptGroupRowProps): import("@emotion/react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeptGroupRow = void 0;
4
+ const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const components_1 = require("../..");
7
+ const Table_1 = require("..");
8
+ const Css_1 = require("../../../Css");
9
+ const hooks_1 = require("../../../hooks");
10
+ const utils_1 = require("../../../utils");
11
+ function KeptGroupRow(props) {
12
+ const { as, columnSizes, style, row, colSpan } = props;
13
+ const CellTag = as === "table" ? "td" : "div";
14
+ const { tableState } = (0, react_1.useContext)(Table_1.TableStateContext);
15
+ const numHiddenSelectedRows = (0, hooks_1.useComputed)(() => tableState.keptSelectedRows.length, [tableState]);
16
+ return ((0, jsx_runtime_1.jsx)(CellTag, { css: {
17
+ ...style.cellCss,
18
+ ...style.keptGroupRowCss,
19
+ ...Css_1.Css.pl0.w(`calc(${columnSizes.join(" + ")})`).$,
20
+ }, ...(as === "table" ? { colSpan } : {}), children: (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.aic.gapPx(12).$, children: [(0, jsx_runtime_1.jsx)("div", { css: Css_1.Css.wPx(38).df.jcc.$, children: (0, jsx_runtime_1.jsx)(Table_1.CollapseToggle, { row: row, compact: true }) }), (0, jsx_runtime_1.jsxs)("div", { css: Css_1.Css.df.aic.gap1.$, children: [(0, jsx_runtime_1.jsx)(components_1.Icon, { icon: "infoCircle", inc: 2 }), `${numHiddenSelectedRows} selected ${(0, utils_1.pluralize)(numHiddenSelectedRows, "row")} hidden due to filters`] })] }) }));
21
+ }
22
+ exports.KeptGroupRow = KeptGroupRow;
@@ -18,6 +18,8 @@ interface RowProps<R extends Kinded> {
18
18
  cellHighlight: boolean;
19
19
  omitRowHover: boolean;
20
20
  hasExpandableHeader: boolean;
21
+ isKeptSelectedRow: boolean;
22
+ isLastKeptSelectionRow: boolean;
21
23
  }
22
24
  declare function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement;
23
25
  /**
@@ -28,6 +28,7 @@ const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
28
28
  const mobx_react_1 = require("mobx-react");
29
29
  const react_1 = __importStar(require("react"));
30
30
  const cell_1 = require("./cell");
31
+ const KeptGroupRow_1 = require("./KeptGroupRow");
31
32
  const sortRows_1 = require("../utils/sortRows");
32
33
  const TableState_1 = require("../utils/TableState");
33
34
  const utils_1 = require("../utils/utils");
@@ -38,7 +39,7 @@ const shallowEqual_1 = require("../../../utils/shallowEqual");
38
39
  // We extract Row to its own mini-component primarily so we can React.memo'ize it.
39
40
  function RowImpl(props) {
40
41
  var _a;
41
- const { as, columns, row, style, rowStyles, sortOn, columnSizes, level, getCount, api, cellHighlight, omitRowHover, hasExpandableHeader, ...others } = props;
42
+ const { as, columns, row, style, rowStyles, sortOn, columnSizes, level, getCount, api, cellHighlight, omitRowHover, hasExpandableHeader, isKeptSelectedRow, isLastKeptSelectionRow, ...others } = props;
42
43
  const { tableState } = (0, react_1.useContext)(TableState_1.TableStateContext);
43
44
  const rowId = `${row.kind}_${row.id}`;
44
45
  const isActive = (0, hooks_1.useComputed)(() => tableState.activeRowId === rowId, [rowId, tableState]);
@@ -46,6 +47,7 @@ function RowImpl(props) {
46
47
  const isHeader = row.kind === utils_1.HEADER;
47
48
  const isTotals = row.kind === utils_1.TOTALS;
48
49
  const isExpandableHeader = row.kind === utils_1.EXPANDABLE_HEADER;
50
+ const isKeptGroupRow = row.kind === utils_1.KEPT_GROUP;
49
51
  const rowStyle = rowStyles === null || rowStyles === void 0 ? void 0 : rowStyles[row.kind];
50
52
  const RowTag = as === "table" ? "tr" : "div";
51
53
  const revealOnRowHoverClass = "revealOnRowHover";
@@ -66,14 +68,15 @@ function RowImpl(props) {
66
68
  [` > .${revealOnRowHoverClass} > *`]: Css_1.Css.invisible.$,
67
69
  [`:hover > .${revealOnRowHoverClass} > *`]: Css_1.Css.visible.$,
68
70
  },
71
+ ...(isLastKeptSelectionRow && Css_1.Css.addIn("&>*", style.keptLastRowCss).$),
69
72
  };
70
73
  let currentColspan = 1;
71
74
  // Keep a running count of how many expanded columns are being shown.
72
75
  let currentExpandedColumnCount = 0;
73
- let firstContentColumnStylesApplied = false;
76
+ let foundFirstContentColumn = false;
74
77
  let minStickyLeftOffset = 0;
75
78
  let expandColumnHidden = false;
76
- return ((0, jsx_runtime_1.jsx)(RowTag, { css: rowCss, ...others, "data-gridrow": true, ...getCount(row.id), children: columns.map((column, columnIndex) => {
79
+ 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) => {
77
80
  var _a, _b, _c, _d, _e;
78
81
  // If the expandable column was hidden, then we need to look at the previous column to format the `expandHeader` and 'header' kinds correctly.
79
82
  const maybeExpandedColumn = expandColumnHidden ? columns[columnIndex - 1] : column;
@@ -94,8 +97,9 @@ function RowImpl(props) {
94
97
  // 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.
95
98
  column = isExpandableHeader ? maybeExpandedColumn : column;
96
99
  const { wrapAction = true, isAction = false } = column;
97
- const applyFirstContentColumnStyles = !isHeader && !isAction && !firstContentColumnStylesApplied;
98
- firstContentColumnStylesApplied || (firstContentColumnStylesApplied = applyFirstContentColumnStyles);
100
+ const isFirstContentColumn = !isAction && !foundFirstContentColumn;
101
+ const applyFirstContentColumnStyles = !isHeader && isFirstContentColumn;
102
+ foundFirstContentColumn || (foundFirstContentColumn = applyFirstContentColumnStyles);
99
103
  if (column.mw) {
100
104
  // Validate the column's minWidth definition if set.
101
105
  if (!column.mw.endsWith("px") && !column.mw.endsWith("%")) {
@@ -138,7 +142,7 @@ function RowImpl(props) {
138
142
  const isExpandable = (0, utils_2.isFunction)(column.expandColumns) ||
139
143
  (column.expandColumns && column.expandColumns.length > 0) ||
140
144
  column.expandedWidth !== undefined;
141
- const content = (0, utils_1.toContent)(maybeContent, isHeader, canSortColumn, sortOn === "client", style, as, alignment, column, isExpandableHeader, isExpandable, minStickyLeftOffset);
145
+ const content = (0, utils_1.toContent)(maybeContent, isHeader, canSortColumn, sortOn === "client", style, as, alignment, column, isExpandableHeader, isExpandable, minStickyLeftOffset, isKeptSelectedRow);
142
146
  (0, sortRows_1.ensureClientSideSortValueIsSortable)(sortOn, isHeader || isTotals || isExpandableHeader, column, columnIndex, maybeContent);
143
147
  const maybeSticky = (_b = (((0, utils_1.isGridCellContent)(maybeContent) && maybeContent.sticky) || column.sticky)) !== null && _b !== void 0 ? _b : undefined;
144
148
  const maybeStickyColumnStyles = maybeSticky && columnSizes
@@ -215,14 +219,14 @@ function RowImpl(props) {
215
219
  const cellOnClick = applyCellHighlight ? () => api.setActiveCellId(cellId) : undefined;
216
220
  const tooltip = (0, utils_1.isGridCellContent)(maybeContent) ? maybeContent.tooltip : undefined;
217
221
  const renderFn = ((rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.renderCell) || (rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.rowLink)) && wrapAction
218
- ? (0, cell_1.rowLinkRenderFn)(as)
222
+ ? (0, cell_1.rowLinkRenderFn)(as, currentColspan)
219
223
  : isHeader || isTotals || isExpandableHeader
220
224
  ? (0, cell_1.headerRenderFn)(column, as, currentColspan)
221
225
  : (rowStyle === null || rowStyle === void 0 ? void 0 : rowStyle.onClick) && wrapAction
222
- ? (0, cell_1.rowClickRenderFn)(as, api)
223
- : (0, cell_1.defaultRenderFn)(as);
226
+ ? (0, cell_1.rowClickRenderFn)(as, api, currentColspan)
227
+ : (0, cell_1.defaultRenderFn)(as, currentColspan);
224
228
  return renderFn(columnIndex, cellCss, content, row, rowStyle, cellClassNames, cellOnClick, tooltip);
225
- }) }));
229
+ })) }));
226
230
  }
227
231
  /**
228
232
  * Memoizes Rows so that re-rendering the table doesn't re-render every single row.
@@ -31,13 +31,13 @@ export type GridCellContent = {
31
31
  /** Allows rendering a specific cell. */
32
32
  export type RenderCellFn<R extends Kinded> = (idx: number, css: Properties, content: ReactNode, row: R, rowStyle: RowStyle<R> | undefined, classNames: string | undefined, onClick: VoidFunction | undefined, tooltip: ReactNode | undefined) => ReactNode;
33
33
  /** Renders our default cell element, i.e. if no row links and no custom renderCell are used. */
34
- export declare const defaultRenderFn: (as: RenderAs) => RenderCellFn<any>;
34
+ export declare const defaultRenderFn: (as: RenderAs, colSpan: number) => RenderCellFn<any>;
35
35
  /**
36
36
  * Sets up the `GridContext` so that header cells can access the current sort settings.
37
37
  * Used for the Header, Totals, and Expanded Header row's cells.
38
38
  * */
39
39
  export declare const headerRenderFn: (column: GridColumnWithId<any>, as: RenderAs, colSpan: number) => RenderCellFn<any>;
40
40
  /** Renders a cell element when a row link is in play. */
41
- export declare const rowLinkRenderFn: (as: RenderAs) => RenderCellFn<any>;
41
+ export declare const rowLinkRenderFn: (as: RenderAs, colSpan: number) => RenderCellFn<any>;
42
42
  /** Renders a cell that will fire the RowStyle.onClick. */
43
- export declare const rowClickRenderFn: (as: RenderAs, api: GridTableApi<any>) => RenderCellFn<any>;
43
+ export declare const rowClickRenderFn: (as: RenderAs, api: GridTableApi<any>, colSpan: number) => RenderCellFn<any>;
@@ -6,9 +6,9 @@ const react_router_dom_1 = require("react-router-dom");
6
6
  const CssReset_1 = require("../../CssReset");
7
7
  const Css_1 = require("../../../Css");
8
8
  /** Renders our default cell element, i.e. if no row links and no custom renderCell are used. */
9
- const defaultRenderFn = (as) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
9
+ const defaultRenderFn = (as, colSpan) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
10
10
  const Cell = as === "table" ? "td" : "div";
11
- return ((0, jsx_runtime_1.jsx)(Cell, { css: { ...css, ...Css_1.Css.cursor("default").$ }, className: classNames, onClick: onClick, children: content }, key));
11
+ return ((0, jsx_runtime_1.jsx)(Cell, { css: { ...css, ...Css_1.Css.cursor("default").$ }, className: classNames, onClick: onClick, ...(as === "table" && { colSpan }), children: content }, key));
12
12
  };
13
13
  exports.defaultRenderFn = defaultRenderFn;
14
14
  /**
@@ -21,20 +21,20 @@ const headerRenderFn = (column, as, colSpan) => (key, css, content, row, rowStyl
21
21
  };
22
22
  exports.headerRenderFn = headerRenderFn;
23
23
  /** Renders a cell element when a row link is in play. */
24
- const rowLinkRenderFn = (as) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
24
+ const rowLinkRenderFn = (as, colSpan) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
25
25
  const to = rowStyle.rowLink(row);
26
26
  if (as === "table") {
27
- return ((0, jsx_runtime_1.jsx)("td", { css: { ...css }, className: classNames, children: (0, jsx_runtime_1.jsx)(react_router_dom_1.Link, { to: to, css: Css_1.Css.noUnderline.color("unset").db.$, className: CssReset_1.navLink, children: content }) }, key));
27
+ return ((0, jsx_runtime_1.jsx)("td", { css: { ...css }, className: classNames, colSpan: colSpan, children: (0, jsx_runtime_1.jsx)(react_router_dom_1.Link, { to: to, css: Css_1.Css.noUnderline.color("unset").db.$, className: CssReset_1.navLink, children: content }) }, key));
28
28
  }
29
29
  return ((0, jsx_runtime_1.jsx)(react_router_dom_1.Link, { to: to, css: { ...Css_1.Css.noUnderline.color("unset").$, ...css }, className: `${CssReset_1.navLink} ${classNames}`, children: content }, key));
30
30
  };
31
31
  exports.rowLinkRenderFn = rowLinkRenderFn;
32
32
  /** Renders a cell that will fire the RowStyle.onClick. */
33
- const rowClickRenderFn = (as, api) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
33
+ const rowClickRenderFn = (as, api, colSpan) => (key, css, content, row, rowStyle, classNames, onClick, tooltip) => {
34
34
  const Cell = as === "table" ? "td" : "div";
35
35
  return ((0, jsx_runtime_1.jsx)(Cell, { ...{ key }, css: { ...css }, className: classNames, onClick: (e) => {
36
36
  rowStyle.onClick(row, api);
37
37
  onClick && onClick();
38
- }, children: content }));
38
+ }, ...(as === "table" && { colSpan }), children: content }));
39
39
  };
40
40
  exports.rowClickRenderFn = rowClickRenderFn;
@@ -1,4 +1,4 @@
1
- import { ObservableSet } from "mobx";
1
+ import { ObservableMap, ObservableSet } from "mobx";
2
2
  import React from "react";
3
3
  import { GridDataRow } from "../components/Row";
4
4
  import { GridSortConfig } from "../GridTable";
@@ -22,8 +22,10 @@ export type SelectedState = "checked" | "unchecked" | "partial";
22
22
  export declare class TableState {
23
23
  private readonly collapsedRows;
24
24
  private persistCollapse;
25
- private readonly selectedRows;
25
+ private readonly rowSelectedState;
26
+ selectedDataRows: ObservableMap<string, GridDataRow<any>>;
26
27
  private matchedRows;
28
+ keptSelectedRows: GridDataRow<any>[];
27
29
  rows: GridDataRow<any>[];
28
30
  activeRowId: string | undefined;
29
31
  activeCellId: string | undefined;
@@ -58,7 +60,8 @@ export declare class TableState {
58
60
  get expandedColumnIds(): string[];
59
61
  toggleExpandedColumn(columnId: string): void;
60
62
  setMatchedRows(rowIds: string[]): void;
61
- get selectedIds(): string[];
63
+ /** Returns either all data rows (non-header, non-totals, etc) that are selected where `row.selectable !== false` */
64
+ get selectedRows(): GridDataRow<any>[];
62
65
  getSelected(id: string): SelectedState;
63
66
  selectRow(id: string, selected: boolean): void;
64
67
  get collapsedIds(): string[];
@@ -32,10 +32,18 @@ class TableState {
32
32
  */
33
33
  constructor() {
34
34
  // A set of just row ids, i.e. not row.kind+row.id
35
- this.collapsedRows = new mobx_1.ObservableSet();
36
- this.selectedRows = new mobx_1.ObservableMap();
35
+ this.collapsedRows = new mobx_1.ObservableSet([]);
36
+ // This will include rows that are selected, are partially selected, or were at once selected but are now unchecked.
37
+ // Rows within this map may be in `this.rows` or removed from `this.rows`.
38
+ this.rowSelectedState = new mobx_1.ObservableMap();
39
+ // Keep a copy of all the selected GridDataRows - Do not include custom row kinds such as `header`, `totals`, etc.
40
+ // This allows us to keep track of rows that were selected, but may no longer be defined in `GridTable.rows`
41
+ // This will be used to determine which rows are considered "kept" rows when filtering is applied or `GridTableProp.rows` changes
42
+ this.selectedDataRows = new mobx_1.ObservableMap();
37
43
  // Set of just row ids. Keeps track of which rows match the filter. Used to filter rows from `selectedIds`
38
44
  this.matchedRows = new mobx_1.ObservableSet();
45
+ // All fully selected data rows that do not match the applied filter, if any. Will not include group/parent rows unless they `inferSelectedState: false`
46
+ this.keptSelectedRows = [];
39
47
  // The current list of rows, basically a useRef.current. Not reactive.
40
48
  this.rows = [];
41
49
  // Keeps track of the 'active' row, formatted `${row.kind}_${row.id}`
@@ -67,10 +75,32 @@ class TableState {
67
75
  });
68
76
  // Whenever our `matchedRows` change (i.e. via filtering) then we need to re-derive header and parent rows' selected state.
69
77
  (0, mobx_1.reaction)(() => [...this.matchedRows.values()].sort(), () => {
78
+ const newlyKeptRows = [...this.selectedDataRows.values()].filter((row) => keptSelectionsFilter(row, this.matchedRows));
79
+ // If the kept rows went from empty to not empty, then introduce the SELECTED_GROUP row as collapsed
80
+ if (newlyKeptRows.length > 0 && this.keptSelectedRows.length === 0) {
81
+ this.collapsedRows.add(utils_1.KEPT_GROUP);
82
+ }
83
+ // When filters are applied, we need to determine if there are any selected rows that are no longer matched.
84
+ if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
85
+ this.keptSelectedRows = newlyKeptRows;
86
+ }
87
+ // Re-derive the selected state for both the header and parent rows
70
88
  const map = new Map();
71
- map.set("header", deriveParentSelected(this.rows.flatMap((row) => this.setNestedSelectedStates(row, map))));
89
+ map.set("header", deriveParentSelected([...this.rows, ...this.keptSelectedRows].flatMap((row) => this.setNestedSelectedStates(row, map))));
72
90
  // Merge the changes back into the selected rows state
73
- this.selectedRows.merge(map);
91
+ this.rowSelectedState.merge(map);
92
+ // Update the selectedDataRows to include those that are fully selected based on the filter changes
93
+ [...this.rowSelectedState.entries()].forEach(([id, state]) => {
94
+ if (this.selectedDataRows.has(id) && state !== "checked") {
95
+ this.selectedDataRows.delete(id);
96
+ }
97
+ else if (!this.selectedDataRows.has(id) && state === "checked") {
98
+ const row = this.rows.find((row) => row.id === id);
99
+ if (row) {
100
+ this.selectedDataRows.set(id, row);
101
+ }
102
+ }
103
+ });
74
104
  }, { equals: mobx_1.comparer.shallow });
75
105
  }
76
106
  loadCollapse(persistCollapse, rows) {
@@ -88,14 +118,22 @@ class TableState {
88
118
  }
89
119
  }
90
120
  loadSelected(rows) {
91
- const allRows = flattenRows(rows);
92
- const selectedRows = allRows.filter((row) => row.initSelected);
93
- // Initialize with selected rows as defined
94
- const map = new Map();
121
+ const selectedRows = [];
122
+ (0, visitor_1.visit)(rows, (row) => row.initSelected && selectedRows.push(row));
123
+ const selectedStateMap = new Map();
124
+ const selectedRowMap = new Map();
95
125
  selectedRows.forEach((row) => {
96
- map.set(row.id, "checked");
126
+ selectedStateMap.set(row.id, "checked");
127
+ selectedRowMap.set(row.id, row);
97
128
  });
98
- this.selectedRows.merge(map);
129
+ this.rowSelectedState.merge(selectedStateMap);
130
+ this.selectedDataRows.merge(selectedRowMap);
131
+ // Determine if we need to initially display the kept selected group
132
+ const newlyKeptRows = selectedRows.filter((row) => keptSelectionsFilter(row, this.matchedRows));
133
+ if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
134
+ this.collapsedRows.add(utils_1.KEPT_GROUP);
135
+ this.keptSelectedRows = newlyKeptRows;
136
+ }
99
137
  }
100
138
  initSortState(sortConfig, columns) {
101
139
  var _a, _b;
@@ -180,6 +218,10 @@ class TableState {
180
218
  }
181
219
  // Finally replace our existing list of rows
182
220
  this.rows = rows;
221
+ // Update the selected rows map to include the rows' updated data
222
+ const selectedRows = new Map();
223
+ (0, visitor_1.visit)(this.rows, (row) => this.selectedDataRows.has(row.id) && selectedRows.set(row.id, row));
224
+ this.selectedDataRows.merge(selectedRows);
183
225
  }
184
226
  setColumns(columns, visibleColumnsStorageKey) {
185
227
  const isInitial = !this.columns || this.columns.length === 0;
@@ -299,59 +341,95 @@ class TableState {
299
341
  this.matchedRows.replace(rowIds);
300
342
  }
301
343
  }
302
- get selectedIds() {
303
- // Return only ids that are fully checked, i.e. not partial
304
- const ids = [...this.selectedRows.entries()]
305
- .filter(([id, v]) => this.matchedRows.has(id) && v === "checked")
306
- .map(([k]) => k);
307
- // Hide our header marker
308
- const headerIndex = ids.indexOf("header");
309
- if (headerIndex > -1) {
310
- ids.splice(headerIndex, 1);
311
- }
312
- return ids;
344
+ /** Returns either all data rows (non-header, non-totals, etc) that are selected where `row.selectable !== false` */
345
+ get selectedRows() {
346
+ return [...this.selectedDataRows.values()].filter((row) => row.selectable !== false && !utils_1.reservedRowKinds.includes(row.kind));
313
347
  }
314
348
  // Should be called in an Observer/useComputed to trigger re-renders
315
349
  getSelected(id) {
316
- // We may not have every row in here, i.e. on 1st page load or after clicking here, so assume unchecked
317
- return this.selectedRows.get(id) || "unchecked";
350
+ var _a;
351
+ // Return the row's selected state if it's been explicitly set. If it is in the selectedDataRows set, then it's selected. Otherwise, it's unchecked.
352
+ return (_a = this.rowSelectedState.get(id)) !== null && _a !== void 0 ? _a : (this.selectedDataRows.has(id) ? "checked" : "unchecked");
318
353
  }
319
354
  selectRow(id, selected) {
355
+ var _a;
320
356
  if (id === "header") {
321
357
  // Select/unselect all has special behavior
322
358
  if (selected) {
323
359
  // Just mash the header + all rows + children as selected
324
- const map = new Map();
325
- map.set("header", "checked");
326
- (0, visitor_1.visit)(this.rows, (row) => this.matchedRows.has(row.id) && row.kind !== "totals" && map.set(row.id, "checked"));
327
- this.selectedRows.replace(map);
360
+ const selectedStateMap = new Map();
361
+ const selectedRowMap = new Map();
362
+ selectedStateMap.set("header", "checked");
363
+ (0, visitor_1.visit)(this.rows, (row) => {
364
+ if (!utils_1.reservedRowKinds.includes(row.kind) && this.matchedRows.has(row.id)) {
365
+ selectedStateMap.set(row.id, "checked");
366
+ selectedRowMap.set(row.id, row);
367
+ }
368
+ });
369
+ this.rowSelectedState.replace(selectedStateMap);
370
+ // Use `merge` to ensure we don't lose any row that were selected, but no longer in `this.rows` (i.e. due to server-side filtering)
371
+ this.selectedDataRows.merge(selectedRowMap);
328
372
  }
329
373
  else {
330
374
  // Similarly "unmash" all rows + children.
331
- this.selectedRows.clear();
375
+ this.rowSelectedState.clear();
376
+ this.selectedDataRows.clear();
377
+ this.keptSelectedRows = [];
332
378
  }
333
379
  }
334
380
  else {
335
381
  // This is the regular/non-header behavior to just add/remove the individual row id,
336
382
  // plus percolate the change down-to-child + up-to-parents.
337
383
  // Find the clicked on row
338
- const curr = findRow(this.rows, id);
384
+ const curr = (_a = findRow(this.rows, id)) !== null && _a !== void 0 ? _a : (this.selectedDataRows.has(id) ? { row: this.selectedDataRows.get(id), parents: [] } : undefined);
339
385
  if (!curr) {
340
386
  return;
341
387
  }
342
388
  // Everything here & down is deterministically on/off
343
- const map = new Map();
344
- (0, visitor_1.visit)([curr.row], (row) => this.matchedRows.has(row.id) && map.set(row.id, selected ? "checked" : "unchecked"));
389
+ const selectedStateMap = new Map();
390
+ (0, visitor_1.visit)([curr.row], (row) => {
391
+ // The `visit` method walks through the selected row and all of its children, if any.
392
+ // Depending on whether we are determining the clicked row's state or its children, then we handle updating the selection differently.
393
+ // We can tell if we are determining a child row's selected state by checking against the row selected `id` and the row we're currently evaluating `row.id`.
394
+ const isClickedRow = row.id === id;
395
+ // Only update the selected states if we're updating the clicked row, or if we are checking a child row, then the row must match the filter.
396
+ // Meaning, rows that are filtered out and displayed in the "selectedGroup" can only change their selection state by interacting with them directly.
397
+ if (isClickedRow || this.matchedRows.has(row.id)) {
398
+ selectedStateMap.set(row.id, selected ? "checked" : "unchecked");
399
+ if (selected) {
400
+ this.selectedDataRows.set(row.id, row);
401
+ }
402
+ else {
403
+ this.selectedDataRows.delete(row.id);
404
+ }
405
+ }
406
+ });
345
407
  // Now walk up the parents and see if they are now-all-checked/now-all-unchecked/some-of-each
346
408
  for (const parent of [...curr.parents].reverse()) {
347
409
  // Only derive selected state of the parent row if `inferSelectedState` is not `false`
348
410
  if (parent.children && parent.inferSelectedState !== false) {
349
- map.set(parent.id, deriveParentSelected(this.getMatchedChildrenStates(parent.children, map)));
411
+ const selectedState = deriveParentSelected(this.getMatchedChildrenStates(parent.children, selectedStateMap));
412
+ if (selectedState === "checked") {
413
+ this.selectedDataRows.set(parent.id, parent);
414
+ }
415
+ else {
416
+ this.selectedDataRows.delete(parent.id);
417
+ }
418
+ selectedStateMap.set(parent.id, selectedState);
350
419
  }
351
420
  }
352
421
  // And do the header + top-level "children" as a final one-off
353
- map.set("header", deriveParentSelected(this.getMatchedChildrenStates(this.rows, map)));
354
- this.selectedRows.merge(map);
422
+ selectedStateMap.set("header", deriveParentSelected(this.getMatchedChildrenStates([...this.rows, ...this.keptSelectedRows], selectedStateMap)));
423
+ // And merge the new selected state map into the existing one
424
+ this.rowSelectedState.merge(selectedStateMap);
425
+ // Lastly, we need to update the `keptSelectedRows` if the row was deselected.
426
+ // (If selected === true, then it's not possible for the row to be in `keptSelectedRows` as you can only select rows that match the filter)
427
+ if (!selected) {
428
+ const newlyKeptRows = [...this.selectedDataRows.values()].filter((row) => keptSelectionsFilter(row, this.matchedRows));
429
+ if (!mobx_1.comparer.shallow(newlyKeptRows, this.keptSelectedRows)) {
430
+ this.keptSelectedRows = newlyKeptRows;
431
+ }
432
+ }
355
433
  }
356
434
  }
357
435
  get collapsedIds() {
@@ -371,8 +449,8 @@ class TableState {
371
449
  collapsedIds.splice(0, collapsedIds.length);
372
450
  }
373
451
  else {
374
- // Otherwise push `header` to the list as a hint that we're in the collapsed-all state
375
- collapsedIds.push("header");
452
+ // Otherwise push `header` and `selectedGroup` to the list as a hint that we're in the collapsed-all state
453
+ collapsedIds.push(utils_1.HEADER, utils_1.KEPT_GROUP);
376
454
  // Find all non-leaf rows so that toggling "all collapsed" -> "all not collapsed" opens
377
455
  // the parent rows of any level.
378
456
  const parentIds = new Set();
@@ -397,6 +475,7 @@ class TableState {
397
475
  else {
398
476
  collapsedIds.splice(i, 1);
399
477
  }
478
+ // TODO: Need to handle the kept selected group row.
400
479
  // 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
401
480
  if (collapsedIds.length === 1 && collapsedIds[0] === "header") {
402
481
  collapsedIds.splice(0, 1);
@@ -415,13 +494,15 @@ class TableState {
415
494
  }
416
495
  getMatchedChildrenStates(children, map) {
417
496
  const respectedChildren = children.flatMap(getChildrenForDerivingSelectState);
497
+ // When determining the children selected states to base the parent's state from, then only base this off of rows that match the filter or are in the "hidden selected" group (via the `filter` below)
418
498
  return respectedChildren
419
- .filter((row) => row.id !== "header" && this.matchedRows.has(row.id))
499
+ .filter((row) => row.id !== "header" && (this.matchedRows.has(row.id) || this.selectedDataRows.has(row.id)))
420
500
  .map((row) => map.get(row.id) || this.getSelected(row.id));
421
501
  }
422
502
  // Recursively traverse through rows to determine selected state of parent rows based on children
503
+ // Returns the selected states for the immediately children (if any) of the row passed in
423
504
  setNestedSelectedStates(row, map) {
424
- if (this.matchedRows.has(row.id)) {
505
+ if (this.matchedRows.has(row.id) || this.selectedDataRows.has(row.id)) {
425
506
  // do not derive selected state if there are no children, or if `inferSelectedState` is set to false
426
507
  if (!row.children || row.inferSelectedState === false) {
427
508
  return [this.getSelected(row.id)];
@@ -437,6 +518,7 @@ class TableState {
437
518
  exports.TableState = TableState;
438
519
  /** Returns the child rows needed for deriving the selected state of a parent/group row */
439
520
  function getChildrenForDerivingSelectState(row) {
521
+ // Only look deeper if the parent row does not infer its selected state
440
522
  if (row.children && row.inferSelectedState === false) {
441
523
  return [row, ...row.children.flatMap(getChildrenForDerivingSelectState)];
442
524
  }
@@ -445,6 +527,7 @@ function getChildrenForDerivingSelectState(row) {
445
527
  /** Provides a context for rows to access their table's `TableState`. */
446
528
  exports.TableStateContext = react_1.default.createContext({
447
529
  get tableState() {
530
+ console.log("wtf?");
448
531
  throw new Error("No TableStateContext provider");
449
532
  },
450
533
  });
@@ -534,3 +617,9 @@ function deriveSortState(currentSortState, clickedKey, initialSortState) {
534
617
  return initialSortState;
535
618
  }
536
619
  exports.deriveSortState = deriveSortState;
620
+ function keptSelectionsFilter(row, matchedRows) {
621
+ return (!matchedRows.has(row.id) &&
622
+ row.selectable !== false &&
623
+ !utils_1.reservedRowKinds.includes(row.kind) &&
624
+ (!row.children || row.inferSelectedState === false));
625
+ }
@@ -41,8 +41,8 @@ function selectColumn(columnDef) {
41
41
  id: "beamSelectColumn",
42
42
  clientSideSort: false,
43
43
  align: "center",
44
- // Defining `w: 48px` to accommodate for the `16px` wide checkbox and `16px` of padding on either side.
45
- w: "48px",
44
+ // Defining `w: 40px` to accommodate for the `16px` wide checkbox and `12px` of padding on either side.
45
+ w: "40px",
46
46
  wrapAction: false,
47
47
  isAction: true,
48
48
  expandColumns: undefined,
@@ -52,6 +52,7 @@ function selectColumn(columnDef) {
52
52
  // Use any of the user's per-row kind methods if they have them.
53
53
  ...columnDef,
54
54
  };
55
+ // Use newMethodMissingProxy so the user can use whatever kinds they want, i.e. `myRowKind: () => ...Toggle... `
55
56
  return (0, utils_2.newMethodMissingProxy)(base, (key) => {
56
57
  return (data, { row }) => ({
57
58
  content: (0, jsx_runtime_1.jsx)(SelectToggle_1.SelectToggle, { id: row.id, disabled: row.selectable === false }),
@@ -82,6 +83,7 @@ function collapseColumn(columnDef) {
82
83
  totals: utils_1.emptyCell,
83
84
  ...columnDef,
84
85
  };
86
+ // Use newMethodMissingProxy so the user can use whatever kinds they want, i.e. `myRowKind: () => ...Collapse... `
85
87
  return (0, utils_2.newMethodMissingProxy)(base, (key) => {
86
88
  return (data, { row, level }) => ({
87
89
  content: (0, jsx_runtime_1.jsx)(CollapseToggle_1.CollapseToggle, { row: row, compact: level > 0 }),
@@ -6,7 +6,7 @@ import { GridStyle } from "../TableStyles";
6
6
  import { GridCellAlignment, GridColumnWithId, Kinded, RenderAs } from "../types";
7
7
  import { Properties } from "../../../Css";
8
8
  /** If a column def return just string text for a given row, apply some default styling. */
9
- export declare function toContent(maybeContent: ReactNode | GridCellContent, isHeader: boolean, canSortColumn: boolean, isClientSideSorting: boolean, style: GridStyle, as: RenderAs, alignment: GridCellAlignment, column: GridColumnWithId<any>, isExpandableHeader: boolean, isExpandable: boolean, minStickyLeftOffset: number): ReactNode;
9
+ export declare function toContent(maybeContent: ReactNode | GridCellContent, isHeader: boolean, canSortColumn: boolean, isClientSideSorting: boolean, style: GridStyle, as: RenderAs, alignment: GridCellAlignment, column: GridColumnWithId<any>, isExpandableHeader: boolean, isExpandable: boolean, minStickyLeftOffset: number, isKeptSelectedRow: boolean): ReactNode;
10
10
  export declare function isGridCellContent(content: ReactNode | GridCellContent): content is GridCellContent;
11
11
  /** Return the content for a given column def applied to a given row. */
12
12
  export declare function applyRowFn<R extends Kinded>(column: GridColumnWithId<R>, row: GridDataRow<R>, api: GridTableApi<R>, level: number, expanded: boolean): ReactNode | GridCellContent;
@@ -23,6 +23,7 @@ export declare function matchesFilter(maybeContent: ReactNode | GridCellContent,
23
23
  export declare const HEADER = "header";
24
24
  export declare const TOTALS = "totals";
25
25
  export declare const EXPANDABLE_HEADER = "expandableHeader";
26
+ export declare const KEPT_GROUP = "keptGroup";
26
27
  export declare const reservedRowKinds: string[];
27
28
  export declare const zIndices: {
28
29
  stickyHeader: number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.zIndices = exports.reservedRowKinds = exports.EXPANDABLE_HEADER = exports.TOTALS = exports.HEADER = exports.matchesFilter = exports.maybeApplyFunction = exports.getJustification = exports.getAlignment = exports.getFirstOrLastCellCss = exports.emptyCell = exports.DESC = exports.ASC = exports.applyRowFn = exports.isGridCellContent = exports.toContent = void 0;
3
+ exports.zIndices = exports.reservedRowKinds = exports.KEPT_GROUP = exports.EXPANDABLE_HEADER = exports.TOTALS = exports.HEADER = exports.matchesFilter = exports.maybeApplyFunction = exports.getJustification = exports.getAlignment = exports.getFirstOrLastCellCss = exports.emptyCell = exports.DESC = exports.ASC = exports.applyRowFn = exports.isGridCellContent = exports.toContent = void 0;
4
4
  const jsx_runtime_1 = require("@emotion/react/jsx-runtime");
5
5
  const Icon_1 = require("../../Icon");
6
6
  const ExpandableHeader_1 = require("../components/ExpandableHeader");
@@ -8,8 +8,12 @@ const SortHeader_1 = require("../components/SortHeader");
8
8
  const Css_1 = require("../../../Css");
9
9
  const getInteractiveElement_1 = require("../../../utils/getInteractiveElement");
10
10
  /** If a column def return just string text for a given row, apply some default styling. */
11
- function toContent(maybeContent, isHeader, canSortColumn, isClientSideSorting, style, as, alignment, column, isExpandableHeader, isExpandable, minStickyLeftOffset) {
11
+ function toContent(maybeContent, isHeader, canSortColumn, isClientSideSorting, style, as, alignment, column, isExpandableHeader, isExpandable, minStickyLeftOffset, isKeptSelectedRow) {
12
12
  var _a, _b, _c;
13
+ // Rows within the kept selection group cannot be collapsed
14
+ if (isKeptSelectedRow && column.id === "beamCollapseColumn") {
15
+ return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
16
+ }
13
17
  let content = isGridCellContent(maybeContent) ? maybeContent.content : maybeContent;
14
18
  if (typeof content === "function") {
15
19
  // Actually create the JSX by calling `content()` here (which should be as late as
@@ -153,7 +157,8 @@ exports.matchesFilter = matchesFilter;
153
157
  exports.HEADER = "header";
154
158
  exports.TOTALS = "totals";
155
159
  exports.EXPANDABLE_HEADER = "expandableHeader";
156
- exports.reservedRowKinds = [exports.HEADER, exports.TOTALS, exports.EXPANDABLE_HEADER];
160
+ exports.KEPT_GROUP = "keptGroup";
161
+ exports.reservedRowKinds = [exports.HEADER, exports.TOTALS, exports.EXPANDABLE_HEADER, exports.KEPT_GROUP];
157
162
  exports.zIndices = {
158
163
  stickyHeader: 4,
159
164
  stickyColumns: 3,
@@ -1,5 +1,6 @@
1
1
  export * from "./useBreakpoint";
2
2
  export * from "./useComputed";
3
+ export * from "./useFilter";
3
4
  export * from "./useGroupBy";
4
5
  export * from "./useHover";
5
6
  export * from "./usePersistedFilter";
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./useBreakpoint"), exports);
18
18
  __exportStar(require("./useComputed"), exports);
19
+ __exportStar(require("./useFilter"), exports);
19
20
  __exportStar(require("./useGroupBy"), exports);
20
21
  __exportStar(require("./useHover"), exports);
21
22
  __exportStar(require("./usePersistedFilter"), exports);
@@ -0,0 +1,10 @@
1
+ import { FilterDefs } from "..";
2
+ interface UsePersistedFilterProps<F> {
3
+ filterDefs: FilterDefs<F>;
4
+ }
5
+ interface FilterHook<F> {
6
+ filter: F;
7
+ setFilter: (filter: F) => void;
8
+ }
9
+ export declare function useFilter<F>({ filterDefs }: UsePersistedFilterProps<F>): FilterHook<F>;
10
+ export {};
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useFilter = void 0;
4
+ const react_1 = require("react");
5
+ const utils_1 = require("../utils");
6
+ function useFilter({ filterDefs }) {
7
+ const [filter, setFilter] = (0, react_1.useState)(Object.fromEntries((0, utils_1.safeEntries)(filterDefs)
8
+ .filter(([key, def]) => def(key).defaultValue !== undefined)
9
+ .map(([key, def]) => [key, def(key).defaultValue])));
10
+ return { setFilter, filter };
11
+ }
12
+ exports.useFilter = useFilter;
@@ -31,3 +31,4 @@ export declare function areArraysEqual(a: any[], b: any[]): boolean;
31
31
  export declare function isPromise(obj: any | Promise<any>): obj is Promise<any>;
32
32
  export declare function isFunction(f: any): f is Function;
33
33
  export declare function isDefined<T extends any>(param: T | undefined | null): param is T;
34
+ export declare function pluralize(count: number | unknown[], noun: string, pluralNoun?: string): string;
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.isDefined = exports.isFunction = exports.isPromise = exports.areArraysEqual = exports.isAbsoluteUrl = exports.EmptyRef = exports.safeEntries = exports.noop = exports.omitKey = exports.safeKeys = exports.maybeCall = exports.toGroupState = exports.toToggleState = void 0;
17
+ exports.pluralize = exports.isDefined = exports.isFunction = exports.isPromise = exports.areArraysEqual = exports.isAbsoluteUrl = exports.EmptyRef = exports.safeEntries = exports.noop = exports.omitKey = exports.safeKeys = exports.maybeCall = exports.toGroupState = exports.toToggleState = void 0;
18
18
  /** Adapts our state to what useToggleState returns in a stateless manner. */
19
19
  function toToggleState(isSelected, onChange) {
20
20
  return {
@@ -97,3 +97,9 @@ function isDefined(param) {
97
97
  return param !== null && param !== undefined;
98
98
  }
99
99
  exports.isDefined = isDefined;
100
+ function pluralize(count, noun, pluralNoun) {
101
+ if ((Array.isArray(count) ? count.length : count) === 1)
102
+ return noun;
103
+ return pluralNoun || `${noun}s`;
104
+ }
105
+ exports.pluralize = pluralize;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.292.0",
3
+ "version": "2.294.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -36,19 +36,19 @@
36
36
  "@homebound/form-state": "^2.17.0",
37
37
  "@internationalized/number": "^3.0.3",
38
38
  "@popperjs/core": "^2.11.6",
39
- "@react-aria/utils": "^3.13.2",
39
+ "@react-aria/utils": "^3.18.0",
40
40
  "change-case": "^4.1.2",
41
41
  "date-fns": "^2.28.0",
42
42
  "dompurify": "^2.3.0",
43
43
  "fast-deep-equal": "^3.1.3",
44
44
  "framer-motion": "^9.0.4",
45
45
  "memoize-one": "^5.2.1",
46
- "react-aria": "^3.24.0",
46
+ "react-aria": "^3.26.0",
47
47
  "react-day-picker": "8.0.7",
48
48
  "react-popper": "^2.3.0",
49
49
  "react-router": "^5.3.4",
50
50
  "react-router-dom": "^5.3.4",
51
- "react-stately": "^3.22.0",
51
+ "react-stately": "^3.24.0",
52
52
  "react-virtuoso": "^4.2.2",
53
53
  "tributejs": "^5.1.3",
54
54
  "trix": "^1.3.1",
@@ -81,16 +81,17 @@
81
81
  "@homebound/tsconfig": "^1.0.3",
82
82
  "@semantic-release/exec": "^6.0.3",
83
83
  "@semantic-release/git": "^10.0.1",
84
- "@storybook/addon-docs": "^7.0.20",
85
- "@storybook/addon-essentials": "^7.0.20",
86
- "@storybook/addon-interactions": "^7.0.20",
87
- "@storybook/addon-links": "^7.0.20",
88
- "@storybook/addons": "^7.0.20",
89
- "@storybook/manager-api": "^7.0.20",
84
+ "@storybook/addon-docs": "^7.0.26",
85
+ "@storybook/addon-essentials": "^7.0.26",
86
+ "@storybook/addon-interactions": "^7.0.26",
87
+ "@storybook/addon-links": "^7.0.26",
88
+ "@storybook/addon-mdx-gfm": "^7.0.26",
89
+ "@storybook/addons": "^7.0.26",
90
+ "@storybook/manager-api": "^7.0.26",
90
91
  "@storybook/mdx2-csf": "1.1.0",
91
- "@storybook/react": "^7.0.20",
92
- "@storybook/react-vite": "^7.0.20",
93
- "@storybook/testing-library": "^0.1.0",
92
+ "@storybook/react": "^7.0.26",
93
+ "@storybook/react-vite": "^7.0.26",
94
+ "@storybook/testing-library": "^0.2.0",
94
95
  "@testing-library/jest-dom": "^5.16.5",
95
96
  "@tsconfig/recommended": "^1.0.2",
96
97
  "@types/dompurify": "^2.2.3",
@@ -118,10 +119,9 @@
118
119
  "prettier": "^2.8.4",
119
120
  "prettier-plugin-organize-imports": "^3.2.2",
120
121
  "react": "^18.2.0",
121
- "react-beautiful-dnd": "^13.1.1",
122
122
  "react-dom": "^18.2.0",
123
123
  "semantic-release": "^20.1.0",
124
- "storybook": "^7.0.20",
124
+ "storybook": "^7.0.26",
125
125
  "storybook-addon-designs": "beta",
126
126
  "ts-jest": "^29.0.5",
127
127
  "ts-node": "^10.9.1",