@absreim/react-bootstrap-data-grid 2.1.0 → 2.3.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.
package/Grid.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface GridProps {
16
16
  editModel?: EditModel;
17
17
  caption?: string;
18
18
  styleModel?: StyleModel;
19
+ useToolbar?: boolean;
20
+ responsive?: boolean;
19
21
  }
20
22
  declare const Grid: FC<GridProps>;
21
23
  export default Grid;
package/Grid.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo, useState } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useId, useMemo, useState } from "react";
4
4
  import ColHeaderCell from "./ColHeaderCell";
5
5
  import useFilter from "./pipeline/useFilter";
6
6
  import ToggleButton from "./ToggleButton";
@@ -18,9 +18,12 @@ import { unwrapAdditionalComponentsStyleModel, unwrapTableStyleModel, } from "./
18
18
  import useCurrentPageRows from "./pipeline/useCurrentPageRows";
19
19
  import isSubset from "./util/isSubset";
20
20
  import useFilterStateStore from "./pipeline/useFilterStateStore";
21
+ import useInterfaces from "./toolbar/useInterfaces";
22
+ import ToolbarContainer from "./toolbar/ToolbarContainer";
23
+ import useExportFn from "./export/useExportFn";
21
24
  var Grid = function (_a) {
22
25
  var _b;
23
- var rows = _a.rows, cols = _a.cols, pagination = _a.pagination, sortModel = _a.sortModel, filterModel = _a.filterModel, selectModel = _a.selectModel, editModel = _a.editModel, caption = _a.caption, styleModel = _a.styleModel;
26
+ var rows = _a.rows, cols = _a.cols, pagination = _a.pagination, sortModel = _a.sortModel, filterModel = _a.filterModel, selectModel = _a.selectModel, editModel = _a.editModel, caption = _a.caption, styleModel = _a.styleModel, useToolbar = _a.useToolbar, responsive = _a.responsive;
24
27
  var normalizedTableFilterModel = useFilterStateStore(filterModel, cols);
25
28
  var editableFilterState = (normalizedTableFilterModel === null || normalizedTableFilterModel === void 0 ? void 0 : normalizedTableFilterModel.tableFilterState) || null;
26
29
  var filteredRows = useFilter(rows, editableFilterState);
@@ -31,6 +34,34 @@ var Grid = function (_a) {
31
34
  var ariaColIndexOffset = showSelectCol ? 1 : 0;
32
35
  var displayRows = useDisplayRows(paginatedRows, cols, ariaColIndexOffset);
33
36
  var _e = useState(false), filterOptionsVisible = _e[0], setFilterOptionsVisible = _e[1];
37
+ var exportFnInfo = useExportFn({
38
+ rows: rows,
39
+ cols: cols,
40
+ filteredRows: filterModel && filteredRows,
41
+ currentPageRows: pagination && paginatedRows,
42
+ });
43
+ var toolbarInterfaceParams = useMemo(function () { return ({
44
+ filtering: useToolbar && filterState && filterModel && normalizedTableFilterModel
45
+ ? {
46
+ filterState: filterState,
47
+ setFilterState: normalizedTableFilterModel.setTableFilterState,
48
+ caption: filterModel.filterTableCaption,
49
+ styleModel: styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterInputTableStyleModel,
50
+ }
51
+ : undefined,
52
+ exporting: useToolbar
53
+ ? { exportFnInfo: exportFnInfo, styleModel: styleModel === null || styleModel === void 0 ? void 0 : styleModel.exportFormStyleModel }
54
+ : undefined,
55
+ }); }, [
56
+ exportFnInfo,
57
+ filterModel,
58
+ filterState,
59
+ normalizedTableFilterModel,
60
+ styleModel === null || styleModel === void 0 ? void 0 : styleModel.exportFormStyleModel,
61
+ styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterInputTableStyleModel,
62
+ useToolbar,
63
+ ]);
64
+ var toolbarInterfaces = useInterfaces(toolbarInterfaceParams);
34
65
  var handleToggleFilterOptions = function () {
35
66
  setFilterOptionsVisible(!filterOptionsVisible);
36
67
  };
@@ -81,11 +112,12 @@ var Grid = function (_a) {
81
112
  selectModel.setSelected(selectModel.selected.filter(function (num) { return num !== index; }));
82
113
  }; };
83
114
  // used to group radio buttons for selection
115
+ var gridId = useId();
84
116
  var getSelectInputModel = function (id, selectModel) {
85
117
  if (selectModel.type === "single") {
86
118
  return {
87
119
  type: "radio",
88
- name: selectModel.groupName,
120
+ name: selectModel.groupName || gridId,
89
121
  };
90
122
  }
91
123
  return {
@@ -159,30 +191,29 @@ var Grid = function (_a) {
159
191
  var unwrappedAdditionalStyleModel = useMemo(function () {
160
192
  return unwrapAdditionalComponentsStyleModel(styleModel === null || styleModel === void 0 ? void 0 : styleModel.additionalComponentsStyleModel);
161
193
  }, [styleModel === null || styleModel === void 0 ? void 0 : styleModel.additionalComponentsStyleModel]);
162
- return (_jsxs("div", { "data-testid": "rbdg-top-level-div", className: classNames(unwrappedAdditionalStyleModel.topLevelDiv), children: [normalizedTableFilterModel && (_jsxs("div", { "data-testid": "rbdg-filter-inputs-div", className: classNames(unwrappedAdditionalStyleModel.filterInputsDiv), children: [_jsx(ToggleButton, { isActive: filterOptionsVisible, label: "".concat(filterOptionsVisible ? "Hide" : "Show ", " Filter Options"), onClick: handleToggleFilterOptions, classes: (_b = styleModel === null || styleModel === void 0 ? void 0 : styleModel.additionalComponentsStyleModel) === null || _b === void 0 ? void 0 : _b.filterUiToggleButton }), filterOptionsVisible && (_jsx(FilterOptionsTable, { caption: filterModel.filterTableCaption, filterState: filterState, setFilterState: normalizedTableFilterModel.setTableFilterState, styleModel: styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterInputTableStyleModel }))] })), _jsxs("div", { "data-testid": "rbdg-table-and-pagination-div", className: classNames(unwrappedAdditionalStyleModel.tableAndPaginationDiv), children: [_jsxs("table", { className: classNames("table", {
163
- "table-hover": rowsAreSelectable,
164
- }, unwrappedTableModel.table), "aria-rowcount": filteredRows.length + 1, children: [caption !== undefined && (_jsx("caption", { className: classNames(unwrappedTableModel.caption), children: caption })), _jsx("thead", { className: classNames(unwrappedTableModel.thead), children: _jsxs("tr", { "aria-rowindex": 1, className: classNames(unwrappedTableModel.theadTr), children: [showSelectCol && (_jsx(SelectAllHeaderCell, { selectionInfo: selectionInfo, onClick: selectAllOnClick, totalRows: rows.length, additionalClasses: unwrappedTableModel.rowSelectColTh })), cols.map(function (_a, index) {
165
- var name = _a.name, label = _a.label, sortable = _a.sortable;
166
- var colSortModel = sortingEnabled && sortable
167
- ? {
168
- sortOrder: (sortColDef === null || sortColDef === void 0 ? void 0 : sortColDef.name) === name ? sortColDef.order : null,
169
- setSortOrder: function (order) {
170
- setSortColDef &&
171
- setSortColDef(order && { name: name, order: order });
172
- },
173
- }
174
- : undefined;
175
- return (_jsx(ColHeaderCell, { label: label, sortModel: colSortModel, ariaColIndex: index + 1 + (showSelectCol ? 1 : 0), additionalClasses: unwrappedTableModel.theadTh(index) }, name));
176
- }), editModel && (_jsx("th", { "aria-colindex": cols.length + 1 + (showSelectCol ? 1 : 0), className: classNames(unwrappedTableModel.editColTh), children: "Edit Controls" }))] }) }), _jsx("tbody", { className: classNames(unwrappedTableModel.tbody), children: displayRows.map(function (row, index) {
177
- return (_jsx(EditableRow, { onClick: getRowClickHandler(row.id), className: classNames({
178
- "table-active": selectedSet.has(row.id),
179
- }, unwrappedTableModel.tbodyTr(row.id, index)), "aria-rowindex": index + 2, dataRowId: row.id, "aria-selected": getAriaSelectedValue(row.id), ariaColIndexOffset: ariaColIndexOffset, cellData: row.contents, updateCallback: getInputStrSubmitCallback &&
180
- getInputStrSubmitCallback(row.id), deleteCallback: (editModel === null || editModel === void 0 ? void 0 : editModel.getDeleteCallback) &&
181
- editModel.getDeleteCallback(row.id), dataCellClasses: function (colIndex) {
182
- return unwrappedTableModel.tbodyTd(row.id, index, colIndex);
183
- }, dataCellInputClasses: function (colIndex) {
184
- return unwrappedTableModel.tbodyTdInput(row.id, index, colIndex);
185
- }, editCellClasses: unwrappedTableModel.editColTd(row.id, index), saveButtonClasses: unwrappedTableModel.editSaveButton(row.id, index), deleteButtonClasses: unwrappedTableModel.editDeleteButton(row.id, index), startButtonClasses: unwrappedTableModel.editStartButton(row.id, index), cancelButtonClasses: unwrappedTableModel.editCancelButton(row.id, index), children: showSelectCol && (_jsx("td", { className: classNames(unwrappedTableModel.rowSelectColTd(row.id, index)), "aria-colindex": 1, children: _jsx(SelectionInput, { selected: selectedSet.has(row.id), selectionInputModel: getSelectInputModel(row.id, selectModel), selectCallback: getSelectHandler(row.id), additionalClasses: unwrappedTableModel.rowSelectInput(row.id, index) }) })) }, row.id));
186
- }) })] }), normalizedModel && (_jsx(Pagination, { normalizedModel: normalizedModel, prePagingNumRows: sortedRows.length }))] })] }));
194
+ var mainTable = (_jsxs("table", { className: classNames("table", {
195
+ "table-hover": rowsAreSelectable,
196
+ }, unwrappedTableModel.table), "aria-rowcount": filteredRows.length + 1, children: [caption !== undefined && (_jsx("caption", { className: classNames(unwrappedTableModel.caption), children: caption })), _jsx("thead", { className: classNames(unwrappedTableModel.thead), children: _jsxs("tr", { "aria-rowindex": 1, className: classNames(unwrappedTableModel.theadTr), children: [showSelectCol && (_jsx(SelectAllHeaderCell, { selectionInfo: selectionInfo, onClick: selectAllOnClick, totalRows: rows.length, additionalClasses: unwrappedTableModel.rowSelectColTh })), cols.map(function (_a, index) {
197
+ var name = _a.name, label = _a.label, sortable = _a.sortable;
198
+ var colSortModel = sortingEnabled && sortable
199
+ ? {
200
+ sortOrder: (sortColDef === null || sortColDef === void 0 ? void 0 : sortColDef.name) === name ? sortColDef.order : null,
201
+ setSortOrder: function (order) {
202
+ setSortColDef && setSortColDef(order && { name: name, order: order });
203
+ },
204
+ }
205
+ : undefined;
206
+ return (_jsx(ColHeaderCell, { label: label, sortModel: colSortModel, ariaColIndex: index + 1 + (showSelectCol ? 1 : 0), additionalClasses: unwrappedTableModel.theadTh(index) }, name));
207
+ }), editModel && (_jsx("th", { "aria-colindex": cols.length + 1 + (showSelectCol ? 1 : 0), className: classNames(unwrappedTableModel.editColTh), children: "Edit Controls" }))] }) }), _jsx("tbody", { className: classNames(unwrappedTableModel.tbody), children: displayRows.map(function (row, index) {
208
+ return (_jsx(EditableRow, { onClick: getRowClickHandler(row.id), className: classNames({
209
+ "table-active": selectedSet.has(row.id),
210
+ }, unwrappedTableModel.tbodyTr(row.id, index)), "aria-rowindex": index + 2, dataRowId: row.id, "aria-selected": getAriaSelectedValue(row.id), ariaColIndexOffset: ariaColIndexOffset, cellData: row.contents, updateCallback: getInputStrSubmitCallback && getInputStrSubmitCallback(row.id), deleteCallback: (editModel === null || editModel === void 0 ? void 0 : editModel.getDeleteCallback) &&
211
+ editModel.getDeleteCallback(row.id), dataCellClasses: function (colIndex) {
212
+ return unwrappedTableModel.tbodyTd(row.id, index, colIndex);
213
+ }, dataCellInputClasses: function (colIndex) {
214
+ return unwrappedTableModel.tbodyTdInput(row.id, index, colIndex);
215
+ }, editCellClasses: unwrappedTableModel.editColTd(row.id, index), saveButtonClasses: unwrappedTableModel.editSaveButton(row.id, index), deleteButtonClasses: unwrappedTableModel.editDeleteButton(row.id, index), startButtonClasses: unwrappedTableModel.editStartButton(row.id, index), cancelButtonClasses: unwrappedTableModel.editCancelButton(row.id, index), children: showSelectCol && (_jsx("td", { className: classNames(unwrappedTableModel.rowSelectColTd(row.id, index)), "aria-colindex": 1, children: _jsx(SelectionInput, { selected: selectedSet.has(row.id), selectionInputModel: getSelectInputModel(row.id, selectModel), selectCallback: getSelectHandler(row.id), additionalClasses: unwrappedTableModel.rowSelectInput(row.id, index) }) })) }, row.id));
216
+ }) })] }));
217
+ return (_jsxs("div", { "data-testid": "rbdg-top-level-div", className: classNames(unwrappedAdditionalStyleModel.topLevelDiv), children: [normalizedTableFilterModel && !useToolbar && (_jsxs("div", { "data-testid": "rbdg-filter-inputs-div", className: classNames(unwrappedAdditionalStyleModel.filterInputsDiv), children: [_jsx(ToggleButton, { isActive: filterOptionsVisible, label: "".concat(filterOptionsVisible ? "Hide" : "Show ", " Filter Options"), onClick: handleToggleFilterOptions, classes: (_b = styleModel === null || styleModel === void 0 ? void 0 : styleModel.additionalComponentsStyleModel) === null || _b === void 0 ? void 0 : _b.filterUiToggleButton }), filterOptionsVisible && (_jsx(FilterOptionsTable, { caption: filterModel.filterTableCaption, filterState: filterState, setFilterState: normalizedTableFilterModel.setTableFilterState, styleModel: styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterInputTableStyleModel }))] })), useToolbar && (_jsx(ToolbarContainer, { interfaces: toolbarInterfaces, styleModel: styleModel === null || styleModel === void 0 ? void 0 : styleModel.toolbarStyleModel })), _jsxs("div", { "data-testid": "rbdg-table-and-pagination-div", className: classNames(unwrappedAdditionalStyleModel.tableAndPaginationDiv), children: [responsive ? (_jsx("div", { "data-testid": "rbdg-table-div", className: "table-responsive", children: mainTable })) : (_jsx(_Fragment, { children: mainTable })), normalizedModel && (_jsx(Pagination, { normalizedModel: normalizedModel, prePagingNumRows: sortedRows.length, containerDivClasses: unwrappedAdditionalStyleModel.paginationUiDiv }))] })] }));
187
218
  };
188
219
  export default Grid;
@@ -0,0 +1,9 @@
1
+ import { ExportFnInfo } from "./useExportFn";
2
+ import { FC } from "react";
3
+ import { ExportFormStyleModel } from "../styling/types";
4
+ export interface ExportFormProps {
5
+ exportFnInfo: ExportFnInfo;
6
+ styleModel?: ExportFormStyleModel;
7
+ }
8
+ declare const ExportForm: FC<ExportFormProps>;
9
+ export default ExportForm;
@@ -0,0 +1,97 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ import { useId, useState } from "react";
14
+ import classNames from "classnames";
15
+ var ExportForm = function (_a) {
16
+ var _b = _a.exportFnInfo, exportFn = _b.exportFn, formattersExist = _b.formattersExist, paginationEnabled = _b.paginationEnabled, filteringEnabled = _b.filteringEnabled, rowCounts = _b.rowCounts, styleModel = _a.styleModel;
17
+ var formId = useId();
18
+ var _c = useState({
19
+ stage: "original",
20
+ formatted: false,
21
+ fileType: "json",
22
+ }), formState = _c[0], setFormState = _c[1];
23
+ var getChangeHandler = function (field, value) {
24
+ return function () {
25
+ setFormState(function (prev) {
26
+ var _a;
27
+ return (__assign(__assign({}, prev), (_a = {}, _a[field] = value, _a)));
28
+ });
29
+ };
30
+ };
31
+ var getRowCountLabel = function (featureType, count) {
32
+ if (count === undefined) {
33
+ return "".concat(featureType, " disabled");
34
+ }
35
+ return "".concat(count, " ").concat(count === 1 ? "row" : "rows");
36
+ };
37
+ var stageOptions = [
38
+ {
39
+ value: "original",
40
+ label: "Original rows (total ".concat(rowCounts.total, " ").concat(rowCounts.total === 1 ? "row" : "rows", ")"),
41
+ disabled: false,
42
+ },
43
+ {
44
+ value: "filtered",
45
+ label: "After filters applied (".concat(getRowCountLabel("filtering", rowCounts.filtered), ")"),
46
+ disabled: !filteringEnabled,
47
+ },
48
+ {
49
+ value: "paged",
50
+ label: "Current page only (".concat(getRowCountLabel("pagination", rowCounts.currentPage), ")"),
51
+ disabled: !paginationEnabled,
52
+ },
53
+ ];
54
+ var formatOptions = [
55
+ {
56
+ formatted: false,
57
+ label: "Use original data",
58
+ disabled: false,
59
+ },
60
+ {
61
+ formatted: true,
62
+ label: "Apply formatters to data".concat(formattersExist ? "" : " (no formatters defined)"),
63
+ disabled: !formattersExist,
64
+ },
65
+ ];
66
+ var fileTypeOptions = [
67
+ {
68
+ fileType: "json",
69
+ label: "JSON",
70
+ },
71
+ {
72
+ fileType: "csv",
73
+ label: "CSV",
74
+ },
75
+ ];
76
+ var handleSubmit = function (event) {
77
+ event.preventDefault();
78
+ var stage = formState.stage, fileType = formState.fileType, formatted = formState.formatted;
79
+ exportFn(stage, fileType, formatted);
80
+ };
81
+ var legendClasses = classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.legend) || []);
82
+ var radioContainerClasses = classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.radioContainer) || ["form-check"]);
83
+ var radioInputClasses = classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.radioInput) || ["form-check-input"]);
84
+ var radioLabelClasses = classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.radioLabel) || ["form-check-label"]);
85
+ var submitButtonClasses = classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.submitButton) || ["btn", "btn-secondary"]);
86
+ return (_jsxs("form", { onSubmit: handleSubmit, children: [_jsxs("fieldset", { children: [_jsx("legend", { className: legendClasses, children: "Choose data to export" }), stageOptions.map(function (_a) {
87
+ var value = _a.value, label = _a.label, disabled = _a.disabled;
88
+ return (_jsxs("div", { className: radioContainerClasses, children: [_jsx("input", { className: radioInputClasses, type: "radio", id: "".concat(formId, "-").concat(value), value: value, checked: formState.stage === value, onChange: getChangeHandler("stage", value), disabled: disabled }), _jsx("label", { className: radioLabelClasses, htmlFor: "".concat(formId, "-").concat(value), children: label })] }, value));
89
+ })] }), _jsxs("fieldset", { children: [_jsx("legend", { className: legendClasses, children: "Choose whether to apply formatters" }), formatOptions.map(function (_a) {
90
+ var formatted = _a.formatted, label = _a.label, disabled = _a.disabled;
91
+ return (_jsxs("div", { className: radioContainerClasses, children: [_jsx("input", { className: radioInputClasses, type: "radio", id: "".concat(formId, "-").concat(formatted), value: String(formatted), checked: formState.formatted === formatted, onChange: getChangeHandler("formatted", formatted), disabled: disabled }), _jsx("label", { className: radioLabelClasses, htmlFor: "".concat(formId, "-").concat(formatted), children: label })] }, String(formatted)));
92
+ })] }), _jsxs("fieldset", { children: [_jsx("legend", { className: legendClasses, children: "Choose the file type" }), fileTypeOptions.map(function (_a) {
93
+ var fileType = _a.fileType, label = _a.label;
94
+ return (_jsxs("div", { className: radioContainerClasses, children: [_jsx("input", { className: radioInputClasses, type: "radio", id: "".concat(formId, "-").concat(fileType), value: fileType, checked: formState.fileType === fileType, onChange: getChangeHandler("fileType", fileType) }), _jsx("label", { className: radioLabelClasses, htmlFor: "".concat(formId, "-").concat(fileType), children: label })] }, fileType));
95
+ })] }), _jsx("button", { type: "submit", className: submitButtonClasses, children: "Submit" })] }));
96
+ };
97
+ export default ExportForm;
@@ -0,0 +1,5 @@
1
+ import { RowId } from "../types";
2
+ export type FormattedExportRow = {
3
+ id: RowId;
4
+ data: Record<string, string | number>;
5
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { ColDef, RowDef } from "../types";
2
+ export interface UseExportFnsParams {
3
+ rows: RowDef[];
4
+ cols: ColDef[];
5
+ filteredRows?: RowDef[];
6
+ currentPageRows?: RowDef[];
7
+ }
8
+ export type Stage = "original" | "filtered" | "paged";
9
+ export type FileType = "csv" | "json";
10
+ export type ExportFn = (stage: Stage, fileType: FileType, formatted: boolean) => void;
11
+ export interface ExportFnInfo {
12
+ exportFn: ExportFn;
13
+ formattersExist: boolean;
14
+ filteringEnabled: boolean;
15
+ paginationEnabled: boolean;
16
+ rowCounts: {
17
+ total: number;
18
+ filtered?: number;
19
+ currentPage?: number;
20
+ };
21
+ }
22
+ declare const useExportFn: (params: UseExportFnsParams) => ExportFnInfo;
23
+ export default useExportFn;
@@ -0,0 +1,115 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { dateToDatetimeInputStr, dateToInputStr } from "../util/datetime";
13
+ import Papa from "papaparse";
14
+ import { useCallback, useMemo } from "react";
15
+ var downloadFile = function (data, filename, type) {
16
+ var blob = new Blob([data], { type: type });
17
+ var url = URL.createObjectURL(blob);
18
+ var a = document.createElement("a");
19
+ a.href = url;
20
+ a.download = filename;
21
+ a.click();
22
+ a.remove();
23
+ setTimeout(function () { return URL.revokeObjectURL(url); }, 0);
24
+ };
25
+ var getDefaultFormatter = function (type) {
26
+ switch (type) {
27
+ case "date":
28
+ return dateToInputStr;
29
+ case "datetime":
30
+ return dateToDatetimeInputStr;
31
+ default:
32
+ return function (a) { return a; };
33
+ }
34
+ };
35
+ var applyFormatters = function (rows, cols, defaultOnly) {
36
+ var colToFormatter = cols.reduce(function (map, _a) {
37
+ var name = _a.name, formatter = _a.formatter, type = _a.type;
38
+ var normalizedFormatter = (!defaultOnly && formatter) || getDefaultFormatter(type);
39
+ map.set(name, normalizedFormatter);
40
+ return map;
41
+ }, new Map());
42
+ return rows.map(function (_a) {
43
+ var id = _a.id, data = _a.data;
44
+ return ({
45
+ id: id,
46
+ data: Object.keys(data).reduce(function (newData, name) {
47
+ var formatter = colToFormatter.get(name);
48
+ newData[name] = formatter(data[name]);
49
+ return newData;
50
+ }, {}),
51
+ });
52
+ });
53
+ };
54
+ var flattenExportRows = function (rows) {
55
+ return rows.map(function (_a) {
56
+ var id = _a.id, data = _a.data;
57
+ return (__assign({ id: id }, data));
58
+ });
59
+ };
60
+ var exportJson = function (rows, cols, useDefaultFormatters) {
61
+ var formattedRows = applyFormatters(rows, cols, useDefaultFormatters);
62
+ var json = JSON.stringify(formattedRows, null, 2);
63
+ downloadFile(json, "export.json", "application/json");
64
+ };
65
+ var exportCsv = function (rows, cols, useDefaultFormatters) {
66
+ var formattedRows = applyFormatters(rows, cols, useDefaultFormatters);
67
+ var flattenedRows = flattenExportRows(formattedRows);
68
+ var csv = Papa.unparse(flattenedRows, { header: true });
69
+ downloadFile(csv, "export.csv", "text/csv");
70
+ };
71
+ var useExportFn = function (_a) {
72
+ var rows = _a.rows, cols = _a.cols, filteredRows = _a.filteredRows, currentPageRows = _a.currentPageRows;
73
+ var formattersExist = useMemo(function () { return cols.reduce(function (prev, _a) {
74
+ var formatter = _a.formatter;
75
+ return prev || !!formatter;
76
+ }, false); }, [cols]);
77
+ var exportFn = useCallback(function (stage, fileType, formatted) {
78
+ if (stage === "filtered" && !filteredRows) {
79
+ throw Error("Cannot export filtered rows because filtering is not enabled for this grid");
80
+ }
81
+ if (stage === "paged" && !currentPageRows) {
82
+ throw Error("Cannot export current page of rows because paging is not enabled for this grid");
83
+ }
84
+ if (formatted && !formattersExist) {
85
+ throw Error("Cannot export formatted rows because formatters are not defined for this grid");
86
+ }
87
+ var exportRows = (function () {
88
+ switch (stage) {
89
+ case "filtered":
90
+ return filteredRows;
91
+ case "paged":
92
+ return currentPageRows;
93
+ default:
94
+ return rows;
95
+ }
96
+ })();
97
+ if (fileType === "csv") {
98
+ exportCsv(exportRows, cols, !formatted);
99
+ return;
100
+ }
101
+ exportJson(exportRows, cols, !formatted);
102
+ }, [cols, currentPageRows, filteredRows, formattersExist, rows]);
103
+ return useMemo(function () { return ({
104
+ exportFn: exportFn,
105
+ formattersExist: formattersExist,
106
+ paginationEnabled: !!currentPageRows,
107
+ filteringEnabled: !!filteredRows,
108
+ rowCounts: {
109
+ total: rows.length,
110
+ filtered: filteredRows === null || filteredRows === void 0 ? void 0 : filteredRows.length,
111
+ currentPage: currentPageRows === null || currentPageRows === void 0 ? void 0 : currentPageRows.length,
112
+ },
113
+ }); }, [currentPageRows, exportFn, filteredRows, formattersExist, rows.length]);
114
+ };
115
+ export default useExportFn;
@@ -10,9 +10,8 @@ var __assign = (this && this.__assign) || function () {
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
12
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
13
- import { useMemo } from "react";
13
+ import { useId } from "react";
14
14
  import { dateFilterSchemeNames, } from "./types";
15
- import { nanoid } from "nanoid/non-secure";
16
15
  import FilterRow from "./FilterRow";
17
16
  import classNames from "classnames";
18
17
  var DateFilterRow = function (_a) {
@@ -35,7 +34,7 @@ var DateFilterRow = function (_a) {
35
34
  };
36
35
  var enabled = filterState.enabled, scheme = filterState.scheme, startDate = filterState.startDate, endDate = filterState.endDate;
37
36
  var inputType = includeTime ? "datetime-local" : "date";
38
- var inputId = useMemo(function () { return nanoid(); }, []);
37
+ var inputId = useId();
39
38
  var startDateInputId = "$startDate-".concat(inputId);
40
39
  var endDateInputId = "$endDate-".concat(inputId);
41
40
  var startDateInputLabel = "".concat(columnLabel, " Column Filter Start Date");
@@ -1,7 +1,7 @@
1
1
  import { FC } from "react";
2
2
  import { EditableTableFilterState, TableFilterState } from "./types";
3
3
  import { FilterInputTableStyleModel } from "../styling/types";
4
- interface FilterOptionsTableProps {
4
+ export interface FilterOptionsTableProps {
5
5
  filterState: TableFilterState;
6
6
  setFilterState: (filterState: EditableTableFilterState) => void;
7
7
  caption?: string;
@@ -112,7 +112,6 @@ var FilterOptionsTable = function (_a) {
112
112
  var editableTableFilterState = convertFilterFormStateToEditableState(formState);
113
113
  setFilterState(editableTableFilterState);
114
114
  };
115
- // TODO: consider using an accordion to show and hide this component
116
- return (_jsxs("form", { onSubmit: onSubmit, children: [_jsxs("table", { className: classNames.apply(void 0, __spreadArray(["table"], unwrappedStyleModel.table, false)), children: [caption && (_jsx("caption", { className: classNames(unwrappedStyleModel.caption), children: caption })), _jsx("thead", { className: classNames.apply(void 0, unwrappedStyleModel.thead), children: _jsx("tr", { className: classNames.apply(void 0, unwrappedStyleModel.theadTr), children: ["Enabled", "Column", "Type", "Operator", "Value"].map(function (colName, index) { return (_jsx("th", { className: classNames.apply(void 0, unwrappedStyleModel.theadTh(index)), children: colName }, index)); }) }) }), _jsx("tbody", { className: classNames.apply(void 0, unwrappedStyleModel.tbody), children: getRows() })] }), _jsx("button", { className: classNames("btn", { "btn-secondary": unwrappedStyleModel.submitButton.length === 0 }, unwrappedStyleModel.submitButton), type: "submit", children: "Submit" })] }));
115
+ return (_jsxs("form", { onSubmit: onSubmit, className: classNames(styleModel === null || styleModel === void 0 ? void 0 : styleModel.form), children: [_jsxs("table", { className: classNames.apply(void 0, __spreadArray(["table"], unwrappedStyleModel.table, false)), children: [caption && (_jsx("caption", { className: classNames(unwrappedStyleModel.caption), children: caption })), _jsx("thead", { className: classNames.apply(void 0, unwrappedStyleModel.thead), children: _jsx("tr", { className: classNames.apply(void 0, unwrappedStyleModel.theadTr), children: ["Enabled", "Column", "Type", "Operator", "Value"].map(function (colName, index) { return (_jsx("th", { className: classNames.apply(void 0, unwrappedStyleModel.theadTh(index)), children: colName }, index)); }) }) }), _jsx("tbody", { className: classNames.apply(void 0, unwrappedStyleModel.tbody), children: getRows() })] }), _jsx("button", { className: classNames("btn", { "btn-secondary": unwrappedStyleModel.submitButton.length === 0 }, unwrappedStyleModel.submitButton), type: "submit", children: "Submit" })] }));
117
116
  };
118
117
  export default FilterOptionsTable;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@absreim/react-bootstrap-data-grid",
3
3
  "description": "Data grid UI component for use with web apps using React and Bootstrap",
4
- "version": "2.1.0",
4
+ "version": "2.3.0",
5
5
  "license": "MIT",
6
6
  "author": "Brook Li",
7
7
  "homepage": "https://react-bootstrap-data-grid.vercel.app/",
@@ -16,7 +16,8 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "classnames": "^2.5.1",
19
- "dayjs": "^1.11.13"
19
+ "dayjs": "^1.11.13",
20
+ "papaparse": "^5.5.3"
20
21
  },
21
22
  "peerDependencies": {
22
23
  "react": "^19",
@@ -1,11 +1,9 @@
1
1
  import { FC } from "react";
2
- import { JustifyContentSetting } from "../types";
3
2
  import { NormalizedPaginationModel } from "./types";
4
3
  export interface PaginationProps {
5
4
  normalizedModel: NormalizedPaginationModel;
6
5
  prePagingNumRows: number;
7
- pageSelectorAriaLabel?: string;
8
- pageSelectorJustifyContent?: JustifyContentSetting;
6
+ containerDivClasses: string[];
9
7
  }
10
8
  declare const Pagination: FC<PaginationProps>;
11
9
  export default Pagination;
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import PageSizeSelector from "./PageSizeSelector";
3
3
  import PageSelector from "./PageSelector";
4
+ import classNames from "classnames";
4
5
  var Pagination = function (_a) {
5
- var normalizedModel = _a.normalizedModel, prePagingNumRows = _a.prePagingNumRows;
6
+ var normalizedModel = _a.normalizedModel, prePagingNumRows = _a.prePagingNumRows, containerDivClasses = _a.containerDivClasses;
6
7
  var pageSizeOptions = normalizedModel.pageSizeOptions, pageSizeIndex = normalizedModel.pageSizeIndex, currentPage = normalizedModel.currentPage, setCurrentPage = normalizedModel.setCurrentPage, setPageSizeIndex = normalizedModel.setPageSizeIndex, componentSize = normalizedModel.componentSize, maxPageButtons = normalizedModel.maxPageButtons, pageSelectorAriaLabel = normalizedModel.pageSelectorAriaLabel, pageSelectorJustifyContent = normalizedModel.pageSelectorJustifyContent;
7
8
  var numPages = Math.ceil(prePagingNumRows / pageSizeOptions[pageSizeIndex]);
8
9
  var pageIndexAwarePageSizeSetter = function (newPageSizeIndex) {
@@ -16,6 +17,8 @@ var Pagination = function (_a) {
16
17
  setCurrentPage(maxPages);
17
18
  }
18
19
  };
19
- return (_jsxs("div", { className: "d-flex justify-content-end gap-2", children: [_jsx(PageSizeSelector, { componentSize: componentSize, pageSizeOptions: pageSizeOptions, pageSizeIndex: pageSizeIndex, handleSetPageSize: pageIndexAwarePageSizeSetter }), _jsx(PageSelector, { numPages: numPages, pageNum: currentPage, numButtons: maxPageButtons, setPageNum: setCurrentPage, size: componentSize, ariaLabel: pageSelectorAriaLabel, alignment: pageSelectorJustifyContent })] }));
20
+ return (_jsxs("div", { "data-testid": "pagination ui container div", className: classNames(containerDivClasses.length > 0
21
+ ? containerDivClasses
22
+ : ["d-flex", "justify-content-end", "gap-2", "px-2"]), children: [_jsx(PageSizeSelector, { componentSize: componentSize, pageSizeOptions: pageSizeOptions, pageSizeIndex: pageSizeIndex, handleSetPageSize: pageIndexAwarePageSizeSetter }), _jsx(PageSelector, { numPages: numPages, pageNum: currentPage, numButtons: maxPageButtons, setPageNum: setCurrentPage, size: componentSize, ariaLabel: pageSelectorAriaLabel, alignment: pageSelectorJustifyContent })] }));
20
23
  };
21
24
  export default Pagination;
@@ -12,7 +12,7 @@ export interface SingleSelectModel {
12
12
  type: "single";
13
13
  selected: RowId | null;
14
14
  setSelected: (selected: RowId | null) => void;
15
- groupName: string;
15
+ groupName?: string;
16
16
  }
17
17
  export type SelectModel = SingleSelectModel | MultiSelectModel;
18
18
  export type MultiExistingSelection = "full" | "partial" | "none";
@@ -48,10 +48,11 @@ export var unwrapFilterInputTableStyleModel = function (filterTableStyleModel) {
48
48
  ? filterTableStyleModel.startDateInput
49
49
  : function () { return []; }, endDateInput: (filterTableStyleModel === null || filterTableStyleModel === void 0 ? void 0 : filterTableStyleModel.endDateInput)
50
50
  ? filterTableStyleModel.endDateInput
51
- : function () { return []; }, submitButton: (filterTableStyleModel === null || filterTableStyleModel === void 0 ? void 0 : filterTableStyleModel.submitButton) || [] })); };
51
+ : function () { return []; }, submitButton: (filterTableStyleModel === null || filterTableStyleModel === void 0 ? void 0 : filterTableStyleModel.submitButton) || [], form: (filterTableStyleModel === null || filterTableStyleModel === void 0 ? void 0 : filterTableStyleModel.form) || [] })); };
52
52
  export var unwrapAdditionalComponentsStyleModel = function (styleModel) { return ({
53
53
  topLevelDiv: (styleModel === null || styleModel === void 0 ? void 0 : styleModel.topLevelDiv) || [],
54
54
  filterInputsDiv: (styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterInputsDiv) || [],
55
55
  tableAndPaginationDiv: (styleModel === null || styleModel === void 0 ? void 0 : styleModel.tableAndPaginationDiv) || [],
56
56
  filterUiToggleButton: (styleModel === null || styleModel === void 0 ? void 0 : styleModel.filterUiToggleButton) || [],
57
+ paginationUiDiv: (styleModel === null || styleModel === void 0 ? void 0 : styleModel.paginationUiDiv) || [],
57
58
  }); };
