@homebound/beam 2.117.3 → 2.118.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.
package/dist/Css.d.ts CHANGED
@@ -2527,6 +2527,9 @@ declare class CssBuilder<T extends Properties1> {
2527
2527
  } & {
2528
2528
  textOverflow: import("csstype").Property.TextOverflow | undefined;
2529
2529
  }>;
2530
+ lh(value: Properties["lineHeight"]): CssBuilder<T & {
2531
+ lineHeight: import("csstype").Property.LineHeight<0 | (string & {})> | undefined;
2532
+ }>;
2530
2533
  get selectNone(): CssBuilder<T & {
2531
2534
  userSelect: import("csstype").Property.UserSelect | undefined;
2532
2535
  }>;
package/dist/Css.js CHANGED
@@ -754,6 +754,7 @@ class CssBuilder {
754
754
  get indent() { return this.add("textIndent", "1em").add("marginTop", 0).add("marginBottom", 0); }
755
755
  get smallCaps() { return this.add("fontVariant", "small-caps"); }
756
756
  get truncate() { return this.add("whiteSpace", "nowrap").add("overflow", "hidden").add("textOverflow", "ellipsis"); }
757
+ lh(value) { return this.add("lineHeight", value); }
757
758
  // userSelect
758
759
  get selectNone() { return this.add("userSelect", "none"); }
759
760
  get selectText() { return this.add("userSelect", "text"); }
@@ -149,6 +149,7 @@ export interface GridTableProps<R extends Kinded, S, X> {
149
149
  /** Whether the header row should be sticky. */
150
150
  stickyHeader?: boolean;
151
151
  stickyOffset?: string;
152
+ /** Configures sorting via a hash, does not need to be stable. */
152
153
  sorting?: GridSortConfig<S>;
153
154
  /** Shown in the first row slot, if there are no rows to show, i.e. 'No rows found'. */
154
155
  fallbackMessage?: string;
@@ -85,7 +85,7 @@ exports.setGridTableDefaults = setGridTableDefaults;
85
85
  */
86
86
  function GridTable(props) {
87
87
  var _a, _b, _c, _d;
88
- const { id = "gridTable", as = "div", columns, rows, style = defaults.style, rowStyles, stickyHeader = defaults.stickyHeader, stickyOffset = "0", xss, sorting, filter, filterMaxRows, fallbackMessage = "No rows found.", infoMessage, setRowCount, observeRows, persistCollapse, api: callerApi, resizeTarget, activeRowId, } = props;
88
+ const { id = "gridTable", as = "div", columns, rows, style = defaults.style, rowStyles, stickyHeader = defaults.stickyHeader, stickyOffset = "0", xss, filter, filterMaxRows, fallbackMessage = "No rows found.", infoMessage, setRowCount, observeRows, persistCollapse, api: callerApi, resizeTarget, activeRowId, } = props;
89
89
  // Create a ref that always contains the latest rows, for our effectively-singleton RowState to use
90
90
  const rowsRef = (0, react_1.useRef)(rows);
91
91
  rowsRef.current = rows;
@@ -127,25 +127,49 @@ function GridTable(props) {
127
127
  // (or us) is resetting component state more than necessary, so we track render counts from
128
128
  // here instead.
129
129
  const { getCount } = (0, useRenderCount_1.useRenderCount)();
130
- const [sortState, setSortKey] = (0, useSortState_1.useSortState)(columns, sorting);
130
+ const columnSizes = (0, columnSizes_1.useSetupColumnSizes)(style, columns, tableRef, resizeTarget);
131
+ // Make a single copy of our current collapsed state, so we'll have a single observer.
132
+ const collapsedIds = (0, hooks_1.useComputed)(() => rowState.collapsedIds, [rowState]);
133
+ const [sortState, setSortKey, sortOn] = (0, useSortState_1.useSortState)(columns, props.sorting);
131
134
  const maybeSorted = (0, react_1.useMemo)(() => {
132
- if ((sorting === null || sorting === void 0 ? void 0 : sorting.on) === "client" && sortState) {
135
+ if (sortOn === "client" && sortState) {
133
136
  // If using client-side sort, the sortState use S = number
134
137
  return (0, sortRows_1.sortRows)(columns, rows, sortState);
135
138
  }
136
139
  return rows;
137
- }, [columns, rows, sorting, sortState]);
138
- const columnSizes = (0, columnSizes_1.useSetupColumnSizes)(style, columns, tableRef, resizeTarget);
139
- // Make a single copy of our current collapsed state, so we'll have a single observer.
140
- const collapsedIds = (0, hooks_1.useComputed)(() => rowState.collapsedIds, [rowState]);
141
- // Filter + flatten + component-ize the sorted rows.
142
- let [headerRows, filteredRows] = (0, react_1.useMemo)(() => {
140
+ }, [columns, rows, sortOn, sortState]);
141
+ // Filter rows - ensures parent rows remain in the list if any children match the filter.
142
+ const filterRows = (0, react_1.useCallback)((acc, row) => {
143
+ var _a, _b, _c;
143
144
  // Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
144
145
  const filters = (filter && filter.split(/ +/)) || [];
146
+ const matches = row.kind === "header" ||
147
+ filters.length === 0 ||
148
+ !!row.pin ||
149
+ filters.every((f) => columns.map((c) => applyRowFn(c, row, api)).some((maybeContent) => matchesFilter(maybeContent, f)));
150
+ // If the row matches, add it in
151
+ if (matches) {
152
+ return acc.concat([[row, (_b = (_a = row.children) === null || _a === void 0 ? void 0 : _a.reduce(filterRows, [])) !== null && _b !== void 0 ? _b : []]]);
153
+ }
154
+ else {
155
+ // Otherwise, maybe one of the children match.
156
+ const isCollapsed = collapsedIds.includes(row.id);
157
+ if (!isCollapsed && !!((_c = row.children) === null || _c === void 0 ? void 0 : _c.length)) {
158
+ const matchedChildren = row.children.reduce(filterRows, []);
159
+ // If some children did match, then add the parent row with its matched children.
160
+ if (matchedChildren.length > 0) {
161
+ return acc.concat([[row, matchedChildren]]);
162
+ }
163
+ }
164
+ }
165
+ return acc;
166
+ }, [filter, collapsedIds, columns]);
167
+ // Flatten + component-ize the sorted rows.
168
+ let [headerRows, filteredRows] = (0, react_1.useMemo)(() => {
145
169
  function makeRowComponent(row, level) {
146
170
  // We only pass sortState to header rows, b/c non-headers rows shouldn't have to re-render on sorting
147
171
  // changes, and so by not passing the sortProps, it means the data rows' React.memo will still cache them.
148
- const sortProps = row.kind === "header" ? { sorting, sortState, setSortKey } : { sorting };
172
+ const sortProps = row.kind === "header" ? { sortOn, sortState, setSortKey } : { sortOn };
149
173
  const RowComponent = observeRows ? ObservedGridRow : MemoizedGridRow;
150
174
  return ((0, jsx_runtime_1.jsx)(RowComponent, Object.assign({}, {
151
175
  as,
@@ -168,48 +192,40 @@ function GridTable(props) {
168
192
  const filteredRows = [];
169
193
  // Misc state to track our nested card-ification, i.e. interleaved actual rows + chrome rows
170
194
  const nestedCards = !!style.nestedCards && new nestedCards_1.NestedCards(columns, filteredRows, style.nestedCards);
171
- // Depth-first to filter
172
- function visit(row, level) {
173
- var _a;
174
- const matches = filters.length === 0 ||
175
- row.pin ||
176
- filters.every((filter) => columns.map((c) => applyRowFn(c, row, api)).some((maybeContent) => matchesFilter(maybeContent, filter)));
177
- let isCard = false;
178
- // Even if we don't pass the filter, one of our children might, so we continue on after this check
179
- if (matches) {
180
- isCard = nestedCards && nestedCards.maybeOpenCard(row);
181
- filteredRows.push([row, makeRowComponent(row, level)]);
182
- }
195
+ function visit([row, children], level) {
196
+ let isCard = nestedCards && nestedCards.maybeOpenCard(row);
197
+ filteredRows.push([row, makeRowComponent(row, level)]);
183
198
  const isCollapsed = collapsedIds.includes(row.id);
184
- if (!isCollapsed && !!((_a = row.children) === null || _a === void 0 ? void 0 : _a.length)) {
185
- nestedCards && matches && nestedCards.addSpacer();
186
- visitRows(row.children, isCard, level + 1);
199
+ if (!isCollapsed && children.length) {
200
+ nestedCards && nestedCards.addSpacer();
201
+ visitRows(children, isCard, level + 1);
187
202
  }
188
203
  !(0, nestedCards_1.isLeafRow)(row) && isCard && nestedCards && nestedCards.closeCard();
189
204
  }
190
205
  function visitRows(rows, addSpacer, level) {
191
206
  const length = rows.length;
192
207
  rows.forEach((row, i) => {
193
- if (row.kind === "header") {
194
- headerRows.push([row, makeRowComponent(row, level)]);
208
+ if (row[0].kind === "header") {
209
+ headerRows.push([row[0], makeRowComponent(row[0], level)]);
195
210
  return;
196
211
  }
197
212
  visit(row, level);
198
213
  addSpacer && nestedCards && i !== length - 1 && nestedCards.addSpacer();
199
214
  });
200
215
  }
216
+ // Call `visitRows` with our a pre-filtered set list
201
217
  // If nestedCards is set, we assume the top-level kind is a card, and so should add spacers between them
202
- visitRows(maybeSorted, !!nestedCards, 0);
218
+ visitRows(maybeSorted.reduce(filterRows, []), !!nestedCards, 0);
203
219
  nestedCards && nestedCards.done();
204
220
  return [headerRows, filteredRows];
205
221
  }, [
206
222
  as,
207
223
  maybeSorted,
208
224
  columns,
209
- filter,
225
+ filterRows,
210
226
  style,
211
227
  rowStyles,
212
- sorting,
228
+ sortOn,
213
229
  setSortKey,
214
230
  sortState,
215
231
  stickyHeader,
@@ -468,7 +484,7 @@ function getFirstOrLastCellCss(style, columnIndex, columns) {
468
484
  // We extract GridRow to its own mini-component primarily so we can React.memo'ize it.
469
485
  function GridRow(props) {
470
486
  var _a;
471
- const { as, columns, row, style, rowStyles, stickyHeader, stickyOffset, sorting, sortState, setSortKey, openCards, columnSizes, level, getCount, api, ...others } = props;
487
+ const { as, columns, row, style, rowStyles, stickyHeader, stickyOffset, sortOn, sortState, setSortKey, openCards, columnSizes, level, getCount, api, ...others } = props;
472
488
  const { rowState } = (0, react_1.useContext)(RowState_1.RowStateContext);
473
489
  const isActive = (0, hooks_1.useComputed)(() => rowState.activeRowId === `${row.kind}_${row.id}`, [row, rowState]);
474
490
  // We treat the "header" kind as special for "good defaults" styling
@@ -511,12 +527,12 @@ function GridRow(props) {
511
527
  }
512
528
  const maybeContent = applyRowFn(column, row, api);
513
529
  currentColspan = isGridCellContent(maybeContent) ? (_a = maybeContent.colspan) !== null && _a !== void 0 ? _a : 1 : 1;
514
- const canSortColumn = ((sorting === null || sorting === void 0 ? void 0 : sorting.on) === "client" && column.clientSideSort !== false) ||
515
- ((sorting === null || sorting === void 0 ? void 0 : sorting.on) === "server" && !!column.serverSideSortKey);
530
+ const canSortColumn = (sortOn === "client" && column.clientSideSort !== false) ||
531
+ (sortOn === "server" && !!column.serverSideSortKey);
516
532
  const alignment = getAlignment(column, maybeContent);
517
533
  const justificationCss = getJustification(column, maybeContent, as, alignment);
518
- const content = toContent(maybeContent, isHeader, canSortColumn, (sorting === null || sorting === void 0 ? void 0 : sorting.on) === "client", style, as, alignment);
519
- (0, sortRows_1.ensureClientSideSortValueIsSortable)(sorting, isHeader, column, columnIndex, maybeContent);
534
+ const content = toContent(maybeContent, isHeader, canSortColumn, sortOn === "client", style, as, alignment);
535
+ (0, sortRows_1.ensureClientSideSortValueIsSortable)(sortOn, isHeader, column, columnIndex, maybeContent);
520
536
  const maybeNestedCardColumnIndex = columnIndex + (style.nestedCards ? 1 : 0);
521
537
  const maybeSticky = (_b = ((isGridCellContent(maybeContent) && maybeContent.sticky) || column.sticky)) !== null && _b !== void 0 ? _b : undefined;
522
538
  const maybeStickyColumnStyles = maybeSticky && columnSizes
@@ -34,6 +34,8 @@ export declare class RowState {
34
34
  get collapsedIds(): string[];
35
35
  isCollapsed(id: string): boolean;
36
36
  toggleCollapsed(id: string): void;
37
+ private getVisibleChildrenStates;
38
+ private setNestedSelectedStates;
37
39
  }
38
40
  /** Provides a context for rows to access their table's `RowState`. */
39
41
  export declare const RowStateContext: React.Context<{
@@ -37,6 +37,13 @@ class RowState {
37
37
  // Make ourselves an observable so that mobx will do caching of .collapseIds so
38
38
  // that it'll be a stable identity for GridTable to useMemo against.
39
39
  (0, mobx_1.makeAutoObservable)(this, { rows: false }); // as any b/c rows is private, so the mapped type doesn't see it
40
+ // Whenever our `visibleRows` change (i.e. via filtering) then we need to re-derive header and parent rows' selected state.
41
+ (0, mobx_1.reaction)(() => [...this.visibleRows.values()].sort(), () => {
42
+ const map = new Map();
43
+ map.set("header", deriveParentSelected(this.rows.current.flatMap((row) => this.setNestedSelectedStates(row, map))));
44
+ // Merge the changes back into the selected rows state
45
+ this.selectedRows.merge(map);
46
+ }, { equals: mobx_1.comparer.shallow });
40
47
  }
41
48
  get selectedIds() {
42
49
  // Return only ids that are fully checked, i.e. not partial
@@ -62,7 +69,7 @@ class RowState {
62
69
  // Just mash the header + all rows + children as selected
63
70
  const map = new Map();
64
71
  map.set("header", "checked");
65
- (0, visitor_1.visit)(this.rows.current, (row) => map.set(row.id, "checked"));
72
+ (0, visitor_1.visit)(this.rows.current, (row) => this.visibleRows.has(row.id) && map.set(row.id, "checked"));
66
73
  this.selectedRows.replace(map);
67
74
  }
68
75
  else {
@@ -80,19 +87,15 @@ class RowState {
80
87
  }
81
88
  // Everything here & down is deterministically on/off
82
89
  const map = new Map();
83
- (0, visitor_1.visit)([curr.row], (row) => map.set(row.id, selected ? "checked" : "unchecked"));
90
+ (0, visitor_1.visit)([curr.row], (row) => this.visibleRows.has(row.id) && map.set(row.id, selected ? "checked" : "unchecked"));
84
91
  // Now walk up the parents and see if they are now-all-checked/now-all-unchecked/some-of-each
85
92
  for (const parent of [...curr.parents].reverse()) {
86
93
  if (parent.children) {
87
- const children = parent.children.map((row) => map.get(row.id) || this.getSelected(row.id));
88
- map.set(parent.id, deriveParentSelected(children));
94
+ map.set(parent.id, deriveParentSelected(this.getVisibleChildrenStates(parent.children, map)));
89
95
  }
90
96
  }
91
97
  // And do the header + top-level "children" as a final one-off
92
- const children = this.rows.current
93
- .filter((row) => row.id !== "header")
94
- .map((row) => map.get(row.id) || this.getSelected(row.id));
95
- map.set("header", deriveParentSelected(children));
98
+ map.set("header", deriveParentSelected(this.getVisibleChildrenStates(this.rows.current, map)));
96
99
  this.selectedRows.merge(map);
97
100
  }
98
101
  }
@@ -145,6 +148,24 @@ class RowState {
145
148
  localStorage.setItem(this.persistCollapse, JSON.stringify(collapsedIds));
146
149
  }
147
150
  }
151
+ getVisibleChildrenStates(children, map) {
152
+ return children
153
+ .filter((row) => row.id !== "header" && this.visibleRows.has(row.id))
154
+ .map((row) => map.get(row.id) || this.getSelected(row.id));
155
+ }
156
+ // Recursively traverse through rows to determine selected state of parent rows based on children
157
+ setNestedSelectedStates(row, map) {
158
+ if (this.visibleRows.has(row.id)) {
159
+ if (!row.children) {
160
+ return [this.getSelected(row.id)];
161
+ }
162
+ const childrenSelectedStates = row.children.flatMap((rc) => this.setNestedSelectedStates(rc, map));
163
+ const parentState = deriveParentSelected(childrenSelectedStates);
164
+ map.set(row.id, parentState);
165
+ return [parentState];
166
+ }
167
+ return [];
168
+ }
148
169
  }
149
170
  exports.RowState = RowState;
150
171
  /** Provides a context for rows to access their table's `RowState`. */
@@ -177,5 +198,5 @@ function findRow(rows, id) {
177
198
  function deriveParentSelected(children) {
178
199
  const allChecked = children.every((child) => child === "checked");
179
200
  const allUnchecked = children.every((child) => child === "unchecked");
180
- return allChecked ? "checked" : allUnchecked ? "unchecked" : "partial";
201
+ return children.length === 0 ? "unchecked" : allChecked ? "checked" : allUnchecked ? "unchecked" : "partial";
181
202
  }
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from "react";
2
- import { GridCellContent, GridColumn, GridDataRow, GridSortConfig, Kinded } from "./GridTable";
3
- import { SortState } from "./useSortState";
2
+ import { GridCellContent, GridColumn, GridDataRow, Kinded } from "./GridTable";
3
+ import { SortOn, SortState } from "./useSortState";
4
4
  export declare function sortRows<R extends Kinded>(columns: GridColumn<R>[], rows: GridDataRow<R>[], sortState: SortState<number>): GridDataRow<R>[];
5
- export declare function ensureClientSideSortValueIsSortable(sorting: GridSortConfig<any> | undefined, isHeader: boolean, column: GridColumn<any>, idx: number, maybeContent: ReactNode | GridCellContent): void;
5
+ export declare function ensureClientSideSortValueIsSortable(sortOn: SortOn, isHeader: boolean, column: GridColumn<any>, idx: number, maybeContent: ReactNode | GridCellContent): void;
@@ -8,7 +8,7 @@ function sortRows(columns, rows, sortState) {
8
8
  // Recursively sort child rows
9
9
  sorted.forEach((row, i) => {
10
10
  if (row.children) {
11
- sorted[i] = { ...sorted[i], children: sortRows(columns, row.children, sortState) };
11
+ sorted[i].children = sortRows(columns, row.children, sortState);
12
12
  }
13
13
  });
14
14
  return sorted;
@@ -64,11 +64,8 @@ function sortValue(value) {
64
64
  }
65
65
  return maybeFn;
66
66
  }
67
- function ensureClientSideSortValueIsSortable(sorting, isHeader, column, idx, maybeContent) {
68
- if (process.env.NODE_ENV !== "production" &&
69
- !isHeader &&
70
- (sorting === null || sorting === void 0 ? void 0 : sorting.on) === "client" &&
71
- column.clientSideSort !== false) {
67
+ function ensureClientSideSortValueIsSortable(sortOn, isHeader, column, idx, maybeContent) {
68
+ if (process.env.NODE_ENV !== "production" && !isHeader && sortOn === "client" && column.clientSideSort !== false) {
72
69
  const value = sortValue(maybeContent);
73
70
  if (!canClientSideSort(value)) {
74
71
  throw new Error(`Column ${idx} passed an unsortable value, use GridCellContent or clientSideSort=false`);
@@ -8,6 +8,7 @@ import { Direction, GridColumn, GridSortConfig, Kinded } from "./GridTable";
8
8
  * b) it's index in the `columns` array, if client-side sorting
9
9
  */
10
10
  export declare type SortState<S> = readonly [S, Direction];
11
+ export declare type SortOn = "client" | "server" | undefined;
11
12
  /** Small custom hook that wraps the "setSortColumn inverts the current sort" logic. */
12
- export declare function useSortState<R extends Kinded, S>(columns: GridColumn<R, S>[], sorting?: GridSortConfig<S>): [SortState<S> | undefined, (value: S) => void];
13
+ export declare function useSortState<R extends Kinded, S>(columns: GridColumn<R, S>[], sorting?: GridSortConfig<S>): [SortState<S> | undefined, (value: S) => void, SortOn];
13
14
  export declare function deriveSortState<S>(currentSortState: SortState<S> | undefined, clickedKey: S, initialSortState: SortState<S> | undefined): SortState<S> | undefined;
@@ -28,20 +28,26 @@ function useSortState(columns, sorting) {
28
28
  else {
29
29
  return sorting === null || sorting === void 0 ? void 0 : sorting.value;
30
30
  }
31
- }, [sorting, columns]);
31
+ },
32
+ // We want to allow the user to not memoize `GridTableProps.sorting` b/c for the
33
+ // initialSortState calc, it's just a bunch of surely hard-coded primitives like
34
+ // sort on client/server, which column is initial.
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
36
+ [columns]);
32
37
  const [sortState, setSortState] = (0, react_1.useState)(initialSortState);
33
38
  // Make a custom setSortKey that is useState-like but contains the ASC->DESC->RESET logic.
39
+ const onSort = (sorting === null || sorting === void 0 ? void 0 : sorting.on) === "server" ? sorting.onSort : undefined;
34
40
  const setSortKey = (0, react_1.useCallback)((clickedKey) => {
35
41
  const newState = deriveSortState(sortState, clickedKey, initialSortState);
36
42
  setSortState(newState);
37
- if ((sorting === null || sorting === void 0 ? void 0 : sorting.on) === "server") {
43
+ if (onSort) {
38
44
  const [newKey, newDirection] = newState !== null && newState !== void 0 ? newState : [undefined, undefined];
39
- sorting.onSort(newKey, newDirection);
45
+ onSort(newKey, newDirection);
40
46
  }
41
47
  },
42
48
  // Note that sorting.onSort is not listed here, so we bind to whatever the 1st sorting.onSort was
43
- [sortState, setSortState]);
44
- return [sortState, setSortKey];
49
+ [initialSortState, sortState, onSort]);
50
+ return [sortState, setSortKey, sorting === null || sorting === void 0 ? void 0 : sorting.on];
45
51
  }
46
52
  exports.useSortState = useSortState;
47
53
  // Exported for testing purposes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.117.3",
3
+ "version": "2.118.1",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -20,8 +20,7 @@
20
20
  "scripts": {
21
21
  "start": "yarn storybook",
22
22
  "build": "yarn copy && ttsc",
23
- "build:truss": "cd ./truss && npm run generate",
24
- "watch:truss": "cd ./truss && watch 'npm run generate' ./",
23
+ "build:truss": "truss",
25
24
  "build:storybook": "build-storybook -s ./testAssets",
26
25
  "test": "jest --maxWorkers 4",
27
26
  "test:watch": "jest --watch",
@@ -78,6 +77,7 @@
78
77
  "@emotion/react": "^11.1.5",
79
78
  "@homebound/rtl-react-router-utils": "^1.0.3",
80
79
  "@homebound/rtl-utils": "^2.51.0",
80
+ "@homebound/truss": "^1.111.3",
81
81
  "@homebound/tsconfig": "^1.0.3",
82
82
  "@semantic-release/exec": "^6.0.3",
83
83
  "@semantic-release/git": "^9.0.0",