diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index b449a15ff..5a002d4f1 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -19,8 +19,6 @@ import { 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"; import { getFormattedConfigTree, notify, @@ -40,50 +38,9 @@ import { OnloadArgs } from "roamjs-components/types"; import renderOverlay from "roamjs-components/util/renderOverlay"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; -import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings"; - -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, display: text }; - } -}; - -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, targetUid: string) => { - e.preventDefault(); - e.stopPropagation(); - const target = parseReference(targetUid); - if (target.type === "block") { - if (e.shiftKey) { - await openBlockInSidebar(target.uid); - return; - } - await window.roamAlphaAPI.ui.mainWindow.openBlock({ - block: { uid: target.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": targetUid }, - }); - } else { - await window.roamAlphaAPI.ui.mainWindow.openPage({ - page: { uid: targetUid }, - }); - } -}; +import { parseReference, SectionChildren } from "./left-sidebar/utils"; +import { ViewGlobalLeftSidebar } from "./left-sidebar/ViewGlobalLeftSidebar"; const toggleFoldedState = ({ isOpen, @@ -115,44 +72,6 @@ const toggleFoldedState = ({ } }; -const SectionChildren = ({ - childrenNodes, - truncateAt, -}: { - childrenNodes: { uid: string; text: string; alias?: { value: string } }[]; - truncateAt?: number; -}) => { - if (!childrenNodes?.length) return null; - return ( - <> - {childrenNodes.map((child) => { - const ref = parseReference(child.text); - const alias = child.alias?.value; - const 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 ( -
-
- {label} -
-
- ); - })} - - ); -}; - const PersonalSectionItem = ({ section, }: { @@ -230,47 +149,6 @@ const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => { ); }; -const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { - const [isOpen, setIsOpen] = useState( - !!config.settings?.folded.value, - ); - if (!config.children?.length) return null; - const isCollapsable = config.settings?.collapsable.value; - - return ( - <> -
{ - if (!isCollapsable || !config.settings) return; - toggleFoldedState({ - isOpen, - setIsOpen, - folded: config.settings.folded, - parentUid: config.settings.uid, - }); - }} - > -
- GLOBAL - {isCollapsable && ( - - - - )} -
-
- {isCollapsable ? ( - - - - ) : ( - - )} - - ); -}; - export const useConfig = () => { const [config, setConfig] = useState( () => getFormattedConfigTree().leftSidebar, @@ -408,7 +286,7 @@ const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { return ( <> - + ); @@ -533,7 +411,9 @@ export const mountLeftSidebar = async ( export const unmountLeftSidebar = (wrapper: HTMLElement): void => { if (!wrapper) return; - const root = wrapper.querySelector(`#${"dg-left-sidebar-root"}`) as HTMLDivElement; + const root = wrapper.querySelector( + `#${"dg-left-sidebar-root"}`, + ) as HTMLDivElement; if (root) { ReactDOM.unmountComponentAtNode(root); root.remove(); diff --git a/apps/roam/src/components/left-sidebar/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/left-sidebar/LeftSidebarGlobalSettings.tsx new file mode 100644 index 000000000..5214d8547 --- /dev/null +++ b/apps/roam/src/components/left-sidebar/LeftSidebarGlobalSettings.tsx @@ -0,0 +1,280 @@ +import React, { useCallback, useEffect, useMemo, useState, memo } from "react"; +import { Button, ButtonGroup } from "@blueprintjs/core"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import getAllPageNames from "roamjs-components/queries/getAllPageNames"; +import type { RoamBasicNode } from "roamjs-components/types"; +import { extractRef } from "roamjs-components/util"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { render as renderToast } from "roamjs-components/components/Toast"; +import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; +import { + getGlobalSetting, + setGlobalSetting, +} from "~/components/settings/block-prop/utils/accessors"; +import { GlobalSettingsSchema } from "~/components/settings/block-prop/utils/zodSchema"; +import { FlagPanel } from "../settings/block-prop/components/FlagPanel"; +import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; +import { CollapsiblePanel } from "../settings/block-prop/components/CollapsiblePanel"; + +const PageItem = memo( + ({ + page, + index, + isFirst, + isLast, + onMove, + onRemove, + }: { + page: RoamBasicNode; + index: number; + isFirst: boolean; + isLast: boolean; + onMove: (index: number, direction: "up" | "down") => void; + onRemove: (page: RoamBasicNode) => void; + }) => { + const pageDisplayTitle = + getPageTitleByPageUid(page.text) || + getTextByBlockUid(extractRef(page.text)) || + page.text; + + return ( +
+
{pageDisplayTitle}
+ +
+ ); + }, +); + +PageItem.displayName = "PageItem"; + +const LeftSidebarGlobalSectionsContent = () => { + const [pages, setPages] = useState([]); + const [newPageInput, setNewPageInput] = useState(""); + const [autocompleteKey, setAutocompleteKey] = useState(0); + const [isInitializing, setIsInitializing] = useState(true); + + const pageNames = useMemo(() => getAllPageNames(), []); + + useEffect(() => { + setIsInitializing(true); + + const leftSidebarSettings = GlobalSettingsSchema.shape[ + "Left Sidebar" + ].parse(getGlobalSetting(["Left Sidebar"]) || {}); + + setPages( + (leftSidebarSettings.Children || []).map((uid) => ({ + uid, + text: uid, + children: [], + })), + ); + setIsInitializing(false); + }, []); + + const movePage = useCallback( + (index: number, direction: "up" | "down") => { + if (direction === "up" && index === 0) return; + if (direction === "down" && index === pages.length - 1) return; + + const newPages = [...pages]; + const [removed] = newPages.splice(index, 1); + const newIndex = direction === "up" ? index - 1 : index + 1; + newPages.splice(newIndex, 0, removed); + void setGlobalSetting( + ["Left Sidebar", "Children"], + newPages.map((p) => p.text), + ); + setPages(newPages); + }, + [pages], + ); + + const addPage = useCallback( + (pageName: string) => { + if (!pageName) return; + + const targetUid = getPageUidByPageTitle(pageName); + if (pages.some((p) => p.text === targetUid)) { + console.warn(`Page "${pageName}" already exists in global section`); + return; + } + + try { + const newPage: RoamBasicNode = { + text: targetUid, + uid: targetUid, + children: [], + }; + + const nextPages = [...pages, newPage]; + void setGlobalSetting( + ["Left Sidebar", "Children"], + [...nextPages.map((p) => p.text)], + ); + setPages(nextPages); + setNewPageInput(""); + setAutocompleteKey((prev) => prev + 1); + } catch (error) { + renderToast({ + content: "Failed to add page", + intent: "danger", + id: "add-page-error", + }); + } + }, + [pages], + ); + + const removePage = useCallback( + (page: RoamBasicNode) => { + const next = pages.filter((p) => p.uid !== page.uid); + void setGlobalSetting( + ["Left Sidebar", "Children"], + next.map((p) => p.text), + ); + setPages(next); + }, + [pages], + ); + + const handlePageInputChange = useCallback((value: string) => { + setNewPageInput(value); + }, []); + + const isAddButtonDisabled = useMemo(() => { + if (!newPageInput) return true; + const targetUid = getPageUidByPageTitle(newPageInput); + return !targetUid || pages.some((p) => p.text === targetUid); + }, [newPageInput, pages]); + + if (isInitializing) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+
+
+ + +
+
+ + + Children + + {pages.length} {pages.length === 1 ? "page" : "pages"} + +
+ } + defaultOpen={true} + > +
+
+ Add pages that will appear for all users +
+
+ void addPage(newPageInput)} + /> +
+ {pages.length > 0 ? ( +
+ {pages.map((page, index) => ( + void removePage(page)} + /> + ))} +
+ ) : ( +
+ No pages added yet +
+ )} +
+ + + ); +}; + +export const LeftSidebarGlobalSections = () => { + return ; +}; diff --git a/apps/roam/src/components/left-sidebar/ViewGlobalLeftSidebar.tsx b/apps/roam/src/components/left-sidebar/ViewGlobalLeftSidebar.tsx new file mode 100644 index 000000000..4fe89db10 --- /dev/null +++ b/apps/roam/src/components/left-sidebar/ViewGlobalLeftSidebar.tsx @@ -0,0 +1,50 @@ +import { getGlobalSetting } from "~/components/settings/block-prop/utils/accessors"; +import { GlobalSettingsSchema } from "~/components/settings/block-prop/utils/zodSchema"; +import { CollapsiblePanel } from "~/components/settings/block-prop/components/CollapsiblePanel"; +import React from "react"; +import { SectionChildren } from "./utils"; + +export const ViewGlobalLeftSidebar = () => { + const settings = GlobalSettingsSchema.shape["Left Sidebar"].parse( + getGlobalSetting(["Left Sidebar"]) || {}, + ); + + const children = settings.Children || []; + const folded = settings.Settings.Folded; + const collapsable = settings.Settings.Collapsable; + + if (!children.length) return null; + + const childrenNodes = children.map((uid) => ({ + uid, + text: uid, + })); + + const header = ( + GLOBAL + ); + + if (collapsable) { + return ( +
+ + + +
+ ); + } + + return ( +
+
+ GLOBAL +
+ +
+ ); +}; diff --git a/apps/roam/src/components/left-sidebar/utils.tsx b/apps/roam/src/components/left-sidebar/utils.tsx new file mode 100644 index 000000000..27d45d5a9 --- /dev/null +++ b/apps/roam/src/components/left-sidebar/utils.tsx @@ -0,0 +1,85 @@ +import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; +import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; +import extractRef from "roamjs-components/util/extractRef"; +import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; +import React from "react"; + +export 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, display: text }; + } +}; + +export const truncate = (s: string, max: number | undefined): string => { + if (!max || max <= 0) return s; + return s.length > max ? `${s.slice(0, max)}...` : s; +}; + +export const openTarget = async (e: React.MouseEvent, targetUid: string) => { + e.preventDefault(); + e.stopPropagation(); + const target = parseReference(targetUid); + if (target.type === "block") { + if (e.shiftKey) { + await openBlockInSidebar(target.uid); + return; + } + await window.roamAlphaAPI.ui.mainWindow.openBlock({ + block: { uid: target.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": targetUid }, + }); + } else { + await window.roamAlphaAPI.ui.mainWindow.openPage({ + page: { uid: targetUid }, + }); + } +}; + +export const SectionChildren = ({ + childrenNodes, + truncateAt, +}: { + childrenNodes: { uid: string; text: string; alias?: { value: string } }[]; + truncateAt?: number; +}) => { + if (!childrenNodes?.length) return null; + return ( + <> + {childrenNodes.map((child) => { + const ref = parseReference(child.text); + const alias = child.alias?.value; + const 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 ( +
+
+ {label} +
+
+ ); + })} + + ); +}; diff --git a/apps/roam/src/components/settings/GeneralSettings.tsx b/apps/roam/src/components/settings/GeneralSettings.tsx index 0ab5a1ce9..1c3bacea8 100644 --- a/apps/roam/src/components/settings/GeneralSettings.tsx +++ b/apps/roam/src/components/settings/GeneralSettings.tsx @@ -3,9 +3,9 @@ import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { DEFAULT_CANVAS_PAGE_FORMAT } from "~/index"; -import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/data/blockPropsSettingsConfig"; +import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; import { Alert, Intent } from "@blueprintjs/core"; -import { FlagPanel } from "./block-prop/FlagPanel"; +import { FlagPanel } from "./block-prop/components/FlagPanel"; const DiscourseGraphHome = () => { const settings = useMemo(() => { diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx deleted file mode 100644 index d9c8d739a..000000000 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState, memo } from "react"; -import { Button, ButtonGroup, 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 { extractRef, 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 getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; -import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; - -const PageItem = memo( - ({ - page, - index, - isFirst, - isLast, - onMove, - onRemove, - }: { - page: RoamBasicNode; - index: number; - isFirst: boolean; - isLast: boolean; - onMove: (index: number, direction: "up" | "down") => void; - onRemove: (page: RoamBasicNode) => void; - }) => { - const pageDisplayTitle = - getPageTitleByPageUid(page.text) || - getTextByBlockUid(extractRef(page.text)) || - page.text; - - return ( -
-
{pageDisplayTitle}
- -
- ); - }, -); - -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 movePage = useCallback( - (index: number, direction: "up" | "down") => { - if (direction === "up" && index === 0) return; - if (direction === "down" && index === pages.length - 1) return; - - const newPages = [...pages]; - const [removed] = newPages.splice(index, 1); - const newIndex = direction === "up" ? index - 1 : index + 1; - newPages.splice(newIndex, 0, removed); - - setPages(newPages); - - if (childrenUid) { - const order = direction === "down" ? newIndex + 1 : newIndex; - - void window.roamAlphaAPI - /* eslint-disable @typescript-eslint/naming-convention */ - .moveBlock({ - location: { "parent-uid": childrenUid, order }, - block: { uid: removed.uid }, - }) - .then(() => { - refreshAndNotify(); - }); - } - }, - [pages, childrenUid], - ); - - const addPage = useCallback( - async (pageName: string) => { - if (!pageName || !childrenUid) return; - - const targetUid = getPageUidByPageTitle(pageName); - if (pages.some((p) => p.text === targetUid)) { - console.warn(`Page "${pageName}" already exists in global section`); - return; - } - - try { - const newPageUid = await createBlock({ - parentUid: childrenUid, - order: "last", - node: { text: targetUid }, - }); - - const newPage: RoamBasicNode = { - text: targetUid, - 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 toggleChildren = useCallback(() => { - setIsExpanded((prev) => !prev); - }, []); - - const isAddButtonDisabled = useMemo(() => { - if (!newPageInput) return true; - const targetUid = getPageUidByPageTitle(newPageInput); - return !targetUid || pages.some((p) => p.text === targetUid); - }, [newPageInput, pages]); - - if (isInitializing || !globalSection) { - return ( -
- Loading... -
- ); - } - - return ( -
-
- - -
- -
-
-
-
- - {pages.length} {pages.length === 1 ? "page" : "pages"} - -
- - -
-
- Add pages that will appear for all users -
-
- void addPage(newPageInput)} - /> -
- {pages.length > 0 ? ( -
- {pages.map((page, index) => ( - 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 deleted file mode 100644 index 3328482c9..000000000 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ /dev/null @@ -1,740 +0,0 @@ -import discourseConfigRef from "~/utils/discourseConfigRef"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; -import getAllPageNames from "roamjs-components/queries/getAllPageNames"; -import { - Button, - ButtonGroup, - Collapse, - Dialog, - InputGroup, -} 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, - PersonalSectionChild, -} 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"; -import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; - -const SectionItem = memo( - ({ - section, - setSettingsDialogSectionUid, - pageNames, - setSections, - index, - isFirst, - isLast, - onMoveSection, - }: { - section: LeftSidebarPersonalSectionConfig; - setSections: Dispatch>; - setSettingsDialogSectionUid: (uid: string | null) => void; - pageNames: string[]; - index: number; - isFirst: boolean; - isLast: boolean; - onMoveSection: (index: number, direction: "up" | "down") => void; - }) => { - const ref = extractRef(section.text); - const blockText = getTextByBlockUid(ref); - const originalName = blockText || section.text; - const [childInput, setChildInput] = useState(""); - const [childInputKey, setChildInputKey] = useState(0); - - const [expandedChildLists, setExpandedChildLists] = useState>( - new Set(), - ); - const isExpanded = expandedChildLists.has(section.uid); - const [childSettingsUid, setChildSettingsUid] = useState( - null, - ); - 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 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 }, - }, - 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; - - const targetUid = getPageUidByPageTitle(childName) || childName.trim(); - - try { - const newChild = await createBlock({ - parentUid: childrenUid, - order: "last", - node: { text: targetUid }, - }); - - setSections((prev) => - prev.map((s) => { - if (s.uid === section.uid) { - return { - ...s, - children: [ - ...(s.children || []), - { - text: targetUid, - uid: newChild, - children: [], - alias: { value: "" }, - }, - ], - }; - } - 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: PersonalSectionChild, - ) => { - 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 moveChild = useCallback( - ( - section: LeftSidebarPersonalSectionConfig, - index: number, - direction: "up" | "down", - ) => { - if (!section.children) return; - if (direction === "up" && index === 0) return; - if (direction === "down" && index === section.children.length - 1) - return; - - const newChildren = [...section.children]; - const [removed] = newChildren.splice(index, 1); - const newIndex = direction === "up" ? index - 1 : index + 1; - newChildren.splice(newIndex, 0, removed); - - setSections((prev) => - prev.map((s) => { - if (s.uid === section.uid) { - return { - ...s, - children: newChildren, - }; - } - return s; - }), - ); - - if (section.childrenUid) { - const order = direction === "down" ? newIndex + 1 : newIndex; - - void window.roamAlphaAPI - /* eslint-disable @typescript-eslint/naming-convention */ - .moveBlock({ - location: { "parent-uid": section.childrenUid, order }, - block: { uid: removed.uid }, - }) - .then(() => { - refreshAndNotify(); - }); - } - }, - [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?.length === 0) || - !section.children; - - return ( -
-
- {!sectionWithoutSettingsAndChildren && ( -
- - {!sectionWithoutSettingsAndChildren && ( - -
-
- void handleAddChild()} - /> -
- - {(section.children || []).length > 0 && ( -
- {(section.children || []).map((child, index) => { - const childAlias = child.alias?.value; - const isSettingsOpen = childSettingsUid === child.uid; - const childDisplayTitle = - getPageTitleByPageUid(child.text) || - getTextByBlockUid(extractRef(child.text)) || - child.text; - return ( -
-
-
- {childAlias ? ( - - - {childAlias} - - - ({childDisplayTitle}) - - - ) : ( - childDisplayTitle - )} -
- -
- { - setChildSettingsUid(null); - refreshAndNotify(); - }} - title={`Settings for "${childDisplayTitle}"`} - style={{ width: "400px" }} - > -
- , - ) => { - const nextValue = event.target.value; - setSections((prev) => - prev.map((s) => - s.uid === section.uid - ? { - ...s, - children: s.children?.map((c) => - c.uid === child.uid - ? { - ...c, - alias: { - ...c.alias, - value: nextValue, - }, - } - : c, - ), - } - : s, - ), - ); - }, - }} - /> -
-
-
- ); - })} -
- )} - - {(!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 [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(""); - 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 moveSection = useCallback( - (index: number, direction: "up" | "down") => { - if (direction === "up" && index === 0) return; - if (direction === "down" && index === sections.length - 1) return; - - const newSections = [...sections]; - const [removed] = newSections.splice(index, 1); - const newIndex = direction === "up" ? index - 1 : index + 1; - newSections.splice(newIndex, 0, removed); - - setSections(newSections); - - if (personalSectionUid) { - const order = direction === "down" ? newIndex + 1 : newIndex; - - void window.roamAlphaAPI - /* eslint-disable @typescript-eslint/naming-convention */ - .moveBlock({ - location: { "parent-uid": personalSectionUid, order }, - block: { uid: removed.uid }, - }) - .then(() => { - refreshAndNotify(); - }); - } - }, - [sections, personalSectionUid], - ); - - 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 -
-
- handleNewSectionInputChange(e.target.value)} - placeholder="Add section …" - onKeyDown={(e) => { - if (e.key === "Enter" && newSectionInput) { - e.preventDefault(); - e.stopPropagation(); - void addSection(newSectionInput); - } - }} - /> -
-
- -
- {sections.map((section, index) => ( -
- -
- ))} -
- - {activeDialogSection && activeDialogSection.settings && ( - setSettingsDialogSectionUid(null)} - title={`Settings for "${activeDialogSection.text}"`} - style={{ width: "500px" }} - > -
-
- -
-
-
- )} -
- ); -}; - -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 7e6069b96..93f5b80b7 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -26,8 +26,8 @@ import HomePersonalSettings from "./HomePersonalSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { FeedbackWidget } from "~/components/BirdEatsBugs"; import { getVersionWithDate } from "~/utils/getVersion"; -import { LeftSidebarPersonalSections } from "./LeftSidebarPersonalSettings"; -import { LeftSidebarGlobalSections } from "./LeftSidebarGlobalSettings"; +import { LeftSidebarPersonalSections } from "../left-sidebar/LeftSidebarPersonalSettings"; +import { LeftSidebarGlobalSections } from "../left-sidebar/LeftSidebarGlobalSettings"; type SectionHeaderProps = { children: React.ReactNode; diff --git a/apps/roam/src/components/settings/block-prop/components/CollapsiblePanel.tsx b/apps/roam/src/components/settings/block-prop/components/CollapsiblePanel.tsx new file mode 100644 index 000000000..876eef00a --- /dev/null +++ b/apps/roam/src/components/settings/block-prop/components/CollapsiblePanel.tsx @@ -0,0 +1,71 @@ +import React, { useState, ReactNode, useCallback } from "react"; +import { Button, Collapse } from "@blueprintjs/core"; +import { getGlobalSetting, setGlobalSetting } from "~/components/settings/block-prop/utils/accessors"; +import z from "zod"; + +type Props = { + header: ReactNode; + children: ReactNode; + settingKey?: string[]; + defaultOpen?: boolean; + className?: string; +}; + +export const CollapsiblePanel = ({ + header, + children, + settingKey, + defaultOpen = false, + className = "", +}: Props) => { + const getPersistedValue = useCallback(() => { + if (!settingKey || settingKey.length === 0) return undefined; + const current = getGlobalSetting(settingKey); + const parsed = z.boolean().safeParse(current); + return parsed.success ? parsed.data : undefined; + }, [settingKey]); + + const [isOpen, setIsOpen] = useState(() => { + const persisted = getPersistedValue(); + return persisted !== undefined ? persisted : defaultOpen; + }); + + const handleToggle = () => { + const newState = !isOpen; + setIsOpen(newState); + if (settingKey && settingKey.length > 0) { + setGlobalSetting(settingKey, newState); + } + }; + + return ( +
+
+
+
+
+ + +
{children}
+
+
+ ); +}; diff --git a/apps/roam/src/components/settings/block-prop/FlagPanel.tsx b/apps/roam/src/components/settings/block-prop/components/FlagPanel.tsx similarity index 89% rename from apps/roam/src/components/settings/block-prop/FlagPanel.tsx rename to apps/roam/src/components/settings/block-prop/components/FlagPanel.tsx index a7dc00990..c6b2745da 100644 --- a/apps/roam/src/components/settings/block-prop/FlagPanel.tsx +++ b/apps/roam/src/components/settings/block-prop/components/FlagPanel.tsx @@ -7,10 +7,10 @@ import { getGlobalSetting, setFeatureFlag, setGlobalSetting, -} from "~/utils/Settings/accessors"; -import { type FeatureFlags } from "~/utils/Settings/zodSchema"; +} from "~/components/settings/block-prop/utils/accessors"; +import { type FeatureFlags } from "~/components/settings/block-prop/utils/zodSchema"; import z from "zod"; -import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/data/blockPropsSettingsConfig"; +import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; type FeatureFlagPath = [ typeof TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags, diff --git a/apps/roam/src/data/blockPropsSettingsConfig.ts b/apps/roam/src/components/settings/block-prop/data/blockPropsSettingsConfig.ts similarity index 100% rename from apps/roam/src/data/blockPropsSettingsConfig.ts rename to apps/roam/src/components/settings/block-prop/data/blockPropsSettingsConfig.ts diff --git a/apps/roam/src/utils/Settings/accessors.ts b/apps/roam/src/components/settings/block-prop/utils/accessors.ts similarity index 91% rename from apps/roam/src/utils/Settings/accessors.ts rename to apps/roam/src/components/settings/block-prop/utils/accessors.ts index 4e81a09a5..61eaf581b 100644 --- a/apps/roam/src/utils/Settings/accessors.ts +++ b/apps/roam/src/components/settings/block-prop/utils/accessors.ts @@ -1,16 +1,16 @@ -import getBlockProps, { type json } from "../getBlockProps"; +import getBlockProps, { type json } from "~/utils/getBlockProps"; import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; -import setBlockProps from "../setBlockProps"; +import setBlockProps from "~/utils/setBlockProps"; import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, TOP_LEVEL_BLOCK_PROP_KEYS, -} from "~/data/blockPropsSettingsConfig"; +} from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; import z from "zod"; import { FeatureFlags, FeatureFlagsSchema, GlobalSettingsSchema, -} from "./zodSchema"; +} from "~/components/settings/block-prop/utils/zodSchema"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); @@ -77,7 +77,7 @@ export const setBlockPropBasedSettings = ({ }); if (keys.length === 1) { - setBlockProps(blockUid, value as Record, true); + setBlockProps(blockUid, value as Record, false); return; } @@ -110,7 +110,7 @@ export const setBlockPropBasedSettings = ({ updatedProps, ); - setBlockProps(blockUid, updatedProps, true); + setBlockProps(blockUid, updatedProps, false); }; export const getFeatureFlag = (key: keyof FeatureFlags): boolean => { diff --git a/apps/roam/src/utils/Settings/pullWatch.ts b/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts similarity index 69% rename from apps/roam/src/utils/Settings/pullWatch.ts rename to apps/roam/src/components/settings/block-prop/utils/pullWatch.ts index 49760fb1b..4dc7f5960 100644 --- a/apps/roam/src/utils/Settings/pullWatch.ts +++ b/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts @@ -1,5 +1,5 @@ -import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/data/blockPropsSettingsConfig"; -import { json, normalizeProps } from "~/utils/getBlockProps"; +import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; +import { type json, normalizeProps } from "~/utils/getBlockProps"; export const setupPullWatchBlockPropsBasedSettings = ( blockUids: Record, @@ -9,6 +9,8 @@ export const setupPullWatchBlockPropsBasedSettings = ( const featureFlagsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; + const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global]; + if (featureFlagsBlockUid) { window.roamAlphaAPI.data.addPullWatch( "[:block/props]", @@ -35,4 +37,14 @@ export const setupPullWatchBlockPropsBasedSettings = ( }, ); } + + if (globalSettingsBlockUid) { + window.roamAlphaAPI.data.addPullWatch( + "[:block/props]", + `[:block/uid "${globalSettingsBlockUid}"]`, + () => { + updateLeftSidebar(leftSidebarContainer); + }, + ); + } }; diff --git a/apps/roam/src/utils/Settings/init.ts b/apps/roam/src/utils/Settings/init.ts deleted file mode 100644 index 38f027b86..000000000 --- a/apps/roam/src/utils/Settings/init.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - TOP_LEVEL_BLOCK_PROP_KEYS, - DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, -} from "~/data/blockPropsSettingsConfig"; -import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; -import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; -import { createPage, createBlock } from "roamjs-components/writes"; - -const ensurePageExists = async (pageTitle: string): Promise => { - let pageUid = getPageUidByPageTitle(pageTitle); - - if (!pageUid) { - pageUid = window.roamAlphaAPI.util.generateUID(); - await createPage({ - title: pageTitle, - uid: pageUid, - }); - } - - return pageUid; -}; - -const ensureBlocksExist = async ( - pageUid: string, -): Promise> => { - const blockTexts = Object.values(TOP_LEVEL_BLOCK_PROP_KEYS); - const existingChildren = getShallowTreeByParentUid(pageUid); - - const blockMap: Record = {}; - existingChildren.forEach((child) => { - blockMap[child.text] = child.uid; - }); - - const missingBlocks = blockTexts.filter((blockText) => !blockMap[blockText]); - - if (missingBlocks.length > 0) { - const createdBlocks = await Promise.all( - missingBlocks.map(async (blockText) => { - const uid = await createBlock({ - parentUid: pageUid, - node: { text: blockText }, - }); - return { text: blockText, uid }; - }), - ); - - createdBlocks.forEach((block) => { - blockMap[block.text] = block.uid; - }); - } - - return blockMap; -}; - -export const initSchema = async (): Promise> => { - const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); - return await ensureBlocksExist(pageUid); -}; diff --git a/apps/roam/src/utils/Settings/zodSchema.ts b/apps/roam/src/utils/Settings/zodSchema.ts deleted file mode 100644 index ecab73e52..000000000 --- a/apps/roam/src/utils/Settings/zodSchema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { z } from "zod"; - -/* eslint-disable @typescript-eslint/naming-convention */ -export const FeatureFlagsSchema = z.object({ - "Enable Left Sidebar": z.boolean().default(false), -}); - -export const GlobalSettingsSchema = z.object({ - "Left Sidebar": z.object({ - Children: z.array(z.string()).default([]), - Settings: z.object({ - Collapsable: z.boolean().default(false), - Folded: z.boolean().default(false), - }), - }), -}); -/* eslint-disable @typescript-eslint/naming-convention */ - -export type FeatureFlags = z.infer; -export type GlobalSettings = z.infer; diff --git a/apps/roam/src/utils/initializeObserversAndListeners.ts b/apps/roam/src/utils/initializeObserversAndListeners.ts index b08dd6858..59c116552 100644 --- a/apps/roam/src/utils/initializeObserversAndListeners.ts +++ b/apps/roam/src/utils/initializeObserversAndListeners.ts @@ -56,9 +56,9 @@ import { import { getCleanTagText } from "~/components/settings/NodeConfig"; import getPleasingColors from "@repo/utils/getPleasingColors"; import { colord } from "colord"; -import { getFeatureFlag } from "./Settings/accessors"; -import { setupPullWatchBlockPropsBasedSettings } from "~/utils/Settings/pullWatch"; -import { initSchema } from "./Settings/init"; +import { getFeatureFlag } from "~/components/settings/block-prop/utils/accessors"; +import { setupPullWatchBlockPropsBasedSettings } from "~/components/settings/block-prop/utils/pullWatch"; +import { initSchema } from "../components/settings/block-prop/utils/init"; const debounce = (fn: () => void, delay = 250) => { let timeout: number;