Skip to content

Commit 5288eaa

Browse files
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>
1 parent e527149 commit 5288eaa

3 files changed

Lines changed: 109 additions & 72 deletions

File tree

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

Lines changed: 80 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Button,
1010
Dialog,
1111
InputGroup,
12-
NonIdealState,
1312
Spinner,
1413
SpinnerSize,
1514
Tag,
@@ -21,6 +20,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
2120
import renderOverlay, {
2221
RoamOverlayProps,
2322
} from "roamjs-components/util/renderOverlay";
23+
import { createBlock } from "roamjs-components/writes";
2424
import {
2525
insertPageRefAtRange,
2626
snapshotInsertTarget,
@@ -37,14 +37,12 @@ import {
3737
type SearchResult,
3838
type SortConfig,
3939
buildSearchIndex,
40-
formatMetadataDate,
4140
searchIndexedNodes,
4241
sortSearchResults,
4342
splitWithHighlights,
4443
stripTypePrefix,
4544
} from "./utils";
4645
import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter";
47-
import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents";
4846
import { AdvancedSearchFooter } from "./AdvancedSearchFooter";
4947

5048
type Props = Record<string, unknown>;
@@ -110,43 +108,6 @@ const ResultRow = ({
110108
</Button>
111109
);
112110

113-
const PreviewPane = ({ result }: { result: SearchResult | null }) => {
114-
if (!result) {
115-
return (
116-
<div className="flex min-h-0 flex-1 items-center justify-center overflow-hidden">
117-
<NonIdealState
118-
icon="search"
119-
title="Search DG nodes"
120-
description="Type a keyword to preview matching discourse graph nodes."
121-
/>
122-
</div>
123-
);
124-
}
125-
const isPage = !!getPageTitleByPageUid(result.uid);
126-
127-
return (
128-
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
129-
<div className="flex-none flex-row gap-2 border-b border-gray-200 px-5 py-3 text-xs text-gray-500">
130-
Created: {formatMetadataDate(result.createdAt)} · Last modified:{" "}
131-
{formatMetadataDate(result.lastModified)} · Author:{" "}
132-
{result.authorName || "Unknown"}
133-
</div>
134-
<div
135-
className="min-h-0 flex-1 overflow-y-auto border-t border-gray-200 px-5 py-3"
136-
onMouseDown={(event) => event.preventDefault()}
137-
>
138-
<div className="pointer-events-none">
139-
{isPage ? (
140-
<RenderRoamPage hideMentions key={result.uid} uid={result.uid} />
141-
) : (
142-
<RenderRoamBlock key={result.uid} uid={result.uid} zoomPath />
143-
)}
144-
</div>
145-
</div>
146-
</div>
147-
);
148-
};
149-
150111
const AdvancedNodeSearchDialog = ({
151112
isOpen,
152113
onClose,
@@ -319,6 +280,51 @@ const AdvancedNodeSearchDialog = ({
319280
: !results.length
320281
? "empty"
321282
: "results";
283+
284+
const onOpenSearchSidebar = useCallback(async () => {
285+
if (contentState !== "results" || !results.length) return;
286+
287+
try {
288+
const parentUid =
289+
(await window.roamAlphaAPI.ui.mainWindow.getOpenPageOrBlockUid()) ||
290+
window.roamAlphaAPI.util.dateToPageUid(new Date());
291+
292+
const sidebarBlockTitle = `Advanced search results: "${debouncedSearchTerm || "(empty query)"}"`;
293+
const sidebarChildren = results.map((result) => ({
294+
text: `[[${result.title}]]`,
295+
}));
296+
297+
const sidebarBlockUid = await createBlock({
298+
parentUid,
299+
order: Number.MAX_VALUE,
300+
node: { text: sidebarBlockTitle, children: sidebarChildren },
301+
});
302+
303+
await window.roamAlphaAPI.ui.rightSidebar.addWindow({
304+
window: {
305+
type: "outline",
306+
// @ts-expect-error - block-uid is valid for outline sidebar windows
307+
// eslint-disable-next-line @typescript-eslint/naming-convention
308+
"block-uid": sidebarBlockUid,
309+
},
310+
});
311+
312+
posthog.capture("Advanced Node Search: Open search sidebar", {
313+
resultCount: results.length,
314+
searchTerm: debouncedSearchTerm,
315+
sortDirection: sort.direction,
316+
sortField: sort.field,
317+
});
318+
onClose();
319+
} catch (error) {
320+
console.error("Failed to open search sidebar results block:", error);
321+
renderToast({
322+
id: "advanced-node-search-sidebar-open-error",
323+
content: "Could not render search results in the right sidebar.",
324+
intent: "danger",
325+
});
326+
}
327+
}, [contentState, debouncedSearchTerm, onClose, results, sort]);
322328
const handleSortChange = useCallback((nextSort: SortConfig): void => {
323329
setSort(nextSort);
324330
}, []);
@@ -357,6 +363,14 @@ const AdvancedNodeSearchDialog = ({
357363
} else if (event.key === "ArrowUp" && results.length) {
358364
event.preventDefault();
359365
setActiveIndex((index) => Math.max(index - 1, 0));
366+
} else if (
367+
event.key === "Enter" &&
368+
event.altKey &&
369+
contentState === "results" &&
370+
results.length
371+
) {
372+
event.preventDefault();
373+
void onOpenSearchSidebar();
360374
} else if (
361375
event.key === "Enter" &&
362376
!event.metaKey &&
@@ -386,15 +400,14 @@ const AdvancedNodeSearchDialog = ({
386400
contentState,
387401
insertTarget,
388402
onClose,
403+
onOpenSearchSidebar,
389404
onInsert,
390405
onOpen,
391406
onOpenInSidebar,
392407
results.length,
393408
],
394409
);
395410

396-
const showSplitView = contentState === "results";
397-
398411
return (
399412
<Dialog
400413
autoFocus={false}
@@ -446,30 +459,25 @@ const AdvancedNodeSearchDialog = ({
446459
/>
447460
</div>
448461
<div className="flex min-h-0 w-full flex-1 overflow-hidden">
449-
{showSplitView ? (
450-
<>
451-
<div
452-
aria-label="Search results"
453-
className="w-1/3 shrink-0 overflow-y-auto border-r border-gray-200 py-1"
454-
ref={resultsPanelRef}
455-
role="listbox"
456-
>
457-
{results.map((result, index) => (
458-
<ResultRow
459-
active={index === activeIndex}
460-
key={result.uid}
461-
keywords={keywords}
462-
nodeConfig={nodeConfigByType[result.type]}
463-
onClick={() => setActiveIndex(index)}
464-
onMouseEnter={() => setActiveIndex(index)}
465-
result={result}
466-
/>
467-
))}
468-
</div>
469-
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
470-
<PreviewPane result={activeResult} />
471-
</div>
472-
</>
462+
{contentState === "results" ? (
463+
<div
464+
aria-label="Search results"
465+
className="w-full overflow-y-auto py-1"
466+
ref={resultsPanelRef}
467+
role="listbox"
468+
>
469+
{results.map((result, index) => (
470+
<ResultRow
471+
active={index === activeIndex}
472+
key={result.uid}
473+
keywords={keywords}
474+
nodeConfig={nodeConfigByType[result.type]}
475+
onClick={() => setActiveIndex(index)}
476+
onMouseEnter={() => setActiveIndex(index)}
477+
result={result}
478+
/>
479+
))}
480+
</div>
473481
) : (
474482
<div className="flex min-h-0 w-full flex-1 items-center justify-center px-4 py-8 text-center text-sm text-gray-500">
475483
{contentState === "indexing" && (
@@ -489,19 +497,24 @@ const AdvancedNodeSearchDialog = ({
489497
<AdvancedSearchFooter
490498
contentState={contentState}
491499
hasActiveResult={!!activeResult}
500+
hasResults={results.length > 0}
492501
insertTarget={insertTarget}
493502
onInsert={() => void onInsert()}
494503
onOpen={() => void onOpen()}
495504
onOpenInSidebar={() => void onOpenInSidebar()}
505+
onOpenSearchSidebar={() => void onOpenSearchSidebar()}
496506
/>
497507
</div>
498508
</Dialog>
499509
);
500510
};
501511

502-
export const renderAdvancedNodeSearchDialog = () =>
512+
export const renderAdvancedNodeSearchSidebar = () =>
503513
renderOverlay({
504514
// eslint-disable-next-line @typescript-eslint/naming-convention
505515
Overlay: AdvancedNodeSearchDialog,
506516
props: {},
507517
});
518+
519+
export const renderAdvancedNodeSearchDialog = () =>
520+
renderAdvancedNodeSearchSidebar();

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ export type AdvancedSearchContentState =
1212
export type AdvancedSearchFooterProps = {
1313
contentState: AdvancedSearchContentState;
1414
hasActiveResult: boolean;
15+
hasResults: boolean;
1516
insertTarget: InsertTarget | null;
1617
onInsert: () => void;
1718
onOpen: () => void;
1819
onOpenInSidebar: () => void;
20+
onOpenSearchSidebar: () => void;
1921
};
2022

2123
const footerKbdClassName =
@@ -99,6 +101,21 @@ const InsertFooterAction = ({
99101
/>
100102
);
101103

104+
export const OpenSearchSidebarFooterAction = ({
105+
disabled,
106+
onOpenSearchSidebar,
107+
}: {
108+
disabled: boolean;
109+
onOpenSearchSidebar: () => void;
110+
}) => (
111+
<FooterShortcutHint
112+
disabled={disabled}
113+
keyIcons={["key-option", "key-enter"]}
114+
label="open search sidebar"
115+
onClick={() => void onOpenSearchSidebar()}
116+
/>
117+
);
118+
102119
const CloseFooterHint = () => (
103120
<span className={footerLabelClassName}>
104121
<kbd className={footerKbdClassName}>
@@ -111,18 +128,25 @@ const CloseFooterHint = () => (
111128
export const AdvancedSearchFooter = ({
112129
contentState,
113130
hasActiveResult,
131+
hasResults,
114132
insertTarget,
115133
onInsert,
116134
onOpen,
117135
onOpenInSidebar,
136+
onOpenSearchSidebar,
118137
}: AdvancedSearchFooterProps) => {
119-
const hasResults = contentState === "results";
120-
const canOpen = hasActiveResult && hasResults;
121-
const canInsert = !!insertTarget && hasActiveResult && hasResults;
138+
const hasResultsState = contentState === "results";
139+
const canOpen = hasActiveResult && hasResultsState;
140+
const canInsert = !!insertTarget && hasActiveResult && hasResultsState;
141+
const canOpenSearchSidebar = hasResults && hasResultsState;
122142

123143
return (
124144
<div className="flex w-full flex-none items-center justify-between border-t border-gray-200 bg-gray-50 px-3 py-2">
125145
<div className="inline-flex shrink-0 items-center gap-3">
146+
<OpenSearchSidebarFooterAction
147+
disabled={!canOpenSearchSidebar}
148+
onOpenSearchSidebar={onOpenSearchSidebar}
149+
/>
126150
{insertTarget && (
127151
<InsertFooterAction disabled={!canInsert} onInsert={onInsert} />
128152
)}

apps/roam/src/utils/registerCommandPaletteCommands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { getUidAndBooleanSetting } from "~/utils/getExportSettings";
4646
import refreshConfigTree from "~/utils/refreshConfigTree";
4747
import { refreshAndNotify } from "~/components/LeftSidebarView";
4848
import { sectionsToBlockProps } from "~/components/settings/LeftSidebarPersonalSettings";
49-
import { renderAdvancedNodeSearchDialog } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog";
49+
import { renderAdvancedNodeSearchSidebar } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog";
5050
import {
5151
getBlockSelection,
5252
insertPageRefAtRange,
@@ -367,7 +367,7 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => {
367367
if (getFeatureFlag("Advanced node search enabled")) {
368368
void addCommand("DG: Open Node Search", () => {
369369
posthog.capture("Node Search: Open Command Triggered");
370-
renderAdvancedNodeSearchDialog();
370+
renderAdvancedNodeSearchSidebar();
371371
});
372372
}
373373
void addCommand("DG: Open - Query drawer", openQueryDrawerWithArgs);

0 commit comments

Comments
 (0)