@monolith-forensics/monolith-ui 2.1.1 → 2.1.3

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.
@@ -191,6 +191,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
191
191
  const [unControlledValue, setUncontrolledValue] = useState(defaultValue);
192
192
  const _value = isControlled.current ? value : unControlledValue;
193
193
  const [selectedSegment, setSelectedSegment] = useState();
194
+ const [draftSegment, setDraftSegment] = useState(null);
194
195
  const [isOpen, setIsOpen] = useState(false);
195
196
  // Check if format is date only and does not include time
196
197
  const isDateOnly = format.match(/(HH|h|mm|ss|SSS|A|Z)/) === null;
@@ -281,6 +282,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
281
282
  const handleClear = (e) => {
282
283
  e.preventDefault();
283
284
  e.stopPropagation();
285
+ clearTypedSegment();
284
286
  commitValue(null);
285
287
  setIsOpen(false);
286
288
  };
@@ -289,6 +291,8 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
289
291
  e.preventDefault();
290
292
  if (segment.type === "separator")
291
293
  return;
294
+ commitSelectedDraftSegment();
295
+ clearTypedSegment();
292
296
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
293
297
  const input = mainRef.current.querySelector("div[data-type='input']");
294
298
  if (input) {
@@ -300,6 +304,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
300
304
  const handleContainerClick = (e) => {
301
305
  e.stopPropagation();
302
306
  e.preventDefault();
307
+ commitSelectedDraftSegment();
303
308
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
304
309
  const input = mainRef.current.querySelector("div[data-type='input']");
305
310
  if (input) {
@@ -307,11 +312,13 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
307
312
  }
308
313
  }
309
314
  setIsOpen(true);
315
+ clearTypedSegment();
310
316
  setSelectedSegment(segments[0]);
311
317
  };
312
318
  const handleCalendarTriggerClick = (e) => {
313
319
  e.stopPropagation();
314
320
  e.preventDefault();
321
+ commitSelectedDraftSegment();
315
322
  if (mainRef === null || mainRef === void 0 ? void 0 : mainRef.current) {
316
323
  const input = mainRef.current.querySelector("div[data-type='input']");
317
324
  if (input) {
@@ -319,6 +326,7 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
319
326
  }
320
327
  }
321
328
  setIsOpen((prev) => !prev);
329
+ clearTypedSegment();
322
330
  setSelectedSegment(segments[0]);
323
331
  };
324
332
  const nextSegment = () => {
@@ -329,6 +337,145 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
329
337
  return next || prev;
330
338
  });
331
339
  };
