diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index a5d6bbdd6..c043604a6 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -1,14 +1,8 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { Button, Dialog, - InputGroup, + Icon, NonIdealState, Spinner, SpinnerSize, @@ -34,6 +28,7 @@ import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors"; import { DEBOUNCE_MS, DEFAULT_SORT_CONFIG, + MAX_RESULTS, type SearchResult, type SortConfig, buildSearchIndex, @@ -46,6 +41,7 @@ import { import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter"; import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents"; import { AdvancedSearchFooter } from "./AdvancedSearchFooter"; +import { NodeTypeChipsSearchInput } from "./NodeTypeChipsSearchInput"; type Props = Record; @@ -160,6 +156,7 @@ const AdvancedNodeSearchDialog = ({ const [sort, setSort] = useState(DEFAULT_SORT_CONFIG); const [discourseNodes, setDiscourseNodes] = useState([]); const [selectedNodeTypeIds, setSelectedNodeTypeIds] = useState([]); + const [isTypeFilterPopoverOpen, setIsTypeFilterPopoverOpen] = useState(false); const miniSearchRef = useRef | null>(null); @@ -205,25 +202,31 @@ const AdvancedNodeSearchDialog = ({ }, [isOpen]); useEffect(() => { - if ( - !isOpen || - isIndexLoading || - indexError || - !debouncedSearchTerm || - !miniSearchRef.current - ) { + const hasTypeFilters = selectedNodeTypeIds.length > 0; + + if (!isOpen || isIndexLoading || indexError || !miniSearchRef.current) { setResults([]); return; } - const scoredHits = searchIndexedNodes({ - miniSearch: miniSearchRef.current, - allResults: allResultsRef.current, - searchTerm: debouncedSearchTerm, - typeFilter: selectedNodeTypeIds.length ? selectedNodeTypeIds : undefined, - }); + if (!debouncedSearchTerm && !hasTypeFilters) { + setResults([]); + return; + } - setResults(sortSearchResults({ hits: scoredHits, sort })); + const scoredHits = debouncedSearchTerm + ? searchIndexedNodes({ + miniSearch: miniSearchRef.current, + allResults: allResultsRef.current, + searchTerm: debouncedSearchTerm, + typeFilter: hasTypeFilters ? selectedNodeTypeIds : undefined, + }) + : allResultsRef.current + .filter((result) => selectedNodeTypeIds.includes(result.type)) + .map((result) => ({ result, score: 1 })); + + const sortedResults = sortSearchResults({ hits: scoredHits, sort }); + setResults(sortedResults.slice(0, MAX_RESULTS)); }, [ debouncedSearchTerm, indexError, @@ -314,7 +317,7 @@ const AdvancedNodeSearchDialog = ({ ? "error" : isIndexLoading ? "indexing" - : !debouncedSearchTerm + : !debouncedSearchTerm && selectedNodeTypeIds.length === 0 ? "initial" : !results.length ? "empty" @@ -349,15 +352,21 @@ const AdvancedNodeSearchDialog = ({ onClose(); }, [activeResult, contentState, onClose]); - const onKeyDown = useCallback( - (event: React.KeyboardEvent) => { + const handleSearchKeyDown = useCallback( + (event: React.KeyboardEvent): void => { if (event.key === "ArrowDown" && results.length) { event.preventDefault(); - setActiveIndex((index) => Math.min(index + 1, results.length - 1)); - } else if (event.key === "ArrowUp" && results.length) { + setActiveIndex((index) => + Math.min(Math.max(index, 0) + 1, results.length - 1), + ); + return; + } + if (event.key === "ArrowUp" && results.length) { event.preventDefault(); setActiveIndex((index) => Math.max(index - 1, 0)); - } else if ( + return; + } + if ( event.key === "Enter" && !event.metaKey && !event.ctrlKey && @@ -367,7 +376,9 @@ const AdvancedNodeSearchDialog = ({ event.preventDefault(); if (event.shiftKey) void onOpenInSidebar(); else void onOpen(); - } else if ( + return; + } + if ( event.key === "Enter" && (event.metaKey || event.ctrlKey) && contentState === "results" && @@ -376,7 +387,10 @@ const AdvancedNodeSearchDialog = ({ ) { event.preventDefault(); void onInsert(); - } else if (event.key === "Escape") { + return; + } + if (event.key === "Escape") { + if (isTypeFilterPopoverOpen) return; event.preventDefault(); onClose(); } @@ -384,6 +398,7 @@ const AdvancedNodeSearchDialog = ({ [ activeResult, contentState, + isTypeFilterPopoverOpen, insertTarget, onClose, onInsert, @@ -393,6 +408,14 @@ const AdvancedNodeSearchDialog = ({ ], ); + const onKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.defaultPrevented) return; + handleSearchKeyDown(event); + }, + [handleSearchKeyDown], + ); + const showSplitView = contentState === "results"; return ( @@ -400,7 +423,7 @@ const AdvancedNodeSearchDialog = ({ autoFocus={false} canEscapeKeyClose canOutsideClickClose - className="flex max-w-4xl flex-col overflow-hidden bg-white p-0" + className="flex w-full max-w-4xl flex-col overflow-hidden bg-white p-0" enforceFocus={false} isOpen={isOpen} onClose={onClose} @@ -416,34 +439,44 @@ const AdvancedNodeSearchDialog = ({ onMouseUp={(event) => event.stopPropagation()} className="flex min-h-0 flex-1 flex-col overflow-hidden" > -
- ) => - setSearchTerm(event.target.value) - } - placeholder="Search discourse nodes..." - value={searchTerm} - /> - - -
{showSplitView ? ( @@ -476,7 +509,7 @@ const AdvancedNodeSearchDialog = ({ )} {contentState === "empty" && ( - No matches. Try another keyword. + No matches. Try another keyword or filter. )} {contentState === "error" && ( diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter.tsx index 0eef336b9..9c2f91a03 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter.tsx @@ -28,6 +28,8 @@ export type DiscourseNodeTypeFilterProps = { selectedTypeIds: string[]; onSelectedTypeIdsChange: (ids: string[]) => void; onPopoverOpenChange?: (isOpen: boolean) => void; + /** Bumps when surrounding layout changes (e.g. chip wrap) so the popover repositions. */ + layoutAnchorKey?: number; }; const getNodeIndicatorColor = (node: DiscourseNode): string => @@ -186,6 +188,7 @@ const FilterPopoverPanel = ({ }; export const DiscourseNodeTypeFilter = ({ + layoutAnchorKey = 0, nodeTypes, onPopoverOpenChange, onSelectedTypeIdsChange, @@ -249,6 +252,11 @@ export const DiscourseNodeTypeFilter = ({ const isTriggerActive = isOpen || isFilterActive; + useEffect(() => { + if (!isOpen) return; + window.dispatchEvent(new Event("resize")); + }, [isOpen, layoutAnchorKey]); + const filterButton = (