@homebound/beam 2.363.0 → 2.365.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.
@@ -131,6 +131,13 @@ export interface GridTableProps<R extends Kinded, X> {
131
131
  infiniteScroll?: InfiniteScroll;
132
132
  /** Callback for when a row is selected or unselected. */
133
133
  onRowSelect?: OnRowSelect<R>;
134
+ /**
135
+ * Custom prefix rows for any CSV output, i.e. a header like "Report X as of date Y with filter Z".
136
+ *
137
+ * We except the `string[][]` to be an array of cells, and `copyToClipboard` and `downloadToCsv` will drop them into
138
+ * the csv output basically unchanged, albeit we will escape any special chars like double quotes and newlines.
139
+ */
140
+ csvPrefixRows?: string[][];
134
141
  /** Drag & drop Callback. */
135
142
  onRowDrop?: (draggedRow: GridDataRow<R>, droppedRow: GridDataRow<R>, indexOffset: number) => void;
136
143
  }
@@ -88,7 +88,7 @@ exports.setGridTableDefaults = setGridTableDefaults;
88
88
  */
89
89
  function GridTable(props) {
90
90
  var _a, _b, _c;
91
- const { id = "gridTable", as = "div", columns: _columns, rows, style: maybeStyle = defaults.style, rowStyles, stickyHeader = defaults.stickyHeader, stickyOffset = 0, xss, filter, filterMaxRows, fallbackMessage = "No rows found.", infoMessage, persistCollapse, resizeTarget, activeRowId, activeCellId, visibleColumnsStorageKey, infiniteScroll, onRowSelect, onRowDrop: droppedCallback, } = props;
91
+ const { id = "gridTable", as = "div", columns: _columns, rows, style: maybeStyle = defaults.style, rowStyles, stickyHeader = defaults.stickyHeader, stickyOffset = 0, xss, filter, filterMaxRows, fallbackMessage = "No rows found.", infoMessage, persistCollapse, resizeTarget, activeRowId, activeCellId, visibleColumnsStorageKey, infiniteScroll, onRowSelect, onRowDrop: droppedCallback, csvPrefixRows, } = props;
92
92
  const columnsWithIds = (0, react_1.useMemo)(() => (0, columns_1.assignDefaultColumnIds)(_columns), [_columns]);
93
93
  // We only use this in as=virtual mode, but keep this here for rowLookup to use
94
94
  const virtuosoRef = (0, react_1.useRef)(null);
@@ -129,10 +129,11 @@ function GridTable(props) {
129
129
  tableState.setRows(rows);
130
130
  tableState.setColumns(columnsWithIds, visibleColumnsStorageKey);
131
131
  tableState.setSearch(filter);
132
+ tableState.setCsvPrefixRows(csvPrefixRows);
132
133
  tableState.activeRowId = activeRowId;
133
134
  tableState.activeCellId = activeCellId;
134
135
  });
135
- }, [tableState, rows, columnsWithIds, visibleColumnsStorageKey, activeRowId, activeCellId, filter]);
136
+ }, [tableState, rows, columnsWithIds, visibleColumnsStorageKey, activeRowId, activeCellId, filter, csvPrefixRows]);
136
137
  const columns = (0, hooks_1.useComputed)(() => {
137
138
  return tableState.visibleColumns;
138
139
  }, [tableState]);
@@ -1,6 +1,6 @@
1
1
  import { MutableRefObject } from "react";
2
2
  import { VirtuosoHandle } from "react-virtuoso";
3
- import { GridRowLookup } from "../index";
3
+ import { GridRowLookup, MaybeFn } from "../index";
4
4
  import { GridDataRow } from "./components/Row";
5
5
  import { DiscriminateUnion, Kinded } from "./types";
6
6
  import { TableState } from "./utils/TableState";