340
+ const clearTypedSegment = useCallback(() => {
341
+ typedKeys.current = "";
342
+ setDraftSegment(null);
343
+ }, []);
344
+ const getSegmentInputLength = useCallback((segment) => {
345
+ switch (segment.pattern) {
346
+ case "YYYY":
347
+ return 4;
348
+ case "SSS":
349
+ return 3;
350
+ case "MM":
351
+ case "DD":
352
+ case "HH":
353
+ case "mm":
354
+ case "ss":
355
+ case "h":
356
+ return 2;
357
+ default:
358
+ return segment.text.length;
359
+ }
360
+ }, []);
361
+ const getMomentWithSegmentValue = useCallback((segment, rawValue, currentMoment) => {
362
+ if (!segment.momentType)
363
+ return null;
364
+ const parsedValue = parseInt(rawValue, 10);
365
+ if (!Number.isFinite(parsedValue))
366
+ return null;
367
+ const nextMoment = currentMoment.clone();
368
+ switch (segment.momentType) {
369
+ case "year": {
370
+ if (parsedValue < 0 || parsedValue > 9999)
371
+ return null;
372
+ const daysInTargetMonth = currentMoment
373
+ .clone()
374
+ .year(parsedValue)
375
+ .month(currentMoment.month())
376
+ .daysInMonth();
377
+ if (currentMoment.date() > daysInTargetMonth)
378
+ return null;
379
+ nextMoment.year(parsedValue);
380
+ break;
381
+ }
382
+ case "month": {
383
+ if (parsedValue < 1 || parsedValue > 12)
384
+ return null;
385
+ const targetMonth = parsedValue - 1;
386
+ const daysInTargetMonth = currentMoment
387
+ .clone()
388
+ .month(targetMonth)
389
+ .daysInMonth();
390
+ if (currentMoment.date() > daysInTargetMonth)
391
+ return null;
392
+ nextMoment.month(targetMonth);
393
+ break;
394
+ }
395
+ case "date": {
396
+ if (parsedValue < 1 || parsedValue > currentMoment.daysInMonth()) {
397
+ return null;
398
+ }
399
+ nextMoment.date(parsedValue);
400
+ break;
401
+ }
402
+ case "hour": {
403
+ const isTwelveHourSegment = segment.pattern === "h";
404
+ if (isTwelveHourSegment) {
405
+ if (parsedValue < 1 || parsedValue > 12)
406
+ return null;
407
+ const isPm = currentMoment.hour() >= 12;
408
+ const nextHour = parsedValue === 12
409
+ ? isPm
410
+ ? 12
411
+ : 0
412
+ : parsedValue + (isPm ? 12 : 0);
413
+ nextMoment.hour(nextHour);
414
+ }
415
+ else {
416
+ if (parsedValue < 0 || parsedValue > 23)
417
+ return null;
418
+ nextMoment.hour(parsedValue);
419
+ }
420
+ break;
421
+ }
422
+ case "minute": {
423
+ if (parsedValue < 0 || parsedValue > 59)
424
+ return null;
425
+ nextMoment.minute(parsedValue);
426
+ break;
427
+ }
428
+ case "second": {
429
+ if (parsedValue < 0 || parsedValue > 59)
430
+ return null;
431
+ nextMoment.second(parsedValue);
432
+ break;
433
+ }
434
+ case "millisecond": {
435
+ if (parsedValue < 0 || parsedValue > 999)
436
+ return null;
437
+ nextMoment.millisecond(parsedValue);
438
+ break;
439
+ }
440
+ default:
441
+ return null;
442
+ }
443
+ if (!nextMoment.isValid())
444
+ return null;
445
+ return nextMoment;
446
+ }, []);
447
+ const commitTypedSegment = useCallback((segment, rawValue) => {
448
+ if (!segment.momentType || !rawValue)
449
+ return false;
450
+ const normalizedValue = ["month", "day"].includes(segment.type) &&
451
+ Array.from(rawValue).every((c) => c === "0")
452
+ ? "01"
453
+ : rawValue;
454
+ const nextMoment = getMomentWithSegmentValue(segment, normalizedValue, getEditableMoment(_value));
455
+ if (!nextMoment)
456
+ return false;
457
+ const nextValue = serializeMoment(nextMoment);
458
+ if (!checkValidRange(nextValue))
459
+ return false;
460
+ commitValue(nextValue);
461
+ return true;
462
+ }, [
463
+ _value,
464
+ getEditableMoment,
465
+ getMomentWithSegmentValue,
466
+ serializeMoment,
467
+ checkValidRange,
468
+ commitValue,
469
+ ]);
470
+ const commitSelectedDraftSegment = useCallback(() => {
471
+ if (!selectedSegment || !typedKeys.current)
472
+ return false;
473
+ if (selectedSegment.pattern === "YYYY" &&
474
+ typedKeys.current.length < getSegmentInputLength(selectedSegment)) {
475
+ return false;
476
+ }
477
+ return commitTypedSegment(selectedSegment, typedKeys.current);
478
+ }, [commitTypedSegment, getSegmentInputLength, selectedSegment]);
332
479
  // prevent click and drag selection
