diff --git a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx index ed7694537..761c76ecf 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchDialog.tsx @@ -21,6 +21,8 @@ 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, snapshotInsertTarget, @@ -311,6 +313,52 @@ const AdvancedNodeSearchDialog = ({ : !results.length ? "empty" : "results"; + + const onOpenSearchSidebar = useCallback(async () => { + if (contentState !== "results" || !results.length) return; + + try { + const sidebarBlockTitle = `Advanced search results: "${debouncedSearchTerm || "(empty query)"}"`; + const sidebarChildren = results.map((result) => ({ + text: `[[${result.title}]]`, + })); + + 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": sidebarPageUid, + }, + }); + + 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 +397,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 +434,7 @@ const AdvancedNodeSearchDialog = ({ contentState, insertTarget, onClose, + onOpenSearchSidebar, onInsert, onOpen, onOpenInSidebar, @@ -481,15 +538,19 @@ const AdvancedNodeSearchDialog = ({ 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..aebbdc301 100644 --- a/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx +++ b/apps/roam/src/components/AdvancedNodeSearchDialog/AdvancedSearchFooter.tsx @@ -16,6 +16,7 @@ export type AdvancedSearchFooterProps = { onInsert: () => void; onOpen: () => void; onOpenInSidebar: () => void; + onOpenSearchSidebar: () => void; }; const footerKbdClassName = @@ -99,6 +100,21 @@ const InsertFooterAction = ({ /> ); +export const OpenSearchSidebarFooterAction = ({ + disabled, + onOpenSearchSidebar, +}: { + disabled: boolean; + onOpenSearchSidebar: () => void; +}) => ( + void onOpenSearchSidebar()} + /> +); + const CloseFooterHint = () => ( @@ -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 (
+ {insertTarget && ( )}