@@ -51,6 +51,18 @@ export type GridTableApi<R extends Kinded> = {
51
51
  deleteRows(ids: string[]): void;
52
52
  getVisibleColumnIds(): string[];
53
53
  setVisibleColumns(ids: string[]): void;
54
+ /**
55
+ * Triggers the table's current content to be downloaded as a CSV file.
56
+ *
57
+ * This currently assumes client-side pagination/sorting, i.e. we have the full dataset in memory.
58
+ */
59
+ downloadToCsv(fileName: string): void;
60
+ /**
61
+ * Copies the table's current content to the clipboard.
62
+ *
63
+ * This currently assumes client-side pagination/sorting, i.e. we have the full dataset in memory.
64
+ */
65
+ copyToClipboard(): Promise<void>;
54
66
  };
55
67
  /** Adds per-row methods to the `api`, i.e. for getting currently-visible children. */
56
68
  export type GridRowApi<R extends Kinded> = GridTableApi<R> & {
@@ -84,4 +96,8 @@ export declare class GridTableApiImpl<R extends Kinded> implements GridTableApi<
84
96
  setVisibleColumns(ids: string[]): void;
85
97
  getVisibleColumnIds(): string[];
86
98
  deleteRows(ids: string[]): void;
99
+ downloadToCsv(fileName: string): void;
100
+ copyToClipboard(): Promise<void>;
101
+ generateCsvContent(): string[];
87
102
  }
103
+ export declare function maybeApply<T>(maybeFn: MaybeFn<T>): T;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GridTableApiImpl = exports.useGridTableApi = void 0;
3
+ exports.maybeApply = exports.GridTableApiImpl = exports.useGridTableApi = void 0;
4
4
  const mobx_1 = require("mobx");
5
5
  const mobx_utils_1 = require("mobx-utils");
6
6
  const react_1 = require("react");
@@ -107,11 +107,87 @@ class GridTableApiImpl {
107
107
  deleteRows(ids) {
108
108
  this.tableState.deleteRows(ids);
109
109
  }
110
+ downloadToCsv(fileName) {
111
+ // Create a link element, set the download attribute with the provided filename
112
+ const link = document.createElement("a");
113
+ if (link.download === undefined)
114
+ throw new Error("This browser does not support the download attribute.");
115
+ // Create a Blob from the CSV content
116
+ const url = URL.createObjectURL(new Blob([this.generateCsvContent().join("\n")], { type: "text/csv;charset=utf-8;" }));
117
+ link.setAttribute("href", url);
118
+ link.setAttribute("download", fileName);
119
+ link.style.visibility = "hidden";
120
+ document.body.appendChild(link);
121
+ link.click();
122
+ document.body.removeChild(link);
123
+ }
124
+ copyToClipboard() {
125
+ // Copy the CSV content to the clipboard
126
+ const content = this.generateCsvContent().join("\n");
127
+ return navigator.clipboard.writeText(content).catch((err) => {
128
+ // Let the user know the copy failed...
129
+ window.alert("Failed to copy to clipboard, probably due to browser restrictions.");
130
+ throw err;
131
+ });
132
+ }
133
+ // visibleForTesting, not part of the GridTableApi
134
+ // ...although maybe it could be public someday, to allow getting the raw the CSV content
135
+ // and then sending it somewhere else, like directly to a gsheet.
136
+ generateCsvContent() {
137
+ var _a, _b;
138
+ const csvPrefixRows = (_b = (_a = this.tableState.csvPrefixRows) === null || _a === void 0 ? void 0 : _a.map((row) => row.map(escapeCsvValue).join(","))) !== null && _b !== void 0 ? _b : [];
139
+ // Convert the array of rows into CSV format
140
+ const dataRows = this.tableState.visibleRows.map((rs) => {
141
+ const values = this.tableState.visibleColumns
142
+ .filter((c) => !c.isAction)
143
+ .map((c) => {
144
+ // Just guessing for level=1
145
+ const maybeContent = (0, index_1.applyRowFn)(c, rs.row, this, 1, true, undefined);
146
+ if ((0, index_1.isGridCellContent)(maybeContent)) {
147
+ const cell = maybeContent;
148
+ const content = maybeApply(cell.content);
149
+ // Anything not isJSX (like a string) we can put into the CSV directly
150
+ if (!(0, index_1.isJSX)(content))
151
+ return content;
152
+ // Otherwise use the value/sortValue values
153
+ return cell.value ? maybeApply(cell.value) : cell.sortValue ? maybeApply(cell.sortValue) : "-";
154
+ }
155
+ else {
156
+ // ReactNode
157
+ return (0, index_1.isJSX)(maybeContent) ? "-" : maybeContent;
158
+ }
159
+ });
160
+ return values.map(toCsvString).map(escapeCsvValue).join(",");
161
+ });
162
+ return [...csvPrefixRows, ...dataRows];
163
+ }
110
164
  }
