Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -34,6 +28,7 @@ import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
import {
DEBOUNCE_MS,
DEFAULT_SORT_CONFIG,
MAX_RESULTS,
type SearchResult,
type SortConfig,
buildSearchIndex,
Expand All @@ -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<string, unknown>;

Expand Down Expand Up @@ -160,6 +156,7 @@ const AdvancedNodeSearchDialog = ({
const [sort, setSort] = useState<SortConfig>(DEFAULT_SORT_CONFIG);
const [discourseNodes, setDiscourseNodes] = useState<DiscourseNode[]>([]);
const [selectedNodeTypeIds, setSelectedNodeTypeIds] = useState<string[]>([]);
const [isTypeFilterPopoverOpen, setIsTypeFilterPopoverOpen] = useState(false);
const miniSearchRef = useRef<MiniSearch<
SearchResult & { id: string }
> | null>(null);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -314,7 +317,7 @@ const AdvancedNodeSearchDialog = ({
? "error"
: isIndexLoading
? "indexing"
: !debouncedSearchTerm
: !debouncedSearchTerm && selectedNodeTypeIds.length === 0
? "initial"
: !results.length
? "empty"
Expand Down Expand Up @@ -351,6 +354,7 @@ const AdvancedNodeSearchDialog = ({

const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.defaultPrevented) return;
if (event.key === "ArrowDown" && results.length) {
event.preventDefault();
setActiveIndex((index) => Math.min(index + 1, results.length - 1));
Expand All @@ -377,13 +381,15 @@ const AdvancedNodeSearchDialog = ({
event.preventDefault();
void onInsert();
} else if (event.key === "Escape") {
if (isTypeFilterPopoverOpen) return;
event.preventDefault();
onClose();
}
},
[
activeResult,
contentState,
isTypeFilterPopoverOpen,
insertTarget,
onClose,
onInsert,
Expand Down Expand Up @@ -416,29 +422,50 @@ const AdvancedNodeSearchDialog = ({
onMouseUp={(event) => event.stopPropagation()}
className="flex min-h-0 flex-1 flex-col overflow-hidden"
>
<div className="flex flex-none items-center gap-2 border-b border-gray-200 px-3 py-2">
<InputGroup
fill
inputRef={inputRef}
leftIcon="search"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSearchTerm(event.target.value)
}
placeholder="Search discourse nodes..."
value={searchTerm}
/>
<DiscourseNodeTypeFilter
nodeTypes={discourseNodes}
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
selectedTypeIds={selectedNodeTypeIds}
/>
<DiscourseNodeSortControl
disabled={isIndexLoading || indexError}
onSortChange={handleSortChange}
sort={sort}
/>
<div className="flex flex-none items-start gap-2 border-b border-gray-200 px-3 py-2">
<div className="flex min-w-0 flex-1 items-center rounded border border-gray-300 bg-white px-2 py-1">
<Icon icon="search" size={16} className="mr-2 text-gray-500" />
<NodeTypeChipsSearchInput
inputRef={inputRef}
nodeTypes={discourseNodes}
onArrowDown={() =>
setActiveIndex((index) =>
Math.min(index + 1, results.length - 1),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard ArrowDown callback when no results

Pressing ArrowDown in the chip input when results.length === 0 (for example, while indexing or after an empty search) computes Math.min(index + 1, -1), which sets activeIndex to -1. If results then appear without another term/filter/sort change, no row is selected and Enter/Cmd+Enter actions silently no-op until the user manually re-navigates. Clamp to a non-negative index or skip updates when there are no results.

Useful? React with 👍 / 👎.

)
}
onArrowUp={() =>
setActiveIndex((index) => Math.max(index - 1, 0))
}
onCmdEnter={() => void onInsert()}
onEnter={() => void onOpen()}
onEscape={() => {
if (isTypeFilterPopoverOpen) return;
onClose();
}}
onSearchTermChange={setSearchTerm}
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
onShiftEnter={() => void onOpenInSidebar()}
searchTerm={searchTerm}
selectedTypeIds={selectedNodeTypeIds}
/>
</div>
<div className="self-start">
<DiscourseNodeTypeFilter
nodeTypes={discourseNodes}
onPopoverOpenChange={setIsTypeFilterPopoverOpen}
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
selectedTypeIds={selectedNodeTypeIds}
/>
</div>
<div className="self-start">
<DiscourseNodeSortControl
disabled={isIndexLoading || indexError}
onSortChange={handleSortChange}
sort={sort}
/>
</div>
<Button
className="shrink-0"
className="shrink-0 self-start"
icon="cross"
minimal
onClick={onClose}
Expand Down Expand Up @@ -476,7 +503,7 @@ const AdvancedNodeSearchDialog = ({
<Spinner size={SpinnerSize.SMALL} />
)}
{contentState === "empty" && (
<span>No matches. Try another keyword.</span>
<span>No matches. Try another keyword or filter.</span>
)}
{contentState === "error" && (
<span>
Expand Down
Loading
Loading