Skip to content

Commit 483c959

Browse files
authored
[ENG-404] Fix multiple things about node search menu (#347)
* fix multiple things about node menu * address PR comments * multi-select as it is * fix the UI, still has position bug * fix finally * address PR comment
1 parent 3b906c0 commit 483c959

File tree

1 file changed

+104
-68
lines changed

1 file changed

+104
-68
lines changed

apps/roam/src/components/DiscourseNodeSearchMenu.tsx

Lines changed: 104 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
MenuItem,
1111
Popover,
1212
Position,
13-
Checkbox,
1413
Button,
1514
InputGroup,
15+
Intent,
1616
} from "@blueprintjs/core";
1717
import ReactDOM from "react-dom";
1818
import getUids from "roamjs-components/dom/getUids";
@@ -56,15 +56,24 @@ const NodeSearchMenu = ({
5656
triggerPosition,
5757
triggerText,
5858
}: { onClose: () => void } & Props) => {
59+
const MENU_WIDTH = 400;
5960
const [activeIndex, setActiveIndex] = useState(0);
6061
const [searchTerm, setSearchTerm] = useState("");
61-
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
6262
const [discourseTypes, setDiscourseTypes] = useState<DiscourseNode[]>([]);
6363
const [checkedTypes, setCheckedTypes] = useState<Record<string, boolean>>({});
6464
const [isLoading, setIsLoading] = useState(true);
6565
const [searchResults, setSearchResults] = useState<Record<string, Result[]>>(
6666
{},
6767
);
68+
const [isFilterMenuVisible, setIsFilterMenuVisible] = useState(false);
69+
const typeIds = useMemo(
70+
() => discourseTypes.map((t) => t.type),
71+
[discourseTypes],
72+
);
73+
const isAllSelected = useMemo(
74+
() => typeIds.length > 0 && typeIds.every((id) => !!checkedTypes[id]),
75+
[typeIds, checkedTypes],
76+
);
6877
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
6978
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
7079
const POPOVER_TOP_OFFSET = 30;
@@ -194,7 +203,7 @@ const NodeSearchMenu = ({
194203

195204
const onSelect = useCallback(
196205
(item: Result) => {
197-
waitForBlock(blockUid, textarea.value).then(() => {
206+
void waitForBlock(blockUid, textarea.value).then(() => {
198207
onClose();
199208

200209
setTimeout(() => {
@@ -352,46 +361,69 @@ const NodeSearchMenu = ({
352361

353362
let currentGlobalIndex = -1;
354363

355-
const handleTypeCheckChange = useCallback(
356-
(typeKey: string, e: React.MouseEvent) => {
357-
e.preventDefault();
358-
e.stopPropagation();
359-
360-
setCheckedTypes((prev) => ({
361-
...prev,
362-
[typeKey]: !prev[typeKey],
363-
}));
364+
const handleTypeCheckChange = useCallback((typeKey: string) => {
365+
setCheckedTypes((prev) => ({
366+
...prev,
367+
[typeKey]: !prev[typeKey],
368+
}));
369+
}, []);
364370

365-
setTimeout(() => {
366-
textarea.focus();
367-
const cursorPos = textarea.selectionStart;
368-
textarea.setSelectionRange(cursorPos, cursorPos);
369-
}, 0);
371+
const handleToggleAll = useCallback(
372+
(checked: boolean) => {
373+
setCheckedTypes(Object.fromEntries(typeIds.map((id) => [id, checked])));
370374
},
371-
[textarea],
375+
[typeIds],
372376
);
373377

374-
const remainFocusOnTextarea = useCallback((e: React.MouseEvent) => {
375-
e.preventDefault();
376-
e.stopPropagation();
377-
}, []);
378-
379-
const toggleFilterMenu = useCallback(
380-
(e: React.MouseEvent) => {
381-
e.preventDefault();
382-
e.stopPropagation();
383-
384-
setIsFilterMenuOpen((prev) => !prev);
378+
const handleSelectOnly = useCallback(
379+
(node: DiscourseNode) => {
380+
const next = Object.fromEntries(
381+
typeIds.map((id) => [id, id === node.type]),
382+
);
383+
setCheckedTypes(next as Record<string, boolean>);
384+
},
385+
[typeIds],
386+
);
385387

386-
setTimeout(() => {
387-
if (textarea) {
388-
textarea.focus();
389-
const cursorPos = textarea.selectionStart;
390-
textarea.setSelectionRange(cursorPos, cursorPos);
391-
}
392-
}, 0);
388+
const renderTypeItem = useCallback(
389+
(item: DiscourseNode) => {
390+
const isSelected = !!checkedTypes[item.type];
391+
return (
392+
<MenuItem
393+
key={item.type}
394+
className="group !p-0"
395+
text={
396+
<div className="flex w-full items-center justify-between">
397+
<div className="flex flex-1 items-center px-2 py-1.5">
398+
<span className="mr-2">{isSelected ? "✓" : " "}</span>
399+
<span>{item.text}</span>
400+
</div>
401+
<Button
402+
minimal
403+
small
404+
className="flex !h-full items-center justify-center !rounded-none px-3 opacity-0 transition-opacity group-hover:opacity-100"
405+
onClick={(e) => {
406+
e.stopPropagation();
407+
handleSelectOnly(item);
408+
}}
409+
onMouseDown={(e) => e.preventDefault()}
410+
>
411+
Only
412+
</Button>
413+
</div>
414+
}
415+
icon={null}
416+
shouldDismissPopover={false}
417+
onClick={(e) => {
418+
e.preventDefault();
419+
e.stopPropagation();
420+
handleTypeCheckChange(item.type);
421+
}}
422+
onMouseDown={(e) => e.preventDefault()}
423+
/>
424+
);
393425
},
394-
[textarea],
426+
[checkedTypes, handleTypeCheckChange, handleSelectOnly],
395427
);
396428

397429
return (
@@ -400,11 +432,12 @@ const NodeSearchMenu = ({
400432
isOpen={true}
401433
canEscapeKeyClose
402434
minimal
435+
usePortal={true}
403436
target={<span />}
404437
position={Position.BOTTOM_LEFT}
405438
modifiers={{
406439
flip: { enabled: true },
407-
preventOverflow: { enabled: true },
440+
preventOverflow: { enabled: true, boundariesElement: "viewport" },
408441
offset: {
409442
enabled: true,
410443
fn: (data) => {
@@ -419,53 +452,54 @@ const NodeSearchMenu = ({
419452
content={
420453
<div
421454
className="discourse-node-search-menu"
422-
style={{ width: "250px" }}
423-
onMouseDown={remainFocusOnTextarea}
424-
onClick={remainFocusOnTextarea}
455+
style={{ width: MENU_WIDTH }}
425456
>
426457
{isLoading ? (
427458
<div className="p-3 text-center text-gray-500">Loading...</div>
428459
) : (
429460
<>
430461
<div
431462
className="discourse-node-search-menu"
432-
style={{ width: "250px" }}
433-
onMouseDown={remainFocusOnTextarea}
434-
onClick={remainFocusOnTextarea}
463+
style={{ width: MENU_WIDTH }}
435464
>
436465
<div className="flex items-center justify-between border-b border-gray-200 p-2">
437-
<div className="text-sm font-semibold">Search Results</div>
438466
<Button
439467
icon="filter"
440468
minimal
441469
small
442-
active={isFilterMenuOpen}
443-
onClick={toggleFilterMenu}
444-
onMouseDown={remainFocusOnTextarea}
445470
title="Filter by type"
471+
onClick={(e) => {
472+
e.stopPropagation();
473+
e.preventDefault();
474+
setIsFilterMenuVisible(!isFilterMenuVisible);
475+
}}
476+
onMouseDown={(e) => e.preventDefault()}
446477
/>
447478
</div>
448-
449-
{isFilterMenuOpen && (
450-
<div className="border-b border-gray-200 p-2">
451-
<div className="mb-2 text-sm font-semibold">
452-
Filter by type:
479+
{isFilterMenuVisible && (
480+
<div>
481+
<div className="flex items-center justify-between px-3 py-2">
482+
<span className="text-sm font-medium text-gray-700">
483+
Filter by Type
484+
</span>
485+
<Button
486+
small
487+
intent={isAllSelected ? Intent.SUCCESS : Intent.PRIMARY}
488+
icon={isAllSelected ? "tick" : "multi-select"}
489+
onMouseDown={(e) => e.preventDefault()}
490+
onClick={(e) => {
491+
e.stopPropagation();
492+
e.preventDefault();
493+
handleToggleAll(!isAllSelected);
494+
}}
495+
>
496+
{isAllSelected ? "All selected" : "Select all"}
497+
</Button>
453498
</div>
454-
<div className="flex flex-wrap gap-2">
455-
{discourseTypes.map((type) => (
456-
<div
457-
key={type.type}
458-
className="inline-flex cursor-pointer items-center"
459-
onClick={(e) => handleTypeCheckChange(type.type, e)}
460-
>
461-
<Checkbox
462-
label={type.text}
463-
checked={checkedTypes[type.type]}
464-
onChange={() => {}}
465-
className="m-0"
466-
/>
467-
</div>
468-
))}
499+
<div className="max-h-48 overflow-y-auto">
500+
<Menu>
501+
{discourseTypes.map((t) => renderTypeItem(t))}
502+
</Menu>
469503
</div>
470504
</div>
471505
)}
@@ -484,8 +518,10 @@ const NodeSearchMenu = ({
484518
<MenuItem
485519
key={item.uid}
486520
text={item.text}
521+
multiline
487522
data-active={isActive}
488523
active={isActive}
524+
shouldDismissPopover={false}
489525
onClick={() => onSelect(item)}
490526
/>
491527
);

0 commit comments

Comments
 (0)