@monolith-forensics/monolith-ui 2.1.1 → 2.1.2

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,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Menu, MenuItemList, StyledInnerItemContainer } from "./components";
4
4
  import { DEFAULT_DROPDOWN_MAX_HEIGHT } from "./constants";
@@ -10,10 +10,13 @@ const getNumericCssLength = (value) => {
10
10
  const pixelMatch = value.trim().match(/^(\d+(?:\.\d+)?)px$/);
11
11
  return pixelMatch ? Number(pixelMatch[1]) : undefined;
12
12
  };
13
- export const DropDownMenu = ({ data, children, defaultValue, value, variant, arrow, size, searchable, grouped, onAddNew, loading, onScroll, onScrollToTop, onScrollToBottom, onSearch, manualSearch, multiselect, enableSelectAll, enableSelectedOptionStyling, renderOption, dynamicOptionHeight, onItemSelect, onChange, buttonProps, TooltipContent, dropDownProps, query, disabled, }) => {
14
- var _a, _b, _c, _d, _e, _f;
15
- const isObjectArray = (_a = Object.keys((data === null || data === void 0 ? void 0 : data[0]) || {})) === null || _a === void 0 ? void 0 : _a.includes("label");
16
- const maxDropdownHeight = (_f = (_e = (_c = getNumericCssLength((_b = dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.style) === null || _b === void 0 ? void 0 : _b.height)) !== null && _c !== void 0 ? _c : getNumericCssLength((_d = dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.style) === null || _d === void 0 ? void 0 : _d.maxHeight)) !== null && _e !== void 0 ? _e : getNumericCssLength(dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.maxDropdownHeight)) !== null && _f !== void 0 ? _f : DEFAULT_DROPDOWN_MAX_HEIGHT;
13
+ export const DropDownMenu = ({ data, children, defaultValue, value, variant, arrow, size, searchable, grouped, onAddNew, loading, loadingMore, onScroll, onScrollToTop, onScrollToBottom, onSearch, searchDebounceMs, manualSearch, multiselect, enableSelectAll, enableSelectedOptionStyling, renderOption, dynamicOptionHeight, onItemSelect, onChange, buttonProps, TooltipContent, dropDownProps, disabled, }) => {
14
+ var _a, _b, _c, _d, _e;
15
+ const isObjectArray = [
16
+ ...(data || []),
17
+ ...(value || defaultValue || []),
18
+ ].some((item) => { var _a; return (_a = Object.keys(item || {})) === null || _a === void 0 ? void 0 : _a.includes("label"); });
19
+ const maxDropdownHeight = (_e = (_d = (_b = getNumericCssLength((_a = dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.style) === null || _a === void 0 ? void 0 : _a.height)) !== null && _b !== void 0 ? _b : getNumericCssLength((_c = dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.style) === null || _c === void 0 ? void 0 : _c.maxHeight)) !== null && _d !== void 0 ? _d : getNumericCssLength(dropDownProps === null || dropDownProps === void 0 ? void 0 : dropDownProps.maxDropdownHeight)) !== null && _e !== void 0 ? _e : DEFAULT_DROPDOWN_MAX_HEIGHT;
17
20
  const [internalSelected, setInternalSelected] = useState(defaultValue || []);
18
21
  const isControlled = value !== undefined;
19
22
  const selected = isControlled ? value || [] : internalSelected;
@@ -45,5 +48,5 @@ export const DropDownMenu = ({ data, children, defaultValue, value, variant, arr
45
48
  const handleScrollToBottom = (e) => {
46
49
  onScrollToBottom === null || onScrollToBottom === void 0 ? void 0 : onScrollToBottom(e);
47
50
  };
48
- 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, dynamicOptionHeight: dynamicOptionHeight, maxDropdownHeight: maxDropdownHeight, selected: selected, TooltipContent: TooltipContent, multiselect: multiselect, enableSelectAll: enableSelectAll, enableSelectedOptionStyling: enableSelectedOptionStyling, size: size, handleAddItem: handleAddItem, handleRemoveItem: handleRemoveItem, handleSetSelected: handleSetSelected, onItemSelect: onItemSelect, renderOption: renderOption, onScroll: onScroll, onScrollToTop: onScrollToTop, onScrollToBottom: handleScrollToBottom, query: query }))] }) }));
51
+ return (_jsx(Menu, { label: children, disabled: disabled, arrow: arrow, buttonSize: size, variant: variant, multiselect: multiselect, buttonProps: buttonProps, onMenuClose: handleMenuClose, dropDownProps: dropDownProps, children: _jsx(StyledInnerItemContainer, { children: _jsx(MenuItemList, { menuItems: data, searchable: searchable, grouped: grouped, onAddNew: onAddNew, onSearch: onSearch, searchDebounceMs: searchDebounceMs, manualSearch: manualSearch, dynamicOptionHeight: dynamicOptionHeight, maxDropdownHeight: maxDropdownHeight, selected: selected, TooltipContent: TooltipContent, multiselect: multiselect, enableSelectAll: enableSelectAll, enableSelectedOptionStyling: enableSelectedOptionStyling, size: size, loading: loading, loadingMore: loadingMore, handleAddItem: handleAddItem, handleRemoveItem: handleRemoveItem, handleSetSelected: handleSetSelected, onItemSelect: onItemSelect, renderOption: renderOption, onScroll: onScroll, onScrollToTop: onScrollToTop, onScrollToBottom: handleScrollToBottom }) }) }));
49
52
  };