333
480
  const handleMouseDown = (e) => {
334
481
  e.preventDefault();
@@ -338,6 +485,8 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
338
485
  return;
339
486
  // tab
340
487
  if (e.key === "Tab") {
488
+ commitSelectedDraftSegment();
489
+ clearTypedSegment();
341
490
  setSelectedSegment(null);
342
491
  setIsOpen(false);
343
492
  return;
@@ -345,28 +494,35 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
345
494
  // Right Arrow
346
495
  if (e.key === "ArrowRight") {
347
496
  e.preventDefault();
497
+ commitSelectedDraftSegment();
498
+ clearTypedSegment();
348
499
  setSelectedSegment((prev) => {
349
500
  if (!prev)
350
501
  return segments[0];
351
502
  const next = segments[prev.index + 2];
352
503
  return next || prev;
353
504
  });
354
- typedKeys.current = ""; // clear typed keys when moving to next segment
355
505
  }
356
506
  // Left Arrow
357
507
  if (e.key === "ArrowLeft") {
358
508
  e.preventDefault();
509
+ commitSelectedDraftSegment();
510
+ clearTypedSegment();
359
511
  setSelectedSegment((prev) => {
360
512
  if (!prev)
361
513
  return segments[0];
362
514
  const next = segments[prev.index - 2];
363
515
  return next || prev;
364
516
  });
365
- typedKeys.current = ""; // clear typed keys when moving to next segment
366
517
  }
367
518
  // Up Arrow
368
519
  if (e.key === "ArrowUp") {
369
520
  e.preventDefault();
521
+ const hadDraftSegment = !!typedKeys.current;
522
+ commitSelectedDraftSegment();
523
+ clearTypedSegment();
524
+ if (hadDraftSegment)
525
+ return;
370
526
  const segmentType = selectedSegment.type;
371
527
  if (segmentType === "separator")
372
528
  return;
@@ -375,6 +531,11 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
375
531
  // Down Arrow
376
532
  if (e.key === "ArrowDown") {
377
533
  e.preventDefault();
534
+ const hadDraftSegment = !!typedKeys.current;
535
+ commitSelectedDraftSegment();
536
+ clearTypedSegment();
537
+ if (hadDraftSegment)
538
+ return;
378
539
  const segmentType = selectedSegment.type;
379
540
  if (segmentType === "separator")
380
541
  return;
@@ -382,43 +543,29 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
382
543
  }
383
544
  // handle paste
384
545
  if (e.key === "v" && (e.metaKey || e.ctrlKey)) {
546
+ clearTypedSegment();
385
547
  handlePaste();
386
548
  }
387
549
  // only allow numbers
388
- if (e.key.match(/[0-9]/)) {
550
+ if (e.key.match(/^[0-9]$/)) {
389
551
  e.preventDefault();
390
- const segmentLength = selectedSegment.text.length;
391
- typedKeys.current += e.key;
392
- let tempValue = typedKeys.current;
393
- if (["month", "day"].includes(selectedSegment.type) &&
394
- Array.from(tempValue).every((c) => c === "0")) {
395
- tempValue = "01";
396
- }
397
- setUncontrolledValue((prev) => {
398
- if (!(selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.momentType))
399
- return prev;
400
- const momentValue = getEditableMoment(prev);
401
- let newValue = moment(momentValue)
402
- .set(selectedSegment.momentType, parseInt(tempValue, 10) -
403
- (selectedSegment.type === "month" ? 1 : 0))
404
- .toISOString();
405
- if (!checkValidRange(newValue))
406
- return prev;
407
- if (isDateOnly) {
408
- newValue = utc
409
- ? moment.utc(newValue).format("YYYY-MM-DD")
410
- : moment(newValue).format("YYYY-MM-DD");
411
- }
412
- if (isControlled.current) {
413
- onChange === null || onChange === void 0 ? void 0 : onChange(newValue);
414
- return prev;
415
- }
416
- onChange === null || onChange === void 0 ? void 0 : onChange(newValue);
417
- return newValue;
552
+ if (!selectedSegment.momentType)
553
+ return;
554
+ const segmentLength = getSegmentInputLength(selectedSegment);
555
+ const nextTypedKeys = typedKeys.current.length >= segmentLength
556
+ ? e.key
557
+ : `${typedKeys.current}${e.key}`;
558
+ typedKeys.current = nextTypedKeys;
559
+ setDraftSegment({
560
+ index: selectedSegment.index,
561
+ text: nextTypedKeys,
418
562
  });
419
- if (typedKeys.current.length === segmentLength) {
420
- nextSegment();
421
- typedKeys.current = "";
563
+ if (nextTypedKeys.length === segmentLength) {
564
+ const didCommit = commitTypedSegment(selectedSegment, nextTypedKeys);
565
+ clearTypedSegment();
566
+ if (didCommit) {
567
+ nextSegment();
568
+ }
422
569
  }
423
570
  }
424
571
  };
@@ -492,11 +639,12 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
492
639
  ) {
493
640
  setIsOpen(false);
494
641
  }
642
+ clearTypedSegment();
495
643
  setSelectedSegment(null); // clear selected segment when clicked outside component
496
644
  };
497
645
  document.addEventListener("click", close);
498
646
  return () => document.removeEventListener("click", close);
499
- }, [refs.floating]);
647
+ }, [clearTypedSegment, refs.floating]);
500
648
  return (_jsxs(StyledContainer, Object.assign({ ref: (ref) => {
501
649
  if (typeof _ref === "function") {
502
650
  _ref(ref);
@@ -507,15 +655,22 @@ const DateInput = forwardRef(({ className, style = {}, defaultValue, value, form
507
655
  e.preventDefault();
508
656
  setSelectedSegment(segments[0]);
509
657
  }, onBlur: () => {
658
+ commitSelectedDraftSegment();
659
+ clearTypedSegment();
510
660
  setSelectedSegment(null);
511
661
  }, "data-empty": !_value, "data-disabled": disabled, role: "textbox", size: size, variant: variant, "data-button-left": enableCalendar, "data-button-right": arrow || clearable, style: style, children: [enableCalendar && (_jsx(CalendarTriggerButton, { size: size, type: "button", "aria-label": "Toggle calendar", onClick: handleCalendarTriggerClick, "data-default-btn": true, children: _jsx(CalendarDaysIcon, {}) })), segments.map((segment, i) => {
662
+ const segmentText = (draftSegment === null || draftSegment === void 0 ? void 0 : draftSegment.index) === segment.index
663
+ ? draftSegment.text
664
+ : segment.text;
512
665
  if (segment.type === "separator") {
513
666
  return (_jsx("div", { className: "separator", tabIndex: -1, onClick: (e) => {
514
667
  e.preventDefault();
515
668
  e.stopPropagation();
516
- }, onFocus: (e) => e.preventDefault(), onPointerDown: (e) => e.preventDefault(), "data-type": "separator", "data-identifier": segment.type, "data-has-space": segment.text.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, children: segment.text }, i));
669
+ }, onFocus: (e) => e.preventDefault(), onPointerDown: (e) => e.preventDefault(), "data-type": "separator", "data-identifier": segment.type, "data-has-space": segmentText.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, children: segmentText }, i));
517
670
  }
518
- return (_jsx(InputSegment, { className: "input", tabIndex: i === 0 ? 0 : -1, onClick: (e) => handleSegmentClick(e, segment), "data-type": "input", size: size, "data-identifier": segment.type, "data-has-space": segment.text.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, "data-selected": (selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.index) === segment.index, children: _value ? segment.text : segment.placeholder }, i));
671
+ return (_jsx(InputSegment, { className: "input", tabIndex: i === 0 ? 0 : -1, onClick: (e) => handleSegmentClick(e, segment), "data-type": "input", size: size, "data-identifier": segment.type, "data-has-space": segmentText.includes(" "), "data-placeholder": segment.placeholder, "data-value": segment.value, "data-selected": (selectedSegment === null || selectedSegment === void 0 ? void 0 : selectedSegment.index) === segment.index, children: _value || (draftSegment === null || draftSegment === void 0 ? void 0 : draftSegment.index) === segment.index
672
+ ? segmentText
673
+ : segment.placeholder }, i));
519
674
  }), utc && _jsx("div", { style: { marginLeft: 5 }, children: "UTC" }), clearable && _value ? (_jsx(ClearButton, { size: size, onClick: handleClear })) : arrow ? (_jsx(ArrowButton, { size: size })) : null] }), !disabled && enableCalendar && isOpen && (_jsx(FloatingPortal, { preserveTabOrder: true, children: _jsx(StyledFloatContainer, Object.assign({ ref: refs.setFloating, style: floatingStyles }, getFloatingProps(), { children: _jsx(StyledContent, { maxDropdownHeight: "fit-content", children: _jsx("div", { children: _jsx(Calendar, { value: getCalendarValue(), clearable: false, min: min, max: max, onChange: (date, meta) => {
520
675
  if (!date) {
521
676
  commitValue(null);
@@ -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.3",
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",