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,10 +1,4 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Button,
Dialog,
Expand All @@ -14,7 +8,6 @@ import {
SpinnerSize,
Tag,
} from "@blueprintjs/core";
import MiniSearch from "minisearch";
import posthog from "posthog-js";
import { render as renderToast } from "roamjs-components/components/Toast";
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
Expand All @@ -31,21 +24,25 @@ import getDiscourseNodes, {
type DiscourseNode,
} from "~/utils/getDiscourseNodes";
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
import { mountAdvancedSearchInSidebar } from "./mountAdvancedSearchInSidebar";
import {
DEBOUNCE_MS,
DEFAULT_SORT_CONFIG,
type SearchResult,
type SortConfig,
buildSearchIndex,
formatMetadataDate,
searchIndexedNodes,
sortSearchResults,
getSearchKeywords,
splitWithHighlights,
stripTypePrefix,
} from "./utils";
import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter";
import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents";
import { AdvancedSearchFooter } from "./AdvancedSearchFooter";
import {
type SearchIndex,
useAdvancedNodeSearchResults,
} from "./useAdvancedNodeSearchResults";

type Props = Record<string, unknown>;

Expand Down Expand Up @@ -156,14 +153,10 @@ const AdvancedNodeSearchDialog = ({
const [isIndexLoading, setIsIndexLoading] = useState(false);
const [indexError, setIndexError] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const [results, setResults] = useState<SearchResult[]>([]);
const [searchIndex, setSearchIndex] = useState<SearchIndex | null>(null);
const [sort, setSort] = useState<SortConfig>(DEFAULT_SORT_CONFIG);
const [discourseNodes, setDiscourseNodes] = useState<DiscourseNode[]>([]);
const [selectedNodeTypeIds, setSelectedNodeTypeIds] = useState<string[]>([]);
const miniSearchRef = useRef<MiniSearch<
SearchResult & { id: string }
> | null>(null);
const allResultsRef = useRef<SearchResult[]>([]);
const resultsPanelRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const [insertTarget, setInsertTarget] = useState<InsertTarget | null>(null);
Expand All @@ -172,8 +165,17 @@ const AdvancedNodeSearchDialog = ({
discourseNodes.map((node) => [node.type, node]),
);

const results = useAdvancedNodeSearchResults({
debouncedSearchTerm,
selectedNodeTypeIds,
sort,
isIndexLoading,
indexError,
searchIndex,
});

const activeResult = results[activeIndex] ?? null;
const keywords = debouncedSearchTerm.split(/\s+/).filter(Boolean);
const keywords = getSearchKeywords(debouncedSearchTerm);

useEffect(() => {
if (!isOpen) return;
Expand All @@ -199,44 +201,16 @@ const AdvancedNodeSearchDialog = ({
setActiveIndex(0);
setSort(DEFAULT_SORT_CONFIG);
setSelectedNodeTypeIds([]);
setResults([]);
setSearchIndex(null);
setIndexError(false);
}
}, [isOpen]);

useEffect(() => {
if (
!isOpen ||
isIndexLoading ||
indexError ||
!debouncedSearchTerm ||
!miniSearchRef.current
) {
setResults([]);
return;
}

const scoredHits = searchIndexedNodes({
miniSearch: miniSearchRef.current,
allResults: allResultsRef.current,
searchTerm: debouncedSearchTerm,
typeFilter: selectedNodeTypeIds.length ? selectedNodeTypeIds : undefined,
});

setResults(sortSearchResults({ hits: scoredHits, sort }));
}, [
debouncedSearchTerm,
indexError,
isIndexLoading,
isOpen,
selectedNodeTypeIds,
sort,
]);

useEffect(() => {
let cancelled = false;
setIsIndexLoading(true);
setIndexError(false);
setSearchIndex(null);

const discourseNodes = getDiscourseNodes().filter(
(node) => node.backedBy === "user",
Expand All @@ -246,8 +220,7 @@ const AdvancedNodeSearchDialog = ({
void buildSearchIndex(discourseNodes)
.then(({ miniSearch, results: indexedResults }) => {
if (cancelled) return;
miniSearchRef.current = miniSearch;
allResultsRef.current = indexedResults;
setSearchIndex({ miniSearch, allResults: indexedResults });
})
.catch((error) => {
console.error("Error building advanced node search index:", error);
Expand Down Expand Up @@ -319,6 +292,42 @@ const AdvancedNodeSearchDialog = ({
: !results.length
? "empty"
: "results";

const onOpenSearchSidebar = useCallback(async () => {
if (contentState !== "results" || !results.length) return;

try {
await mountAdvancedSearchInSidebar({
query: debouncedSearchTerm,
results,
selectedNodeTypeIds,
sort,
});

posthog.capture("Advanced Node Search: Dock search sidebar", {
resultCount: results.length,
searchTerm: debouncedSearchTerm,
selectedNodeTypeCount: selectedNodeTypeIds.length,
sortDirection: sort.direction,
sortField: sort.field,
});
onClose();
} catch (error) {
console.error("Failed to dock search results in the sidebar:", error);
renderToast({
id: "advanced-node-search-sidebar-open-error",
content: "Could not dock search results in the right sidebar.",
intent: "danger",
});
}
}, [
contentState,
debouncedSearchTerm,
onClose,
results,
selectedNodeTypeIds,
sort,
]);
const handleSortChange = useCallback((nextSort: SortConfig): void => {
setSort(nextSort);
}, []);
Expand Down Expand Up @@ -357,6 +366,14 @@ const AdvancedNodeSearchDialog = ({
} else if (event.key === "ArrowUp" && results.length) {
event.preventDefault();
setActiveIndex((index) => Math.max(index - 1, 0));
} else if (
event.key === "Enter" &&
event.altKey &&
contentState === "results" &&
results.length
) {
event.preventDefault();
void onOpenSearchSidebar();
} else if (
event.key === "Enter" &&
!event.metaKey &&
Expand Down Expand Up @@ -386,6 +403,7 @@ const AdvancedNodeSearchDialog = ({
contentState,
insertTarget,
onClose,
onOpenSearchSidebar,
onInsert,
onOpen,
onOpenInSidebar,
Expand Down Expand Up @@ -493,6 +511,7 @@ const AdvancedNodeSearchDialog = ({
onInsert={() => void onInsert()}
onOpen={() => void onOpen()}
onOpenInSidebar={() => void onOpenInSidebar()}
onOpenSearchSidebar={() => void onOpenSearchSidebar()}
/>
</div>
</Dialog>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type AdvancedSearchFooterProps = {
onInsert: () => void;
onOpen: () => void;
onOpenInSidebar: () => void;
onOpenSearchSidebar: () => void;
};

const footerKbdClassName =
Expand Down Expand Up @@ -99,6 +100,21 @@ const InsertFooterAction = ({
/>
);

const OpenSearchSidebarFooterAction = ({
disabled,
onOpenSearchSidebar,
}: {
disabled: boolean;
onOpenSearchSidebar: () => void;
}) => (
<FooterShortcutHint
disabled={disabled}
keyIcons={["key-option", "key-enter"]}
label="dock results"
onClick={() => void onOpenSearchSidebar()}
/>
);

const CloseFooterHint = () => (
<span className={footerLabelClassName}>
<kbd className={footerKbdClassName}>
Expand All @@ -115,14 +131,20 @@ export const AdvancedSearchFooter = ({
onInsert,
onOpen,
onOpenInSidebar,
onOpenSearchSidebar,
}: AdvancedSearchFooterProps) => {
const hasResults = contentState === "results";
const canOpen = hasActiveResult && hasResults;
const canInsert = !!insertTarget && hasActiveResult && hasResults;
const canOpenSearchSidebar = hasResults;

return (
<div className="flex w-full flex-none items-center justify-between border-t border-gray-200 bg-gray-50 px-3 py-2">
<div className="inline-flex shrink-0 items-center gap-3">
<OpenSearchSidebarFooterAction
disabled={!canOpenSearchSidebar}
onOpenSearchSidebar={onOpenSearchSidebar}
/>
{insertTarget && (
<InsertFooterAction disabled={!canInsert} onInsert={onInsert} />
)}
Expand Down
Loading