From 1a3d3611c1211bc51aa86073aaecd0057ee56b65 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 22 May 2026 19:24:53 -0400 Subject: [PATCH 1/4] 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 --- .../AdvancedSearchDialog.tsx | 147 ++++++++++-------- .../AdvancedSearchFooter.tsx | 30 +++- .../utils/registerCommandPaletteCommands.ts | 4 +- 3 files changed, 109 insertions(+), 72 deletions(-) diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index ed7694537..f8f6189d7 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -9,7 +9,6 @@ import { Button, Dialog, InputGroup, - NonIdealState, Spinner, SpinnerSize, Tag, @@ -21,6 +20,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import renderOverlay, { RoamOverlayProps, } from "roamjs-components/util/renderOverlay"; +import { createBlock } from "roamjs-components/writes"; import { insertPageRefAtRange, snapshotInsertTarget, @@ -37,13 +37,11 @@ import { type SearchResult, type SortConfig, buildSearchIndex, - formatMetadataDate, searchIndexedNodes, sortSearchResults, splitWithHighlights, stripTypePrefix, } from "./utils"; -import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents"; import { AdvancedSearchFooter } from "./AdvancedSearchFooter"; type Props = Record; @@ -109,43 +107,6 @@ const ResultRow = ({ ); -const PreviewPane = ({ result }: { result: SearchResult | null }) => { - if (!result) { - return ( -
- -
- ); - } - const isPage = !!getPageTitleByPageUid(result.uid); - - return ( -
-
- Created: {formatMetadataDate(result.createdAt)} · Last modified:{" "} - {formatMetadataDate(result.lastModified)} · Author:{" "} - {result.authorName || "Unknown"} -
-
event.preventDefault()} - > -
- {isPage ? ( - - ) : ( - - )} -
-
-
- ); -}; - const AdvancedNodeSearchDialog = ({ isOpen, onClose, @@ -311,6 +272,51 @@ const AdvancedNodeSearchDialog = ({ : !results.length ? "empty" : "results"; + + const onOpenSearchSidebar = useCallback(async () => { + if (contentState !== "results" || !results.length) return; + + try { + const parentUid = + (await window.roamAlphaAPI.ui.mainWindow.getOpenPageOrBlockUid()) || + window.roamAlphaAPI.util.dateToPageUid(new Date()); + + const sidebarBlockTitle = `Advanced search results: "${debouncedSearchTerm || "(empty query)"}"`; + const sidebarChildren = results.map((result) => ({ + text: `[[${result.title}]]`, + })); + + const sidebarBlockUid = await createBlock({ + parentUid, + order: Number.MAX_VALUE, + node: { text: sidebarBlockTitle, children: sidebarChildren }, + }); + + await window.roamAlphaAPI.ui.rightSidebar.addWindow({ + window: { + type: "outline", + // @ts-expect-error - block-uid is valid for outline sidebar windows + // eslint-disable-next-line @typescript-eslint/naming-convention + "block-uid": sidebarBlockUid, + }, + }); + + posthog.capture("Advanced Node Search: Open search sidebar", { + resultCount: results.length, + searchTerm: debouncedSearchTerm, + sortDirection: sort.direction, + sortField: sort.field, + }); + onClose(); + } catch (error) { + console.error("Failed to open search sidebar results block:", error); + renderToast({ + id: "advanced-node-search-sidebar-open-error", + content: "Could not render search results in the right sidebar.", + intent: "danger", + }); + } + }, [contentState, debouncedSearchTerm, onClose, results, sort]); const handleSortChange = useCallback((nextSort: SortConfig): void => { setSort(nextSort); }, []); @@ -349,6 +355,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 && @@ -378,6 +392,7 @@ const AdvancedNodeSearchDialog = ({ contentState, insertTarget, onClose, + onOpenSearchSidebar, onInsert, onOpen, onOpenInSidebar, @@ -385,8 +400,6 @@ const AdvancedNodeSearchDialog = ({ ], ); - const showSplitView = contentState === "results"; - return (
- {showSplitView ? ( - <> -
- {results.map((result, index) => ( - setActiveIndex(index)} - onMouseEnter={() => setActiveIndex(index)} - result={result} - /> - ))} -
-
- -
- + {contentState === "results" ? ( +
+ {results.map((result, index) => ( + setActiveIndex(index)} + onMouseEnter={() => setActiveIndex(index)} + result={result} + /> + ))} +
) : (
{contentState === "indexing" && ( @@ -477,19 +485,24 @@ const AdvancedNodeSearchDialog = ({ 0} insertTarget={insertTarget} onInsert={() => void onInsert()} onOpen={() => void onOpen()} onOpenInSidebar={() => void onOpenInSidebar()} + onOpenSearchSidebar={() => void onOpenSearchSidebar()} />
); }; -export const renderAdvancedNodeSearchDialog = () => +export const renderAdvancedNodeSearchSidebar = () => renderOverlay({ // eslint-disable-next-line @typescript-eslint/naming-convention Overlay: AdvancedNodeSearchDialog, props: {}, }); + +export const renderAdvancedNodeSearchDialog = () => + renderAdvancedNodeSearchSidebar(); diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx index 26f001084..46cd8c404 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx @@ -12,10 +12,12 @@ export type AdvancedSearchContentState = export type AdvancedSearchFooterProps = { contentState: AdvancedSearchContentState; hasActiveResult: boolean; + hasResults: boolean; insertTarget: InsertTarget | null; onInsert: () => void; onOpen: () => void; onOpenInSidebar: () => void; + onOpenSearchSidebar: () => void; }; const footerKbdClassName = @@ -99,6 +101,21 @@ const InsertFooterAction = ({ /> ); +export const OpenSearchSidebarFooterAction = ({ + disabled, + onOpenSearchSidebar, +}: { + disabled: boolean; + onOpenSearchSidebar: () => void; +}) => ( + void onOpenSearchSidebar()} + /> +); + const CloseFooterHint = () => ( @@ -111,18 +128,25 @@ const CloseFooterHint = () => ( export const AdvancedSearchFooter = ({ contentState, hasActiveResult, + hasResults, insertTarget, onInsert, onOpen, onOpenInSidebar, + onOpenSearchSidebar, }: AdvancedSearchFooterProps) => { - const hasResults = contentState === "results"; - const canOpen = hasActiveResult && hasResults; - const canInsert = !!insertTarget && hasActiveResult && hasResults; + const hasResultsState = contentState === "results"; + const canOpen = hasActiveResult && hasResultsState; + const canInsert = !!insertTarget && hasActiveResult && hasResultsState; + const canOpenSearchSidebar = hasResults && hasResultsState; return (
+ {insertTarget && ( )} diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 96d5433f8..6e8893f40 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -46,7 +46,7 @@ import { getUidAndBooleanSetting } from "~/utils/getExportSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { refreshAndNotify } from "~/components/LeftSidebarView"; import { sectionsToBlockProps } from "~/components/settings/LeftSidebarPersonalSettings"; -import { renderAdvancedNodeSearchDialog } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog"; +import { renderAdvancedNodeSearchSidebar } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog"; import { getBlockSelection, insertPageRefAtRange, @@ -367,7 +367,7 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { if (getFeatureFlag("Advanced node search enabled")) { void addCommand("DG: Open Node Search", () => { posthog.capture("Node Search: Open Command Triggered"); - renderAdvancedNodeSearchDialog(); + renderAdvancedNodeSearchSidebar(); }); } void addCommand("DG: Open - Query drawer", openQueryDrawerWithArgs); From b574390592d45c1e432a638dcd40b5e9459d4bc4 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 22 May 2026 19:31:15 -0400 Subject: [PATCH 2/4] revert irrelevant changes --- .../AdvancedSearchDialog.tsx | 85 ++++++++++++++----- .../utils/registerCommandPaletteCommands.ts | 4 +- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index f8f6189d7..a5cc65978 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -9,6 +9,7 @@ import { Button, Dialog, InputGroup, + NonIdealState, Spinner, SpinnerSize, Tag, @@ -37,11 +38,13 @@ import { type SearchResult, type SortConfig, buildSearchIndex, + formatMetadataDate, searchIndexedNodes, sortSearchResults, splitWithHighlights, stripTypePrefix, } from "./utils"; +import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents"; import { AdvancedSearchFooter } from "./AdvancedSearchFooter"; type Props = Record; @@ -107,6 +110,43 @@ const ResultRow = ({ ); +const PreviewPane = ({ result }: { result: SearchResult | null }) => { + if (!result) { + return ( +
+ +
+ ); + } + const isPage = !!getPageTitleByPageUid(result.uid); + + return ( +
+
+ Created: {formatMetadataDate(result.createdAt)} · Last modified:{" "} + {formatMetadataDate(result.lastModified)} · Author:{" "} + {result.authorName || "Unknown"} +
+
event.preventDefault()} + > +
+ {isPage ? ( + + ) : ( + + )} +
+
+
+ ); +}; + const AdvancedNodeSearchDialog = ({ isOpen, onClose, @@ -400,6 +440,8 @@ const AdvancedNodeSearchDialog = ({ ], ); + const showSplitView = contentState === "results"; + return (
- {contentState === "results" ? ( -
- {results.map((result, index) => ( - setActiveIndex(index)} - onMouseEnter={() => setActiveIndex(index)} - result={result} - /> - ))} -
+ {showSplitView ? ( + <> +
+ {results.map((result, index) => ( + setActiveIndex(index)} + onMouseEnter={() => setActiveIndex(index)} + result={result} + /> + ))} +
+
+ +
+ ) : (
{contentState === "indexing" && ( diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 6e8893f40..96d5433f8 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -46,7 +46,7 @@ import { getUidAndBooleanSetting } from "~/utils/getExportSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { refreshAndNotify } from "~/components/LeftSidebarView"; import { sectionsToBlockProps } from "~/components/settings/LeftSidebarPersonalSettings"; -import { renderAdvancedNodeSearchSidebar } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog"; +import { renderAdvancedNodeSearchDialog } from "~/components/AdvancedNodeSearchDialog/AdvancedSearchDialog"; import { getBlockSelection, insertPageRefAtRange, @@ -367,7 +367,7 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { if (getFeatureFlag("Advanced node search enabled")) { void addCommand("DG: Open Node Search", () => { posthog.capture("Node Search: Open Command Triggered"); - renderAdvancedNodeSearchSidebar(); + renderAdvancedNodeSearchDialog(); }); } void addCommand("DG: Open - Query drawer", openQueryDrawerWithArgs); From 4aa0a6820fc6c6a6f3a3016a8bf11675b2e8d202 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 23 May 2026 14:42:21 -0400 Subject: [PATCH 3/4] switch to add page --- .../AdvancedSearchDialog.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index a5cc65978..a44bd1563 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -21,6 +21,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import renderOverlay, { RoamOverlayProps, } from "roamjs-components/util/renderOverlay"; +import createPage from "roamjs-components/writes/createPage"; import { createBlock } from "roamjs-components/writes"; import { insertPageRefAtRange, @@ -317,27 +318,28 @@ const AdvancedNodeSearchDialog = ({ if (contentState !== "results" || !results.length) return; try { - const parentUid = - (await window.roamAlphaAPI.ui.mainWindow.getOpenPageOrBlockUid()) || - window.roamAlphaAPI.util.dateToPageUid(new Date()); - const sidebarBlockTitle = `Advanced search results: "${debouncedSearchTerm || "(empty query)"}"`; const sidebarChildren = results.map((result) => ({ text: `[[${result.title}]]`, })); - const sidebarBlockUid = await createBlock({ - parentUid, - order: Number.MAX_VALUE, - node: { text: sidebarBlockTitle, children: sidebarChildren }, - }); + const sidebarPageUid = await createPage({ title: sidebarBlockTitle }); + await Promise.all( + sidebarChildren.map((node, order) => + createBlock({ + parentUid: sidebarPageUid, + order, + node, + }), + ), + ); await window.roamAlphaAPI.ui.rightSidebar.addWindow({ window: { type: "outline", // @ts-expect-error - block-uid is valid for outline sidebar windows // eslint-disable-next-line @typescript-eslint/naming-convention - "block-uid": sidebarBlockUid, + "block-uid": sidebarPageUid, }, }); From d392a6af2f09d79c668cd0d61796d7b837c10147 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Sat, 23 May 2026 23:32:39 -0400 Subject: [PATCH 4/4] cleanup --- .../AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx | 1 - .../AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx | 10 ++++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index a44bd1563..761c76ecf 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -534,7 +534,6 @@ const AdvancedNodeSearchDialog = ({ 0} insertTarget={insertTarget} onInsert={() => void onInsert()} onOpen={() => void onOpen()} diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx index 46cd8c404..aebbdc301 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx @@ -12,7 +12,6 @@ export type AdvancedSearchContentState = export type AdvancedSearchFooterProps = { contentState: AdvancedSearchContentState; hasActiveResult: boolean; - hasResults: boolean; insertTarget: InsertTarget | null; onInsert: () => void; onOpen: () => void; @@ -128,17 +127,16 @@ const CloseFooterHint = () => ( export const AdvancedSearchFooter = ({ contentState, hasActiveResult, - hasResults, insertTarget, onInsert, onOpen, onOpenInSidebar, onOpenSearchSidebar, }: AdvancedSearchFooterProps) => { - const hasResultsState = contentState === "results"; - const canOpen = hasActiveResult && hasResultsState; - const canInsert = !!insertTarget && hasActiveResult && hasResultsState; - const canOpenSearchSidebar = hasResults && hasResultsState; + const hasResults = contentState === "results"; + const canOpen = hasActiveResult && hasResults; + const canInsert = !!insertTarget && hasActiveResult && hasResults; + const canOpenSearchSidebar = hasResults; return (