111
165
  exports.GridTableApiImpl = GridTableApiImpl;
166
+ function toCsvString(value) {
167
+ if (value === null || value === undefined)
168
+ return "";
169
+ if (typeof value === "string")
170
+ return value;
171
+ if (typeof value === "number")
172
+ return value.toString();
173
+ if (typeof value === "boolean")
174
+ return value ? "true" : "false";
175
+ return String(value);
176
+ }
177
+ function escapeCsvValue(value) {
178
+ // Wrap values with special chars in quotes, and double quotes themselves
179
+ if (value.includes('"') || value.includes(",") || value.includes("\n")) {
180
+ return `"${value.replace(/"/g, '""')}"`;
181
+ }
182
+ return value;
183
+ }
112
184
  function bindMethods(instance) {
113
185
  Object.getOwnPropertyNames(Object.getPrototypeOf(instance)).forEach((key) => {
114
186
  if (instance[key] instanceof Function && key !== "constructor")
115
187
  instance[key] = instance[key].bind(instance);
116
188
  });
117
189
  }
190
+ function maybeApply(maybeFn) {
191
+ return typeof maybeFn === "function" ? maybeFn() : maybeFn;
192
+ }
193
+ exports.maybeApply = maybeApply;
@@ -119,14 +119,16 @@ function RowImpl(props) {
119
119
  currentColspan -= 1;
120
120
  return null;
121
121
  }
122
- const maybeContent = (0, utils_1.applyRowFn)(column, row, rowApi, level, isExpanded, {
122
+ // Combine all our drag stuff into a mini-context/parameter object...
123
+ const dragData = {
123
124
  rowRenderRef: ref,
124
125
  onDragStart,
125
126
  onDragEnd,
126
127
  onDrop,
127
128
  onDragEnter,
128
129
  onDragOver: onDragOverDebounced,
129
- });
130
+ };
131
+ const maybeContent = (0, utils_1.applyRowFn)(column, row, rowApi, level, isExpanded, dragData);
130
132
  // Only use the `numExpandedColumns` as the `colspan` when rendering the "Expandable Header"
131
133
  currentColspan =
132
134
  (0, utils_1.isGridCellContent)(maybeContent) && typeof maybeContent.colspan === "number"
@@ -6,10 +6,14 @@ import { Properties, Typography } from "../../../Css";
6
6
  /**
7
7
  * Allows a cell to be more than just a RectNode, i.e. declare its alignment or
8
8
  * primitive value for filtering and sorting.
9
+ *
10
+ * For a given column, the `GridColumn` can either return a static `GridCellContent`, or
11
+ * more likely use a function that returns a per-column/per-row `GridCellContent` that defines
12
+ * the value (and it's misc alignment/css/etc) for this specific cell.
9
13
  */