@@ -26,7 +26,7 @@ export const MenuItem = styled(forwardRef((_a, forwardedRef) => {
26
26
  return (_jsx(Tooltip, { content: TooltipContent ? _jsx(TooltipContent, { data: itemData }) : null, side: "left", children: _jsx(Button, Object.assign({}, props, { ref: useMergeRefs([
27
27
  item.ref,
28
28
  forwardedRef,
29
- ]), type: "button", role: "menuitem", tabIndex: isActive ? 0 : -1, disabled: disabled, justify: "start", color: props.color, selected: false }, menu.getItemProps({
29
+ ]), type: "button", role: "menuitem", tabIndex: isActive ? 0 : -1, "data-multiselect": multiselect, disabled: disabled, justify: "start", color: props.color, selected: false }, menu.getItemProps({
30
30
  onClick(event) {
31
31
  var _a;
32
32
  (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props, event);
@@ -104,5 +104,9 @@ export const MenuItem = styled(forwardRef((_a, forwardedRef) => {
104
104
  white-space: ${({ $dynamicHeight }) => $dynamicHeight ? "normal" : "nowrap"};
105
105
  text-overflow: ${({ $dynamicHeight }) => $dynamicHeight ? "clip" : "ellipsis"};
106
106
  }
107
+
108
+ &[data-multiselect="true"] [data-position="left"] {
109
+ padding-inline-end: ${({ size = "sm" }) => `${getControlSizeTokens(size).iconGap}px`};
110
+ }
107
111
  }
108
112
  `;
@@ -1,11 +1,12 @@
1
1
  import { ComponentType } from "react";
2
2
  import { Size } from "../../core";
3
- import { DropDownItem, DropDownMenuProps } from "../types";
3
+ import { DropDownItem } from "../types";
4
4
  export declare const MenuItemList: React.FC<{
5
5
  menuItems: DropDownItem[];
6
6
  searchable?: boolean;
7
7
  searchValue?: string;
8
8
  onSearch?: (value: string) => void;
9
+ searchDebounceMs?: number;
9
10
  manualSearch?: boolean;
10
11
  dynamicOptionHeight?: boolean;
11
12
  selected?: DropDownItem[];
@@ -25,5 +26,6 @@ export declare const MenuItemList: React.FC<{
25
26
  onScrollToTop?: (e: Event) => void;
26
27
  onScrollToBottom?: (e: Event) => void;
27
28
  maxDropdownHeight?: number;
28
- query?: DropDownMenuProps["query"];
29
+ loading?: boolean;
30
+ loadingMore?: boolean;
29
31
  }>;
@@ -1,14 +1,3 @@
1
- var __rest = (this && this.__rest) || function (s, e) {
2
- var t = {};
3
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
- t[p] = s[p];
5
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
- t[p[i]] = s[p[i]];
9
- }
10
- return t;
11
- };
12
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
13
2
  import { useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
14
3
  import styled from "styled-components";
@@ -18,7 +7,6 @@ import CheckBox from "../../CheckBox";
18
7
  import { getControlSizeTokens } from "../../core";
19
8
  import { DropDownMenu } from "../DropDownMenu";
20
9
  import { MenuItem } from "./MenuItem";
21
- import { useInfiniteQuery } from "@tanstack/react-query";
22
10
  import { useDebouncedCallback } from "use-debounce";
23
11
  import { SearchInput } from "./SearchInput";
24
12
  import Loader from "../../Loader";
@@ -108,6 +96,17 @@ const GroupHeader = styled.div `
108
96
  min-width: fit-content;
109
97
  }
110
98
  `;
99
+ const LoadingMoreRow = styled.div `
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ gap: 8px;
104
+ color: ${(props) => props.theme.palette.text.secondary};
105
+ min-height: ${({ $size = "sm" }) => `${getControlSizeTokens($size).menuRowHeight}px`};
106
+ padding: ${({ $size = "sm" }) => `0px ${getControlSizeTokens($size).menuItemPaddingX}px`};
107
+ font-size: ${({ $size = "sm" }) => `${getControlSizeTokens($size).supportingFontSize}px`};
108
+ box-sizing: border-box;
109
+ `;
111
110
  const MeasuredRow = ({ children, index, setItemSize, style }) => {
112
111
  const rowRef = useRef(null);
113
112
  useLayoutEffect(() => {
@@ -129,32 +128,20 @@ const MeasuredRow = ({ children, index, setItemSize, style }) => {
129
128
  }, [children, index, setItemSize]);
130
129
  return (_jsx("div", { style: Object.assign(Object.assign({}, style), { overflow: "visible" }), children: _jsx("div", { ref: rowRef, children: children }) }));
131
130
  };
132
- export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dynamicOptionHeight, selected, TooltipContent, multiselect, enableSelectAll, enableSelectedOptionStyling, grouped, onAddNew, size, handleAddItem, handleRemoveItem, handleSetSelected, onItemSelect, renderOption, onScroll, onScrollToTop, onScrollToBottom, maxDropdownHeight = DEFAULT_DROPDOWN_MAX_HEIGHT, query, }) => {
131
+ export const MenuItemList = ({ menuItems, searchable, onSearch, searchDebounceMs = 150, manualSearch, dynamicOptionHeight, selected, TooltipContent, multiselect, enableSelectAll, enableSelectedOptionStyling, grouped, onAddNew, size, handleAddItem, handleRemoveItem, handleSetSelected, onItemSelect, renderOption, onScroll, onScrollToTop, onScrollToBottom, maxDropdownHeight = DEFAULT_DROPDOWN_MAX_HEIGHT, loading, loadingMore, }) => {
133
132
  const [searchValue, setSearchValue] = useState("");
134
- const _a = query !== null && query !== void 0 ? query : {}, { queryKey, queryFn, getNextPageParam, initialPageParam } = _a, rest = __rest(_a, ["queryKey", "queryFn", "getNextPageParam", "initialPageParam"]);
135
- const targetElm = useRef(null);
136
133
  const listElm = useRef(null);
137
134
  const fixedListRef = useRef(null);
138
135
  const variableListRef = useRef(null);
139
136
  const hasHandledInitialSelectedScroll = useRef(false);
140
137
  const itemSizeMap = useRef({});
141
- const [viewPortDimensions, setViewPortDimensions] = useState({
142
- width: 0,
143
- height: 0,
144
- });
145
138
  const [, setMeasurementRevision] = useState(0);
146
- const { data: infiniteQueryResult, isLoading: isLoadingInfiniteQuery, fetchNextPage, } = useInfiniteQuery(Object.assign({ queryKey: (queryKey === null || queryKey === void 0 ? void 0 : queryKey({ search: searchValue })) || [], queryFn: queryFn, getNextPageParam: (query === null || query === void 0 ? void 0 : query.getNextPageParam) || (() => undefined), initialPageParam: (query === null || query === void 0 ? void 0 : query.initialPageParam) || 1, enabled: !!query }, rest));
147
- const infinteOptionsData = infiniteQueryResult;
148
- const infiniteSelectOptions = infinteOptionsData || [];
149
- const _options = !!query ? infiniteSelectOptions : menuItems;
150
- const debouncedFetchNextPage = useDebouncedCallback(() => {
151
- if (!isLoading && !!query) {
152
- fetchNextPage();
153
- }
154
- }, 50);
155
- const handleSearch = useDebouncedCallback((e) => {
156
- setSearchValue(e.target.value);
157
- }, 150);
139
+ const handleSearch = useDebouncedCallback((value) => {
140
+ setSearchValue(value);
141
+ }, searchDebounceMs);
142
+ const handleExternalSearch = useDebouncedCallback((value) => {
143
+ onSearch === null || onSearch === void 0 ? void 0 : onSearch(value);
144
+ }, searchDebounceMs);
158
145
  const handleItemClick = (item, isSelected) => {
159
146
  var _a;
160
147
  if (multiselect) {
@@ -166,7 +153,7 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
166
153
  onItemSelect === null || onItemSelect === void 0 ? void 0 : onItemSelect(item);
167
154
  (_a = item === null || item === void 0 ? void 0 : item.onClick) === null || _a === void 0 ? void 0 : _a.call(item, item);
168
155
  };
169
- const visibleItems = _options.filter((item) => item.visible !== false);
156
+ const visibleItems = menuItems.filter((item) => item.visible !== false);
170
157
  const filteredItems = searchable
171
158
  ? filterMenuItems(visibleItems, searchValue)
172
159
  : visibleItems;
@@ -191,12 +178,23 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
191
178
  },
192
179
  ];
193
180
  }
181
+ if (loadingMore) {
182
+ items = [
183
+ ...items,
184
+ {
185
+ _isLoadingMore: true,
186
+ label: "Loading more...",
187
+ value: "__loading_more__",
188
+ },
189
+ ];
190
+ }
194
191
  return items;
195
192
  })();
196
193
  const selectedKeys = useMemo(() => new Set((selected || []).map(getItemKey)), [selected]);
197
194
  const isItemSelected = (item) => selectedKeys.has(getItemKey(item));
198
195
  const selectableItems = useMemo(() => displayItems.filter((item) => !item._isGroupHeader &&
199
196
  !item._isAddNew &&
197
+ !item._isLoadingMore &&
200
198
  !item.disabled &&
201
199
  !item.items), [displayItems]);
202
200
  const selectableKeys = useMemo(() => new Set(selectableItems.map(getItemKey)), [selectableItems]);
@@ -205,12 +203,13 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
205
203
  selectedSelectableItems.length === selectableItems.length;
206
204
  const someSelectableItemsSelected = selectedSelectableItems.length > 0 && !allSelectableItemsSelected;
207
205
  const selectedDisplayIndex = displayItems.findIndex((item) => {
208
- if (item._isGroupHeader || item._isAddNew)
206
+ if (item._isGroupHeader || item._isAddNew || item._isLoadingMore) {
209
207
  return false;
208
+ }
210
209
  return isItemSelected(item);
211
210
  });
212
211
  const selectedDisplayKey = selected === null || selected === void 0 ? void 0 : selected.map(getItemKey).join("|");
213
- const isLoading = isLoadingInfiniteQuery;
212
+ const isLoading = loading;
214
213
  const handleOnScroll = useCallback((event) => {
215
214
  const scrollTolerance = 15;
216
215
  onScroll === null || onScroll === void 0 ? void 0 : onScroll(event);
@@ -220,9 +219,8 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
220
219
  }
221
220
  if (scrollHeight - scrollTop <= clientHeight + scrollTolerance) {
222
221
  onScrollToBottom === null || onScrollToBottom === void 0 ? void 0 : onScrollToBottom(event);
223
- debouncedFetchNextPage();
224
222
  }
225
- }, [debouncedFetchNextPage, onScroll, onScrollToBottom, onScrollToTop]);
223
+ }, [onScroll, onScrollToBottom, onScrollToTop]);
226
224
  const handleToggleSelectAll = () => {
227
225
  if (selectableItems.length === 0)
228
226
  return;
@@ -249,12 +247,12 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
249
247
  const estimatedDynamicItemHeight = itemHeight + 16;
250
248
  const itemCount = (displayItems === null || displayItems === void 0 ? void 0 : displayItems.length) || 0;
251
249
  const displayItemsKey = displayItems
252
- .map((item) => `${item.value}:${item.label}:${item._isGroupHeader ? "g" : "i"}`)
250
+ .map((item) => `${item.value}:${item.label}:${item._isGroupHeader ? "g" : item._isLoadingMore ? "l" : "i"}`)
253
251
  .join("|");
254
252
  const getDynamicItemSize = (index) => {
255
- var _a;
253
+ var _a, _b;
256
254
  return itemSizeMap.current[index] ||
257
- (((_a = displayItems[index]) === null || _a === void 0 ? void 0 : _a._isGroupHeader)
255
+ (((_a = displayItems[index]) === null || _a === void 0 ? void 0 : _a._isGroupHeader) || ((_b = displayItems[index]) === null || _b === void 0 ? void 0 : _b._isLoadingMore)
258
256
  ? itemHeight
259
257
  : estimatedDynamicItemHeight);
260
258
  };
@@ -271,32 +269,8 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
271
269
  DROPDOWN_CONTENT_VERTICAL_PADDING -
272
270
  searchHeight -
273
271
  selectAllHeight);
274
- const availableHeight = viewPortDimensions.height || maxListHeight;
275
- const height = Math.min(estimatedHeight, availableHeight, maxListHeight);
272
+ const height = Math.min(estimatedHeight, maxListHeight);
276
273
  const width = "100%";
277
- useLayoutEffect(() => {
278
- const target = targetElm.current;
279
- if (!target)
280
- return;
281
- const updateDimensions = () => {
282
- const nextDimensions = target.getBoundingClientRect();
283
- setViewPortDimensions((currentDimensions) => {
284
- const width = Math.ceil(nextDimensions.width);
285
- const height = Math.floor(nextDimensions.height);
286
- if (currentDimensions.width === width &&
287
- currentDimensions.height === height) {
288
- return currentDimensions;
289
- }
290
- return { width, height };
291
- });
292
- };
293
- updateDimensions();
294
- if (typeof ResizeObserver === "undefined")
295
- return;
296
- const observer = new ResizeObserver(updateDimensions);
297
- observer.observe(target);
298
- return () => observer.disconnect();
299
- }, [displayItemsKey, isLoading]);
300
274
  const setItemSize = useCallback((index, measuredSize) => {
301
275
  var _a;
302
276
  if (itemSizeMap.current[index] === measuredSize)
@@ -342,6 +316,9 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
342
316
  if (item._isGroupHeader) {
343
317
  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));
344
318
  }
319
+ if (item._isLoadingMore) {
320
+ return (_jsxs(LoadingMoreRow, { "$size": size, style: style, children: [_jsx(Loader, { size: 14 }), _jsx("span", { children: "Loading more..." })] }, "__loading_more__"));
321
+ }
345
322
  if (item._isAddNew) {
346
323
  return (_jsx(MenuItem, { className: "MenuItem", size: size, leftSection: _jsx(Plus, { size: 14 }), onClick: (e) => {
347
324
  e.preventDefault();
@@ -366,11 +343,12 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
366
343
  (typeof item === "string" || typeof item === "number" ? item : null) }, index));
367
344
  };
368
345
  return (_jsxs(MenuItemListRoot, { children: [searchable && (_jsx(SearchInput, { variant: "outlined", size: size, placeholder: "Search", defaultValue: searchValue, onChange: (e) => {
346
+ const nextValue = e.target.value;
369
347
  if (!manualSearch) {
370
- handleSearch(e);
348
+ handleSearch(nextValue);
371
349
  }
372
- onSearch === null || onSearch === void 0 ? void 0 : onSearch(e.target.value);
373
- } })), multiselect && enableSelectAll && selectableItems.length > 0 && (_jsx(MenuItem, { className: "MenuItem", size: size, leftSection: _jsx(CheckBox, { value: allSelectableItemsSelected, partialCheck: someSelectableItemsSelected, size: size, onChange: handleToggleSelectAll }), onClick: (e) => {
350
+ handleExternalSearch(nextValue);
351
+ } })), multiselect && enableSelectAll && selectableItems.length > 0 && (_jsx(MenuItem, { className: "MenuItem", size: size, multiselect: true, leftSection: _jsx(CheckBox, { value: allSelectableItemsSelected, partialCheck: someSelectableItemsSelected, size: size, onChange: handleToggleSelectAll }), onClick: (e) => {
374
352
  e.preventDefault();
375
353
  e.stopPropagation();
376
354
  handleToggleSelectAll();
@@ -382,7 +360,7 @@ export const MenuItemList = ({ menuItems, searchable, onSearch, manualSearch, dy
382
360
  alignItems: "center",
383
361
  height: "100%",
384
362
  padding: "10px 0",
385
- }, children: [_jsx("div", { style: { fontSize: `${sizeTokens.supportingFontSize}px` }, children: "Loading..." }), _jsx(Loader, {})] })), !isLoading && (_jsx(ListViewPort, { ref: targetElm, children: dynamicListEnabled ? (_jsx(VariableSizeList, { ref: variableListRef, itemData: displayItems, overscanCount: overscanCount, height: height, width: width, itemCount: itemCount, itemSize: getDynamicItemSize, outerElementType: ListScroller, outerRef: listElm, children: ({ data, index, style }) => {
363
+ }, children: [_jsx("div", { style: { fontSize: `${sizeTokens.supportingFontSize}px` }, children: "Loading..." }), _jsx(Loader, {})] })), !isLoading && (_jsx(ListViewPort, { children: dynamicListEnabled ? (_jsx(VariableSizeList, { ref: variableListRef, itemData: displayItems, overscanCount: overscanCount, height: height, width: width, itemCount: itemCount, itemSize: getDynamicItemSize, outerElementType: ListScroller, outerRef: listElm, children: ({ data, index, style }) => {
386
364
  const item = data === null || data === void 0 ? void 0 : data[index];
387
365
  if (!item)
388
366
  return null;
@@ -3,7 +3,6 @@ import { Size, Variant } from "../core";
3
3
  import Input from "../Input";
4
4
  import { ButtonProps } from "../Button";
5
5
  import { StyledContent } from "./components";
6
- import { InfiniteData, UseInfiniteQueryOptions } from "@tanstack/react-query";
7
6
  export type DropDownItem = {
8
7
  toLowerCase?: () => string;
9
8
  label: string;
@@ -44,25 +43,18 @@ export type DropDownMenuProps = {
44
43
  onScrollToBottom?: (e: Event) => void;
45
44
  onSearch?: (value: string) => void;
46
45
  searchable?: boolean;
46
+ searchDebounceMs?: number;
47
47
  manualSearch?: boolean;
48
48
  grouped?: boolean;
49
49
  dynamicOptionHeight?: boolean;
50
50
  onAddNew?: (value: string) => void;
51
51
  loading?: boolean;
52
+ loadingMore?: boolean;
52
53
  arrow?: boolean;
53
54
  dropDownProps?: ComponentPropsWithoutRef<typeof StyledContent>;
54
55
  buttonRender?: (props: any) => ReactElement;
55
56
  buttonProps?: ButtonProps;
56
57
  disabled?: boolean;
57
- query?: Omit<UseInfiniteQueryOptions, "select" | "queryKey" | "queryFn" | "getNextPageParam" | "initialPageParam"> & {
58
- select: (data: InfiniteData<unknown>) => DropDownItem[];
59
- queryKey: (args: {
60
- search?: string;
61
- }) => UseInfiniteQueryOptions["queryKey"];
62
- queryFn: UseInfiniteQueryOptions["queryFn"];
63
- getNextPageParam: UseInfiniteQueryOptions["getNextPageParam"];
64
- initialPageParam: UseInfiniteQueryOptions["initialPageParam"];
65
- };
66
58
  };
67
59
  export type MenuProps = {
68
60
  label?: any;
@@ -1,4 +1,4 @@
1
- import type { DropDownItem, DropDownMenuProps } from "../DropDownMenu";
1
+ import type { DropDownItem } from "../DropDownMenu";
2
2
  import type { SuperDatePickerProps } from "../SuperDatePicker";
3
3
  export interface UseQueryFilterProps {
4
4
  value?: Query | null;
@@ -28,6 +28,22 @@ export type Operator = {
28
28
  };
29
29
  export type InputType = "text" | "number" | "date" | "datetime" | "multiselect";
30
30
  export type BetweenEditor = "inputs" | "superDatePicker";
31
+ export type FilterOptionsQueryResult = {
32
+ items: DropDownItem[];
33
+ nextPageParam?: unknown;
34
+ };
35
+ export type FilterOptionsQuery = {
36
+ cacheKey: string | ((args: {
37
+ search?: string;
38
+ }) => string);
39
+ initialPageParam?: unknown;
40
+ staleTime?: number;
41
+ loadPage: (args: {
42
+ search?: string;
43
+ pageParam: unknown;
44
+ signal: AbortSignal;
45
+ }) => Promise<FilterOptionsQueryResult>;
46
+ };
31
47
  export interface FilterDefinition {
32
48
  dataField: string;
33
49
  label: string;
@@ -39,11 +55,12 @@ export interface FilterDefinition {
39
55
  isoString?: boolean;
40
56
  placeholder?: string;
41
57
  selectOptions?: DropDownItem[];
58
+ manualSearch?: boolean;
42
59
  superDatePickerProps?: Omit<SuperDatePickerProps, "defaultValue" | "format" | "onChange" | "size" | "value" | "variant" | "width">;
43
60
  dropDownOptions?: {
44
61
  style?: React.CSSProperties;
45
62
  };
46
- query?: DropDownMenuProps["query"];
63
+ query?: FilterOptionsQuery;
47
64
  }
48
65
  export interface Rule {
49
66
  id?: string;
@@ -1,17 +1,168 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
3
  import { useTheme } from "styled-components";
3
4
  import { DropDownMenu } from "../../DropDownMenu";
4
- import { QUERY_FILTER_SELECT_VALUE_LABEL, } from "../QueryFilter.constants";
5
+ import { QUERY_FILTER_SELECT_VALUE_LABEL } from "../QueryFilter.constants";
6
+ const DEFAULT_FILTER_OPTIONS_STALE_TIME = 5 * 60 * 1000;
7
+ const DEFAULT_INITIAL_PAGE_PARAM = 1;
8
+ const filterOptionsCache = new Map();
9
+ const filterOptionsRequestCache = new Map();
10
+ const getItemKey = (item) => item && typeof item === "object" && "value" in item ? item.value : item;
11
+ const mergeDropDownItems = (...itemLists) => {
12
+ const seen = new Set();
13
+ const merged = [];
14
+ for (const item of itemLists.flat()) {
15
+ const key = getItemKey(item);
16
+ if (seen.has(key))
17
+ continue;
18
+ seen.add(key);
19
+ merged.push(item);
20
+ }
21
+ return merged;
22
+ };
23
+ const getCacheKey = (query, search) => {
24
+ if (typeof query.cacheKey === "function") {
25
+ return query.cacheKey({ search });
26
+ }
27
+ return `${query.cacheKey}:${search || ""}`;
28
+ };
29
+ const hasNextPageParam = (pageParam) => pageParam !== undefined && pageParam !== null;
30
+ const getPageRequestKey = (cacheKey, pageParam) => {
31
+ try {
32
+ return `${cacheKey}:${JSON.stringify(pageParam)}`;
33
+ }
34
+ catch (_a) {
35
+ return `${cacheKey}:${String(pageParam)}`;
36
+ }
37
+ };
38
+ const loadFilterOptionsPage = (query, search, pageParam) => {
39
+ const cacheKey = getCacheKey(query, search);
40
+ const requestKey = getPageRequestKey(cacheKey, pageParam);
41
+ const pendingRequest = filterOptionsRequestCache.get(requestKey);
42
+ if (pendingRequest) {
43
+ return pendingRequest;
44
+ }
45
+ const abortController = new AbortController();
46
+ const request = query
47
+ .loadPage({
48
+ search,
49
+ pageParam,
50
+ signal: abortController.signal,
51
+ })
52
+ .finally(() => {
53
+ filterOptionsRequestCache.delete(requestKey);
54
+ });
55
+ filterOptionsRequestCache.set(requestKey, request);
56
+ return request;
57
+ };
58
+ const useFilterOptionsQuery = (query) => {
59
+ const [search, setSearch] = useState("");
60
+ const [items, setItems] = useState([]);
61
+ const [nextPageParam, setNextPageParam] = useState();
62
+ const [isLoading, setIsLoading] = useState(false);
63
+ const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
64
+ const initialRequestRef = useRef(0);
65
+ const nextPageRequestRef = useRef(0);
66
+ useEffect(() => {
67
+ var _a, _b;
68
+ if (!query) {
69
+ setItems([]);
70
+ setNextPageParam(undefined);
71
+ setIsLoading(false);
72
+ setIsFetchingNextPage(false);
73
+ return;
74
+ }
75
+ let isCurrent = true;
76
+ const requestId = initialRequestRef.current + 1;
77
+ initialRequestRef.current = requestId;
78
+ const cacheKey = getCacheKey(query, search);
79
+ const staleTime = (_a = query.staleTime) !== null && _a !== void 0 ? _a : DEFAULT_FILTER_OPTIONS_STALE_TIME;
80
+ const cached = filterOptionsCache.get(cacheKey);
81
+ if (cached && Date.now() - cached.timestamp < staleTime) {
82
+ setItems(cached.items);
83
+ setNextPageParam(cached.nextPageParam);
84
+ setIsLoading(false);
85
+ setIsFetchingNextPage(false);
86
+ return;
87
+ }
88
+ setIsLoading(true);
89
+ setIsFetchingNextPage(false);
90
+ setItems([]);
91
+ setNextPageParam(undefined);
92
+ loadFilterOptionsPage(query, search, (_b = query.initialPageParam) !== null && _b !== void 0 ? _b : DEFAULT_INITIAL_PAGE_PARAM)
93
+ .then((result) => {
94
+ if (!isCurrent || initialRequestRef.current !== requestId)
95
+ return;
96
+ setItems(result.items);
97
+ setNextPageParam(result.nextPageParam);
98
+ filterOptionsCache.set(cacheKey, Object.assign(Object.assign({}, result), { timestamp: Date.now() }));
99
+ })
100
+ .catch((error) => {
101
+ if (!isCurrent || initialRequestRef.current !== requestId)
102
+ return;
103
+ console.error("Failed to load filter options", error);
104
+ })
105
+ .finally(() => {
106
+ if (!isCurrent || initialRequestRef.current !== requestId)
107
+ return;
108
+ setIsLoading(false);
109
+ });
110
+ return () => {
111
+ isCurrent = false;
112
+ };
113
+ }, [query, search]);
114
+ const loadNextPage = useCallback(() => {
115
+ if (!query || isLoading || isFetchingNextPage)
116
+ return;
117
+ if (!hasNextPageParam(nextPageParam))
118
+ return;
119
+ const requestId = nextPageRequestRef.current + 1;
120
+ nextPageRequestRef.current = requestId;
121
+ const cacheKey = getCacheKey(query, search);
122
+ setIsFetchingNextPage(true);
123
+ loadFilterOptionsPage(query, search, nextPageParam)
124
+ .then((result) => {
125
+ if (nextPageRequestRef.current !== requestId)
126
+ return;
127
+ setItems((currentItems) => {
128
+ const nextItems = mergeDropDownItems(currentItems, result.items);
129
+ filterOptionsCache.set(cacheKey, {
130
+ items: nextItems,
131
+ nextPageParam: result.nextPageParam,
132
+ timestamp: Date.now(),
133
+ });
134
+ return nextItems;
135
+ });
136
+ setNextPageParam(result.nextPageParam);
137
+ })
138
+ .catch((error) => {
139
+ if (nextPageRequestRef.current !== requestId)
140
+ return;
141
+ console.error("Failed to load more filter options", error);
142
+ })
143
+ .finally(() => {
144
+ if (nextPageRequestRef.current !== requestId)
145
+ return;
146
+ setIsFetchingNextPage(false);
147
+ });
148
+ }, [isFetchingNextPage, isLoading, nextPageParam, query, search]);
149
+ return {
150
+ items,
151
+ isLoading,
152
+ isFetchingNextPage,
153
+ loadNextPage,
154
+ setSearch,
155
+ };
156
+ };
5
157
  export const MultiSelectEditor = ({ rule, filterDef, onChange }) => {
6
- var _a, _b;
158
+ var _a, _b, _c;
7
159
  const theme = useTheme();
160
+ const optionsQuery = useFilterOptionsQuery(filterDef === null || filterDef === void 0 ? void 0 : filterDef.query);
8
161
  const handleChange = (selected, diff) => {
9
162
  onChange === null || onChange === void 0 ? void 0 : onChange(selected, diff);
10
163
  };
11
- const options = [
12
- ...((filterDef === null || filterDef === void 0 ? void 0 : filterDef.selectOptions) || []),
13
- ...(rule.options || []),
14
- ];
164
+ const dropDownOptions = useMemo(() => mergeDropDownItems((filterDef === null || filterDef === void 0 ? void 0 : filterDef.selectOptions) || [], optionsQuery.items || []), [filterDef === null || filterDef === void 0 ? void 0 : filterDef.selectOptions, optionsQuery.items]);
165
+ const options = useMemo(() => mergeDropDownItems(dropDownOptions, rule.options || []), [dropDownOptions, rule.options]);
15
166
  const selected = ((_a = rule.value) === null || _a === void 0 ? void 0 : _a.map((value) => {
16
167
  const option = options.find((o) => o.value == value);
17
168
  return option || { label: value, value };
@@ -20,7 +171,8 @@ export const MultiSelectEditor = ({ rule, filterDef, onChange }) => {
20
171
  if (Array.isArray(rule.value) && rule.value.length > 1) {
21
172
  display = `${rule.value.length} ${(filterDef === null || filterDef === void 0 ? void 0 : filterDef.pluralLabel) || "Values"}`;
22
173
  }
23
- return (_jsx(DropDownMenu, { data: (filterDef === null || filterDef === void 0 ? void 0 : filterDef.selectOptions) || [], variant: "outlined", multiselect: true, searchable: true, size: "xs", buttonProps: {
174
+ const manualSearch = (_b = filterDef === null || filterDef === void 0 ? void 0 : filterDef.manualSearch) !== null && _b !== void 0 ? _b : false;
175
+ return (_jsx(DropDownMenu, { data: dropDownOptions, variant: "outlined", multiselect: true, searchable: true, size: "xs", buttonProps: {
24
176
  title: QUERY_FILTER_SELECT_VALUE_LABEL,
25
177
  variant: "contained",
26
178
  size: "xxs",
@@ -39,6 +191,8 @@ export const MultiSelectEditor = ({ rule, filterDef, onChange }) => {
39
191
  : theme.palette.text.secondary,
40
192
  },
41
193
  }, dropDownProps: {
42
- style: Object.assign({ width: 200, maxWidth: 400 }, (_b = filterDef === null || filterDef === void 0 ? void 0 : filterDef.dropDownOptions) === null || _b === void 0 ? void 0 : _b.style),
43
- }, value: selected, onChange: handleChange, query: filterDef === null || filterDef === void 0 ? void 0 : filterDef.query, children: display }));
194
+ style: Object.assign({ width: 200, maxWidth: 400 }, (_c = filterDef === null || filterDef === void 0 ? void 0 : filterDef.dropDownOptions) === null || _c === void 0 ? void 0 : _c.style),
195
+ }, loading: optionsQuery.isLoading && dropDownOptions.length === 0, loadingMore: optionsQuery.isFetchingNextPage, value: selected, onChange: handleChange, manualSearch: manualSearch, onSearch: (filterDef === null || filterDef === void 0 ? void 0 : filterDef.query) && manualSearch
196
+ ? (value) => optionsQuery.setSearch(value)
197
+ : undefined, onScrollToBottom: (filterDef === null || filterDef === void 0 ? void 0 : filterDef.query) ? optionsQuery.loadNextPage : undefined, children: display }));
44
198
  };
@@ -40,7 +40,7 @@ const CONTROL_SIZE_TOKENS = {
40
40
  inputPaddingX: 10,
41
41
  buttonPaddingX: 12,
42
42
  iconSize: 14,
43
- iconGap: 6,
43
+ iconGap: 4,
44
44
  adornmentWidth: 28,
45
45
  menuRowHeight: 28,
46
46
  menuItemPaddingX: 10,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monolith-forensics/monolith-ui",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "author": "Matt Danner (Monolith Forensics LLC)",
@@ -85,12 +85,10 @@
85
85
  "use-debounce": "^10.0.0"
86
86
  },
87
87
  "peerDependencies": {
88
- "@tanstack/react-query": "^5.59.16",
89
88
  "react": "^18.3.1",
90
89
  "react-dom": "^18.3.1"
91
90
  },
92
91
  "devDependencies": {
93
- "@tanstack/react-query": "5.59.16",
94
92
  "@types/d3-scale": "^4.0.9",
95
93
  "@types/d3-shape": "^3.1.8",
96
94
  "@types/overlayscrollbars": "^1.12.5",