Skip to content

Commit 9e41900

Browse files
ENG-1738: Render advanced search results as sidebar block (#1071)
* ENG-1738: Render advanced search results as sidebar block Switch advanced search sidebar behavior to create a single summary block with wikilink children, and wire Option+Enter/footer action to open that block in the right sidebar. This aligns the flow with Roam's native sidebar result rendering while keeping the search dialog focused on result-list interaction. Co-authored-by: Cursor <cursoragent@cursor.com> * revert irrelevant changes * switch to add page * cleanup * current progress, new UI * cleanup * cleanup and add filter and sort state * new layout * cleanup * address PR comments * fix type * simplify * revert redundant changes 1 * further simplify * further simplify * Simplify docked search sidebar mount and persistence. Collapse storage into a single mount module, use getWindows() diff to find new sidebar windows, and keep only init + sidebar-reopen sync for restore-on-reload. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix await-thenable lint in getRoamSidebarWindows. Wrap getWindows() in Promise.resolve so ESLint accepts the await while still handling sync or async API returns. Co-authored-by: Cursor <cursoragent@cursor.com> * Remount docked search sidebar after extension reload. Skip restore only when a panel is actively mounted, and remove stale DOM roots on cleanup so persisted windows rehydrate after reload. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 271f990 commit 9e41900

9 files changed

Lines changed: 789 additions & 46 deletions

apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
SpinnerSize,
99
Tag,
1010
} from "@blueprintjs/core";
11-
import MiniSearch from "minisearch";
1211
import posthog from "posthog-js";
1312
import { render as renderToast } from "roamjs-components/components/Toast";
1413
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
@@ -25,6 +24,7 @@ import getDiscourseNodes, {
2524
type DiscourseNode,
2625
} from "~/utils/getDiscourseNodes";
2726
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
27+
import { mountAdvancedSearchInSidebar } from "./mountAdvancedSearchInSidebar";
2828
import {
2929
DEBOUNCE_MS,
3030
DEFAULT_SORT_CONFIG,
@@ -33,14 +33,17 @@ import {
3333
buildSearchIndex,
3434
formatBadgeText,
3535
formatMetadataDate,
36-
searchIndexedNodes,
37-
sortSearchResults,
36+
getSearchKeywords,
3837
splitWithHighlights,
3938
stripTypePrefix,
4039
} from "./utils";
4140
import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter";
4241
import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents";
4342
import { AdvancedSearchFooter } from "./AdvancedSearchFooter";
43+
import {
44+
type SearchIndex,
45+
useAdvancedNodeSearchResults,
46+
} from "./useAdvancedNodeSearchResults";
4447

4548
type Props = Record<string, unknown>;
4649

@@ -154,14 +157,10 @@ const AdvancedNodeSearchDialog = ({
154157
const [isIndexLoading, setIsIndexLoading] = useState(false);
155158
const [indexError, setIndexError] = useState(false);
156159
const [activeIndex, setActiveIndex] = useState(0);
157-
const [results, setResults] = useState<SearchResult[]>([]);
160+
const [searchIndex, setSearchIndex] = useState<SearchIndex | null>(null);
158161
const [sort, setSort] = useState<SortConfig>(DEFAULT_SORT_CONFIG);
159162
const [discourseNodes, setDiscourseNodes] = useState<DiscourseNode[]>([]);
160163
const [selectedNodeTypeIds, setSelectedNodeTypeIds] = useState<string[]>([]);
161-
const miniSearchRef = useRef<MiniSearch<
162-
SearchResult & { id: string }
163-
> | null>(null);
164-
const allResultsRef = useRef<SearchResult[]>([]);
165164
const resultsPanelRef = useRef<HTMLDivElement | null>(null);
166165
const inputRef = useRef<HTMLInputElement | null>(null);
167166
const [insertTarget, setInsertTarget] = useState<InsertTarget | null>(null);
@@ -170,8 +169,17 @@ const AdvancedNodeSearchDialog = ({
170169
discourseNodes.map((node) => [node.type, node]),
171170
);
172171

172+
const results = useAdvancedNodeSearchResults({
173+
debouncedSearchTerm,
174+
selectedNodeTypeIds,
175+
sort,
176+
isIndexLoading,
177+
indexError,
178+
searchIndex,
179+
});
180+
173181
const activeResult = results[activeIndex] ?? null;
174-
const keywords = debouncedSearchTerm.split(/\s+/).filter(Boolean);
182+
const keywords = getSearchKeywords(debouncedSearchTerm);
175183

176184
useEffect(() => {
177185
if (!isOpen) return;
@@ -197,44 +205,16 @@ const AdvancedNodeSearchDialog = ({
197205
setActiveIndex(0);
198206
setSort(DEFAULT_SORT_CONFIG);
199207
setSelectedNodeTypeIds([]);
200-
setResults([]);
208+
setSearchIndex(null);
201209
setIndexError(false);
202210
}
203211
}, [isOpen]);
204212

205-
useEffect(() => {
206-
if (
207-
!isOpen ||
208-
isIndexLoading ||
209-
indexError ||
210-
!debouncedSearchTerm ||
211-
!miniSearchRef.current
212-
) {
213-
setResults([]);
214-
return;
215-
}
216-
217-
const scoredHits = searchIndexedNodes({
218-
miniSearch: miniSearchRef.current,
219-
allResults: allResultsRef.current,
220-
searchTerm: debouncedSearchTerm,
221-
typeFilter: selectedNodeTypeIds.length ? selectedNodeTypeIds : undefined,
222-
});
223-
224-
setResults(sortSearchResults({ hits: scoredHits, sort }));
225-
}, [
226-
debouncedSearchTerm,
227-
indexError,
228-
isIndexLoading,
229-
isOpen,
230-
selectedNodeTypeIds,
231-
sort,
232-
]);
233-
234213
useEffect(() => {
235214
let cancelled = false;
236215
setIsIndexLoading(true);
237216
setIndexError(false);
217+
setSearchIndex(null);
238218

239219
const discourseNodes = getDiscourseNodes().filter(
240220
(node) => node.backedBy === "user",
@@ -244,8 +224,7 @@ const AdvancedNodeSearchDialog = ({
244224
void buildSearchIndex(discourseNodes)
245225
.then(({ miniSearch, results: indexedResults }) => {
246226
if (cancelled) return;
247-
miniSearchRef.current = miniSearch;
248-
allResultsRef.current = indexedResults;
227+
setSearchIndex({ miniSearch, allResults: indexedResults });
249228
})
250229
.catch((error) => {
251230
console.error("Error building advanced node search index:", error);
@@ -317,6 +296,42 @@ const AdvancedNodeSearchDialog = ({
317296
: !results.length
318297
? "empty"
319298
: "results";
299+
300+
const onOpenSearchSidebar = useCallback(async () => {
301+
if (contentState !== "results" || !results.length) return;
302+
303+
try {
304+
await mountAdvancedSearchInSidebar({
305+
query: debouncedSearchTerm,
306+
results,
307+
selectedNodeTypeIds,
308+
sort,
309+
});
310+
311+
posthog.capture("Advanced Node Search: Dock search sidebar", {
312+
resultCount: results.length,
313+
searchTerm: debouncedSearchTerm,
314+
selectedNodeTypeCount: selectedNodeTypeIds.length,
315+
sortDirection: sort.direction,
316+
sortField: sort.field,
317+
});
318+
onClose();
319+
} catch (error) {
320+
console.error("Failed to dock search results in the sidebar:", error);
321+
renderToast({
322+
id: "advanced-node-search-sidebar-open-error",
323+
content: "Could not dock search results in the right sidebar.",
324+
intent: "danger",
325+
});
326+
}
327+
}, [
328+
contentState,
329+
debouncedSearchTerm,
330+
onClose,
331+
results,
332+
selectedNodeTypeIds,
333+
sort,
334+
]);
320335
const handleSortChange = useCallback((nextSort: SortConfig): void => {
321336
setSort(nextSort);
322337
}, []);
@@ -355,6 +370,14 @@ const AdvancedNodeSearchDialog = ({
355370
} else if (event.key === "ArrowUp" && results.length) {
356371
event.preventDefault();
357372
setActiveIndex((index) => Math.max(index - 1, 0));
373+
} else if (
374+
event.key === "Enter" &&
375+
event.altKey &&
376+
contentState === "results" &&
377+
results.length
378+
) {
379+
event.preventDefault();
380+
void onOpenSearchSidebar();
358381
} else if (
359382
event.key === "Enter" &&
360383
!event.metaKey &&
@@ -384,6 +407,7 @@ const AdvancedNodeSearchDialog = ({
384407
contentState,
385408
insertTarget,
386409
onClose,
410+
onOpenSearchSidebar,
387411
onInsert,
388412
onOpen,
389413
onOpenInSidebar,
@@ -491,6 +515,7 @@ const AdvancedNodeSearchDialog = ({
491515
onInsert={() => void onInsert()}
492516
onOpen={() => void onOpen()}
493517
onOpenInSidebar={() => void onOpenInSidebar()}
518+
onOpenSearchSidebar={() => void onOpenSearchSidebar()}
494519
/>
495520
</div>
496521
</Dialog>

apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type AdvancedSearchFooterProps = {
1616
onInsert: () => void;
1717
onOpen: () => void;
1818
onOpenInSidebar: () => void;
19+
onOpenSearchSidebar: () => void;
1920
};
2021

2122
const footerKbdClassName =
@@ -99,6 +100,21 @@ const InsertFooterAction = ({
99100
/>
100101
);
101102

103+
const OpenSearchSidebarFooterAction = ({
104+
disabled,
105+
onOpenSearchSidebar,
106+
}: {
107+
disabled: boolean;
108+
onOpenSearchSidebar: () => void;
109+
}) => (
110+
<FooterShortcutHint
111+
disabled={disabled}
112+
keyIcons={["key-option", "key-enter"]}
113+
label="dock results"
114+
onClick={() => void onOpenSearchSidebar()}
115+
/>
116+
);
117+
102118
const CloseFooterHint = () => (
103119
<span className={footerLabelClassName}>
104120
<kbd className={footerKbdClassName}>
@@ -115,14 +131,20 @@ export const AdvancedSearchFooter = ({
115131
onInsert,
116132
onOpen,
117133
onOpenInSidebar,
134+
onOpenSearchSidebar,
118135
}: AdvancedSearchFooterProps) => {
119136
const hasResults = contentState === "results";
120137
const canOpen = hasActiveResult && hasResults;
121138
const canInsert = !!insertTarget && hasActiveResult && hasResults;
139+
const canOpenSearchSidebar = hasResults;
122140

123141
return (
124142
<div className="flex w-full flex-none items-center justify-between border-t border-gray-200 bg-gray-50 px-3 py-2">
125143
<div className="inline-flex shrink-0 items-center gap-3">
144+
<OpenSearchSidebarFooterAction
145+
disabled={!canOpenSearchSidebar}
146+
onOpenSearchSidebar={onOpenSearchSidebar}
147+
/>
126148
{insertTarget && (
127149
<InsertFooterAction disabled={!canInsert} onInsert={onInsert} />
128150
)}

0 commit comments

Comments
 (0)