@@ -31,15 +31,32 @@ export type FilterInputTableStyleModel = SharedTableStyleModel & {
31
31
  startDateInput?: (rowIndex: number) => string[];
32
32
  endDateInput?: (rowIndex: number) => string[];
33
33
  submitButton?: string[];
34
+ form?: string[];
34
35
  };
35
36
  export interface AdditionalComponentsStyleModel {
36
37
  topLevelDiv?: string[];
37
38
  filterInputsDiv?: string[];
38
39
  tableAndPaginationDiv?: string[];
39
40
  filterUiToggleButton?: string[];
41
+ paginationUiDiv?: string[];
42
+ }
43
+ export interface ToolbarStyleModel {
44
+ activeButton?: string[];
45
+ inactiveButton?: string[];
46
+ toolbar?: string[];
47
+ interfaceContainer?: string[];
48
+ }
49
+ export interface ExportFormStyleModel {
50
+ legend?: string[];
51
+ radioContainer?: string[];
52
+ radioInput?: string[];
53
+ radioLabel?: string[];
54
+ submitButton?: string[];
40
55
  }
41
56
  export interface StyleModel {
42
57
  mainTableStyleModel?: TableStyleModel;
43
58
  filterInputTableStyleModel?: FilterInputTableStyleModel;
44
59
  additionalComponentsStyleModel?: AdditionalComponentsStyleModel;
60
+ toolbarStyleModel?: ToolbarStyleModel;
61
+ exportFormStyleModel?: ExportFormStyleModel;
45
62
  }
