From 6531061caab12ffad349a5ce29705f53fc21ea8f Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 19 Apr 2026 20:13:34 -0400 Subject: [PATCH 01/15] ENG-1640: First stab --- .../src/components/LeftSidebarCommands.tsx | 8 + apps/roam/src/components/LeftSidebarView.tsx | 87 +++++--- .../settings/LeftSidebarGlobalSettings.tsx | 13 +- .../settings/LeftSidebarPersonalSettings.tsx | 7 +- apps/roam/src/styles/styles.css | 14 ++ .../utils/registerCommandPaletteCommands.ts | 185 +++++++++--------- 6 files changed, 196 insertions(+), 118 deletions(-) create mode 100644 apps/roam/src/components/LeftSidebarCommands.tsx diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx new file mode 100644 index 000000000..4c3fe5e78 --- /dev/null +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -0,0 +1,8 @@ +import { OnloadArgs } from "roamjs-components/types"; +import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands"; + +export const commands: Record Promise> = { + "Create Node": async (ola: OnloadArgs) => { + createDiscourseNodeFromCommand(ola.extensionAPI); + }, +}; diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 4647d3238..11797446b 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -8,6 +8,7 @@ import React, { } from "react"; import ReactDOM from "react-dom"; import { + Button, Collapse, Icon, Popover, @@ -43,10 +44,13 @@ import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings"; import posthog from "posthog-js"; +import { commands } from "~/components/LeftSidebarCommands"; const parseReference = (text: string) => { const extracted = extractRef(text); - if (text.startsWith("((") && text.endsWith("))")) { + if (commands[text]) { + return { type: "command" as const, uid: text, display: text }; + } else if (text.startsWith("((") && text.endsWith("))")) { return { type: "block" as const, uid: extracted, display: text }; } else { return { type: "page" as const, display: text }; @@ -58,7 +62,11 @@ const truncate = (s: string, max: number | undefined): string => { return s.length > max ? `${s.slice(0, max)}...` : s; }; -const openTarget = async (e: React.MouseEvent, targetUid: string) => { +const openTarget = async ( + e: React.MouseEvent, + targetUid: string, + onloadArgs: OnloadArgs, +) => { e.preventDefault(); e.stopPropagation(); const target = parseReference(targetUid); @@ -66,6 +74,10 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { targetType: target.type, openInSidebar: e.shiftKey, }); + if (target.type === "command") { + await commands[target.uid](onloadArgs); + return; + } if (target.type === "block") { if (e.shiftKey) { await openBlockInSidebar(target.uid); @@ -123,9 +135,11 @@ const toggleFoldedState = ({ const SectionChildren = ({ childrenNodes, truncateAt, + onloadArgs, }: { childrenNodes: { uid: string; text: string; alias?: { value: string } }[]; truncateAt?: number; + onloadArgs: OnloadArgs; }) => { if (!childrenNodes?.length) return null; return ( @@ -134,23 +148,31 @@ const SectionChildren = ({ const ref = parseReference(child.text); const alias = child.alias?.value; const display = - ref.type === "page" - ? getPageTitleByPageUid(ref.display) - : getTextByBlockUid(ref.uid); + ref.type === "command" + ? ref.display + : ref.type === "page" + ? getPageTitleByPageUid(ref.display) + : getTextByBlockUid(ref.uid); const label = alias || truncate(display, truncateAt); const onClick = (e: React.MouseEvent) => { - return void openTarget(e, child.text); + return void openTarget(e, child.text, onloadArgs); }; return (
-
- {label} -
+ {ref.type === "command" ? ( + + ) : ( +
+ {label} +
+ )}
); })} @@ -160,8 +182,10 @@ const SectionChildren = ({ const PersonalSectionItem = ({ section, + onloadArgs, }: { section: LeftSidebarPersonalSectionConfig; + onloadArgs: OnloadArgs; }) => { const titleRef = parseReference(section.text); const blockText = useMemo( @@ -213,13 +237,20 @@ const PersonalSectionItem = ({ ); }; -const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => { +const PersonalSections = ({ + config, + onloadArgs, +}: { + config: LeftSidebarConfig; + onloadArgs: OnloadArgs; +}) => { const sections = config.personal.sections || []; if (!sections.length) return null; @@ -228,14 +259,20 @@ const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => {
{sections.map((section) => (
- +
))}
); }; -const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { +const GlobalSection = ({ + config, + onloadArgs, +}: { + config: LeftSidebarConfig["global"]; + onloadArgs: OnloadArgs; +}) => { const [isOpen, setIsOpen] = useState( !!config.settings?.folded.value, ); @@ -267,10 +304,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { {isCollapsable ? ( - + ) : ( - + )} ); @@ -413,8 +456,8 @@ const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { return ( <> - - + + ); }; @@ -444,7 +487,7 @@ const migrateFavorites = async () => { } const results = window.roamAlphaAPI.q(` - [:find ?uid + [:find ?uid :where [?e :page/sidebar] [?e :block/uid ?uid]] `); diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index 838346ab5..11e15b771 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -19,6 +19,7 @@ import { refreshAndNotify } from "~/components/LeftSidebarView"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import posthog from "posthog-js"; +import { commands } from "~/components/LeftSidebarCommands"; const pagesToUids = (pages: RoamBasicNode[]) => pages.map((p) => p.text); @@ -93,6 +94,10 @@ const LeftSidebarGlobalSectionsContent = ({ const [isExpanded, setIsExpanded] = useState(true); const pageNames = useMemo(() => getAllPageNames(), []); + const pageAndCommandNames = useMemo( + () => [...pageNames, ...Object.keys(commands)], + [pageNames], + ); useEffect(() => { const initialize = async () => { @@ -185,8 +190,9 @@ const LeftSidebarGlobalSectionsContent = ({ const addPage = useCallback( async (pageName: string) => { if (!pageName || !childrenUid) return; - - const targetUid = getPageUidByPageTitle(pageName); + const targetUid = commands[pageName] + ? pageName + : getPageUidByPageTitle(pageName); if (pages.some((p) => p.text === targetUid)) { console.warn(`Page "${pageName}" already exists in global section`); return; @@ -262,6 +268,7 @@ const LeftSidebarGlobalSectionsContent = ({ const isAddButtonDisabled = useMemo(() => { if (!newPageInput) return true; + if (commands[newPageInput]) return false; const targetUid = getPageUidByPageTitle(newPageInput); return !targetUid || pages.some((p) => p.text === targetUid); }, [newPageInput, pages]); @@ -335,7 +342,7 @@ const LeftSidebarGlobalSectionsContent = ({ value={newPageInput} setValue={handlePageInputChange} placeholder="Add page…" - options={pageNames} + options={pageAndCommandNames} maxItemsDisplayed={50} autoFocus onConfirm={() => void addPage(newPageInput)} diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 74487b780..5afa8d931 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -39,6 +39,7 @@ import { refreshAndNotify } from "~/components/LeftSidebarView"; import { memo, Dispatch, SetStateAction } from "react"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import posthog from "posthog-js"; +import { commands } from "~/components/LeftSidebarCommands"; /* eslint-disable @typescript-eslint/naming-convention */ export const sectionsToBlockProps = ( @@ -693,6 +694,10 @@ const LeftSidebarPersonalSectionsContent = ({ }, [sections, settingsDialogSectionUid]); const pageNames = useMemo(() => getAllPageNames(), []); + const pageAndCommandNames = useMemo( + () => [...pageNames, ...Object.keys(commands)], + [pageNames], + ); if (!personalSectionUid) { return null; @@ -735,7 +740,7 @@ const LeftSidebarPersonalSectionsContent = ({ { + const activeElement = document.activeElement; + const isFocusedTextarea = + activeElement instanceof HTMLTextAreaElement && + activeElement.classList.contains("rm-block-input") && + getUids(activeElement).blockUid === uid; + if (isFocusedTextarea) { + return { + selectionStart: activeElement.selectionStart, + selectionEnd: activeElement.selectionEnd, + selectedText: activeElement.value.substring( + activeElement.selectionStart, + activeElement.selectionEnd, + ), + }; + } + const textareas = document.querySelectorAll("textarea.rm-block-input"); + for (const el of textareas) { + const textarea = el as HTMLTextAreaElement; + if (getUids(textarea).blockUid === uid) { + return { + selectionStart: textarea.selectionStart, + selectionEnd: textarea.selectionEnd, + selectedText: textarea.value.substring( + textarea.selectionStart, + textarea.selectionEnd, + ), + }; + } + } + const textLength = (getTextByBlockUid(uid) || "").length; + return { + selectionStart: textLength, + selectionEnd: textLength, + selectedText: "", + }; +}; + +const createDiscourseNodeFromCommand = ( + extensionAPI: OnloadArgs["extensionAPI"], +) => { + posthog.capture("Discourse Node: Create Command Triggered"); + const focusedBlock = window.roamAlphaAPI.ui.getFocusedBlock(); + const uid = focusedBlock?.["block-uid"]; + const windowId = focusedBlock?.["window-id"] || "main-window"; + + const { selectionStart, selectionEnd, selectedText } = uid + ? getBlockSelection(uid) + : { selectionStart: 0, selectionEnd: 0, selectedText: "" }; + + renderModifyNodeDialog({ + mode: "create", + nodeType: "", + initialValue: { text: selectedText, uid: "" }, + extensionAPI, + onSuccess: async (result) => { + if (!uid) { + renderToast({ + id: "create-discourse-node-command-no-block", + content: "No block focused to insert a discourse node.", + }); + return; + } + const originalText = getTextByBlockUid(uid) || ""; + const pageRef = `[[${result.text}]]`; + const newText = `${originalText.substring(0, selectionStart)}${pageRef}${originalText.substring(selectionEnd)}`; + const newCursorPosition = selectionStart + pageRef.length; + + await updateBlock({ uid, text: newText }); + + await window.roamAlphaAPI.ui.setBlockFocusAndSelection({ + location: { + // eslint-disable-next-line @typescript-eslint/naming-convention + "block-uid": uid, + // eslint-disable-next-line @typescript-eslint/naming-convention + "window-id": windowId, + }, + selection: { start: newCursorPosition }, + }); + return; + }, + onClose: () => {}, + }); +}; + export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const { extensionAPI } = onloadArgs; @@ -166,95 +257,6 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { renderSettings({ onloadArgs }); }; - type BlockSelection = { - selectionStart: number; - selectionEnd: number; - selectedText: string; - }; - - const getBlockSelection = (uid: string): BlockSelection => { - const activeElement = document.activeElement; - const isFocusedTextarea = - activeElement instanceof HTMLTextAreaElement && - activeElement.classList.contains("rm-block-input") && - getUids(activeElement).blockUid === uid; - if (isFocusedTextarea) { - return { - selectionStart: activeElement.selectionStart, - selectionEnd: activeElement.selectionEnd, - selectedText: activeElement.value.substring( - activeElement.selectionStart, - activeElement.selectionEnd, - ), - }; - } - const textareas = document.querySelectorAll("textarea.rm-block-input"); - for (const el of textareas) { - const textarea = el as HTMLTextAreaElement; - if (getUids(textarea).blockUid === uid) { - return { - selectionStart: textarea.selectionStart, - selectionEnd: textarea.selectionEnd, - selectedText: textarea.value.substring( - textarea.selectionStart, - textarea.selectionEnd, - ), - }; - } - } - const textLength = (getTextByBlockUid(uid) || "").length; - return { - selectionStart: textLength, - selectionEnd: textLength, - selectedText: "", - }; - }; - - const createDiscourseNodeFromCommand = () => { - posthog.capture("Discourse Node: Create Command Triggered"); - const focusedBlock = window.roamAlphaAPI.ui.getFocusedBlock(); - const uid = focusedBlock?.["block-uid"]; - const windowId = focusedBlock?.["window-id"] || "main-window"; - - const { selectionStart, selectionEnd, selectedText } = uid - ? getBlockSelection(uid) - : { selectionStart: 0, selectionEnd: 0, selectedText: "" }; - - renderModifyNodeDialog({ - mode: "create", - nodeType: "", - initialValue: { text: selectedText, uid: "" }, - extensionAPI, - onSuccess: async (result) => { - if (!uid) { - renderToast({ - id: "create-discourse-node-command-no-block", - content: "No block focused to insert a discourse node.", - }); - return; - } - const originalText = getTextByBlockUid(uid) || ""; - const pageRef = `[[${result.text}]]`; - const newText = `${originalText.substring(0, selectionStart)}${pageRef}${originalText.substring(selectionEnd)}`; - const newCursorPosition = selectionStart + pageRef.length; - - await updateBlock({ uid, text: newText }); - - await window.roamAlphaAPI.ui.setBlockFocusAndSelection({ - location: { - // eslint-disable-next-line @typescript-eslint/naming-convention - "block-uid": uid, - // eslint-disable-next-line @typescript-eslint/naming-convention - "window-id": windowId, - }, - selection: { start: newCursorPosition }, - }); - return; - }, - onClose: () => {}, - }); - }; - const toggleDiscourseContextOverlay = async () => { const currentValue = (extensionAPI.settings.get("discourse-context-overlay") as boolean) ?? @@ -312,9 +314,8 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; // Roam organizes commands alphabetically - void addCommand( - "DG: Create/Insert discourse node", - createDiscourseNodeFromCommand, + void addCommand("DG: Create/Insert discourse node", () => + createDiscourseNodeFromCommand(extensionAPI), ); void addCommand("DG: Export - Current page", exportCurrentPage); void addCommand("DG: Export - Discourse graph", exportDiscourseGraph); From 55137d4d689b92626c474ecc0aaddafe2e617c61 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 20 Apr 2026 09:27:55 -0400 Subject: [PATCH 02/15] Add command menu --- .../src/components/LeftSidebarCommands.tsx | 27 +++++++++++++++++++ .../settings/LeftSidebarGlobalSettings.tsx | 25 ++++++++++++++--- .../settings/LeftSidebarPersonalSettings.tsx | 20 +++++++++++++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index 4c3fe5e78..c46283c03 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -1,3 +1,5 @@ +import React, { MouseEvent } from "react"; +import { Popover, Position, Button, Menu, MenuItem } from "@blueprintjs/core"; import { OnloadArgs } from "roamjs-components/types"; import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands"; @@ -6,3 +8,28 @@ export const commands: Record Promise> = { createDiscourseNodeFromCommand(ola.extensionAPI); }, }; + +export const sidebarCommandPopover = (onSelect: (value: string) => void) => { + const handleClick = (event: MouseEvent) => { + onSelect((event.target as Node).textContent!); + }; + + return ( + + {Object.keys(commands).map((commandName) => ( + + ))} + + } + position={Position.BOTTOM_LEFT} + > + ) : ( diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index 4ada305f9..515ab7bdc 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -169,16 +169,13 @@ width: 100%; } -.roam-sidebar-container .bp3-button:not([class*="bp3-intent-"]) { - color: rgba(75, 85, 99, var(--tw-text-opacity)); +.roam-sidebar-container .bp3-dark .bp3-button:not([class*="bp3-intent-"]) { + color: #8b97a5; + background-color: #333f4c; } -.roam-sidebar-container .bp3-button.bp3-minimal:not([class*="bp3-intent-"]) { - border-color: rgba(75, 85, 99, var(--tw-text-opacity)); - border-width: 1px; - border-style: solid; -} - -/*.roam-sidebar-container .bp3-button:not([class*="bp3-intent-"]):hover { +.roam-sidebar-container + .bp3-dark + .bp3-button:not([class*="bp3-intent-"]):hover { color: #f5f8fa; -}*/ +} From 32bd784713ddb09a514469f0c8389b76f5541eed Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 13:47:31 -0400 Subject: [PATCH 08/15] naming --- apps/roam/src/components/LeftSidebarCommands.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index 99c2a29af..cf0c50436 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -3,11 +3,14 @@ import { Popover, Position, Button, Menu, MenuItem } from "@blueprintjs/core"; import { OnloadArgs } from "roamjs-components/types"; import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands"; -export const commands: Record Promise> = { +export const commands: Record< + string, + (onloadArgs: OnloadArgs) => Promise +> = { /* eslint-disable @typescript-eslint/require-await */ // eslint-disable-next-line @typescript-eslint/naming-convention - "Create Node": async (ola: OnloadArgs) => { - createDiscourseNodeFromCommand(ola.extensionAPI); + "{create node}": async (onloadArgs: OnloadArgs) => { + createDiscourseNodeFromCommand(onloadArgs.extensionAPI); // typescript-eslint/naming-convention }, /* eslint-enable @typescript-eslint/require-await */ From a5251e25cb0c70a8f01750644397cd475414f50c Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 13:49:01 -0400 Subject: [PATCH 09/15] lint --- apps/roam/src/components/LeftSidebarCommands.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index cf0c50436..3e31c250d 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent } from "react"; +import React from "react"; import { Popover, Position, Button, Menu, MenuItem } from "@blueprintjs/core"; import { OnloadArgs } from "roamjs-components/types"; import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands"; From 6d63f0dedb460b908dd25003b27ca211d4d78698 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 23 Apr 2026 14:56:46 -0400 Subject: [PATCH 10/15] clean command name --- apps/roam/src/components/LeftSidebarCommands.tsx | 7 +++++++ apps/roam/src/components/LeftSidebarView.tsx | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index 3e31c250d..d1be02934 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -3,6 +3,13 @@ import { Popover, Position, Button, Menu, MenuItem } from "@blueprintjs/core"; import { OnloadArgs } from "roamjs-components/types"; import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCommands"; +export const cleanCommandName = (name: string): string => { + if (name.startsWith("{") && name.endsWith("}")) + name = name.substring(1, name.length - 1); + // Should we make it title case as well? + return name; +}; + export const commands: Record< string, (onloadArgs: OnloadArgs) => Promise diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index d5b499ece..d6085110b 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -44,7 +44,7 @@ import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings"; import posthog from "posthog-js"; -import { commands } from "~/components/LeftSidebarCommands"; +import { commands, cleanCommandName } from "~/components/LeftSidebarCommands"; const parseReference = (text: string) => { const extracted = extractRef(text); @@ -161,7 +161,7 @@ const SectionChildren = ({
{ref.type === "command" ? ( ) : (
Date: Fri, 24 Apr 2026 08:53:24 -0400 Subject: [PATCH 11/15] As requested in review --- apps/roam/src/components/LeftSidebarCommands.tsx | 4 +++- .../settings/LeftSidebarGlobalSettings.tsx | 14 +++++++++----- .../settings/LeftSidebarPersonalSettings.tsx | 13 ++++++++----- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index d1be02934..e1e25e838 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -6,7 +6,9 @@ import { createDiscourseNodeFromCommand } from "~/utils/registerCommandPaletteCo export const cleanCommandName = (name: string): string => { if (name.startsWith("{") && name.endsWith("}")) name = name.substring(1, name.length - 1); - // Should we make it title case as well? + name = name.trim(); + // sentence case + name = name.charAt(0).toUpperCase() + name.slice(1); return name; }; diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index 4be4bbc74..a038d464e 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -222,8 +222,7 @@ const LeftSidebarGlobalSectionsContent = ({ pagesToUids(updatedPages), ); - setNewPageInput(""); - setAutocompleteKey((prev) => prev + 1); + resetAutocomplete(""); posthog.capture("Left Sidebar Global Settings: Page Added", { pageName, }); @@ -266,10 +265,15 @@ const LeftSidebarGlobalSectionsContent = ({ setNewPageInput(value); }, []); - const setPageValue = useCallback((value: string) => { - setNewPageInput(value); + const resetAutocomplete = useCallback((nextValue = "") => { + setNewPageInput(nextValue); + + // AutocompleteInput renders from its internal `query` state, which is only + // initialized from the external `value` prop on mount. Bump the key to remount + // it so the displayed input reflects the new parent state. setAutocompleteKey((prev) => prev + 1); }, []); + const toggleChildren = useCallback(() => { setIsExpanded((prev) => !prev); }, []); @@ -364,7 +368,7 @@ const LeftSidebarGlobalSectionsContent = ({ onClick={() => void addPage(newPageInput)} title="Add page" /> - {sidebarCommandPopover(setPageValue)} + {sidebarCommandPopover(resetAutocomplete)}
{pages.length > 0 ? (
diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index c154557c7..556876b5f 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -336,14 +336,17 @@ const SectionItem = memo( const handleAddChild = useCallback(async () => { if (childInput && section.childrenUid) { await addChildToSection(section, section.childrenUid, childInput); - setChildInput(""); - setChildInputKey((prev) => prev + 1); + resetAutocomplete(""); refreshAndNotify(); } }, [childInput, section, addChildToSection]); - const setPageValue = useCallback((value: string) => { - setChildInput(value); + const resetAutocomplete = useCallback((nextValue = "") => { + setChildInput(nextValue); + + // AutocompleteInput renders from its internal `query` state, which is only + // initialized from the external `value` prop on mount. Bump the key to remount + // it so the displayed input reflects the new parent state. setChildInputKey((prev) => prev + 1); }, []); @@ -441,7 +444,7 @@ const SectionItem = memo( onClick={() => void handleAddChild()} title="Add child" /> - {sidebarCommandPopover(setPageValue)} + {sidebarCommandPopover(resetAutocomplete)}
{(section.children || []).length > 0 && ( From 39315fe07a3a3dbd26fee640ecf083ef4b827652 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 24 Apr 2026 09:11:16 -0400 Subject: [PATCH 12/15] memoization comment from Devin; linting fixes --- .../settings/LeftSidebarGlobalSettings.tsx | 27 +++++++++---------- .../settings/LeftSidebarPersonalSettings.tsx | 16 +++++------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index a038d464e..2b22180ef 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -96,11 +96,10 @@ const LeftSidebarGlobalSectionsContent = ({ const [isInitializing, setIsInitializing] = useState(true); const [isExpanded, setIsExpanded] = useState(true); - const pageNames = useMemo(() => getAllPageNames(), []); - const commandNames = Object.keys(commands); + const commandNames = useMemo(() => Object.keys(commands), []); const pageAndCommandNames = useMemo( - () => [...pageNames, ...commandNames], - [pageNames, commandNames], + () => [...getAllPageNames(), ...commandNames], + [commandNames], ); useEffect(() => { @@ -191,6 +190,15 @@ const LeftSidebarGlobalSectionsContent = ({ [pages, childrenUid], ); + const resetAutocomplete = useCallback((nextValue = "") => { + setNewPageInput(nextValue); + + // AutocompleteInput renders from its internal `query` state, which is only + // initialized from the external `value` prop on mount. Bump the key to remount + // it so the displayed input reflects the new parent state. + setAutocompleteKey((prev) => prev + 1); + }, []); + const addPage = useCallback( async (pageName: string) => { if (!pageName || !childrenUid) return; @@ -235,7 +243,7 @@ const LeftSidebarGlobalSectionsContent = ({ }); } }, - [childrenUid, pages], + [childrenUid, pages, resetAutocomplete], ); const removePage = useCallback( @@ -265,15 +273,6 @@ const LeftSidebarGlobalSectionsContent = ({ setNewPageInput(value); }, []); - const resetAutocomplete = useCallback((nextValue = "") => { - setNewPageInput(nextValue); - - // AutocompleteInput renders from its internal `query` state, which is only - // initialized from the external `value` prop on mount. Bump the key to remount - // it so the displayed input reflects the new parent state. - setAutocompleteKey((prev) => prev + 1); - }, []); - const toggleChildren = useCallback(() => { setIsExpanded((prev) => !prev); }, []); diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 556876b5f..34a1e1bdd 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -333,14 +333,6 @@ const SectionItem = memo( [setSections, sectionsRef], ); - const handleAddChild = useCallback(async () => { - if (childInput && section.childrenUid) { - await addChildToSection(section, section.childrenUid, childInput); - resetAutocomplete(""); - refreshAndNotify(); - } - }, [childInput, section, addChildToSection]); - const resetAutocomplete = useCallback((nextValue = "") => { setChildInput(nextValue); @@ -350,6 +342,14 @@ const SectionItem = memo( setChildInputKey((prev) => prev + 1); }, []); + const handleAddChild = useCallback(async () => { + if (childInput && section.childrenUid) { + await addChildToSection(section, section.childrenUid, childInput); + resetAutocomplete(""); + refreshAndNotify(); + } + }, [childInput, section, addChildToSection, resetAutocomplete]); + const sectionWithoutSettingsAndChildren = (!section.settings && section.children?.length === 0) || !section.children; From e75099331ea3651fd0fd1a5b1b8aadec90d9c62a Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 24 Apr 2026 09:30:46 -0400 Subject: [PATCH 13/15] More review comments --- apps/roam/src/components/LeftSidebarCommands.tsx | 6 +++++- apps/roam/src/components/LeftSidebarView.tsx | 10 ++++++---- .../components/settings/LeftSidebarGlobalSettings.tsx | 6 +++--- .../settings/LeftSidebarPersonalSettings.tsx | 4 ++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarCommands.tsx b/apps/roam/src/components/LeftSidebarCommands.tsx index e1e25e838..c3f74e038 100644 --- a/apps/roam/src/components/LeftSidebarCommands.tsx +++ b/apps/roam/src/components/LeftSidebarCommands.tsx @@ -25,7 +25,11 @@ export const commands: Record< /* eslint-enable @typescript-eslint/require-await */ }; -export const sidebarCommandPopover = (onSelect: (value: string) => void) => { +export const SidebarCommandPopover = ({ + onSelect, +}: { + onSelect: (value: string) => void; +}) => { return ( +
{ref.type === "command" ? ( - + + + ) : (
pages.map((p) => p.text); @@ -348,7 +348,7 @@ const LeftSidebarGlobalSectionsContent = ({
Add pages that will appear for all users
-
+
void addPage(newPageInput)} title="Add page" /> - {sidebarCommandPopover(resetAutocomplete)} +
{pages.length > 0 ? (
diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 34a1e1bdd..112f06f58 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -41,7 +41,7 @@ import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageU import posthog from "posthog-js"; import { commands, - sidebarCommandPopover, + SidebarCommandPopover, } from "~/components/LeftSidebarCommands"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -444,7 +444,7 @@ const SectionItem = memo( onClick={() => void handleAddChild()} title="Add child" /> - {sidebarCommandPopover(resetAutocomplete)} +
{(section.children || []).length > 0 && ( From 6315f40969bed8e6f9f0d73a8dcdbf02ea3ba5d3 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 24 Apr 2026 09:32:55 -0400 Subject: [PATCH 14/15] restrict css scope (Devin) --- apps/roam/src/styles/styles.css | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index 515ab7bdc..4675b34e0 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -169,13 +169,11 @@ width: 100%; } -.roam-sidebar-container .bp3-dark .bp3-button:not([class*="bp3-intent-"]) { +#dg-left-sidebar-root .bp3-dark .bp3-button:not([class*="bp3-intent-"]) { color: #8b97a5; background-color: #333f4c; } -.roam-sidebar-container - .bp3-dark - .bp3-button:not([class*="bp3-intent-"]):hover { +#dg-left-sidebar-root .bp3-dark .bp3-button:not([class*="bp3-intent-"]):hover { color: #f5f8fa; } From 79d061a5369e252f8891771735c4c2bc0553ba88 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 24 Apr 2026 17:18:30 -0400 Subject: [PATCH 15/15] commands come before pages --- apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx | 2 +- .../src/components/settings/LeftSidebarPersonalSettings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index 080c268fc..96c2ca993 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -98,7 +98,7 @@ const LeftSidebarGlobalSectionsContent = ({ const commandNames = useMemo(() => Object.keys(commands), []); const pageAndCommandNames = useMemo( - () => [...getAllPageNames(), ...commandNames], + () => [...commandNames, ...getAllPageNames()], [commandNames], ); diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 112f06f58..30cbe215e 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -707,7 +707,7 @@ const LeftSidebarPersonalSectionsContent = ({ const pageNames = useMemo(() => getAllPageNames(), []); const pageAndCommandNames = useMemo( - () => [...pageNames, ...Object.keys(commands)], + () => [...Object.keys(commands), ...pageNames], [pageNames], );