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,18 +1,11 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Button,
Dialog,
InputGroup,
NonIdealState,
Spinner,
SpinnerSize,
Tag,
} from "@blueprintjs/core";
import MiniSearch from "minisearch";
import posthog from "posthog-js";
Expand All @@ -30,86 +23,27 @@ import { DiscourseNodeSortControl } from "~/components/DiscourseNodeSortControl"
import getDiscourseNodes, {
type DiscourseNode,
} from "~/utils/getDiscourseNodes";
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
import { openSearchResultInMain } from "~/utils/advancedSearchNavigation";
import { openDgSearchInSidebar } from "~/utils/openDgSearchInSidebar";
import {
DEBOUNCE_MS,
DEFAULT_SORT_CONFIG,
type SearchResult,
type SortConfig,
buildSearchIndex,
formatMetadataDate,
getSearchKeywords,
searchIndexedNodes,
sortSearchResults,
splitWithHighlights,
stripTypePrefix,
} from "./utils";
import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter";
import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents";
import { AdvancedSearchFooter } from "./AdvancedSearchFooter";
import { AdvancedSearchDialogResultsList } from "./AdvancedSearchSidebarPanel";

type Props = Record<string, unknown>;

const getNodeBadgeText = (node: DiscourseNode): string =>
(node.tag?.trim() || node.text).slice(0, 3).toUpperCase();

const getTagStyle = (node: DiscourseNode | undefined): React.CSSProperties => {
const color = node?.canvasSettings?.color;
if (!color) return { flexShrink: 0 };
return { ...getNodeTagStyles(color), flexShrink: 0 };
};

const renderHighlightedText = (
text: string,
keywords: string[],
): React.ReactNode =>
splitWithHighlights(text, keywords).map((segment, index) =>
segment.isMatch ? (
<mark key={`${segment.text}-${index}`}>{segment.text}</mark>
) : (
<React.Fragment key={`${segment.text}-${index}`}>
{segment.text}
</React.Fragment>
),
);

const ResultRow = ({
active,
keywords,
nodeConfig,
onClick,
onMouseEnter,
result,
}: {
active: boolean;
keywords: string[];
nodeConfig: DiscourseNode | undefined;
onClick: () => void;
onMouseEnter: () => void;
result: SearchResult;
}) => (
<Button
alignText="left"
aria-selected={active}
className="flex-none !items-start gap-2 !px-3 !py-2"
fill
minimal
onClick={onClick}
onMouseEnter={onMouseEnter}
role="option"
style={{
background: active ? "rgba(95, 87, 192, 0.08)" : undefined,
boxShadow: active ? "inset 3px 0 0 #5f57c0" : undefined,
}}
>
<Tag minimal style={getTagStyle(nodeConfig)}>
{nodeConfig ? getNodeBadgeText(nodeConfig) : result.nodeTypeLabel}
</Tag>
<span className="min-w-0 break-words text-sm leading-snug text-gray-900">
{renderHighlightedText(stripTypePrefix(result.title), keywords)}
</span>
</Button>
);

const PreviewPane = ({ result }: { result: SearchResult | null }) => {
if (!result) {
return (
Expand Down Expand Up @@ -173,7 +107,7 @@ const AdvancedNodeSearchDialog = ({
);

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

useEffect(() => {
if (!isOpen) return;
Expand Down Expand Up @@ -319,19 +253,50 @@ const AdvancedNodeSearchDialog = ({
: !results.length
? "empty"
: "results";

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

try {
await openDgSearchInSidebar({
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);
}, []);

const onOpen = useCallback(async () => {
if (!activeResult || contentState !== "results") return;

const uid = activeResult.uid;
if (getPageTitleByPageUid(uid)) {
await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } });
} else {
await window.roamAlphaAPI.ui.mainWindow.openBlock({ block: { uid } });
}
await openSearchResultInMain(activeResult.uid);
onClose();
}, [activeResult, contentState, onClose]);

Expand All @@ -357,6 +322,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 +359,7 @@ const AdvancedNodeSearchDialog = ({
contentState,
insertTarget,
onClose,
onOpenSearchSidebar,
onInsert,
onOpen,
onOpenInSidebar,
Expand Down Expand Up @@ -454,17 +428,13 @@ const AdvancedNodeSearchDialog = ({
ref={resultsPanelRef}
role="listbox"
>
{results.map((result, index) => (
<ResultRow
active={index === activeIndex}
key={result.uid}
keywords={keywords}
nodeConfig={nodeConfigByType[result.type]}
onClick={() => setActiveIndex(index)}
onMouseEnter={() => setActiveIndex(index)}
result={result}
/>
))}
<AdvancedSearchDialogResultsList
activeIndex={activeIndex}
keywords={keywords}
nodeConfigByType={nodeConfigByType}
onSelect={setActiveIndex}
results={results}
/>
</div>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<PreviewPane result={activeResult} />
Expand Down Expand Up @@ -493,15 +463,19 @@ const AdvancedNodeSearchDialog = ({
onInsert={() => void onInsert()}
onOpen={() => void onOpen()}
onOpenInSidebar={() => void onOpenInSidebar()}
onOpenSearchSidebar={() => void onOpenSearchSidebar()}
/>
</div>
</Dialog>
);
};

export const renderAdvancedNodeSearchDialog = () =>
export const renderAdvancedNodeSearchSidebar = () =>
renderOverlay({
// eslint-disable-next-line @typescript-eslint/naming-convention
Overlay: AdvancedNodeSearchDialog,
props: {},
});

export const renderAdvancedNodeSearchDialog = () =>
renderAdvancedNodeSearchSidebar();
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 = ({
/>
);

export 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