Skip to content

Commit 0f66bc2

Browse files
committed
current progress, new UI
1 parent 49d80fb commit 0f66bc2

7 files changed

Lines changed: 423 additions & 30 deletions

File tree

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

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU
2121
import renderOverlay, {
2222
RoamOverlayProps,
2323
} from "roamjs-components/util/renderOverlay";
24-
import createPage from "roamjs-components/writes/createPage";
25-
import { createBlock } from "roamjs-components/writes";
2624
import {
2725
insertPageRefAtRange,
2826
snapshotInsertTarget,
@@ -46,7 +44,9 @@ import {
4644
stripTypePrefix,
4745
} from "./utils";
4846
import { DiscourseNodeTypeFilter } from "~/components/AdvancedNodeSearchDialog/DiscourseNodeTypeFilter";
47+
import { RenderRoamBlock, RenderRoamPage } from "~/utils/roamReactComponents";
4948
import { AdvancedSearchFooter } from "./AdvancedSearchFooter";
49+
import { openDgSearchInSidebar } from "./openDgSearchInSidebar";
5050

5151
type Props = Record<string, unknown>;
5252

@@ -102,7 +102,7 @@ const ResultRow = ({
102102
boxShadow: active ? "inset 3px 0 0 #5f57c0" : undefined,
103103
}}
104104
>
105-
<Tag minimal style={getTagStyle(nodeConfig)}>
105+
<Tag className="shrink-0" minimal style={getTagStyle(nodeConfig)}>
106106
{nodeConfig ? getNodeBadgeText(nodeConfig) : result.nodeTypeLabel}
107107
</Tag>
108108
<span className="min-w-0 break-words text-sm leading-snug text-gray-900">
@@ -325,43 +325,24 @@ const AdvancedNodeSearchDialog = ({
325325
if (contentState !== "results" || !results.length) return;
326326

327327
try {
328-
const sidebarBlockTitle = `Advanced search results: "${debouncedSearchTerm || "(empty query)"}"`;
329-
const sidebarChildren = results.map((result) => ({
330-
text: `[[${result.title}]]`,
331-
}));
332-
333-
const sidebarPageUid = await createPage({ title: sidebarBlockTitle });
334-
await Promise.all(
335-
sidebarChildren.map((node, order) =>
336-
createBlock({
337-
parentUid: sidebarPageUid,
338-
order,
339-
node,
340-
}),
341-
),
342-
);
343-
344-
await window.roamAlphaAPI.ui.rightSidebar.addWindow({
345-
window: {
346-
type: "outline",
347-
// @ts-expect-error - block-uid is valid for outline sidebar windows
348-
// eslint-disable-next-line @typescript-eslint/naming-convention
349-
"block-uid": sidebarPageUid,
350-
},
328+
await openDgSearchInSidebar({
329+
query: debouncedSearchTerm,
330+
sort,
331+
results,
351332
});
352333

353-
posthog.capture("Advanced Node Search: Open search sidebar", {
334+
posthog.capture("Advanced Node Search: Dock search sidebar", {
354335
resultCount: results.length,
355336
searchTerm: debouncedSearchTerm,
356337
sortDirection: sort.direction,
357338
sortField: sort.field,
358339
});
359340
onClose();
360341
} catch (error) {
361-
console.error("Failed to open search sidebar results block:", error);
342+
console.error("Failed to dock search results in the sidebar:", error);
362343
renderToast({
363344
id: "advanced-node-search-sidebar-open-error",
364-
content: "Could not render search results in the right sidebar.",
345+
content: "Could not dock search results in the right sidebar.",
365346
intent: "danger",
366347
});
367348
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export const OpenSearchSidebarFooterAction = ({
110110
<FooterShortcutHint
111111
disabled={disabled}
112112
keyIcons={["key-option", "key-enter"]}
113-
label="open search sidebar"
113+
label="dock results"
114114
onClick={() => void onOpenSearchSidebar()}
115115
/>
116116
);
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React from "react";
2+
import { Button, Icon, Tag } from "@blueprintjs/core";
3+
import getRoamUrl from "roamjs-components/dom/getRoamUrl";
4+
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
5+
import type { DiscourseNode } from "~/utils/getDiscourseNodes";
6+
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
7+
import {
8+
type SearchResult,
9+
splitWithHighlights,
10+
stripTypePrefix,
11+
} from "./utils";
12+
import { openSearchResultFromLinkEvent } from "./openSearchResult";
13+
14+
const getNodeBadgeText = (node: DiscourseNode): string =>
15+
(node.tag?.trim() || node.text).slice(0, 3).toUpperCase();
16+
17+
const getTagStyle = (node: DiscourseNode | undefined): React.CSSProperties => {
18+
const color = node?.canvasSettings?.color;
19+
if (!color) return {};
20+
return getNodeTagStyles(color) ?? {};
21+
};
22+
23+
export const renderHighlightedText = (
24+
text: string,
25+
keywords: string[],
26+
): React.ReactNode =>
27+
splitWithHighlights(text, keywords).map((segment, index) =>
28+
segment.isMatch ? (
29+
<mark key={`${segment.text}-${index}`}>{segment.text}</mark>
30+
) : (
31+
<React.Fragment key={`${segment.text}-${index}`}>
32+
{segment.text}
33+
</React.Fragment>
34+
),
35+
);
36+
37+
type AdvancedSearchDialogResultsListProps = {
38+
activeIndex: number;
39+
keywords: string[];
40+
nodeConfigByType: Record<string, DiscourseNode>;
41+
onSelect: (index: number) => void;
42+
results: SearchResult[];
43+
};
44+
45+
export const AdvancedSearchDialogResultsList = ({
46+
activeIndex,
47+
keywords,
48+
nodeConfigByType,
49+
onSelect,
50+
results,
51+
}: AdvancedSearchDialogResultsListProps) => (
52+
<>
53+
{results.map((result, index) => (
54+
<Button
55+
alignText="left"
56+
aria-selected={index === activeIndex}
57+
className="flex-none !items-start gap-2 !px-3 !py-2"
58+
fill
59+
key={result.uid}
60+
minimal
61+
onClick={() => onSelect(index)}
62+
onMouseEnter={() => onSelect(index)}
63+
role="option"
64+
style={{
65+
background:
66+
index === activeIndex ? "rgba(95, 87, 192, 0.08)" : undefined,
67+
boxShadow:
68+
index === activeIndex ? "inset 3px 0 0 #5f57c0" : undefined,
69+
}}
70+
>
71+
<Tag
72+
className="shrink-0"
73+
minimal
74+
style={getTagStyle(nodeConfigByType[result.type])}
75+
>
76+
{nodeConfigByType[result.type]
77+
? getNodeBadgeText(nodeConfigByType[result.type])
78+
: result.nodeTypeLabel}
79+
</Tag>
80+
<span className="min-w-0 break-words text-sm leading-snug text-gray-900">
81+
{renderHighlightedText(stripTypePrefix(result.title), keywords)}
82+
</span>
83+
</Button>
84+
))}
85+
</>
86+
);
87+
88+
type AdvancedSearchSidebarResultsListProps = {
89+
keywords: string[];
90+
results: SearchResult[];
91+
};
92+
93+
export const AdvancedSearchSidebarResultsList = ({
94+
keywords,
95+
results,
96+
}: AdvancedSearchSidebarResultsListProps) => (
97+
<div className="rm-search-query-content">
98+
{results.map((result) => {
99+
const displayTitle = stripTypePrefix(result.title);
100+
const pageTitle = getPageTitleByPageUid(result.uid) || displayTitle;
101+
102+
return (
103+
<div
104+
className="rm-search-query__page-row dont-focus-block"
105+
key={result.uid}
106+
>
107+
<Icon
108+
className="rm-search-query__page-row-icon"
109+
icon="document"
110+
iconSize={16}
111+
/>
112+
<span className="rm-search-query__page-row-title">
113+
<a
114+
className="rm-page-ref rm-page-ref--link"
115+
data-link-title={pageTitle}
116+
data-link-uid={result.uid}
117+
href={getRoamUrl(result.uid)}
118+
onMouseDown={(event) => {
119+
if (event.shiftKey) {
120+
event.preventDefault();
121+
event.stopPropagation();
122+
void openSearchResultFromLinkEvent({
123+
uid: result.uid,
124+
shiftKey: true,
125+
});
126+
}
127+
}}
128+
onClick={(event) => {
129+
if (event.shiftKey) {
130+
event.preventDefault();
131+
event.stopPropagation();
132+
}
133+
}}
134+
>
135+
<span className="rm-page__title cursor-pointer">
136+
{renderHighlightedText(displayTitle, keywords)}
137+
</span>
138+
</a>
139+
</span>
140+
</div>
141+
);
142+
})}
143+
</div>
144+
);
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { NonIdealState, Spinner, SpinnerSize } from "@blueprintjs/core";
3+
import MiniSearch from "minisearch";
4+
import getDiscourseNodes from "~/utils/getDiscourseNodes";
5+
import {
6+
DEBOUNCE_MS,
7+
type SearchResult,
8+
type SortConfig,
9+
buildSearchIndex,
10+
searchIndexedNodes,
11+
sortSearchResults,
12+
} from "./utils";
13+
import type { AdvancedNodeSearchSession } from "./advancedSearchSession";
14+
import { AdvancedSearchSidebarResultsList } from "./AdvancedSearchResultsList";
15+
16+
type AdvancedSearchSidebarPanelProps = {
17+
initialSession: AdvancedNodeSearchSession;
18+
};
19+
20+
export const AdvancedSearchSidebarPanel = ({
21+
initialSession,
22+
}: AdvancedSearchSidebarPanelProps) => {
23+
const [searchTerm, setSearchTerm] = useState(initialSession.query);
24+
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(
25+
initialSession.query,
26+
);
27+
const [sort] = useState<SortConfig>(initialSession.sort);
28+
const [results, setResults] = useState<SearchResult[]>(
29+
initialSession.results,
30+
);
31+
const [isIndexLoading, setIsIndexLoading] = useState(true);
32+
const [indexError, setIndexError] = useState(false);
33+
34+
const miniSearchRef = useRef<MiniSearch<
35+
SearchResult & { id: string }
36+
> | null>(null);
37+
const allResultsRef = useRef<SearchResult[]>([]);
38+
39+
const keywords = debouncedSearchTerm.split(/\s+/).filter(Boolean);
40+
41+
useEffect(() => {
42+
const timeout = setTimeout(
43+
() => setDebouncedSearchTerm(searchTerm.trim()),
44+
DEBOUNCE_MS,
45+
);
46+
return () => clearTimeout(timeout);
47+
}, [searchTerm]);
48+
49+
useEffect(() => {
50+
let cancelled = false;
51+
setIsIndexLoading(true);
52+
setIndexError(false);
53+
54+
const discourseNodes = getDiscourseNodes().filter(
55+
(node) => node.backedBy === "user",
56+
);
57+
58+
void buildSearchIndex(discourseNodes)
59+
.then(({ miniSearch, results: indexedResults }) => {
60+
if (cancelled) return;
61+
miniSearchRef.current = miniSearch;
62+
allResultsRef.current = indexedResults;
63+
})
64+
.catch((error) => {
65+
console.error(
66+
"Error building advanced node search sidebar index:",
67+
error,
68+
);
69+
if (!cancelled) setIndexError(true);
70+
})
71+
.finally(() => {
72+
if (!cancelled) setIsIndexLoading(false);
73+
});
74+
75+
return () => {
76+
cancelled = true;
77+
};
78+
}, []);
79+
80+
useEffect(() => {
81+
if (
82+
isIndexLoading ||
83+
indexError ||
84+
!debouncedSearchTerm ||
85+
!miniSearchRef.current
86+
) {
87+
if (!debouncedSearchTerm) setResults([]);
88+
return;
89+
}
90+
91+
const scoredHits = searchIndexedNodes({
92+
miniSearch: miniSearchRef.current,
93+
allResults: allResultsRef.current,
94+
searchTerm: debouncedSearchTerm,
95+
});
96+
97+
setResults(sortSearchResults({ hits: scoredHits, sort }));
98+
}, [debouncedSearchTerm, indexError, isIndexLoading, sort]);
99+
100+
const resultLabel =
101+
results.length === 1 ? "1 result" : `${results.length} results`;
102+
103+
return (
104+
<div className="dg-node-search-sidebar rm-sidebar-search box-border w-full min-w-0">
105+
<div className="dg-node-search-sidebar__input-row -ml-2 mr-2 box-border flex w-full min-w-0 items-center">
106+
<input
107+
className="bp3-input dg-node-search-sidebar__input box-border block w-full min-w-0 max-w-full"
108+
onChange={(event) => setSearchTerm(event.target.value)}
109+
placeholder="Search discourse nodes..."
110+
type="text"
111+
value={searchTerm}
112+
/>
113+
</div>
114+
<div className="rm-reference-main rm-search-query-content">
115+
{indexError ? (
116+
<NonIdealState
117+
icon="error"
118+
title="Search unavailable"
119+
description="Reload the extension and try again."
120+
/>
121+
) : (
122+
<>
123+
<span className="ml-[9px] text-[0.9em]">
124+
{isIndexLoading && !results.length
125+
? "Loading…"
126+
: debouncedSearchTerm
127+
? resultLabel
128+
: "Type to search"}
129+
</span>
130+
{isIndexLoading && !results.length ? (
131+
<div className="flex justify-center py-4">
132+
<Spinner size={SpinnerSize.SMALL} />
133+
</div>
134+
) : (
135+
<>
136+
{debouncedSearchTerm && results.length > 0 && (
137+
<AdvancedSearchSidebarResultsList
138+
keywords={keywords}
139+
results={results}
140+
/>
141+
)}
142+
{debouncedSearchTerm && !results.length && !isIndexLoading && (
143+
<p className="px-2 py-3 text-sm text-gray-500">
144+
No matches. Try another keyword.
145+
</p>
146+
)}
147+
</>
148+
)}
149+
</>
150+
)}
151+
</div>
152+
</div>
153+
);
154+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { SearchResult, SortConfig } from "./utils";
2+
3+
export type AdvancedNodeSearchSession = {
4+
query: string;
5+
sort: SortConfig;
6+
results: SearchResult[];
7+
};

0 commit comments

Comments
 (0)