@monolith-forensics/monolith-ui 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Menu, MenuItemList, StyledInnerItemContainer } from "./components";
4
- export const DropDownMenu = ({ data, children, defaultValue, variant, arrow, size, searchable, loading, onScroll, onScrollToTop, onScrollToBottom, onSearch, manualSearch, multiselect, renderOption, onItemSelect, onChange, buttonProps, TooltipContent, dropDownProps, query, disabled, }) => {
4
+ export const DropDownMenu = ({ data, children, defaultValue, variant, arrow, size, searchable, grouped, onAddNew, loading, onScroll, onScrollToTop, onScrollToBottom, onSearch, manualSearch, multiselect, renderOption, onItemSelect, onChange, buttonProps, TooltipContent, dropDownProps, query, disabled, }) => {
5
5
  var _a;
6
6
  const isObjectArray = (_a = Object.keys((data === null || data === void 0 ? void 0 : data[0]) || {})) === null || _a === void 0 ? void 0 : _a.includes("label");
7
7
  const [selected, setSelected] = useState(defaultValue || []);
@@ -25,5 +25,5 @@ export const DropDownMenu = ({ data, children, defaultValue, variant, arrow, siz
25
25
  const handleScrollToBottom = (e) => {
26
26
  onScrollToBottom === null || onScrollToBottom === void 0 ? void 0 : onScrollToBottom(e);
27
27
  };
28
- return (_jsx(Menu, { label: children, disabled: disabled, arrow: arrow, buttonSize: size, variant: variant, multiselect: multiselect, buttonProps: buttonProps, onMenuClose: handleMenuClose, dropDownProps: dropDownProps, children: _jsxs(StyledInnerItemContainer, { children: [loading && _jsx("div", { children: "Loading..." }), !loading && (_jsx(MenuItemList, { menuItems: data, searchable: searchable, onSearch: onSearch, manualSearch: manualSearch, selected: selected, TooltipContent: TooltipContent, multiselect: multiselect, size: size, handleAddItem: handleAddItem, handleRemoveItem: handleRemoveItem, onItemSelect: onItemSelect, renderOption: renderOption, onScroll: onScroll, onScrollToTop: onScrollToTop, onScrollToBottom: handleScrollToBottom, query: query }))] }) }));
28
+ return (_jsx(Menu, { label: children, disabled: disabled, arrow: arrow, buttonSize: size, variant: variant, multiselect: multiselect, buttonProps: buttonProps, onMenuClose: handleMenuClose, dropDownProps: dropDownProps, children: _jsxs(StyledInnerItemContainer, { children: [loading && _jsx("div", { children: "Loading..." }), !loading && (_jsx(MenuItemList, { menuItems: data, searchable: searchable, grouped: grouped, onAddNew: onAddNew, onSearch: onSearch, manualSearch: manualSearch, selected: selected, TooltipContent: TooltipContent, multiselect: multiselect, size: size, handleAddItem: handleAddItem, handleRemoveItem: handleRemoveItem, onItemSelect: onItemSelect, renderOption: renderOption, onScroll: onScroll, onScrollToTop: onScrollToTop, onScrollToBottom: handleScrollToBottom, query: query }))] }) }));
29
29
  };
@@ -10,6 +10,8 @@ export declare const MenuItemList: React.FC<{
10
10
  selected?: DropDownItem[];
11
11
  TooltipContent?: ComponentType<any>;
12
12
  multiselect?: boolean;
13
+ grouped?: boolean;
14
+ onAddNew?: (value: string) => void;
13
15
  size?: Size;
14
16
  handleAddItem: (item: DropDownItem) => void;
15
17
  handleRemoveItem: (item: DropDownItem) => void;
@@ -13,6 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
13
13
  import { useLayoutEffect, useRef, useState } from "react";
14
14
  import styled from "styled-components";
15
15
  import { FixedSizeList } from "react-window";
16
+ import { Plus } from "lucide-react";
16
17
  import CheckBox from "../../CheckBox";
17
18
  import { DropDownMenu } from "../DropDownMenu";
18
19
  import { MenuItem } from "./MenuItem";
@@ -30,11 +31,71 @@ const filterMenuItems = (menuItems, searchValue) => {
30
31
  return (_c = item.toLowerCase) === null || _c === void 0 ? void 0 : _c.call(item).includes(searchValue.toLowerCase());
31
32
  });
32
33
  };
34
+ const buildGroupedDisplayList = (items) => {
35
+ const groups = {};
36
+ for (const item of items) {
37
+ const key = item.group || "Other";
38
+ if (!groups[key])
39
+ groups[key] = [];
40
+ groups[key].push(item);
41
+ }
42
+ const sortedKeys = Object.keys(groups).sort((a, b) => a.localeCompare(b));
43
+ const result = [];
44
+ for (const key of sortedKeys) {
45
+ result.push({
46
+ _isGroupHeader: true,
47
+ label: key,
48
+ value: `__group__${key}`,
49
+ });
50
+ result.push(...groups[key]);
51
+ }
52
+ return result;
53
+ };
33
54
  const ListViewPort = styled.div.attrs({ className: "ListViewPort" }) `
34
55
  display: flex;
35
56
  flex-direction: column;
36
57
  `;
37
- export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, selected, TooltipContent, multiselect, size, handleAddItem, handleRemoveItem, onItemSelect, renderOption, onScroll, onScrollToTop, onScrollToBottom, query, }) => {
58
+ const GroupHeader = styled.div `
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ color: ${(props) => props.theme.palette.text.secondary};
63
+ font-weight: 500;
64
+ user-select: none;
65
+ padding: ${({ $size }) => $size === "xs"
66
+ ? "2px 8px"
67
+ : $size === "sm"
68
+ ? "4px 10px"
69
+ : $size === "md"
70
+ ? "4px 12px"
71
+ : $size === "lg"
72
+ ? "5px 14px"
73
+ : $size === "xl"
74
+ ? "6px 16px"
75
+ : "2px 8px"};
76
+ font-size: ${({ $size }) => $size === "xs"
77
+ ? "10px"
78
+ : $size === "sm"
79
+ ? "11px"
80
+ : $size === "md"
81
+ ? "13px"
82
+ : $size === "lg"
83
+ ? "15px"
84
+ : $size === "xl"
85
+ ? "17px"
86
+ : "10px"};
87
+
88
+ .group-line {
89
+ border-top: 1px solid ${(props) => props.theme.palette.divider};
90
+ flex: 1;
91
+ }
92
+
93
+ .group-label {
94
+ white-space: nowrap;
95
+ min-width: fit-content;
96
+ }
97
+ `;
98
+ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, selected, TooltipContent, multiselect, grouped, onAddNew, size, handleAddItem, handleRemoveItem, onItemSelect, renderOption, onScroll, onScrollToTop, onScrollToBottom, query, }) => {
38
99
  var _a;
39
100
  const [searchValue, setSearchValue] = useState("");
40
101
  const _b = query !== null && query !== void 0 ? query : {}, { queryKey, queryFn, getNextPageParam, initialPageParam } = _b, rest = __rest(_b, ["queryKey", "queryFn", "getNextPageParam", "initialPageParam"]);
@@ -69,6 +130,29 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, se
69
130
  const filteredItems = searchable
70
131
  ? filterMenuItems(visibleItems, searchValue)
71
132
  : visibleItems;
133
+ const hasExactMatch = searchValue.trim() !== "" &&
134
+ filteredItems.some((item) => {
135
+ var _a, _b;
136
+ return ((_a = item === null || item === void 0 ? void 0 : item.label) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === searchValue.trim().toLowerCase() ||
137
+ ((_b = item === null || item === void 0 ? void 0 : item.value) === null || _b === void 0 ? void 0 : _b.toString().toLowerCase()) ===
138
+ searchValue.trim().toLowerCase();
139
+ });
140
+ const displayItems = (() => {
141
+ let items = grouped
142
+ ? buildGroupedDisplayList(filteredItems)
143
+ : filteredItems;
144
+ if (onAddNew && searchValue.trim() && !hasExactMatch) {
145
+ items = [
146
+ ...items,
147
+ {
148
+ _isAddNew: true,
149
+ label: `Add "${searchValue.trim()}"`,
150
+ value: searchValue.trim(),
151
+ },
152
+ ];
153
+ }
154
+ return items;
155
+ })();
72
156
  const isObjectArray = (_a = Object.keys((visibleItems === null || visibleItems === void 0 ? void 0 : visibleItems[0]) || {})) === null || _a === void 0 ? void 0 : _a.includes("label");
73
157
  const isLoading = isLoadingInfiniteQuery;
74
158
  const handleOnScroll = (event) => {
@@ -114,7 +198,7 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, se
114
198
  }, [targetElm.current, isLoading]);
115
199
  const overscanCount = 10;
116
200
  const itemHeight = 25;
117
- const itemCount = (filteredItems === null || filteredItems === void 0 ? void 0 : filteredItems.length) || 0;
201
+ const itemCount = (displayItems === null || displayItems === void 0 ? void 0 : displayItems.length) || 0;
118
202
  const height = viewPortDimensions.height
119
203
  ? viewPortDimensions.height
120
204
  : itemCount * itemHeight < 200
@@ -134,8 +218,20 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, se
134
218
  alignItems: "center",
135
219
  height: "100%",
136
220
  padding: "10px 0",
137
- }, children: [_jsx("div", { style: { fontSize: "12px" }, children: "Loading..." }), _jsx(Loader, {})] })), !isLoading && (_jsx(ListViewPort, { ref: targetElm, children: _jsx(FixedSizeList, { itemData: filteredItems, overscanCount: overscanCount, height: height, width: width, itemCount: itemCount, itemSize: itemHeight, outerRef: listElm, children: ({ data, index, style }) => {
138
- const item = (data === null || data === void 0 ? void 0 : data[index]) || {};
221
+ }, children: [_jsx("div", { style: { fontSize: "12px" }, children: "Loading..." }), _jsx(Loader, {})] })), !isLoading && (_jsx(ListViewPort, { ref: targetElm, children: _jsx(FixedSizeList, { itemData: displayItems, overscanCount: overscanCount, height: height, width: width, itemCount: itemCount, itemSize: itemHeight, outerRef: listElm, children: ({ data, index, style }) => {
222
+ const item = data === null || data === void 0 ? void 0 : data[index];
223
+ if (!item)
224
+ return null;
225
+ if (item._isGroupHeader) {
226
+ return (_jsxs(GroupHeader, { "$size": size, style: style, children: [_jsx("span", { className: "group-line" }), _jsx("span", { className: "group-label", children: item.label }), _jsx("span", { className: "group-line" })] }, item.value));
227
+ }
228
+ if (item._isAddNew) {
229
+ return (_jsx(MenuItem, { className: "MenuItem", size: size, leftSection: _jsx(Plus, { size: 14 }), onClick: (e) => {
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ onAddNew === null || onAddNew === void 0 ? void 0 : onAddNew(item.value);
233
+ }, style: style, children: item.label }, "__add_new__"));
234
+ }
139
235
  const isSelected = !!(selected === null || selected === void 0 ? void 0 : selected.find((s) => {
140
236
  return isObjectArray ? (s === null || s === void 0 ? void 0 : s.value) === (item === null || item === void 0 ? void 0 : item.value) : s === item;
141
237
  }));
@@ -15,6 +15,7 @@ export type DropDownItem = {
15
15
  rightSection?: React.ReactNode;
16
16
  disabled?: boolean;
17
17
  visible?: boolean;
18
+ group?: string;
18
19
  };
19
20
  export type SearchInputProps = React.ComponentPropsWithoutRef<typeof Input>;
20
21
  export type StyledContentProps = {
@@ -42,6 +43,8 @@ export type DropDownMenuProps = {
42
43
  onSearch?: (value: string) => void;
43
44
  searchable?: boolean;
44
45
  manualSearch?: boolean;
46
+ grouped?: boolean;
47
+ onAddNew?: (value: string) => void;
45
48
  loading?: boolean;
46
49
  arrow?: boolean;
47
50
  dropDownProps?: ComponentPropsWithoutRef<typeof StyledContent>;
@@ -381,7 +381,7 @@ triggerFocus = false, triggerOpen = false, onFocused, onOpened, }) => {
381
381
  // ============================================================================
382
382
  // Render
383
383
  // ============================================================================
384
- return (_jsxs(StyledContainer, { className: className, children: [label && (_jsx(FieldLabel, { error: error, asterisk: required, size: size, description: description, children: label })), _jsxs(StyledInputContainer, { ref: refs.setReference, onMouseDown: () => setIsOpen(true), width: width, onKeyDown: handleKeyDown, "data-open": isOpen, "data-disabled": disabled, tabIndex: -1, children: [_jsx(Input, { ref: inputRef, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, autoFocus: focused, disabled: disabled, tabIndex: disabled ? -1 : 0, placeholder: placeholder, size: size, readOnly: !searchable && !allowCustomValue, "data-button-right": arrow || clearable, style: isOpen ? { borderColor: theme.palette.primary.main } : {} }), renderActionButton()] }), isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, { ref: (ref) => {
384
+ return (_jsxs(StyledContainer, { className: className, children: [label && (_jsx(FieldLabel, { error: error, asterisk: required, size: size, description: description, children: label })), _jsxs(StyledInputContainer, { ref: refs.setReference, onMouseDown: () => !disabled && setIsOpen(true), width: width, onKeyDown: handleKeyDown, "data-open": isOpen, "data-disabled": disabled, tabIndex: -1, children: [_jsx(Input, { ref: inputRef, value: inputValue, onChange: handleInputChange, onFocus: handleFocus, autoFocus: focused, disabled: disabled, tabIndex: disabled ? -1 : 0, placeholder: placeholder, size: size, readOnly: !searchable && !allowCustomValue, "data-button-right": arrow || clearable, style: isOpen ? { borderColor: theme.palette.primary.main } : {} }), renderActionButton()] }), isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, { ref: (ref) => {
385
385
  containerRef.current = ref;
386
386
  refs.setFloating(ref);
387
387
  }, style: floatingStyles, className: "mfFloating", children: _jsxs(StyledContent, Object.assign({ className: "mfFloatingContent", style: {
@@ -34,7 +34,7 @@ export const StyledInputContainer = styled.div `
34
34
 
35
35
  &[data-disabled="true"] {
36
36
  opacity: 0.5;
37
- pointer-events: none;
37
+ cursor: not-allowed;
38
38
 
39
39
  > * {
40
40
  pointer-events: none;
@@ -30,7 +30,7 @@ const FlexedRow = styled.div `
30
30
  `;
31
31
  const TableMenu = () => {
32
32
  var _a, _b, _c, _d, _e;
33
- const { data, columnState, searchState, toggleColumnVisibility, tableMenuOptions, runSearch, enableSelection, compactState, toggleCompact, totalRecords, getCalculatedSelectionTotal, filterState, handleFilterChange, clearSearch, } = useTable();
33
+ const { data, columnState, searchState, toggleColumnVisibility, tableMenuOptions, runSearch, enableSelection, compactState, toggleCompact, totalRecords, getCalculatedSelectionTotal, handleTableExport, filterState, handleFilterChange, clearSearch, } = useTable();
34
34
  const inputRef = useRef(null);
35
35
  if ((tableMenuOptions === null || tableMenuOptions === void 0 ? void 0 : tableMenuOptions.enabled) !== true)
36
36
  return null;
@@ -111,10 +111,7 @@ const TableMenu = () => {
111
111
  title: "Export List to XLSX",
112
112
  size: "xxs",
113
113
  style: { padding: "0px 4px" },
114
- }, onItemSelect: (item) => {
115
- var _a, _b;
116
- return (_b = (_a = tableMenuOptions === null || tableMenuOptions === void 0 ? void 0 : tableMenuOptions.exportOptions) === null || _a === void 0 ? void 0 : _a.onExport) === null || _b === void 0 ? void 0 : _b.call(_a, item.value);
117
- }, dropDownProps: {
114
+ }, onItemSelect: (item) => void handleTableExport(item.value), dropDownProps: {
118
115
  style: { width: 175, maxWidth: 400 },
119
116
  }, children: _jsx(DownloadIcon, { size: 14 }) })), (compactOptions === null || compactOptions === void 0 ? void 0 : compactOptions.enabled) === true && (_jsx(Button, { variant: "outlined", title: "Toggle Compact", size: "xxs", style: { padding: "0px 4px" }, onClick: () => toggleCompact(), children: compactState ? _jsx(Rows4Icon, { size: 14 }) : _jsx(Rows3Icon, { size: 14 }) })), (columnSelectorOptions === null || columnSelectorOptions === void 0 ? void 0 : columnSelectorOptions.enabled) === true && (_jsx(DropDownMenu, { variant: "outlined", size: "xs", data: columnState.map((col) => ({
120
117
  label: (col === null || col === void 0 ? void 0 : col.caption) || col.dataField,
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  var __rest = (this && this.__rest) || function (s, e) {
2
11
  var t = {};
3
12
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
@@ -18,6 +27,7 @@ import shortUUID from "short-uuid";
18
27
  import { SelectionStatus } from "./enums";
19
28
  import moment from "moment";
20
29
  import { useControlled } from "../Utilities";
30
+ import { exportTableToExcel } from "./Utils";
21
31
  const calculateSelectionTotal = (selectionState, totalRecords, dataLength = 0) => {
22
32
  if (!selectionState) {
23
33
  return 0;
@@ -37,7 +47,7 @@ const calculateSelectionTotal = (selectionState, totalRecords, dataLength = 0) =
37
47
  };
38
48
  export const TableContext = createContext(null);
39
49
  const TableProvider = (_a) => {
40
- var { children, columns, data, keyField, tableInstanceRef, stateStorage, tableMenuOptions, manualSorting, manualFiltering, manualSearch, height, maxHeight, minHeight, focusedRowId, enableColumnResize, enableSorting, compact, totalRecords, onColumnStateChange, onColumnReorder, onColumnHeaderClick, onSort, onRowUpdated, tableElement, headerRowElm, tableDimensions, targetElm, listElm, defaultSelectionState, selectionState, onSelectionChange, defaultFilterState, filterState, onFilterChange } = _a, props = __rest(_a, ["children", "columns", "data", "keyField", "tableInstanceRef", "stateStorage", "tableMenuOptions", "manualSorting", "manualFiltering", "manualSearch", "height", "maxHeight", "minHeight", "focusedRowId", "enableColumnResize", "enableSorting", "compact", "totalRecords", "onColumnStateChange", "onColumnReorder", "onColumnHeaderClick", "onSort", "onRowUpdated", "tableElement", "headerRowElm", "tableDimensions", "targetElm", "listElm", "defaultSelectionState", "selectionState", "onSelectionChange", "defaultFilterState", "filterState", "onFilterChange"]);
50
+ var { children, columns, data, keyField, tableInstanceRef, stateStorage, tableMenuOptions, manualSorting, manualFiltering, manualSearch, manualExport, height, maxHeight, minHeight, focusedRowId, enableColumnResize, enableSorting, compact, totalRecords, onColumnStateChange, onColumnReorder, onColumnHeaderClick, onSort, onRowUpdated, tableElement, headerRowElm, tableDimensions, targetElm, listElm, defaultSelectionState, selectionState, onSelectionChange, defaultFilterState, filterState, onFilterChange } = _a, props = __rest(_a, ["children", "columns", "data", "keyField", "tableInstanceRef", "stateStorage", "tableMenuOptions", "manualSorting", "manualFiltering", "manualSearch", "manualExport", "height", "maxHeight", "minHeight", "focusedRowId", "enableColumnResize", "enableSorting", "compact", "totalRecords", "onColumnStateChange", "onColumnReorder", "onColumnHeaderClick", "onSort", "onRowUpdated", "tableElement", "headerRowElm", "tableDimensions", "targetElm", "listElm", "defaultSelectionState", "selectionState", "onSelectionChange", "defaultFilterState", "filterState", "onFilterChange"]);
41
51
  const _columns = useMemo(() => columns
42
52
  .map((child, index) => {
43
53
  if (child.dataField === "__key") {
@@ -192,6 +202,18 @@ const TableProvider = (_a) => {
192
202
  const clearSearch = () => {
193
203
  updateSearchState("");
194
204
  };
205
+ const handleTableExport = (exportType) => __awaiter(void 0, void 0, void 0, function* () {
206
+ var _a, _b;
207
+ if (manualExport === true) {
208
+ (_b = (_a = tableMenuOptions === null || tableMenuOptions === void 0 ? void 0 : tableMenuOptions.exportOptions) === null || _a === void 0 ? void 0 : _a.onExport) === null || _b === void 0 ? void 0 : _b.call(_a, exportType);
209
+ return;
210
+ }
211
+ yield exportTableToExcel({
212
+ columns: columnState,
213
+ data: _data,
214
+ exportMode: exportType,
215
+ });
216
+ });
195
217
  const updateSortState = (sortState) => {
196
218
  var _a;
197
219
  setSortState(sortState);
@@ -209,20 +231,36 @@ const TableProvider = (_a) => {
209
231
  // sort data
210
232
  return data.sort((a, b) => {
211
233
  if (sortState) {
234
+ const aValue = a[sortState.dataField];
235
+ const bValue = b[sortState.dataField];
236
+ const aIsEmpty = aValue === null || aValue === undefined;
237
+ const bIsEmpty = bValue === null || bValue === undefined;
238
+ // Treat empty values as the smallest values.
239
+ if (aIsEmpty && bIsEmpty) {
240
+ return 0;
241
+ }
242
+ if (aIsEmpty || bIsEmpty) {
243
+ if (sortState.dir === "asc") {
244
+ return aIsEmpty ? -1 : 1;
245
+ }
246
+ if (sortState.dir === "desc") {
247
+ return aIsEmpty ? 1 : -1;
248
+ }
249
+ }
212
250
  if (sortState.dir === "asc") {
213
- if (a[sortState.dataField] < b[sortState.dataField]) {
251
+ if (aValue < bValue) {
214
252
  return -1;
215
253
  }
216
- if (a[sortState.dataField] > b[sortState.dataField]) {
254
+ if (aValue > bValue) {
217
255
  return 1;
218
256
  }
219
257
  return 0;
220
258
  }
221
259
  else if (sortState.dir === "desc") {
222
- if (a[sortState.dataField] > b[sortState.dataField]) {
260
+ if (aValue > bValue) {
223
261
  return -1;
224
262
  }
225
- if (a[sortState.dataField] < b[sortState.dataField]) {
263
+ if (aValue < bValue) {
226
264
  return 1;
227
265
  }
228
266
  return 0;
@@ -583,6 +621,7 @@ const TableProvider = (_a) => {
583
621
  toggleColumnVisibility,
584
622
  runSearch,
585
623
  clearSearch,
624
+ handleTableExport,
586
625
  handleFilterChange, filterState: _filterState, compactState,
587
626
  toggleCompact,
588
627
  getCalculatedSelectionTotal,
@@ -1,3 +1,4 @@
1
1
  export { default as resolveColumnResize } from "./resolveColumnResize";
2
2
  export { default as syncColumnState } from "./syncColumnState";
3
3
  export { default as resizeHandler } from "./resizeHandler";
4
+ export { exportTableToExcel } from "./tableExport";
@@ -1,3 +1,4 @@
1
1
  export { default as resolveColumnResize } from "./resolveColumnResize";
2
2
  export { default as syncColumnState } from "./syncColumnState";
3
3
  export { default as resizeHandler } from "./resizeHandler";
4
+ export { exportTableToExcel } from "./tableExport";
@@ -0,0 +1,9 @@
1
+ import { TableExportOptions } from "../enums";
2
+ import { ColumnState } from "../types";
3
+ type TableExportParams = {
4
+ columns: ColumnState[];
5
+ data: any[];
6
+ exportMode: TableExportOptions;
7
+ };
8
+ export declare const exportTableToExcel: ({ columns, data, exportMode, }: TableExportParams) => Promise<void>;
9
+ export {};
@@ -0,0 +1,102 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { isValidElement } from "react";
11
+ import { TableExportOptions } from "../enums";
12
+ const getColumnHeader = (column) => {
13
+ return column.caption || column.dataField;
14
+ };
15
+ const toExportablePrimitive = (value) => {
16
+ if (value === undefined || value === null)
17
+ return null;
18
+ if (typeof value === "string" ||
19
+ typeof value === "number" ||
20
+ typeof value === "boolean" ||
21
+ value instanceof Date) {
22
+ return value;
23
+ }
24
+ if (value instanceof RegExp) {
25
+ return value.toString();
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map((item) => toExportablePrimitive(item)).join(", ");
29
+ }
30
+ if (typeof value === "object") {
31
+ try {
32
+ return JSON.stringify(value);
33
+ }
34
+ catch (_a) {
35
+ return String(value);
36
+ }
37
+ }
38
+ return String(value);
39
+ };
40
+ const extractTextFromNode = (node) => {
41
+ if (node === null || node === undefined || typeof node === "boolean") {
42
+ return "";
43
+ }
44
+ if (typeof node === "string" || typeof node === "number") {
45
+ return String(node);
46
+ }
47
+ if (Array.isArray(node)) {
48
+ return node.map((child) => extractTextFromNode(child)).join("");
49
+ }
50
+ if (isValidElement(node)) {
51
+ return extractTextFromNode(node.props.children);
52
+ }
53
+ return "";
54
+ };
55
+ const getExportCellValue = (row, column) => {
56
+ const rawValue = row[column.dataField];
57
+ if (!column.render) {
58
+ return toExportablePrimitive(rawValue);
59
+ }
60
+ const renderedText = extractTextFromNode(column.render({ rowData: row }));
61
+ if (renderedText.trim().length > 0) {
62
+ return renderedText;
63
+ }
64
+ return toExportablePrimitive(rawValue);
65
+ };
66
+ const getColumnsForExport = (columns, exportMode) => {
67
+ return columns
68
+ .filter((column) => {
69
+ if (exportMode === TableExportOptions.ExportVisible) {
70
+ return column.visible !== false;
71
+ }
72
+ return true;
73
+ })
74
+ .sort((a, b) => a.orderValue - b.orderValue);
75
+ };
76
+ const getFileName = () => {
77
+ const timestamp = new Date().toISOString().replaceAll(":", "-");
78
+ return `table-export-${timestamp}.xlsx`;
79
+ };
80
+ const downloadBuffer = (buffer) => {
81
+ const blob = new Blob([buffer], {
82
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
83
+ });
84
+ const url = window.URL.createObjectURL(blob);
85
+ const anchor = document.createElement("a");
86
+ anchor.href = url;
87
+ anchor.download = getFileName();
88
+ anchor.click();
89
+ window.URL.revokeObjectURL(url);
90
+ };
91
+ export const exportTableToExcel = (_a) => __awaiter(void 0, [_a], void 0, function* ({ columns, data, exportMode, }) {
92
+ const { default: ExcelJS } = yield import("exceljs");
93
+ const exportColumns = getColumnsForExport(columns, exportMode);
94
+ const workbook = new ExcelJS.Workbook();
95
+ const worksheet = workbook.addWorksheet("Table Export");
96
+ worksheet.addRow(exportColumns.map((column) => getColumnHeader(column)));
97
+ data.forEach((row) => {
98
+ worksheet.addRow(exportColumns.map((column) => getExportCellValue(row, column)));
99
+ });
100
+ const buffer = yield workbook.xlsx.writeBuffer();
101
+ downloadBuffer(buffer);
102
+ });
@@ -134,6 +134,7 @@ export type TableContextType = {
134
134
  toggleColumnVisibility: (dataField: string) => void;
135
135
  runSearch: (query: string) => void;
136
136
  clearSearch: () => void;
137
+ handleTableExport: (e: TableExportOptions) => Promise<void> | void;
137
138
  handleFilterChange: (query: Query) => void;
138
139
  selectAllRows: () => void;
139
140
  clearSelections: () => void;
@@ -1,9 +1,17 @@
1
1
  import React, { ReactNode } from "react";
2
2
  import { Size } from "../core";
3
- import { DropDownItem } from "..";
4
- export type InsertableItem = {
5
- label: string;
6
- value: string;
3
+ import { DropDownMenuProps } from "../DropDownMenu/types";
4
+ export type ActionMenuOptions = {
5
+ /**
6
+ * Visual style of the action menu trigger.
7
+ * - `"ellipsis"` renders a minimal `...` icon button.
8
+ * - `"dropdown"` renders an outlined button with a label and arrow (default).
9
+ */
10
+ variant?: "ellipsis" | "dropdown";
11
+ /** Label for the trigger button. Only used when `variant` is `"dropdown"`. Defaults to `"Insert"`. */
12
+ label?: ReactNode;
13
+ /** Props forwarded to the underlying DropDownMenu component. `data` carries the menu items. */
14
+ menuOptions: DropDownMenuProps;
7
15
  };
8
16
  export interface TextAreaInputProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
9
17
  variant?: "contained" | "filled" | "outlined" | "text";
@@ -19,14 +27,7 @@ export interface TextAreaInputProps extends React.TextareaHTMLAttributes<HTMLTex
19
27
  onHeightChange?: (height: number, meta: {
20
28
  rowHeight: number;
21
29
  }) => void;
22
- showActionMenu?: boolean;
23
- actionMenuOptions?: Array<{
24
- value: string;
25
- label: string;
26
- }>;
27
- onActionMenuSelect?: (item: DropDownItem) => void;
28
- insertableItems?: InsertableItem[];
29
- onInsertItem?: (item: InsertableItem) => void;
30
+ actionMenuOptions?: ActionMenuOptions;
30
31
  }
31
32
  declare const TextAreaInput: React.ForwardRefExoticComponent<TextAreaInputProps & React.RefAttributes<HTMLTextAreaElement>>;
32
33
  export default TextAreaInput;
@@ -11,34 +11,9 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import styled from "styled-components";
14
- import { forwardRef, useState, useRef, useEffect } from "react";
15
- import { TextArea, FieldLabel, DropDownMenu, SelectBox, } from "..";
14
+ import { forwardRef, useRef } from "react";
15
+ import { TextArea, FieldLabel, DropDownMenu } from "..";
16
16
  import { MoreHorizontal } from "lucide-react";
17
- const DEFAULT_ACTIONS = [
18
- { value: "clear", label: "Clear Text" },
19
- { value: "insert", label: "Insert Item" },
20
- ];
21
- const TextAreaWrapper = styled.div `
22
- position: relative;
23
- `;
24
- const InsertMenuOverlay = styled.div `
25
- position: absolute;
26
- top: 0;
27
- left: 0;
28
- right: 0;
29
- z-index: 10;
30
- opacity: ${({ $visible }) => ($visible ? 1 : 0)};
31
- pointer-events: ${({ $visible }) => ($visible ? "auto" : "none")};
32
- transform: ${({ $visible }) => $visible ? "translateY(0)" : "translateY(-4px)"};
33
- transition:
34
- opacity 0.2s ease,
35
- transform 0.2s ease;
36
- `;
37
- const StyledInsertSelectBox = styled(SelectBox) `
38
- width: 100%;
39
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
40
- border: 1px solid ${({ theme }) => theme.palette.primary.main};
41
- `;
42
17
  const StyledMoreHorizontal = styled(MoreHorizontal) `
43
18
  color: ${({ theme }) => theme.palette.text.secondary};
44
19
  &:hover {
@@ -46,21 +21,10 @@ const StyledMoreHorizontal = styled(MoreHorizontal) `
46
21
  }
47
22
  `;
48
23
  const TextAreaInput = forwardRef((props, ref) => {
49
- const {
50
- // UI
51
- label, error, required, colSpan = 1, size = "sm", description, maxRows = 6, minRows = 3, onHeightChange, cacheMeasurements,
52
- // Action menu
53
- showActionMenu = false, actionMenuOptions = DEFAULT_ACTIONS, onActionMenuSelect,
54
- // Insertable items
55
- insertableItems, onInsertItem } = props,
56
- // Rest of props for TextArea
57
- rest = __rest(props, ["label", "error", "required", "colSpan", "size", "description", "maxRows", "minRows", "onHeightChange", "cacheMeasurements", "showActionMenu", "actionMenuOptions", "onActionMenuSelect", "insertableItems", "onInsertItem"]);
58
- // State for insert menu visibility
59
- const [showInsertMenu, setShowInsertMenu] = useState(false);
60
- const [triggerSelectBoxOpen, setTriggerSelectBoxOpen] = useState(false);
24
+ const { label, error, required, colSpan = 1, size = "sm", description, maxRows = 6, minRows = 3, onHeightChange, cacheMeasurements, actionMenuOptions } = props, rest = __rest(props, ["label", "error", "required", "colSpan", "size", "description", "maxRows", "minRows", "onHeightChange", "cacheMeasurements", "actionMenuOptions"]);
25
+ const { variant: menuVariant = "dropdown", label: menuLabel = "Insert", menuOptions = { data: [] }, } = actionMenuOptions !== null && actionMenuOptions !== void 0 ? actionMenuOptions : {};
26
+ const { data: menuData = [], onItemSelect: consumerOnItemSelect, onAddNew: consumerOnAddNew } = menuOptions, restMenuOptions = __rest(menuOptions, ["data", "onItemSelect", "onAddNew"]);
61
27
  const textareaRef = useRef(null);
62
- const insertMenuRef = useRef(null);
63
- // Merge refs
64
28
  const mergedRef = (node) => {
65
29
  textareaRef.current = node;
66
30
  if (typeof ref === "function") {
@@ -70,79 +34,40 @@ const TextAreaInput = forwardRef((props, ref) => {
70
34
  ref.current = node;
71
35
  }
72
36
  };
73
- // Handle click outside to close insert menu
74
- useEffect(() => {
75
- if (!showInsertMenu)
37
+ const insertAtCursor = (text) => {
38
+ var _a;
39
+ const textarea = textareaRef.current;
40
+ if (!textarea)
76
41
  return;
77
- const handleClickOutside = (event) => {
78
- if (insertMenuRef.current &&
79
- !insertMenuRef.current.contains(event.target)) {
80
- setShowInsertMenu(false);
81
- }
82
- };
83
- document.addEventListener("mousedown", handleClickOutside);
84
- return () => {
85
- document.removeEventListener("mousedown", handleClickOutside);
86
- };
87
- }, [showInsertMenu]);
88
- const handleActionSelect = (item) => {
89
- if (item.value === "insert" && (insertableItems === null || insertableItems === void 0 ? void 0 : insertableItems.length)) {
90
- setShowInsertMenu(true);
91
- // Trigger SelectBox to open using the new enhanced props
92
- setTriggerSelectBoxOpen(true);
93
- }
94
- else if (item.value === "clear") {
95
- // Built-in clear functionality
96
- const textarea = textareaRef.current;
97
- if (textarea) {
98
- textarea.value = "";
99
- textarea.focus();
100
- // Trigger change event so controlled components update
101
- const event = new Event("input", { bubbles: true });
102
- textarea.dispatchEvent(event);
103
- }
104
- onActionMenuSelect === null || onActionMenuSelect === void 0 ? void 0 : onActionMenuSelect(item);
42
+ const start = textarea.selectionStart;
43
+ const end = textarea.selectionEnd;
44
+ const currentValue = textarea.value;
45
+ const newValue = currentValue.slice(0, start) + text + currentValue.slice(end);
46
+ const nativeSet = (_a = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")) === null || _a === void 0 ? void 0 : _a.set;
47
+ nativeSet === null || nativeSet === void 0 ? void 0 : nativeSet.call(textarea, newValue);
48
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
49
+ textarea.focus();
50
+ textarea.setSelectionRange(start + text.length, start + text.length);
51
+ };
52
+ const handleItemSelect = (item) => {
53
+ if (consumerOnItemSelect) {
54
+ consumerOnItemSelect(item);
105
55
  }
106
56
  else {
107
- onActionMenuSelect === null || onActionMenuSelect === void 0 ? void 0 : onActionMenuSelect(item);
57
+ insertAtCursor(String(item.value));
108
58
  }
109
59
  };
110
- const handleSelectBoxOpened = () => {
111
- // Reset the trigger after SelectBox has opened
112
- setTriggerSelectBoxOpen(false);
113
- };
114
- const handleInsertSelect = (value, option) => {
115
- console.log("Selected value:", value, "Selected option:", option); // Debug log
116
- // SelectBox passes (value, option) - we want the full option object
117
- const item = option;
118
- if (!item || !item.value) {
119
- console.warn("Invalid item selected:", item);
120
- setShowInsertMenu(false);
121
- return;
122
- }
123
- if (onInsertItem) {
124
- onInsertItem(item);
60
+ const handleAddNew = (value) => {
61
+ if (consumerOnAddNew) {
62
+ consumerOnAddNew(value);
125
63
  }
126
64
  else {
127
- // Default behavior: insert at current cursor position
128
- const textarea = textareaRef.current;
129
- if (textarea) {
130
- const start = textarea.selectionStart;
131
- const end = textarea.selectionEnd;
132
- const currentValue = textarea.value;
133
- const newValue = currentValue.slice(0, start) + item.value + currentValue.slice(end);
134
- textarea.value = newValue;
135
- textarea.focus();
136
- textarea.setSelectionRange(start + item.value.length, start + item.value.length);
137
- // Trigger change event
138
- const event = new Event("input", { bubbles: true });
139
- textarea.dispatchEvent(event);
140
- }
65
+ insertAtCursor(value);
141
66
  }
142
- setShowInsertMenu(false);
143
67
  };
144
- return (_jsxs("div", { style: { gridColumn: `span ${colSpan}`, height: "fit-content" }, children: [label && (_jsx(FieldLabel, { asterisk: required, error: error, description: description, size: size, actionComponent: showActionMenu ? (_jsx(DropDownMenu, { data: actionMenuOptions, variant: "text", size: "xs", arrow: false, onItemSelect: handleActionSelect, buttonProps: {
145
- "aria-label": "Open actions",
68
+ const hasMenuItems = menuData.length > 0;
69
+ return (_jsxs("div", { style: { gridColumn: `span ${colSpan}`, height: "fit-content" }, children: [label && (_jsx(FieldLabel, { asterisk: required, error: error, description: description, size: size, actionComponent: hasMenuItems ? (menuVariant === "ellipsis" ? (_jsx(DropDownMenu, Object.assign({ data: menuData, variant: "text", size: "xs", arrow: false, searchable: true, onAddNew: handleAddNew, onItemSelect: handleItemSelect, buttonProps: {
70
+ "aria-label": "Insert item",
146
71
  style: {
147
72
  minWidth: "auto",
148
73
  border: "none",
@@ -152,7 +77,14 @@ const TextAreaInput = forwardRef((props, ref) => {
152
77
  height: 16,
153
78
  width: 16,
154
79
  },
155
- }, children: _jsx(StyledMoreHorizontal, { size: 16 }) })) : null, children: label })), _jsxs(TextAreaWrapper, { children: [_jsx(TextArea, Object.assign({ ref: mergedRef, size: size, maxRows: maxRows, minRows: minRows, onHeightChange: onHeightChange, cacheMeasurements: cacheMeasurements }, rest)), showInsertMenu && (insertableItems === null || insertableItems === void 0 ? void 0 : insertableItems.length) && (_jsx(InsertMenuOverlay, { ref: insertMenuRef, "$visible": showInsertMenu, children: _jsx(StyledInsertSelectBox, { data: insertableItems, placeholder: "Select item to insert...", searchable: true, clearable: false, arrow: false, focused: showInsertMenu, openOnFocus: true, triggerOpen: triggerSelectBoxOpen, onOpened: handleSelectBoxOpened, onChange: handleInsertSelect, size: size }) }))] })] }));
80
+ }, dropDownProps: {
81
+ style: { width: 200, maxWidth: 400 },
82
+ } }, restMenuOptions, { children: _jsx(StyledMoreHorizontal, { size: 16 }) }))) : (_jsx(DropDownMenu, Object.assign({ data: menuData, variant: "outlined", size: "xs", arrow: true, searchable: true, onAddNew: handleAddNew, onItemSelect: handleItemSelect, buttonProps: {
83
+ title: typeof menuLabel === "string" ? menuLabel : undefined,
84
+ size: "xxs",
85
+ }, dropDownProps: {
86
+ style: { width: 200, maxWidth: 400 },
87
+ } }, restMenuOptions, { children: menuLabel })))) : null, children: label })), _jsx(TextArea, Object.assign({ ref: mergedRef, size: size, maxRows: maxRows, minRows: minRows, onHeightChange: onHeightChange, cacheMeasurements: cacheMeasurements }, rest))] }));
156
88
  });
157
89
  TextAreaInput.displayName = "TextAreaInput";
158
90
  export default TextAreaInput;
@@ -1 +1,2 @@
1
1
  export { default } from "./TextAreaInput";
2
+ export type { ActionMenuOptions, TextAreaInputProps } from "./TextAreaInput";
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { default as IconButton } from "./IconButton";
16
16
  export { default as DateInput } from "./DateInput";
17
17
  export { default as TextArea } from "./TextArea";
18
18
  export { default as TextAreaInput } from "./TextAreaInput";
19
+ export type { ActionMenuOptions, TextAreaInputProps } from "./TextAreaInput";
19
20
  export { default as TagBox } from "./TagBox";
20
21
  export type { TagBoxProps, TagboxOption } from "./TagBox";
21
22
  export { default as FieldLabel } from "./FieldLabel";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monolith-forensics/monolith-ui",
3
- "version": "1.4.3",
3
+ "version": "1.5.1",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "author": "Matt Danner (Monolith Forensics LLC)",
@@ -59,6 +59,7 @@
59
59
  "@uiw/codemirror-theme-vscode": "^4.23.6",
60
60
  "@uiw/react-codemirror": "^4.23.6",
61
61
  "deepmerge": "^4.3.1",
62
+ "exceljs": "^4.4.0",
62
63
  "lucide-react": "^0.469.0",
63
64
  "moment": "^2.29.1",
64
65
  "overlayscrollbars": "^2.6.0",