From 0061fd2cbfc5180e1faeb79d3c4adbc10d3fdba2 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 13 Sep 2025 12:01:45 +0530 Subject: [PATCH 01/10] fix style --- apps/roam/src/components/LeftSidebarView.tsx | 293 ++++++++++++++++++ .../utils/initializeObserversAndListeners.ts | 13 + 2 files changed, 306 insertions(+) create mode 100644 apps/roam/src/components/LeftSidebarView.tsx diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx new file mode 100644 index 000000000..fb6c71af1 --- /dev/null +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -0,0 +1,293 @@ +import React, { useMemo, useState } from "react"; +import ReactDOM from "react-dom"; +import { Collapse, Icon } from "@blueprintjs/core"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; +import extractRef from "roamjs-components/util/extractRef"; +import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; +import type { + LeftSidebarConfig, + LeftSidebarPersonalSectionConfig, +} from "~/utils/getLeftSidebarSettings"; +import { createBlock } from "roamjs-components/writes"; +import deleteBlock from "roamjs-components/writes/deleteBlock"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; + +const parseReference = (text: string) => { + const extracted = extractRef(text); + if (text.startsWith("((") && text.endsWith("))")) { + return { type: "block" as const, uid: extracted, display: text }; + } else { + return { type: "page" as const, title: extracted, display: extracted }; + } +}; + +const truncate = (s: string, max: number | undefined): string => { + if (!max || max <= 0) return s; + return s.length > max ? `${s.slice(0, max)}...` : s; +}; + +const openTarget = async (e: React.MouseEvent, sectionTitle: string) => { + e.preventDefault(); + e.stopPropagation(); + const target = parseReference(sectionTitle); + + if (target.type === "block") { + if (e.shiftKey) { + await openBlockInSidebar(target.uid); + return; + } + await window.roamAlphaAPI.ui.mainWindow.openBlock({ + block: { uid: target.uid }, + }); + return; + } + + const uid = getPageUidByPageTitle(target.title); + if (!uid) return; + if (e.shiftKey) { + await window.roamAlphaAPI.ui.rightSidebar.addWindow({ + // @ts-expect-error - todo test + // eslint-disable-next-line @typescript-eslint/naming-convention + window: { type: "outline", "block-uid": uid }, + }); + } else { + await window.roamAlphaAPI.ui.mainWindow.openPage({ page: { uid } }); + } +}; + +const toggleFoldedState = ({ + isOpen, + setIsOpen, + folded, + parentUid, +}: { + isOpen: boolean; + setIsOpen: React.Dispatch>; + folded: { uid?: string; value: boolean }; + parentUid: string; +}) => { + if (isOpen) { + setIsOpen(false); + if (folded.uid) { + void deleteBlock(folded.uid); + folded.uid = undefined; + folded.value = false; + } + } else { + setIsOpen(true); + const newUid = window.roamAlphaAPI.util.generateUID(); + void createBlock({ + parentUid, + node: { text: "Folded", uid: newUid }, + }); + folded.uid = newUid; + folded.value = true; + } +}; + +const SectionChildren = ({ + childrenNodes, + truncateAt, +}: { + childrenNodes: { uid: string; text: string }[]; + truncateAt?: number; +}) => { + if (!childrenNodes?.length) return null; + return ( + <> + {childrenNodes.map((child) => { + const ref = parseReference(child.text); + const label = truncate(ref.display, truncateAt); + const onClick = (e: React.MouseEvent) => { + return void openTarget(e, child.text); + }; + return ( +
+
+ {label} +
+
+ ); + })} + + ); +}; + +const PersonalSectionItem = ({ + section, +}: { + section: LeftSidebarPersonalSectionConfig; +}) => { + const ref = useMemo(() => extractRef(section.text), [section.text]); + const blockText = useMemo(() => getTextByBlockUid(ref), [ref]); + const truncateAt = section.settings?.truncateResult.value; + const [isOpen, setIsOpen] = useState( + !!section.settings?.folded.value || false, + ); + const alias = section.settings?.alias?.value; + const titleRef = parseReference(section.text); + + if (section.sectionWithoutSettingsAndChildren) { + const onClick = (e: React.MouseEvent) => { + return void openTarget(e, section.text); + }; + return ( +
+
+ {blockText || titleRef.title} +
+
+
+ ); + } + + const handleTitleClick = (e: React.MouseEvent) => { + if (e.shiftKey) { + return void openTarget(e, section.text); + } + if (!section.settings) return; + + toggleFoldedState({ + isOpen, + setIsOpen, + folded: section.settings.folded, + parentUid: section.settings.uid || "", + }); + }; + + const handleDoubleClick = (e: React.MouseEvent) => { + return void openTarget(e, section.text); + }; + + return ( + <> +
+
+ + {alias || blockText || titleRef.display} + + {(section.children?.length || 0) > 0 && ( + + + + )} +
+
+
+ + + + + ); +}; + +const PersonalSections = ({ + config, +}: { + config: LeftSidebarConfig["personal"]; +}) => { + const sections = config.sections || []; + if (!sections.length) return null; + return ( +
+ {sections.map((s) => ( + + ))} +
+ ); +}; + +const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { + const [isOpen, setIsOpen] = useState(!!config.folded.value); + if (!config.children?.length) return null; + const isCollapsable = config.collapsable.value; + + return ( + <> +
{ + if (!isCollapsable) return; + toggleFoldedState({ + isOpen, + setIsOpen, + folded: config.folded, + parentUid: config.uid, + }); + }} + > +
+ Global + {isCollapsable && ( + + + + )} +
+
+
+ {isCollapsable ? ( + + + + ) : ( + + )} + + ); +}; + +const LeftSidebarView = () => { + const config = useMemo(() => getFormattedConfigTree().leftSidebar, []); + + return ( + <> + + + + ); +}; + +export const mountLeftSidebar = (wrapper: HTMLElement): void => { + if (!wrapper) return; + wrapper.innerHTML = ""; + + const existingStarred = wrapper.querySelector(".starred-pages"); + if (existingStarred && !existingStarred.id.includes("dg-left-sidebar-root")) { + try { + existingStarred.remove(); + } catch (e) { + console.warn( + "[DG][LeftSidebar] failed to remove default starred-pages", + e, + ); + } + } + + const id = "dg-left-sidebar-root"; + let root = wrapper.querySelector(`#${id}`) as HTMLDivElement; + if (!root) { + root = document.createElement("div"); + root.id = id; + root.className = "starred-pages overflow-scroll"; + root.onmousedown = (e) => e.stopPropagation(); + wrapper.appendChild(root); + } else { + root.className = "starred-pages overflow-scroll"; + } + ReactDOM.render(, root); +}; + +export default LeftSidebarView; diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 98ff16a9d..df1624313 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -46,6 +46,7 @@ import { import { renderNodeTagPopupButton } from "./renderNodeTagPopup"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; import { getSetting } from "./extensionSettings"; +import { mountLeftSidebar } from "~/components/LeftSidebarView"; const debounce = (fn: () => void, delay = 250) => { let timeout: number; @@ -100,6 +101,17 @@ export const initObservers = async ({ render: (b) => renderQueryBlock(b, onloadArgs), }); + const leftSidebarObserver = createHTMLObserver({ + tag: "DIV", + useBody: true, + className: "starred-pages-wrapper", + callback: (el) => { + console.log("[DG][LeftSidebar] leftSidebarObserver callback", el); + const container = el as HTMLDivElement; + mountLeftSidebar(container); + }, + }); + const nodeTagPopupButtonObserver = createHTMLObserver({ className: "rm-page-ref--tag", tag: "SPAN", @@ -327,6 +339,7 @@ export const initObservers = async ({ linkedReferencesObserver, graphOverviewExportObserver, nodeTagPopupButtonObserver, + leftSidebarObserver, ].filter((o): o is MutationObserver => !!o), listeners: { pageActionListener, From 50355e7b995c785f444af2295b4c660aff64d00d Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 13 Sep 2025 12:15:19 +0530 Subject: [PATCH 02/10] fix styling --- apps/roam/src/components/LeftSidebarView.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index fb6c71af1..afccbaf78 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -138,15 +138,15 @@ const PersonalSectionItem = ({ return void openTarget(e, section.text); }; return ( -
+ <>
{blockText || titleRef.title}

-
+ ); } @@ -170,7 +170,7 @@ const PersonalSectionItem = ({ return ( <> -
+
{alias || blockText || titleRef.display} @@ -217,7 +217,7 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { return ( <>
{ if (!isCollapsable) return; toggleFoldedState({ From cf219702ddef82924df50d7d2a11ec5a75484988 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 13 Sep 2025 16:19:34 +0530 Subject: [PATCH 03/10] address coderabbit --- apps/roam/src/components/LeftSidebarView.tsx | 25 ++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index afccbaf78..804e2cd4e 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -124,14 +124,17 @@ const PersonalSectionItem = ({ }: { section: LeftSidebarPersonalSectionConfig; }) => { - const ref = useMemo(() => extractRef(section.text), [section.text]); - const blockText = useMemo(() => getTextByBlockUid(ref), [ref]); + const titleRef = parseReference(section.text); + const blockText = useMemo( + () => + titleRef.type === "block" ? getTextByBlockUid(titleRef.uid) : undefined, + [titleRef], + ); const truncateAt = section.settings?.truncateResult.value; const [isOpen, setIsOpen] = useState( !!section.settings?.folded.value || false, ); const alias = section.settings?.alias?.value; - const titleRef = parseReference(section.text); if (section.sectionWithoutSettingsAndChildren) { const onClick = (e: React.MouseEvent) => { @@ -264,21 +267,13 @@ export const mountLeftSidebar = (wrapper: HTMLElement): void => { if (!wrapper) return; wrapper.innerHTML = ""; - const existingStarred = wrapper.querySelector(".starred-pages"); - if (existingStarred && !existingStarred.id.includes("dg-left-sidebar-root")) { - try { - existingStarred.remove(); - } catch (e) { - console.warn( - "[DG][LeftSidebar] failed to remove default starred-pages", - e, - ); - } - } - const id = "dg-left-sidebar-root"; let root = wrapper.querySelector(`#${id}`) as HTMLDivElement; if (!root) { + const existingStarred = wrapper.querySelector(".starred-pages"); + if (existingStarred) { + existingStarred.remove(); + } root = document.createElement("div"); root.id = id; root.className = "starred-pages overflow-scroll"; From 9fadab2992613bca7473c93aa1fbd981c5eef0c7 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 15 Sep 2025 20:50:58 +0530 Subject: [PATCH 04/10] fix double click to navigate, shift click to open in sidebar --- apps/roam/src/components/LeftSidebarView.tsx | 29 ++++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 804e2cd4e..f4868cc02 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -43,7 +43,7 @@ const openTarget = async (e: React.MouseEvent, sectionTitle: string) => { return; } - const uid = getPageUidByPageTitle(target.title); + const uid = getPageUidByPageTitle(sectionTitle); if (!uid) return; if (e.shiftKey) { await window.roamAlphaAPI.ui.rightSidebar.addWindow({ @@ -153,10 +153,7 @@ const PersonalSectionItem = ({ ); } - const handleTitleClick = (e: React.MouseEvent) => { - if (e.shiftKey) { - return void openTarget(e, section.text); - } + const handleChevronClick = (e: React.MouseEvent) => { if (!section.settings) return; toggleFoldedState({ @@ -171,15 +168,21 @@ const PersonalSectionItem = ({ return void openTarget(e, section.text); }; + const handleTitleClick = (e: React.MouseEvent) => { + if (e.shiftKey) { + return void openTarget(e, section.text); + } + }; + return ( <>
- + {alias || blockText || titleRef.display} {(section.children?.length || 0) > 0 && ( - + )} @@ -213,21 +216,23 @@ const PersonalSections = ({ }; const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { - const [isOpen, setIsOpen] = useState(!!config.folded.value); + const [isOpen, setIsOpen] = useState( + !!config.settings?.folded.value, + ); if (!config.children?.length) return null; - const isCollapsable = config.collapsable.value; + const isCollapsable = config.settings?.collapsable.value; return ( <>
{ - if (!isCollapsable) return; + if (!isCollapsable || !config.settings) return; toggleFoldedState({ isOpen, setIsOpen, - folded: config.folded, - parentUid: config.uid, + folded: config.settings.folded, + parentUid: config.settings.uid, }); }} > From ef127651c039f048bf9a397524d9c7a0fafff1a3 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 17 Sep 2025 21:59:16 +0530 Subject: [PATCH 05/10] address Michael and Johnny review --- apps/roam/src/components/LeftSidebarView.tsx | 21 ++++--------------- .../utils/initializeObserversAndListeners.ts | 1 - 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index f4868cc02..8eccfdc89 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -146,9 +146,8 @@ const PersonalSectionItem = ({ className="sidebar-title-button cursor-pointer rounded-sm py-1 font-semibold leading-normal" onClick={onClick} > - {blockText || titleRef.title} + {(blockText || titleRef.title)?.toUpperCase()}
-
); } @@ -164,22 +163,12 @@ const PersonalSectionItem = ({ }); }; - const handleDoubleClick = (e: React.MouseEvent) => { - return void openTarget(e, section.text); - }; - - const handleTitleClick = (e: React.MouseEvent) => { - if (e.shiftKey) { - return void openTarget(e, section.text); - } - }; - return ( <>
- - {alias || blockText || titleRef.display} + void openTarget(e, section.text)}> + {(alias || blockText || titleRef.display).toUpperCase()} {(section.children?.length || 0) > 0 && ( @@ -188,7 +177,6 @@ const PersonalSectionItem = ({ )}
-
{ }} >
- Global + GLOBAL {isCollapsable && ( @@ -245,7 +233,6 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { )}
-
{isCollapsable ? ( diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index df1624313..e14bcd4b3 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -106,7 +106,6 @@ export const initObservers = async ({ useBody: true, className: "starred-pages-wrapper", callback: (el) => { - console.log("[DG][LeftSidebar] leftSidebarObserver callback", el); const container = el as HTMLDivElement; mountLeftSidebar(container); }, From 548d8980bb99fae8e4170507b8cedd86e81b7b67 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 20 Sep 2025 11:22:52 +0530 Subject: [PATCH 06/10] 1. Use (BETA) setting, on hover change UI to reflect its clickable state --- apps/roam/src/components/LeftSidebarView.tsx | 16 +++++++++++----- .../components/settings/HomePersonalSettings.tsx | 16 ++++++++++++++++ apps/roam/src/data/userSettings.ts | 1 + apps/roam/src/styles/styles.css | 6 ++++++ .../src/utils/initializeObserversAndListeners.ts | 3 +++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 8eccfdc89..8c23c77ac 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -165,13 +165,19 @@ const PersonalSectionItem = ({ return ( <> -
+
- void openTarget(e, section.text)}> + void openTarget(e, section.text)} + > {(alias || blockText || titleRef.display).toUpperCase()} {(section.children?.length || 0) > 0 && ( - + )} @@ -213,7 +219,7 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { return ( <>
{ if (!isCollapsable || !config.settings) return; toggleFoldedState({ @@ -227,7 +233,7 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
GLOBAL {isCollapsable && ( - + )} diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index 6200b22e9..5ad649f7c 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -17,6 +17,7 @@ import { AUTO_CANVAS_RELATIONS_KEY, DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY, DISCOURSE_TOOL_SHORTCUT_KEY, + LEFT_SIDEBAR_ENABLED_KEY, } from "~/data/userSettings"; import KeyboardShortcutInput from "./KeyboardShortcutInput"; import { getSetting, setSetting } from "~/utils/extensionSettings"; @@ -199,6 +200,21 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { } /> + { + const target = e.target as HTMLInputElement; + setSetting(LEFT_SIDEBAR_ENABLED_KEY, target.checked); + }} + labelElement={ + <> + (BETA) Left Sidebar + + + } + />
); }; diff --git a/apps/roam/src/data/userSettings.ts b/apps/roam/src/data/userSettings.ts index df0fce8fd..e7732418f 100644 --- a/apps/roam/src/data/userSettings.ts +++ b/apps/roam/src/data/userSettings.ts @@ -7,3 +7,4 @@ export const AUTO_CANVAS_RELATIONS_KEY = "auto-canvas-relations"; export const DISCOURSE_TOOL_SHORTCUT_KEY = "discourse-tool-shortcut"; export const DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY = "discourse-context-overlay-in-canvas"; +export const LEFT_SIDEBAR_ENABLED_KEY = "left-sidebar-enabled"; diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index caa4b3100..cdc43b96a 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -135,3 +135,9 @@ width: 100px; justify-content: space-between; } + +.sidebar-title-button:hover, +.sidebar-title-button-chevron:hover { + background-color: #10161a; + color: #f5f8fa; +} diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index e14bcd4b3..c709d4daa 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -47,6 +47,7 @@ import { renderNodeTagPopupButton } from "./renderNodeTagPopup"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; import { getSetting } from "./extensionSettings"; import { mountLeftSidebar } from "~/components/LeftSidebarView"; +import { LEFT_SIDEBAR_ENABLED_KEY } from "~/data/userSettings"; const debounce = (fn: () => void, delay = 250) => { let timeout: number; @@ -106,6 +107,8 @@ export const initObservers = async ({ useBody: true, className: "starred-pages-wrapper", callback: (el) => { + const isLeftSidebarEnabled = getSetting(LEFT_SIDEBAR_ENABLED_KEY, false); + if (!isLeftSidebarEnabled) return; const container = el as HTMLDivElement; mountLeftSidebar(container); }, From 061f4e044d391609375e3ef24b5cbd8afd304f50 Mon Sep 17 00:00:00 2001 From: Siddharth Yadav Date: Sat, 20 Sep 2025 14:10:07 +0530 Subject: [PATCH 07/10] ENG-853 left sidebar global and personal settings (#441) * left sidebar global and personal settings * address coderabbit * address ui feedback * add toast, fix void lint * text in center * sync settings and left sidebar rendering * simplify props * fix lint * 1. Address review 2. Use uid instead of username 3. Use settings block for global --- apps/roam/src/components/LeftSidebarView.tsx | 56 +- .../settings/LeftSidebarGlobalSettings.tsx | 327 ++++++++++ .../settings/LeftSidebarPersonalSettings.tsx | 586 ++++++++++++++++++ .../roam/src/components/settings/Settings.tsx | 14 + apps/roam/src/utils/discourseConfigRef.ts | 12 + apps/roam/src/utils/getLeftSidebarSettings.ts | 40 +- 6 files changed, 988 insertions(+), 47 deletions(-) create mode 100644 apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx create mode 100644 apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 8c23c77ac..d73f6393e 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -1,10 +1,14 @@ -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom"; import { Collapse, Icon } from "@blueprintjs/core"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; import extractRef from "roamjs-components/util/extractRef"; -import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; +import { + getFormattedConfigTree, + notify, + subscribe, +} from "~/utils/discourseConfigRef"; import type { LeftSidebarConfig, LeftSidebarPersonalSectionConfig, @@ -12,13 +16,15 @@ import type { import { createBlock } from "roamjs-components/writes"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import { Dispatch, SetStateAction } from "react"; const parseReference = (text: string) => { const extracted = extractRef(text); if (text.startsWith("((") && text.endsWith("))")) { return { type: "block" as const, uid: extracted, display: text }; } else { - return { type: "page" as const, title: extracted, display: extracted }; + return { type: "page" as const, display: text }; } }; @@ -63,7 +69,7 @@ const toggleFoldedState = ({ parentUid, }: { isOpen: boolean; - setIsOpen: React.Dispatch>; + setIsOpen: Dispatch>; folded: { uid?: string; value: boolean }; parentUid: string; }) => { @@ -136,23 +142,7 @@ const PersonalSectionItem = ({ ); const alias = section.settings?.alias?.value; - if (section.sectionWithoutSettingsAndChildren) { - const onClick = (e: React.MouseEvent) => { - return void openTarget(e, section.text); - }; - return ( - <> -
- {(blockText || titleRef.title)?.toUpperCase()} -
- - ); - } - - const handleChevronClick = (e: React.MouseEvent) => { + const handleChevronClick = () => { if (!section.settings) return; toggleFoldedState({ @@ -250,8 +240,30 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { ); }; +export const useConfig = () => { + const [config, setConfig] = useState( + () => getFormattedConfigTree().leftSidebar, + ); + useEffect(() => { + const handleUpdate = () => { + setConfig(getFormattedConfigTree().leftSidebar); + }; + const unsubscribe = subscribe(handleUpdate); + return () => { + unsubscribe(); + }; + }, []); + return config; +}; + +export const refreshAndNotify = () => { + refreshConfigTree(); + notify(); +}; + + const LeftSidebarView = () => { - const config = useMemo(() => getFormattedConfigTree().leftSidebar, []); + const config = useConfig(); return ( <> diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx new file mode 100644 index 000000000..52a457798 --- /dev/null +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -0,0 +1,327 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Collapse } from "@blueprintjs/core"; +import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import getAllPageNames from "roamjs-components/queries/getAllPageNames"; +import createBlock from "roamjs-components/writes/createBlock"; +import deleteBlock from "roamjs-components/writes/deleteBlock"; +import type { RoamBasicNode } from "roamjs-components/types"; +import { getSubTree } from "roamjs-components/util"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import discourseConfigRef from "~/utils/discourseConfigRef"; +import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; +import { getLeftSidebarGlobalSectionConfig } from "~/utils/getLeftSidebarSettings"; +import { LeftSidebarGlobalSectionConfig } from "~/utils/getLeftSidebarSettings"; +import { render as renderToast } from "roamjs-components/components/Toast"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import { refreshAndNotify } from "~/components/LeftSidebarView"; +import { memo } from "react"; + +const PageItem = memo( + ({ + page, + onRemove, + }: { + page: RoamBasicNode; + onRemove: (page: RoamBasicNode) => void; + }) => { + return ( +
+ {page.text} +
+ ); + }, +); + +PageItem.displayName = "PageItem"; + +const LeftSidebarGlobalSectionsContent = ({ + leftSidebar, +}: { + leftSidebar: RoamBasicNode; +}) => { + const [globalSection, setGlobalSection] = + useState(null); + const [pages, setPages] = useState([]); + const [childrenUid, setChildrenUid] = useState(null); + const [newPageInput, setNewPageInput] = useState(""); + const [autocompleteKey, setAutocompleteKey] = useState(0); + const [isInitializing, setIsInitializing] = useState(true); + const [isExpanded, setIsExpanded] = useState(true); + + const pageNames = useMemo(() => getAllPageNames(), []); + + useEffect(() => { + const initialize = async () => { + setIsInitializing(true); + const globalSectionText = "Global-Section"; + const config = getLeftSidebarGlobalSectionConfig(leftSidebar.children); + + const existingGlobalSection = leftSidebar.children.find( + (n) => n.text === globalSectionText, + ); + + if (!existingGlobalSection) { + try { + const globalSectionUid = await createBlock({ + parentUid: leftSidebar.uid, + order: 0, + node: { text: globalSectionText }, + }); + const settingsUid = await createBlock({ + parentUid: globalSectionUid, + order: 0, + node: { text: "Settings" }, + }); + const childrenUid = await createBlock({ + parentUid: globalSectionUid, + order: 0, + node: { text: "Children" }, + }); + setChildrenUid(childrenUid || null); + setPages([]); + setGlobalSection({ + uid: globalSectionUid, + settings: { + uid: settingsUid, + collapsable: { uid: undefined, value: false }, + folded: { uid: undefined, value: false }, + }, + childrenUid, + children: [], + }); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to create global section", + intent: "danger", + id: "create-global-section-error", + }); + } + } else { + setChildrenUid(config.childrenUid || null); + setPages(config.children || []); + setGlobalSection(config); + } + setIsInitializing(false); + }; + + void initialize(); + }, [leftSidebar]); + + const addPage = useCallback( + async (pageName: string) => { + if (!pageName || !childrenUid) return; + + if (pages.some((p) => p.text === pageName)) { + console.warn(`Page "${pageName}" already exists in global section`); + return; + } + + try { + const newPageUid = await createBlock({ + parentUid: childrenUid, + order: "last", + node: { text: pageName }, + }); + + const newPage: RoamBasicNode = { + text: pageName, + uid: newPageUid, + children: [], + }; + + setPages((prev) => [...prev, newPage]); + setNewPageInput(""); + setAutocompleteKey((prev) => prev + 1); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to add page", + intent: "danger", + id: "add-page-error", + }); + } + }, + [childrenUid, pages], + ); + + const removePage = useCallback(async (page: RoamBasicNode) => { + try { + await deleteBlock(page.uid); + setPages((prev) => prev.filter((p) => p.uid !== page.uid)); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to remove page", + intent: "danger", + id: "remove-page-error", + }); + } + }, []); + + const handlePageInputChange = useCallback((value: string) => { + setNewPageInput(value); + }, []); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && newPageInput) { + e.preventDefault(); + e.stopPropagation(); + void addPage(newPageInput); + } + }, + [newPageInput, addPage], + ); + + const toggleChildren = useCallback(() => { + setIsExpanded((prev) => !prev); + }, []); + + const isAddButtonDisabled = useMemo( + () => !newPageInput || pages.some((p) => p.text === newPageInput), + [newPageInput, pages], + ); + + if (isInitializing || !globalSection) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
+ + +
+ +
+
+
+
+ + {pages.length} {pages.length === 1 ? "page" : "pages"} + +
+ + +
+
+ Add pages that will appear for all users +
+
+ +
+ {pages.length > 0 ? ( +
+ {pages.map((page) => ( + void removePage(page)} + /> + ))} +
+ ) : ( +
+ No pages added yet +
+ )} +
+
+
+
+ ); +}; + +export const LeftSidebarGlobalSections = () => { + const [leftSidebar, setLeftSidebar] = useState(null); + + useEffect(() => { + const loadData = () => { + refreshConfigTree(); + + const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); + const updatedSettings = discourseConfigRef.tree; + const leftSidebarNode = getSubTree({ + tree: updatedSettings, + parentUid: configPageUid, + key: "Left Sidebar", + }); + + setTimeout(() => { + refreshAndNotify(); + }, 10); + setLeftSidebar(leftSidebarNode); + }; + + void loadData(); + }, []); + + if (!leftSidebar) { + return null; + } + + return ; +}; diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx new file mode 100644 index 000000000..e4806f1d7 --- /dev/null +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -0,0 +1,586 @@ +import discourseConfigRef from "~/utils/discourseConfigRef"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import getAllPageNames from "roamjs-components/queries/getAllPageNames"; +import { Button, Dialog, Collapse, Tooltip } from "@blueprintjs/core"; +import createBlock from "roamjs-components/writes/createBlock"; +import deleteBlock from "roamjs-components/writes/deleteBlock"; +import type { RoamBasicNode } from "roamjs-components/types"; +import NumberPanel from "roamjs-components/components/ConfigPanels/NumberPanel"; +import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; +import { + LeftSidebarPersonalSectionConfig, + getLeftSidebarPersonalSectionConfig, +} from "~/utils/getLeftSidebarSettings"; +import { extractRef, getSubTree } from "roamjs-components/util"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; +import { render as renderToast } from "roamjs-components/components/Toast"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import { refreshAndNotify } from "~/components/LeftSidebarView"; +import { memo, Dispatch, SetStateAction } from "react"; + +const SectionItem = memo( + ({ + section, + setSettingsDialogSectionUid, + pageNames, + setSections, + }: { + section: LeftSidebarPersonalSectionConfig; + setSections: Dispatch>; + setSettingsDialogSectionUid: (uid: string | null) => void; + pageNames: string[]; + }) => { + const ref = extractRef(section.text); + const blockText = getTextByBlockUid(ref); + const originalName = blockText || section.text; + const alias = section.settings?.alias?.value; + const [childInput, setChildInput] = useState(""); + const [childInputKey, setChildInputKey] = useState(0); + + const [expandedChildLists, setExpandedChildLists] = useState>( + new Set(), + ); + const isExpanded = expandedChildLists.has(section.uid); + const toggleChildrenList = useCallback((sectionUid: string) => { + setExpandedChildLists((prev) => { + const next = new Set(prev); + if (next.has(sectionUid)) { + next.delete(sectionUid); + } else { + next.add(sectionUid); + } + return next; + }); + }, []); + + const convertToComplexSection = useCallback( + async (section: LeftSidebarPersonalSectionConfig) => { + try { + const settingsUid = await createBlock({ + parentUid: section.uid, + order: 0, + node: { text: "Settings" }, + }); + const foldedUid = await createBlock({ + parentUid: settingsUid, + order: 0, + node: { text: "Folded" }, + }); + const truncateSettingUid = await createBlock({ + parentUid: settingsUid, + order: 1, + node: { text: "Truncate-result?", children: [{ text: "75" }] }, + }); + const aliasUid = await createBlock({ + parentUid: settingsUid, + order: 2, + node: { text: "Alias" }, + }); + + const childrenUid = await createBlock({ + parentUid: section.uid, + order: 1, + node: { text: "Children" }, + }); + + setSections((prev) => + prev.map((s) => { + if (s.uid === section.uid) { + return { + ...s, + settings: { + uid: settingsUid, + folded: { uid: foldedUid, value: false }, + truncateResult: { uid: truncateSettingUid, value: 75 }, + alias: { uid: aliasUid, value: "" }, + }, + childrenUid, + children: [], + }; + } + return s; + }), + ); + + setExpandedChildLists((prev) => new Set([...prev, section.uid])); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to convert to complex section", + intent: "danger", + id: "convert-to-complex-section-error", + }); + } + }, + [setSections], + ); + + const removeSection = useCallback( + async (section: LeftSidebarPersonalSectionConfig) => { + try { + await deleteBlock(section.uid); + + setSections((prev) => prev.filter((s) => s.uid !== section.uid)); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to remove section", + intent: "danger", + id: "remove-section-error", + }); + } + }, + [setSections], + ); + + const addChildToSection = useCallback( + async ( + section: LeftSidebarPersonalSectionConfig, + childrenUid: string, + childName: string, + ) => { + if (!childName || !childrenUid) return; + + try { + const newChild = await createBlock({ + parentUid: childrenUid, + order: "last", + node: { text: childName }, + }); + + setSections((prev) => + prev.map((s) => { + if (s.uid === section.uid) { + return { + ...s, + children: [ + ...(s.children || []), + { + text: childName, + uid: newChild, + children: [], + }, + ], + }; + } + return s; + }), + ); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to add child", + intent: "danger", + id: "add-child-error", + }); + } + }, + [setSections], + ); + const removeChild = useCallback( + async ( + section: LeftSidebarPersonalSectionConfig, + child: RoamBasicNode, + ) => { + try { + await deleteBlock(child.uid); + + setSections((prev) => + prev.map((s) => { + if (s.uid === section.uid) { + return { + ...s, + children: s.children?.filter((c) => c.uid !== child.uid), + }; + } + return s; + }), + ); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to remove child", + intent: "danger", + id: "remove-child-error", + }); + } + }, + [setSections], + ); + + const handleAddChild = useCallback(async () => { + if (childInput && section.childrenUid) { + await addChildToSection(section, section.childrenUid, childInput); + setChildInput(""); + setChildInputKey((prev) => prev + 1); + refreshAndNotify(); + } + }, [childInput, section, addChildToSection]); + + const sectionWithoutSettingsAndChildren = + !section.settings && !section.children; + + return ( +
+
+ {!sectionWithoutSettingsAndChildren && ( +
+ + {!sectionWithoutSettingsAndChildren && ( + +
+
{ + if (e.key === "Enter" && childInput) { + e.preventDefault(); + e.stopPropagation(); + void handleAddChild(); + } + }} + > + +
+ + {(section.children || []).length > 0 && ( +
+ {(section.children || []).map((child) => ( +
+ {child.text} +
+ ))} +
+ )} + + {(!section.children || section.children.length === 0) && ( +
+ No children added yet +
+ )} +
+
+ )} +
+ ); + }, +); + +SectionItem.displayName = "SectionItem"; + +const LeftSidebarPersonalSectionsContent = ({ + leftSidebar, +}: { + leftSidebar: RoamBasicNode; +}) => { + const [sections, setSections] = useState( + [], + ); + const [personalSectionUid, setPersonalSectionUid] = useState( + null, + ); + const [newSectionInput, setNewSectionInput] = useState(""); + const [autocompleteKey, setAutocompleteKey] = useState(0); + const [settingsDialogSectionUid, setSettingsDialogSectionUid] = useState< + string | null + >(null); + + useEffect(() => { + const initialize = async () => { + const userUid = window.roamAlphaAPI.user.uid(); + const personalSectionText = userUid + "/Personal-Section"; + + const personalSection = leftSidebar.children.find( + (n) => n.text === personalSectionText, + ); + + if (!personalSection) { + const newSectionUid = await createBlock({ + parentUid: leftSidebar.uid, + order: 0, + node: { + text: personalSectionText, + }, + }); + setPersonalSectionUid(newSectionUid); + setSections([]); + } else { + setPersonalSectionUid(personalSection.uid); + const loadedSections = getLeftSidebarPersonalSectionConfig( + leftSidebar.children, + ).sections; + setSections(loadedSections); + } + }; + + void initialize(); + }, [leftSidebar]); + + const addSection = useCallback( + async (sectionName: string) => { + if (!sectionName || !personalSectionUid) return; + if (sections.some((s) => s.text === sectionName)) return; + + try { + const newBlock = await createBlock({ + parentUid: personalSectionUid, + order: "last", + node: { text: sectionName }, + }); + + setSections((prev) => [ + ...prev, + { + text: sectionName, + uid: newBlock, + settings: undefined, + children: undefined, + childrenUid: undefined, + } as LeftSidebarPersonalSectionConfig, + ]); + + setNewSectionInput(""); + setAutocompleteKey((prev) => prev + 1); + refreshAndNotify(); + } catch (error) { + renderToast({ + content: "Failed to add section", + intent: "danger", + id: "add-section-error", + }); + } + }, + [personalSectionUid, sections], + ); + + const handleNewSectionInputChange = useCallback((value: string) => { + setNewSectionInput(value); + }, []); + + const activeDialogSection = useMemo(() => { + return sections.find((s) => s.uid === settingsDialogSectionUid) || null; + }, [sections, settingsDialogSectionUid]); + + const pageNames = useMemo(() => getAllPageNames(), []); + + if (!personalSectionUid) { + return null; + } + + return ( +
+
+
+ Add pages or create custom sections with settings and children +
+
{ + if (e.key === "Enter" && newSectionInput) { + e.preventDefault(); + e.stopPropagation(); + void addSection(newSectionInput); + } + }} + > + +
+
+ +
+ {sections.map((section) => ( + + ))} +
+ + {activeDialogSection && activeDialogSection.settings && ( + setSettingsDialogSectionUid(null)} + title={`Settings for "${activeDialogSection.text}"`} + style={{ width: "500px" }} + > +
+
+ + + { + setSections((prev) => + prev.map((s) => { + if (s.uid === activeDialogSection.uid && s.settings) { + return { + ...s, + settings: { + ...s.settings, + alias: { ...s.settings.alias, value }, + }, + }; + } + return s; + }), + ); + }} + /> +
+
+
+ )} +
+ ); +}; + +export const LeftSidebarPersonalSections = () => { + const [leftSidebar, setLeftSidebar] = useState(null); + + useEffect(() => { + const loadData = () => { + refreshConfigTree(); + + const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); + const updatedSettings = discourseConfigRef.tree; + const leftSidebarNode = getSubTree({ + tree: updatedSettings, + parentUid: configPageUid, + key: "Left Sidebar", + }); + + setTimeout(() => { + refreshAndNotify(); + }, 10); + setLeftSidebar(leftSidebarNode); + }; + + void loadData(); + }, []); + + if (!leftSidebar) { + return null; + } + + return ; +}; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index b210f4eda..db87f457e 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -27,6 +27,8 @@ import refreshConfigTree from "~/utils/refreshConfigTree"; import { FeedbackWidget } from "~/components/BirdEatsBugs"; import SuggestiveModeSettings from "./SuggestiveModeSettings"; import { getVersionWithDate } from "~/utils/getVersion"; +import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; +import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; type SectionHeaderProps = { children: React.ReactNode; @@ -147,6 +149,12 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={} /> + } + /> Global Settings @@ -162,6 +170,12 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={} /> + } + /> void>(); + +export const subscribe = (listener: () => void) => { + listeners.add(listener); + return () => listeners.delete(listener); +}; + +export const notify = () => { + listeners.forEach(listener => listener()); +}; + + type FormattedConfigTree = { settingsUid: string; grammarUid: string; diff --git a/apps/roam/src/utils/getLeftSidebarSettings.ts b/apps/roam/src/utils/getLeftSidebarSettings.ts index 0a4edeeee..a0c9a8dda 100644 --- a/apps/roam/src/utils/getLeftSidebarSettings.ts +++ b/apps/roam/src/utils/getLeftSidebarSettings.ts @@ -19,7 +19,6 @@ type LeftSidebarPersonalSectionSettings = { export type LeftSidebarPersonalSectionConfig = { uid: string; text: string; - sectionWithoutSettingsAndChildren: boolean; settings?: LeftSidebarPersonalSectionSettings; children?: RoamBasicNode[]; childrenUid?: string; @@ -31,7 +30,7 @@ type LeftSidebarGlobalSectionSettings = { folded: BooleanSetting; }; -type LeftSidebarGlobalSectionConfig = { +export type LeftSidebarGlobalSectionConfig = { uid: string; settings?: LeftSidebarGlobalSectionSettings; children: RoamBasicNode[]; @@ -39,6 +38,7 @@ type LeftSidebarGlobalSectionConfig = { }; export type LeftSidebarConfig = { + uid: string; global: LeftSidebarGlobalSectionConfig; personal: { uid: string; @@ -65,7 +65,7 @@ const getGlobalSectionSettings = ( }; }; -const getLeftSidebarGlobalSectionConfig = ( +export const getLeftSidebarGlobalSectionConfig = ( leftSidebarChildren: RoamBasicNode[], ): LeftSidebarGlobalSectionConfig => { const globalSectionNode = getSubTree({ @@ -131,7 +131,7 @@ const getPersonalSectionSettings = ( }; }; -const getLeftSidebarPersonalSectionConfig = ( +export const getLeftSidebarPersonalSectionConfig = ( leftSidebarChildren: RoamBasicNode[], ): { uid: string; sections: LeftSidebarPersonalSectionConfig[] } => { const userUid = window.roamAlphaAPI.user.uid(); @@ -156,27 +156,15 @@ const getLeftSidebarPersonalSectionConfig = ( const childrenNode = sectionNode.children?.find( (child) => child.text === "Children", ); - - const sectionWithoutSettingsAndChildren = !settingsNode && !childrenNode; - - if (sectionWithoutSettingsAndChildren) { - return { - uid: sectionNode.uid, - text: sectionNode.text, - sectionWithoutSettingsAndChildren: true, - }; - } else { - return { - uid: sectionNode.uid, - text: sectionNode.text, - settings: settingsNode - ? getPersonalSectionSettings(settingsNode) - : undefined, - children: childrenNode?.children || [], - childrenUid: childrenNode?.uid || "", - sectionWithoutSettingsAndChildren: false, - }; - } + return { + uid: sectionNode.uid, + text: sectionNode.text, + settings: settingsNode + ? getPersonalSectionSettings(settingsNode) + : undefined, + children: childrenNode?.children || [], + childrenUid: childrenNode?.uid || "", + }; }, ); @@ -192,10 +180,12 @@ export const getLeftSidebarSettings = ( const leftSidebarNode = globalTree.find( (node) => node.text === "Left Sidebar", ); + const leftSidebarUid = leftSidebarNode?.uid || ""; const leftSidebarChildren = leftSidebarNode?.children || []; const global = getLeftSidebarGlobalSectionConfig(leftSidebarChildren); const personal = getLeftSidebarPersonalSectionConfig(leftSidebarChildren); return { + uid: leftSidebarUid, global, personal, }; From 09f93b66ef278702c9e0d1d6776f60f321d82472 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 25 Sep 2025 15:22:55 +0530 Subject: [PATCH 08/10] add button for quick settings, full width sections, on click toggles --- apps/roam/src/components/LeftSidebarView.tsx | 168 ++++++++++++++++-- .../roam/src/components/settings/Settings.tsx | 20 ++- apps/roam/src/styles/styles.css | 6 +- .../utils/initializeObserversAndListeners.ts | 2 +- 4 files changed, 174 insertions(+), 22 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index d73f6393e..38aa6e3a3 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -1,6 +1,24 @@ -import React, { useEffect, useMemo, useState } from "react"; +/* eslint-disable @typescript-eslint/naming-convention */ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import ReactDOM from "react-dom"; -import { Collapse, Icon } from "@blueprintjs/core"; +import { + Collapse, + Icon, + Popover, + Menu, + MenuItem, + MenuDivider, + Divider, + Position, + PopoverInteractionKind, + TabId, +} from "@blueprintjs/core"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; import extractRef from "roamjs-components/util/extractRef"; @@ -18,6 +36,9 @@ import deleteBlock from "roamjs-components/writes/deleteBlock"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { Dispatch, SetStateAction } from "react"; +import { SettingsDialog } from "./settings/Settings"; +import { OnloadArgs } from "roamjs-components/types"; +import renderOverlay from "roamjs-components/util/renderOverlay"; const parseReference = (text: string) => { const extracted = extractRef(text); @@ -37,7 +58,6 @@ const openTarget = async (e: React.MouseEvent, sectionTitle: string) => { e.preventDefault(); e.stopPropagation(); const target = parseReference(sectionTitle); - if (target.type === "block") { if (e.shiftKey) { await openBlockInSidebar(target.uid); @@ -109,7 +129,7 @@ const SectionChildren = ({ return void openTarget(e, child.text); }; return ( -
+
-
+
- void openTarget(e, section.text)} +
{ + if ((section.children?.length || 0) > 0) { + handleChevronClick(); + } else { + void openTarget(e, section.text); + } + }} > {(alias || blockText || titleRef.display).toUpperCase()} - +
{(section.children?.length || 0) > 0 && ( { return ( <>
{ if (!isCollapsable || !config.settings) return; toggleFoldedState({ @@ -261,19 +287,133 @@ export const refreshAndNotify = () => { notify(); }; +const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const menuTriggerRef = useRef(null); -const LeftSidebarView = () => { - const config = useConfig(); + const handleGlobalPointerDownCapture = useCallback( + (e: Event) => { + if (!isMenuOpen) return; + const target = e.target as Node | null; + if (!target) return; + + if (menuTriggerRef.current && menuTriggerRef.current.contains(target)) { + return; + } + const popoverEl = document.querySelector(".dg-leftsidebar-popover"); + if (popoverEl && popoverEl.contains(target)) { + return; + } + + setIsMenuOpen(false); + }, + [isMenuOpen], + ); + + useEffect(() => { + if (!isMenuOpen) return; + console.log("handleGlobalPointerDownCapture"); + const opts = { capture: true } as AddEventListenerOptions; + window.addEventListener( + "mousedown", + handleGlobalPointerDownCapture as EventListener, + opts, + ); + window.addEventListener( + "pointerdown", + handleGlobalPointerDownCapture as EventListener, + opts, + ); + return () => { + window.removeEventListener( + "mousedown", + handleGlobalPointerDownCapture as EventListener, + opts, + ); + window.removeEventListener( + "pointerdown", + handleGlobalPointerDownCapture as EventListener, + opts, + ); + }; + }, [handleGlobalPointerDownCapture]); + const renderSettingsDialog = (tabId: TabId) => { + renderOverlay({ + Overlay: SettingsDialog, + props: { + onloadArgs, + selectedTabId: tabId, + }, + }); + }; + + return ( + <> + +
+
+ + +
+ FAVOURITES +
+ setIsMenuOpen(next)} + onClose={() => setIsMenuOpen(false)} + popoverClassName="dg-leftsidebar-popover" + minimal + content={ + + + { + renderSettingsDialog("left-sidebar-global-settings"); + setIsMenuOpen(false); + }} + /> + { + renderSettingsDialog("left-sidebar-personal-settings"); + setIsMenuOpen(false); + }} + /> + + } + > + + + + +
+ + ); +}; + +const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { + const config = useConfig(); return ( <> + ); }; -export const mountLeftSidebar = (wrapper: HTMLElement): void => { +export const mountLeftSidebar = ( + wrapper: HTMLElement, + onloadArgs: OnloadArgs, +): void => { if (!wrapper) return; wrapper.innerHTML = ""; @@ -292,7 +432,7 @@ export const mountLeftSidebar = (wrapper: HTMLElement): void => { } else { root.className = "starred-pages overflow-scroll"; } - ReactDOM.render(, root); + ReactDOM.render(, root); }; export default LeftSidebarView; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index db87f457e..7a503ee3e 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -64,24 +64,32 @@ export const SettingsDialog = ({ onloadArgs, isOpen, onClose, + selectedTabId, }: { onloadArgs: OnloadArgs; isOpen?: boolean; onClose?: () => void; + selectedTabId?: TabId; }) => { const extensionAPI = onloadArgs.extensionAPI; const settings = getFormattedConfigTree(); const nodes = getDiscourseNodes().filter(excludeDefaultNodes); - const [selectedTabId, setSelectedTabId] = useState( - "discourse-graph-home-personal", + const [activeTabId, setActiveTabId] = useState( + selectedTabId ?? "discourse-graph-home-personal", ); + useEffect(() => { + if (selectedTabId) { + setActiveTabId(selectedTabId); + } + }, [selectedTabId]); + useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { if (e.ctrlKey && e.shiftKey && e.key === "A") { e.stopPropagation(); e.preventDefault(); - setSelectedTabId("secret-admin-panel"); + setActiveTabId("secret-admin-panel"); } }; @@ -129,8 +137,8 @@ export const SettingsDialog = ({ `} setSelectedTabId(id)} - selectedTabId={selectedTabId} + onChange={(id) => setActiveTabId(id)} + selectedTabId={activeTabId} vertical={true} renderActiveTabPanelOnly={true} > @@ -206,7 +214,7 @@ export const SettingsDialog = ({ uid={settings.nodesUid} parentUid={settings.grammarUid} defaultValue={[]} - setSelectedTabId={setSelectedTabId} + setSelectedTabId={setActiveTabId} isPopup={true} /> } diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index cdc43b96a..86a659f80 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -137,7 +137,11 @@ } .sidebar-title-button:hover, -.sidebar-title-button-chevron:hover { +.sidebar-title-button-add:hover { background-color: #10161a; color: #f5f8fa; } + +.starred-pages-wrapper { + padding: 0 !important; +} diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index c709d4daa..6f1334e73 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -110,7 +110,7 @@ export const initObservers = async ({ const isLeftSidebarEnabled = getSetting(LEFT_SIDEBAR_ENABLED_KEY, false); if (!isLeftSidebarEnabled) return; const container = el as HTMLDivElement; - mountLeftSidebar(container); + mountLeftSidebar(container, onloadArgs); }, }); From d6e1a103d94ac355262acab5abe1f25980297029 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 25 Sep 2025 16:38:22 +0530 Subject: [PATCH 09/10] use global settings for BETA left sidebar feature --- apps/roam/src/components/LeftSidebarView.tsx | 6 ++-- .../components/settings/GeneralSettings.tsx | 9 ++++++ .../settings/HomePersonalSettings.tsx | 16 ---------- .../settings/LeftSidebarGlobalSettings.tsx | 3 +- apps/roam/src/data/userSettings.ts | 1 - apps/roam/src/styles/styles.css | 4 --- apps/roam/src/utils/discourseConfigRef.ts | 10 ++++-- .../utils/initializeObserversAndListeners.ts | 32 +++++++++++-------- 8 files changed, 40 insertions(+), 41 deletions(-) diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 38aa6e3a3..8e2628551 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -175,7 +175,7 @@ const PersonalSectionItem = ({ return ( <> -
+
{ return ( <>
{ if (!isCollapsable || !config.settings) return; toggleFoldedState({ @@ -352,7 +352,7 @@ const FavouritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { <>
-
+
diff --git a/apps/roam/src/components/settings/GeneralSettings.tsx b/apps/roam/src/components/settings/GeneralSettings.tsx index 76221eac2..ac1ab5cae 100644 --- a/apps/roam/src/components/settings/GeneralSettings.tsx +++ b/apps/roam/src/components/settings/GeneralSettings.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from "react"; import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; +import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index"; @@ -29,6 +30,14 @@ const DiscourseGraphHome = () => { value={settings.canvasPageFormat.value} defaultValue={DEFAULT_CANVAS_PAGE_FORMAT} /> +
); }; diff --git a/apps/roam/src/components/settings/HomePersonalSettings.tsx b/apps/roam/src/components/settings/HomePersonalSettings.tsx index 5ad649f7c..6200b22e9 100644 --- a/apps/roam/src/components/settings/HomePersonalSettings.tsx +++ b/apps/roam/src/components/settings/HomePersonalSettings.tsx @@ -17,7 +17,6 @@ import { AUTO_CANVAS_RELATIONS_KEY, DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY, DISCOURSE_TOOL_SHORTCUT_KEY, - LEFT_SIDEBAR_ENABLED_KEY, } from "~/data/userSettings"; import KeyboardShortcutInput from "./KeyboardShortcutInput"; import { getSetting, setSetting } from "~/utils/extensionSettings"; @@ -200,21 +199,6 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { } /> - { - const target = e.target as HTMLInputElement; - setSetting(LEFT_SIDEBAR_ENABLED_KEY, target.checked); - }} - labelElement={ - <> - (BETA) Left Sidebar - - - } - />
); }; diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index 52a457798..cebb853ae 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState, memo } from "react"; import { Button, Collapse } from "@blueprintjs/core"; import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; @@ -15,7 +15,6 @@ import { LeftSidebarGlobalSectionConfig } from "~/utils/getLeftSidebarSettings"; import { render as renderToast } from "roamjs-components/components/Toast"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { refreshAndNotify } from "~/components/LeftSidebarView"; -import { memo } from "react"; const PageItem = memo( ({ diff --git a/apps/roam/src/data/userSettings.ts b/apps/roam/src/data/userSettings.ts index e7732418f..df0fce8fd 100644 --- a/apps/roam/src/data/userSettings.ts +++ b/apps/roam/src/data/userSettings.ts @@ -7,4 +7,3 @@ export const AUTO_CANVAS_RELATIONS_KEY = "auto-canvas-relations"; export const DISCOURSE_TOOL_SHORTCUT_KEY = "discourse-tool-shortcut"; export const DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY = "discourse-context-overlay-in-canvas"; -export const LEFT_SIDEBAR_ENABLED_KEY = "left-sidebar-enabled"; diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index 86a659f80..309914fb7 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -141,7 +141,3 @@ background-color: #10161a; color: #f5f8fa; } - -.starred-pages-wrapper { - padding: 0 !important; -} diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 1ba296453..b152a3930 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -4,6 +4,8 @@ import { StringSetting, ExportConfigWithUids, getUidAndStringSetting, + getUidAndBooleanSetting, + BooleanSetting, } from "./getExportSettings"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; @@ -29,10 +31,9 @@ export const subscribe = (listener: () => void) => { }; export const notify = () => { - listeners.forEach(listener => listener()); + listeners.forEach((listener) => listener()); }; - type FormattedConfigTree = { settingsUid: string; grammarUid: string; @@ -43,6 +44,7 @@ type FormattedConfigTree = { canvasPageFormat: StringSetting; suggestiveMode: SuggestiveModeConfigWithUids; leftSidebar: LeftSidebarConfig; + leftSidebarEnabled: BooleanSetting; }; export const getFormattedConfigTree = (): FormattedConfigTree => { @@ -71,6 +73,10 @@ export const getFormattedConfigTree = (): FormattedConfigTree => { }), suggestiveMode: getSuggestiveModeConfigAndUids(configTreeRef.tree), leftSidebar: getLeftSidebarSettings(configTreeRef.tree), + leftSidebarEnabled: getUidAndBooleanSetting({ + tree: configTreeRef.tree, + text: "(BETA) Left Sidebar", + }), }; }; export default configTreeRef; diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index 6f1334e73..723c52310 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -47,7 +47,7 @@ import { renderNodeTagPopupButton } from "./renderNodeTagPopup"; import { formatHexColor } from "~/components/settings/DiscourseNodeCanvasSettings"; import { getSetting } from "./extensionSettings"; import { mountLeftSidebar } from "~/components/LeftSidebarView"; -import { LEFT_SIDEBAR_ENABLED_KEY } from "~/data/userSettings"; +import { getUidAndBooleanSetting } from "./getExportSettings"; const debounce = (fn: () => void, delay = 250) => { let timeout: number; @@ -102,18 +102,6 @@ export const initObservers = async ({ render: (b) => renderQueryBlock(b, onloadArgs), }); - const leftSidebarObserver = createHTMLObserver({ - tag: "DIV", - useBody: true, - className: "starred-pages-wrapper", - callback: (el) => { - const isLeftSidebarEnabled = getSetting(LEFT_SIDEBAR_ENABLED_KEY, false); - if (!isLeftSidebarEnabled) return; - const container = el as HTMLDivElement; - mountLeftSidebar(container, onloadArgs); - }, - }); - const nodeTagPopupButtonObserver = createHTMLObserver({ className: "rm-page-ref--tag", tag: "SPAN", @@ -204,6 +192,24 @@ export const initObservers = async ({ ) as IKeyCombo) || undefined; const personalTrigger = personalTriggerCombo?.key; const personalModifiers = getModifiersFromCombo(personalTriggerCombo); + + const leftSidebarObserver = createHTMLObserver({ + tag: "DIV", + useBody: true, + className: "starred-pages-wrapper", + callback: (el) => { + const isLeftSidebarEnabled = getUidAndBooleanSetting({ + tree: configTree, + text: "(BETA) Left Sidebar", + }).value; + const container = el as HTMLDivElement; + if (isLeftSidebarEnabled) { + container.style.padding = "0"; + mountLeftSidebar(container, onloadArgs); + } + }, + }); + const handleNodeMenuRender = (target: HTMLElement, evt: KeyboardEvent) => { if ( target.tagName === "TEXTAREA" && From b8ff5e5b274a66764304acd1d938328ce931b5fa Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 26 Sep 2025 13:06:33 +0530 Subject: [PATCH 10/10] nix useeffect --- apps/roam/src/components/settings/Settings.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 7a503ee3e..08c31aff5 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -78,11 +78,6 @@ export const SettingsDialog = ({ selectedTabId ?? "discourse-graph-home-personal", ); - useEffect(() => { - if (selectedTabId) { - setActiveTabId(selectedTabId); - } - }, [selectedTabId]); useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => {