@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.
- package/dist/DateInput/DateInput.js +192 -37
- package/dist/DropDownMenu/DropDownMenu.js +9 -6
- package/dist/DropDownMenu/components/MenuItem.js +5 -1
- package/dist/DropDownMenu/components/MenuItemList.d.ts +4 -2
- package/dist/DropDownMenu/components/MenuItemList.js +46 -68
- package/dist/DropDownMenu/types.d.ts +2 -10
- package/dist/QueryFilter/QueryFilter.types.d.ts +19 -2
- package/dist/QueryFilter/components/MultiSelectEditor.js +163 -9
- package/dist/core/controlSizes.js +1 -1
- package/package.json +1 -3
|
@@ -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(
|
|
550
|
+
if (e.key.match(/^[0-9]$/)) {
|
|
389
551
|
e.preventDefault();
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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 (
|
|
420
|
-
|
|
421
|
-
|
|
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":
|
|
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":
|
|
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
|
|
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,
|
|
14
|
-
var _a, _b, _c, _d, _e
|
|
15
|
-
const isObjectArray =
|
|
16
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
}, [
|
|
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
|
|
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(
|
|
348
|
+
handleSearch(nextValue);
|
|
371
349
|
}
|
|
372
|
-
|
|
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, {
|
|
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
|
|
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?:
|
|
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
|
|
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
|
|
12
|
-
|
|
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
|
-
|
|
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 }, (
|
|
43
|
-
}, value: selected, onChange: handleChange,
|
|
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
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monolith-forensics/monolith-ui",
|
|
3
|
-
"version": "2.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",
|