@@ -0,0 +1,12 @@
1
+ import { ToolbarOption } from "./types";
2
+ import { FC } from "react";
3
+ export interface ToolbarProps {
4
+ enabledFeatures: Partial<Record<ToolbarOption, boolean>>;
5
+ option: ToolbarOption | null;
6
+ setOption: (option: ToolbarOption | null) => void;
7
+ toolbarClasses?: string[];
8
+ activeClasses?: string[];
9
+ inactiveClasses?: string[];
10
+ }
11
+ declare const Toolbar: FC<ToolbarProps>;
12
+ export default Toolbar;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import classNames from "classnames";
3
+ var buttonSpecs = {
4
+ filtering: {
5
+ label: "Filtering",
6
+ icon: (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", fill: "currentColor", viewBox: "0 0 16 16", children: _jsx("path", { d: "M6 10.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-2-3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5" }) })),
7
+ },
8
+ exporting: {
9
+ label: "Export",
10
+ icon: (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", fill: "currentColor", viewBox: "0 0 16 16", children: [_jsx("path", { d: "M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" }), _jsx("path", { d: "M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" })] })),
11
+ },
12
+ };
13
+ // TODO: figure out tabindex and accessibility
14
+ var Toolbar = function (_a) {
15
+ var enabledFeatures = _a.enabledFeatures, option = _a.option, setOption = _a.setOption, toolbarClasses = _a.toolbarClasses, activeClasses = _a.activeClasses, inactiveClasses = _a.inactiveClasses;
16
+ return (_jsx("div", { className: classNames(toolbarClasses || ["hstack", "gap-2", "justify-content-start", "px-2"]), role: "toolbar", children: Object.keys(buttonSpecs)
17
+ .filter(function (toolbarOption) { return !!enabledFeatures[toolbarOption]; })
18
+ .map(function (toolbarOption) { return (_jsx("button", { "aria-label": buttonSpecs[toolbarOption].label, "aria-roledescription": "Grouped toggle button to show/hide ".concat(toolbarOption, " UI"), "aria-pressed": option === toolbarOption, className: classNames.apply(void 0, (option === toolbarOption
19
+ ? activeClasses || ["btn", "btn-outline-secondary", "active"]
20
+ : inactiveClasses || ["btn", "btn-outline-secondary"])), title: buttonSpecs[toolbarOption].label, onClick: function () {
21
+ setOption(option === toolbarOption
22
+ ? null
23
+ : toolbarOption);
24
+ }, children: buttonSpecs[toolbarOption].icon }, toolbarOption)); }) }));
25
+ };
26
+ export default Toolbar;
@@ -0,0 +1,9 @@
1
+ import { ToolbarInterfaces } from "./types";
2
+ import { FC } from "react";
3
+ import { ToolbarStyleModel } from "../styling/types";
4
+ interface ToolbarContainerProps {
5
+ interfaces: ToolbarInterfaces;
6
+ styleModel?: ToolbarStyleModel;
7
+ }
8
+ declare const ToolbarContainer: FC<ToolbarContainerProps>;
9
+ export default ToolbarContainer;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import Toolbar from "./Toolbar";
4
+ import classNames from "classnames";
5
+ var ToolbarContainer = function (_a) {
6
+ var interfaces = _a.interfaces, styleModel = _a.styleModel;
7
+ var _b = useState(null), option = _b[0], setOption = _b[1];
8
+ var enabledFeatures = Object.keys(interfaces).reduce(function (prev, toolbarOption) {
9
+ prev[toolbarOption] =
10
+ !!interfaces[toolbarOption];
11
+ return prev;
12
+ }, {});
13
+ // TODO: mention in documentation that Bootstrap 5.3 is required due to the
14
+ // use of the z-index utility
15
+ return (_jsxs("div", { className: "vstack", "data-testid": "toolbar container", children: [_jsx(Toolbar, { enabledFeatures: enabledFeatures, option: option, setOption: setOption, toolbarClasses: styleModel === null || styleModel === void 0 ? void 0 : styleModel.toolbar, activeClasses: styleModel === null || styleModel === void 0 ? void 0 : styleModel.activeButton, inactiveClasses: styleModel === null || styleModel === void 0 ? void 0 : styleModel.inactiveButton }), _jsx("div", { className: "position-relative", children: option !== null && (_jsx("div", { "data-testid": "toolbar feature interface content container", className: classNames((styleModel === null || styleModel === void 0 ? void 0 : styleModel.interfaceContainer) || [
16
+ "position-absolute",
17
+ "z-1",
18
+ "bg-body",
19
+ "border",
20
+ "shadow",
21
+ "p-2",
22
+ ]), children: interfaces[option] })) })] }));
23
+ };
24
+ export default ToolbarContainer;
@@ -0,0 +1,3 @@
1
+ import { ReactNode } from "react";
2
+ export type ToolbarOption = "filtering" | "exporting";
3
+ export type ToolbarInterfaces = Partial<Record<ToolbarOption, ReactNode>>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { FilterOptionsTableProps } from "../filtering/FilterOptionsTable";
2
+ import { ToolbarInterfaces } from "./types";
3
+ import { ExportFormProps } from "../export/ExportForm";
4
+ export interface InterfaceParams {
5
+ filtering?: FilterOptionsTableProps;
6
+ exporting?: ExportFormProps;
7
+ }
8
+ declare const useInterfaces: (params: InterfaceParams) => ToolbarInterfaces;
9
+ export default useInterfaces;
@@ -0,0 +1,23 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import { jsx as _jsx } from "react/jsx-runtime";
13
+ import FilterOptionsTable from "../filtering/FilterOptionsTable";
14
+ import { useMemo } from "react";
15
+ import ExportForm from "../export/ExportForm";
16
+ var useInterfaces = function (_a) {
17
+ var filtering = _a.filtering, exporting = _a.exporting;
18
+ return useMemo(function () { return ({
19
+ filtering: filtering ? _jsx(FilterOptionsTable, __assign({}, filtering)) : undefined,
20
+ exporting: exporting ? _jsx(ExportForm, __assign({}, exporting)) : undefined,
21
+ }); }, [exporting, filtering]);
22
+ };
23
+ export default useInterfaces;