diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index 5635c1d00..c551739b6 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -42,6 +42,8 @@ import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByPar import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; import { migrateLeftSidebarSettings } from "~/utils/migrateLeftSidebarSettings"; +import { useLeftSidebarGlobalSettings } from "./settings/utils/hooks"; +import { setGlobalSetting } from "./settings/utils/accessors"; const parseReference = (text: string) => { const extracted = extractRef(text); @@ -230,26 +232,37 @@ 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; +const GlobalSection = () => { + const globalSettings = useLeftSidebarGlobalSettings(); + const children = globalSettings.Children || []; + const isCollapsable = globalSettings.Settings?.Collapsable ?? false; + const isFolded = globalSettings.Settings?.Folded ?? false; + + const [isOpen, setIsOpen] = useState(isFolded); + + useEffect(() => { + setIsOpen(isFolded); + }, [isFolded]); + + const handleToggleFold = useCallback(() => { + if (!isCollapsable) return; + const newFoldedState = !isOpen; + setIsOpen(newFoldedState); + setGlobalSetting(["Left Sidebar", "Settings", "Folded"], newFoldedState); + }, [isCollapsable, isOpen]); + + if (!children.length) return null; + + const childrenNodes = children.map((uid) => ({ + uid, + text: uid, + })); return ( <>
{ - if (!isCollapsable || !config.settings) return; - toggleFoldedState({ - isOpen, - setIsOpen, - folded: config.settings.folded, - parentUid: config.settings.uid, - }); - }} + onClick={handleToggleFold} >
GLOBAL @@ -262,10 +275,10 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => {
{isCollapsable ? ( - + ) : ( - + )} ); @@ -408,7 +421,7 @@ const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { return ( <> - + ); diff --git a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx index d9c8d739a..07be24ea3 100644 --- a/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarGlobalSettings.tsx @@ -1,22 +1,23 @@ -import React, { useCallback, useEffect, useMemo, useState, memo } from "react"; +import React, { useCallback, 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 { extractRef } 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"; +import { + getGlobalSetting, + setGlobalSetting, +} from "~/components/settings/utils/accessors"; +import { LeftSidebarGlobalSettingsSchema } from "~/components/settings/utils/zodSchema"; +import { GlobalFlagPanel } from "./components/BlockPropSettingPanels"; + +type PageData = { + uid: string; + text: string; +}; const PageItem = memo( ({ @@ -27,12 +28,12 @@ const PageItem = memo( onMove, onRemove, }: { - page: RoamBasicNode; + page: PageData; index: number; isFirst: boolean; isLast: boolean; onMove: (index: number, direction: "up" | "down") => void; - onRemove: (page: RoamBasicNode) => void; + onRemove: (page: PageData) => void; }) => { const pageDisplayTitle = getPageTitleByPageUid(page.text) || @@ -74,79 +75,25 @@ const PageItem = memo( PageItem.displayName = "PageItem"; -const LeftSidebarGlobalSectionsContent = ({ - leftSidebar, -}: { - leftSidebar: RoamBasicNode; -}) => { - const [globalSection, setGlobalSection] = - useState(null); - const [pages, setPages] = useState([]); - const [childrenUid, setChildrenUid] = useState(null); +const LeftSidebarGlobalSectionsContent = () => { + const [pages, setPages] = useState(() => { + const leftSidebarSettings = LeftSidebarGlobalSettingsSchema.parse( + getGlobalSetting(["Left Sidebar"]) ?? {}, + ); + return (leftSidebarSettings.Children || []).map((uid: string) => ({ + uid, + text: uid, + })); + }); 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 updateChildren = useCallback((newChildren: string[]) => { + setGlobalSetting(["Left Sidebar", "Children"], newChildren); + }, []); const movePage = useCallback( (index: number, direction: "up" | "down") => { @@ -159,75 +106,52 @@ const LeftSidebarGlobalSectionsContent = ({ 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(); - }); - } + updateChildren(newPages.map((p) => p.text)); }, - [pages, childrenUid], + [pages, updateChildren], ); const addPage = useCallback( - async (pageName: string) => { - if (!pageName || !childrenUid) return; + (pageName: string) => { + if (!pageName) return; const targetUid = getPageUidByPageTitle(pageName); + if (!targetUid) { + renderToast({ + content: `Page "${pageName}" not found`, + intent: "warning", + id: "page-not-found", + }); + return; + } + 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: [], - }; + const newPage: PageData = { + uid: targetUid, + text: targetUid, + }; - setPages((prev) => [...prev, newPage]); - setNewPageInput(""); - setAutocompleteKey((prev) => prev + 1); - refreshAndNotify(); - } catch (error) { - renderToast({ - content: "Failed to add page", - intent: "danger", - id: "add-page-error", - }); - } + const nextPages = [...pages, newPage]; + setPages(nextPages); + updateChildren(nextPages.map((p) => p.text)); + setNewPageInput(""); + setAutocompleteKey((prev) => prev + 1); }, - [childrenUid, pages], + [pages, updateChildren], ); - 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 removePage = useCallback( + (page: PageData) => { + const nextPages = pages.filter((p) => p.uid !== page.uid); + setPages(nextPages); + updateChildren(nextPages.map((p) => p.text)); + }, + [pages, updateChildren], + ); const handlePageInputChange = useCallback((value: string) => { setNewPageInput(value); @@ -243,14 +167,6 @@ const LeftSidebarGlobalSectionsContent = ({ return !targetUid || pages.some((p) => p.text === targetUid); }, [newPageInput, pages]); - if (isInitializing || !globalSection) { - return ( -
- Loading... -
- ); - } - return (
- -
@@ -312,14 +223,14 @@ const LeftSidebarGlobalSectionsContent = ({ options={pageNames} maxItemsDisplayed={50} autoFocus - onConfirm={() => void addPage(newPageInput)} + onConfirm={() => addPage(newPageInput)} />
@@ -333,7 +244,7 @@ const LeftSidebarGlobalSectionsContent = ({ isFirst={index === 0} isLast={index === pages.length - 1} onMove={movePage} - onRemove={() => void removePage(page)} + onRemove={removePage} /> ))}
@@ -350,32 +261,5 @@ const LeftSidebarGlobalSectionsContent = ({ }; 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 ; + return ; }; diff --git a/apps/roam/src/components/settings/utils/hooks.ts b/apps/roam/src/components/settings/utils/hooks.ts index caba47be5..42467ac20 100644 --- a/apps/roam/src/components/settings/utils/hooks.ts +++ b/apps/roam/src/components/settings/utils/hooks.ts @@ -1,14 +1,22 @@ -import { useState, useEffect } from "react"; -import { getFeatureFlag } from "./accessors"; -import type { FeatureFlags } from "./zodSchema"; +import { useState, useEffect, useCallback } from "react"; +import { getFeatureFlag, getGlobalSetting } from "./accessors"; +import type { FeatureFlags, LeftSidebarGlobalSettings } from "./zodSchema"; +import { LeftSidebarGlobalSettingsSchema } from "./zodSchema"; +import type { json } from "~/utils/getBlockProps"; const FEATURE_FLAG_CHANGE_EVENT = "discourse-graph:feature-flag-change"; +const GLOBAL_SETTING_CHANGE_EVENT = "discourse-graph:global-setting-change"; type FeatureFlagChangeDetail = { key: keyof FeatureFlags; value: boolean; }; +type GlobalSettingChangeDetail = { + keys: string[]; + value: json; +}; + export const emitFeatureFlagChange = ( key: keyof FeatureFlags, value: boolean, @@ -20,6 +28,14 @@ export const emitFeatureFlagChange = ( ); }; +export const emitGlobalSettingChange = (keys: string[], value: json): void => { + window.dispatchEvent( + new CustomEvent(GLOBAL_SETTING_CHANGE_EVENT, { + detail: { keys, value }, + }), + ); +}; + export const useFeatureFlag = (key: keyof FeatureFlags): boolean => { const [value, setValue] = useState(() => getFeatureFlag(key)); @@ -39,3 +55,31 @@ export const useFeatureFlag = (key: keyof FeatureFlags): boolean => { return value; }; + +export const useLeftSidebarGlobalSettings = (): LeftSidebarGlobalSettings => { + const [settings, setSettings] = useState(() => { + const raw = getGlobalSetting(["Left Sidebar"]); + return LeftSidebarGlobalSettingsSchema.parse(raw ?? {}); + }); + + const refreshSettings = useCallback(() => { + const raw = getGlobalSetting(["Left Sidebar"]); + setSettings(LeftSidebarGlobalSettingsSchema.parse(raw ?? {})); + }, []); + + useEffect(() => { + const handleChange = (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail.keys[0] === "Left Sidebar") { + refreshSettings(); + } + }; + + window.addEventListener(GLOBAL_SETTING_CHANGE_EVENT, handleChange); + return () => { + window.removeEventListener(GLOBAL_SETTING_CHANGE_EVENT, handleChange); + }; + }, [refreshSettings]); + + return settings; +}; diff --git a/apps/roam/src/components/settings/utils/pullWatchers.ts b/apps/roam/src/components/settings/utils/pullWatchers.ts index b60bf73a5..8ecab714b 100644 --- a/apps/roam/src/components/settings/utils/pullWatchers.ts +++ b/apps/roam/src/components/settings/utils/pullWatchers.ts @@ -22,7 +22,7 @@ import { initializeSupabaseSync, setSyncActivity, } from "~/utils/syncDgNodesToSupabase"; -import { emitFeatureFlagChange } from "./hooks"; +import { emitFeatureFlagChange, emitGlobalSettingChange } from "./hooks"; type PullWatchCallback = (before: unknown, after: unknown) => void; @@ -134,28 +134,18 @@ export const featureFlagHandlers: Partial< export const globalSettingsHandlers: Partial< Record > = { - // Add handlers as needed: - // "Trigger": (newValue) => { ... }, - // "Canvas Page Format": (newValue) => { ... }, - // "Left Sidebar": (newValue) => { ... }, - // "Export": (newValue) => { ... }, - // "Suggestive Mode": (newValue) => { ... }, + "Left Sidebar": (newValue, oldValue) => { + if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) { + emitGlobalSettingChange(["Left Sidebar"], newValue); + } + }, }; export const personalSettingsHandlers: Partial< Record -> = { - // Add handlers as needed: - // "Left Sidebar": (newValue) => { ... }, - // "Discourse Context Overlay": (newValue) => { ... }, - // "Page Preview": (newValue) => { ... }, - // etc. -}; +> = {}; -export const discourseNodeHandlers: DiscourseNodeHandler[] = [ - // Add handlers as needed: - // (nodeType, newSettings, oldSettings) => { ... }, -]; +export const discourseNodeHandlers: DiscourseNodeHandler[] = []; export const setupPullWatchSettings = ( blockUids: Record,