10
14
  export type GridCellContent = {
11
15
  /** The JSX content of the cell. Virtual tables that client-side sort should use a function to avoid perf overhead. */
12
- content: ReactNode | (() => ReactNode);
16
+ content: MaybeFn<ReactNode>;
13
17
  alignment?: GridCellAlignment;
14
18
  /** Allow value to be a function in case it's a dynamic value i.e. reading from an inline-edited proxy. */
15
19
  value?: MaybeFn<number | string | Date | boolean | null | undefined>;
@@ -32,6 +32,8 @@ export declare class TableState<R extends Kinded> {
32
32
  activeCellId: string | undefined;
33
33
  /** Stores the current client-side type-ahead search/filter. */
34
34
  search: string[];
35
+ /** Stores whether CSVs should have some prefix rows. */
36
+ csvPrefixRows: string[][] | undefined;
35
37
  sortConfig: GridSortConfig | undefined;
36
38
  sort: SortState;
37
39
  private initialSortState;
@@ -50,6 +52,7 @@ export declare class TableState<R extends Kinded> {
50
52
  setRows(rows: GridDataRow<R>[]): void;
51
53
  setColumns(columns: GridColumnWithId<R>[], visibleColumnsStorageKey: string | undefined): void;
52
54
  setSearch(search: string | undefined): void;
55
+ setCsvPrefixRows(csvPrefixRows: string[][] | undefined): void;
53
56
  get visibleRows(): RowState<R>[];
54
57
  /** Returns visible columns, i.e. those that are visible + any expanded children. */
55
58
  get visibleColumns(): GridColumnWithId<R>[];
@@ -42,6 +42,8 @@ class TableState {
42
42
  this.activeCellId = undefined;
43
43
  /** Stores the current client-side type-ahead search/filter. */
44
44
  this.search = [];
45
+ /** Stores whether CSVs should have some prefix rows. */
46
+ this.csvPrefixRows = undefined;
45
47
  // Tracks the active sort column(s), so GridTable or SortHeaders can reactively
46
48
  // re-render (for GridTable, only if client-side sorting)
47
49
  this.sort = {};
@@ -143,6 +145,9 @@ class TableState {
143
145
  // Break up "foo bar" into `[foo, bar]` and a row must match both `foo` and `bar`
144
146
  this.search = (search && search.split(/ +/)) || [];
145
147
  }
148
+ setCsvPrefixRows(csvPrefixRows) {
149
+ this.csvPrefixRows = csvPrefixRows;
150
+ }
146
151
  get visibleRows() {
147
152
  return this.rowStates.visibleRows;
148
153
  }
@@ -22,6 +22,8 @@ export declare const ASC: "ASC";
22
22
  export declare const DESC: "DESC";
23
23
  export declare const emptyCell: GridCellContent;
24
24
  export declare function getFirstOrLastCellCss<R extends Kinded>(style: GridStyle, columnIndex: number, columns: GridColumnWithId<R>[]): Properties;
25
+ /** A heuristic to detect the result of `React.createElement` / i.e. JSX. */
26
+ export declare function isJSX(content: any): boolean;
25
27
  export declare function getAlignment(column: GridColumnWithId<any>, maybeContent: ReactNode | GridCellContent): GridCellAlignment;
26
28
  export declare function getJustification(column: GridColumnWithId<any>, maybeContent: ReactNode | GridCellContent, as: RenderAs, alignment: GridCellAlignment): {
27
29
  textAlign: import("csstype").Property.TextAlign | undefined;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getTableRefWidthStyles = exports.recursivelyGetContainingRow = exports.isCursorBelowMidpoint = exports.insertAtIndex = exports.loadArrayOrUndefined = 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;
3
+ exports.getTableRefWidthStyles = exports.recursivelyGetContainingRow = exports.isCursorBelowMidpoint = exports.insertAtIndex = exports.loadArrayOrUndefined = exports.zIndices = exports.reservedRowKinds = exports.KEPT_GROUP = exports.EXPANDABLE_HEADER = exports.TOTALS = exports.HEADER = exports.matchesFilter = exports.maybeApplyFunction = exports.getJustification = exports.getAlignment = exports.isJSX = 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");
@@ -98,6 +98,7 @@ exports.getFirstOrLastCellCss = getFirstOrLastCellCss;
98
98
  function isJSX(content) {
99
99
  return typeof content === "object" && content && "type" in content && "props" in content;
100
100
  }
101
+ exports.isJSX = isJSX;
101
102
  const alignmentToJustify = {
102
103
  left: "flex-start",
103
104
  center: "center",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homebound/beam",
3
- "version": "2.363.0",
3
+ "version": "2.365.0",
4
4
  "author": "Homebound",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",