Skip to content

Commit dfa6bad

Browse files
committed
new layout
1 parent d70090f commit dfa6bad

6 files changed

Lines changed: 166 additions & 60 deletions

File tree

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -470,12 +470,9 @@ const AdvancedNodeSearchDialog = ({
470470
);
471471
};
472472

473-
export const renderAdvancedNodeSearchSidebar = () =>
473+
export const renderAdvancedNodeSearchDialog = () =>
474474
renderOverlay({
475475
// eslint-disable-next-line @typescript-eslint/naming-convention
476476
Overlay: AdvancedNodeSearchDialog,
477477
props: {},
478478
});
479-
480-
export const renderAdvancedNodeSearchDialog = () =>
481-
renderAdvancedNodeSearchSidebar();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const InsertFooterAction = ({
100100
/>
101101
);
102102

103-
export const OpenSearchSidebarFooterAction = ({
103+
const OpenSearchSidebarFooterAction = ({
104104
disabled,
105105
onOpenSearchSidebar,
106106
}: {

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

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import React, { useEffect, useMemo, useRef, useState } from "react";
2-
import { NonIdealState, Spinner, SpinnerSize } from "@blueprintjs/core";
2+
import {
3+
Button,
4+
Icon,
5+
InputGroup,
6+
NonIdealState,
7+
Spinner,
8+
SpinnerSize,
9+
Tag,
10+
} from "@blueprintjs/core";
311
import MiniSearch from "minisearch";
412
import getDiscourseNodes from "~/utils/getDiscourseNodes";
513
import type { DockedSearchState } from "~/utils/openDgSearchInSidebar";
@@ -11,11 +19,9 @@ import {
1119
searchIndexedNodes,
1220
sortSearchResults,
1321
} from "./utils";
14-
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
1522
import { hasActiveTypeFilter } from "~/utils/discourseNodeTypeFilter";
23+
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
1624
import { SORT_FIELD_LABELS, isNonDefaultSort, type SortConfig } from "./utils";
17-
18-
import { Button, Icon, Tag } from "@blueprintjs/core";
1925
import getRoamUrl from "roamjs-components/dom/getRoamUrl";
2026
import type { DiscourseNode } from "~/utils/getDiscourseNodes";
2127
import { getNodeTagStyles } from "~/utils/getDiscourseNodeColors";
@@ -162,7 +168,14 @@ type AdvancedSearchDockedFiltersProps = {
162168
const getNodeIndicatorColor = (node: DiscourseNode): string =>
163169
formatHexColor(node.canvasSettings?.color) || "#6b7280";
164170

165-
export const AdvancedSearchDockedFilters = ({
171+
type SidebarIndexCache = {
172+
miniSearch: MiniSearch<SearchResult & { id: string }>;
173+
results: SearchResult[];
174+
};
175+
176+
let cachedSidebarIndex: SidebarIndexCache | null = null;
177+
178+
const AdvancedSearchDockedFilters = ({
166179
discourseNodes,
167180
selectedNodeTypeIds,
168181
sort,
@@ -218,7 +231,13 @@ export const AdvancedSearchDockedFilters = ({
218231
);
219232
};
220233

221-
export const AdvancedSearchSidebarPanel = (dockedState: DockedSearchState) => {
234+
type AdvancedSearchSidebarPanelProps = {
235+
dockedState: DockedSearchState;
236+
};
237+
238+
export const AdvancedSearchSidebarPanel = ({
239+
dockedState,
240+
}: AdvancedSearchSidebarPanelProps) => {
222241
const {
223242
query,
224243
results: dockedResults,
@@ -257,21 +276,40 @@ export const AdvancedSearchSidebarPanel = (dockedState: DockedSearchState) => {
257276
setIsIndexLoading(true);
258277
setIndexError(false);
259278

279+
const applyIndex = ({
280+
miniSearch,
281+
results: indexedResults,
282+
}: SidebarIndexCache): void => {
283+
if (cancelled) return;
284+
miniSearchRef.current = miniSearch;
285+
allResultsRef.current = indexedResults;
286+
setIsIndexLoading(false);
287+
};
288+
289+
if (cachedSidebarIndex) {
290+
applyIndex(cachedSidebarIndex);
291+
return () => {
292+
cancelled = true;
293+
};
294+
}
295+
260296
void buildSearchIndex(discourseNodes)
261297
.then(({ miniSearch, results: indexedResults }) => {
262-
if (cancelled) return;
263-
miniSearchRef.current = miniSearch;
264-
allResultsRef.current = indexedResults;
298+
cachedSidebarIndex = {
299+
miniSearch,
300+
results: indexedResults,
301+
};
302+
applyIndex(cachedSidebarIndex);
265303
})
266304
.catch((error) => {
267305
console.error(
268306
"Error building advanced node search sidebar index:",
269307
error,
270308
);
271-
if (!cancelled) setIndexError(true);
272-
})
273-
.finally(() => {
274-
if (!cancelled) setIsIndexLoading(false);
309+
if (!cancelled) {
310+
setIndexError(true);
311+
setIsIndexLoading(false);
312+
}
275313
});
276314

277315
return () => {
@@ -313,11 +351,12 @@ export const AdvancedSearchSidebarPanel = (dockedState: DockedSearchState) => {
313351
return (
314352
<div className="dg-node-search-sidebar box-border w-full min-w-0">
315353
<div className="dg-node-search-sidebar__input-row -ml-2 mr-2 box-border flex w-full min-w-0 items-center">
316-
<input
317-
className="bp3-input dg-node-search-sidebar__input box-border block w-full min-w-0 max-w-full"
354+
<InputGroup
355+
className="dg-node-search-sidebar__input box-border block w-full min-w-0 max-w-full"
356+
fill
357+
leftIcon="search"
318358
onChange={(event) => setSearchTerm(event.target.value)}
319359
placeholder="Search discourse nodes..."
320-
type="text"
321360
value={searchTerm}
322361
/>
323362
</div>

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {
1212
Popover,
1313
Position,
1414
} from "@blueprintjs/core";
15-
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
1615
import { type DiscourseNode } from "~/utils/getDiscourseNodes";
1716
import {
1817
NODE_TYPE_FILTER_SEARCH_THRESHOLD,
1918
filterDiscourseNodesByQuery,
2019
fromPopoverSelectedIds,
20+
getDiscourseNodeIndicatorColor,
2121
getSelectAllCheckState,
2222
hasActiveTypeFilter,
2323
toPopoverSelectedIds,
@@ -30,9 +30,6 @@ export type DiscourseNodeTypeFilterProps = {
3030
onPopoverOpenChange?: (isOpen: boolean) => void;
3131
};
3232

33-
const getNodeIndicatorColor = (node: DiscourseNode): string =>
34-
formatHexColor(node.canvasSettings?.color) || "#000";
35-
3633
const NodeTypeFilterRow = ({
3734
isChecked,
3835
node,
@@ -52,7 +49,7 @@ const NodeTypeFilterRow = ({
5249
<span className="inline-flex items-center gap-2 font-normal">
5350
<span
5451
className="h-3 w-3 rounded-full"
55-
style={{ backgroundColor: getNodeIndicatorColor(node) }}
52+
style={{ backgroundColor: getDiscourseNodeIndicatorColor(node) }}
5653
/>
5754
<span className="font-normal">{node.text}</span>
5855
</span>

apps/roam/src/utils/discourseNodeTypeFilter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { type DiscourseNode } from "~/utils/getDiscourseNodes";
2+
import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings";
23

34
/* Advanced search: when `selectedTypeIds` has no values, show all node types; otherwise, filter to the selected types. */
45
export const NODE_TYPE_FILTER_SEARCH_THRESHOLD = 7;
56

67
export type SelectAllCheckState = "off" | "indeterminate" | "on";
78

9+
export const getDiscourseNodeIndicatorColor = (
10+
node: DiscourseNode,
11+
fallback = "#000",
12+
): string => formatHexColor(node.canvasSettings?.color) || fallback;
13+
814
export const hasActiveTypeFilter = ({
915
selectedTypeIds,
1016
allTypeIds,

apps/roam/src/utils/openDgSearchInSidebar.tsx

Lines changed: 101 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
} from "~/components/AdvancedNodeSearchDialog/utils";
88

99
const SIDEBAR_ROOT_ID = "dg-node-search-sidebar-root";
10+
const OUTLINE_WRAPPER_SELECTOR =
11+
"#roam-right-sidebar-content .rm-sidebar-outline-wrapper";
12+
const SIDEBAR_OPEN_WIDTH_PX = 40;
13+
const MAX_WRAPPER_WAIT_FRAMES = 30;
1014

1115
export type DockedSearchState = {
1216
query: string;
@@ -17,41 +21,90 @@ export type DockedSearchState = {
1721

1822
let unmountSidebarSearch: (() => void) | null = null;
1923

20-
const waitForLatestSidebarWindow = async (): Promise<HTMLElement> => {
21-
for (let attempt = 0; attempt < 40; attempt += 1) {
22-
const windows =
23-
document.querySelectorAll<HTMLElement>(".rm-sidebar-window");
24-
const latest = windows[windows.length - 1];
25-
if (latest) return latest;
24+
const isRightSidebarOpen = (): boolean => {
25+
const sidebar = document.getElementById("right-sidebar");
26+
return (
27+
!!sidebar && sidebar.getBoundingClientRect().width > SIDEBAR_OPEN_WIDTH_PX
28+
);
29+
};
30+
31+
const getOutlineWrapperCount = (): number =>
32+
document.querySelectorAll(OUTLINE_WRAPPER_SELECTOR).length;
33+
34+
const getLatestOutlineWrapper = (): HTMLElement | null => {
35+
const wrappers = document.querySelectorAll<HTMLElement>(
36+
OUTLINE_WRAPPER_SELECTOR,
37+
);
38+
return wrappers[wrappers.length - 1] ?? null;
39+
};
40+
41+
const waitForOutlineWrapper = async (
42+
minCount: number,
43+
): Promise<HTMLElement> => {
44+
for (let attempt = 0; attempt < MAX_WRAPPER_WAIT_FRAMES; attempt += 1) {
45+
const wrappers = document.querySelectorAll<HTMLElement>(
46+
OUTLINE_WRAPPER_SELECTOR,
47+
);
48+
if (wrappers.length >= minCount && wrappers.length > 0) {
49+
return wrappers[wrappers.length - 1];
50+
}
2651
await new Promise<void>((resolve) => {
2752
requestAnimationFrame(() => resolve());
2853
});
2954
}
3055
throw new Error("Sidebar window did not appear");
3156
};
3257

33-
const setSidebarWindowTitle = (windowEl: HTMLElement): void => {
34-
const titleEl = windowEl.querySelector<HTMLElement>(
58+
const openRightSidebar = async ({
59+
anchorPageUid,
60+
wrapperCountBefore,
61+
}: {
62+
anchorPageUid: string;
63+
wrapperCountBefore: number;
64+
}): Promise<HTMLElement | null> => {
65+
const rightSidebar = window.roamAlphaAPI.ui.rightSidebar as {
66+
open?: () => Promise<void>;
67+
};
68+
if (rightSidebar.open) {
69+
await rightSidebar.open();
70+
return null;
71+
}
72+
73+
await addOutlineSidebarWindow(anchorPageUid);
74+
return waitForOutlineWrapper(Math.max(wrapperCountBefore + 1, 1));
75+
};
76+
77+
const addOutlineSidebarWindow = async (blockUid: string): Promise<void> => {
78+
await window.roamAlphaAPI.ui.rightSidebar.addWindow({
79+
window: {
80+
type: "outline",
81+
// @ts-expect-error - block-uid is valid for outline sidebar windows
82+
// eslint-disable-next-line @typescript-eslint/naming-convention
83+
"block-uid": blockUid,
84+
},
85+
});
86+
};
87+
88+
const setSidebarWindowTitle = (outlineWrapper: HTMLElement): void => {
89+
const pane = outlineWrapper.closest<HTMLElement>(
90+
"#roam-right-sidebar-content .sidebar-content > *",
91+
);
92+
const titleEl = pane?.querySelector<HTMLElement>(
3593
".window-headers span[style*='font-weight']",
3694
);
3795
if (titleEl) titleEl.textContent = "DG node search";
3896
};
3997

40-
const mountPanelInSidebarWindow = ({
98+
const mountPanelInOutlineWrapper = ({
4199
dockedState,
42-
windowEl,
100+
outlineWrapper,
43101
}: {
44102
dockedState: DockedSearchState;
45-
windowEl: HTMLElement;
103+
outlineWrapper: HTMLElement;
46104
}): void => {
47105
unmountSidebarSearch?.();
48106
unmountSidebarSearch = null;
49107

50-
const outlineWrapper = windowEl.querySelector(".rm-sidebar-outline-wrapper");
51-
if (!outlineWrapper) {
52-
throw new Error("Sidebar outline wrapper not found");
53-
}
54-
55108
outlineWrapper.innerHTML = "";
56109

57110
const root = document.createElement("div");
@@ -62,32 +115,46 @@ const mountPanelInSidebarWindow = ({
62115
outlineWrapper.appendChild(root);
63116

64117
unmountSidebarSearch = renderWithUnmount(
65-
<AdvancedSearchSidebarPanel {...dockedState} />,
118+
<AdvancedSearchSidebarPanel dockedState={dockedState} />,
66119
root,
67120
);
68121
};
69122

70123
export const openDgSearchInSidebar = async (
71124
dockedState: DockedSearchState,
72125
): Promise<void> => {
73-
const anchorPageUid = window.roamAlphaAPI.util.dateToPageUid(new Date());
126+
const anchorPageUid =
127+
(await window.roamAlphaAPI.ui.mainWindow.getOpenPageOrBlockUid()) ||
128+
window.roamAlphaAPI.util.dateToPageUid(new Date());
129+
const wrapperCountBefore = getOutlineWrapperCount();
74130

75-
await window.roamAlphaAPI.ui.rightSidebar.addWindow({
76-
window: {
77-
type: "outline",
78-
// @ts-expect-error - block-uid is valid for outline sidebar windows
79-
// eslint-disable-next-line @typescript-eslint/naming-convention
80-
"block-uid": anchorPageUid,
81-
},
82-
});
131+
if (!isRightSidebarOpen()) {
132+
const openedWrapper = await openRightSidebar({
133+
anchorPageUid,
134+
wrapperCountBefore,
135+
});
136+
if (openedWrapper) {
137+
setSidebarWindowTitle(openedWrapper);
138+
mountPanelInOutlineWrapper({
139+
dockedState,
140+
outlineWrapper: openedWrapper,
141+
});
142+
return;
143+
}
144+
}
83145

84-
const sidebarWindow = await waitForLatestSidebarWindow();
85-
setSidebarWindowTitle(sidebarWindow);
86-
mountPanelInSidebarWindow({ dockedState, windowEl: sidebarWindow });
87-
};
146+
const existingWrapper = getLatestOutlineWrapper();
147+
if (existingWrapper) {
148+
setSidebarWindowTitle(existingWrapper);
149+
mountPanelInOutlineWrapper({
150+
dockedState,
151+
outlineWrapper: existingWrapper,
152+
});
153+
return;
154+
}
88155

89-
export const unmountDgSearchSidebar = (): void => {
90-
unmountSidebarSearch?.();
91-
unmountSidebarSearch = null;
92-
document.getElementById(SIDEBAR_ROOT_ID)?.remove();
156+
await addOutlineSidebarWindow(anchorPageUid);
157+
const outlineWrapper = await waitForOutlineWrapper(wrapperCountBefore + 1);
158+
setSidebarWindowTitle(outlineWrapper);
159+
mountPanelInOutlineWrapper({ dockedState, outlineWrapper });
93160
};

0 commit comments

Comments
 (0)