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 @@ -349,15 +352,21 @@ const AdvancedNodeSearchDialog = ({
onClose();
}, [activeResult, contentState, onClose]);

const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
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 &&
Expand All @@ -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" &&
Expand All @@ -376,14 +387,18 @@ const AdvancedNodeSearchDialog = ({
) {
event.preventDefault();
void onInsert();
} else if (event.key === "Escape") {
return;
}
if (event.key === "Escape") {
if (isTypeFilterPopoverOpen) return;
event.preventDefault();
onClose();
}
},
[
activeResult,
contentState,
isTypeFilterPopoverOpen,
insertTarget,
onClose,
onInsert,
Expand All @@ -393,14 +408,22 @@ const AdvancedNodeSearchDialog = ({
],
);

const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.defaultPrevented) return;
handleSearchKeyDown(event);
},
[handleSearchKeyDown],
);

const showSplitView = contentState === "results";

return (
<Dialog
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}
Expand All @@ -416,34 +439,44 @@ 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}
/>
<Button
className="shrink-0"
icon="cross"
minimal
onClick={onClose}
title="Close search"
/>
<div className="flex w-full flex-none items-start gap-2 border-b border-gray-200 px-3 py-2">
<div className="flex min-h-9 min-w-0 flex-1 items-center rounded border border-gray-300 bg-white px-2 py-1">
<Icon
className="mr-2 shrink-0 self-center text-gray-500"
icon="search"
size={16}
/>
<NodeTypeChipsSearchInput
inputRef={inputRef}
nodeTypes={discourseNodes}
onSearchKeyDown={handleSearchKeyDown}
onSearchTermChange={setSearchTerm}
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
searchTerm={searchTerm}
selectedTypeIds={selectedNodeTypeIds}
/>
</div>
<div className="flex h-9 shrink-0 items-center gap-1">
<DiscourseNodeTypeFilter
layoutAnchorKey={selectedNodeTypeIds.length}
nodeTypes={discourseNodes}
onPopoverOpenChange={setIsTypeFilterPopoverOpen}
onSelectedTypeIdsChange={setSelectedNodeTypeIds}
selectedTypeIds={selectedNodeTypeIds}
/>
<DiscourseNodeSortControl
disabled={isIndexLoading || indexError}
onSortChange={handleSortChange}
sort={sort}
/>
<Button
className="shrink-0"
icon="cross"
minimal
onClick={onClose}
title="Close search"
/>
</div>
</div>
<div className="flex min-h-0 w-full flex-1 overflow-hidden">
{showSplitView ? (
Expand Down Expand Up @@ -476,7 +509,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
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down Expand Up @@ -186,6 +188,7 @@ const FilterPopoverPanel = ({
};

export const DiscourseNodeTypeFilter = ({
layoutAnchorKey = 0,
nodeTypes,
onPopoverOpenChange,
onSelectedTypeIdsChange,
Expand Down Expand Up @@ -249,6 +252,11 @@ export const DiscourseNodeTypeFilter = ({

const isTriggerActive = isOpen || isFilterActive;

useEffect(() => {
if (!isOpen) return;
window.dispatchEvent(new Event("resize"));
}, [isOpen, layoutAnchorKey]);

const filterButton = (
<span className="relative inline-flex shrink-0 items-center">
<Button
Expand Down Expand Up @@ -314,7 +322,7 @@ export const DiscourseNodeTypeFilter = ({
onInteraction={handlePopoverInteraction}
popoverClassName="p-0 overflow-hidden"
popoverRef={popoverRef}
position={Position.BOTTOM_RIGHT}
position={Position.BOTTOM}
target={filterButton}
usePortal
/>
Expand Down
Loading