diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index c551739b6..198a2ede7 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -1,11 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import ReactDOM from "react-dom"; import { Collapse, @@ -21,29 +15,23 @@ import { import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import openBlockInSidebar from "roamjs-components/writes/openBlockInSidebar"; import extractRef from "roamjs-components/util/extractRef"; -import { - getFormattedConfigTree, - notify, - subscribe, -} from "~/utils/discourseConfigRef"; -import type { - LeftSidebarConfig, - LeftSidebarPersonalSectionConfig, -} from "~/utils/getLeftSidebarSettings"; +import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; 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"; import { SettingsDialog } from "./settings/Settings"; 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"; -import { useLeftSidebarGlobalSettings } from "./settings/utils/hooks"; -import { setGlobalSetting } from "./settings/utils/accessors"; +import { + useLeftSidebarGlobalSettings, + useLeftSidebarPersonalSettings, +} from "./settings/utils/hooks"; +import { getGlobalSetting, setGlobalSetting, setPersonalSetting } from "./settings/utils/accessors"; +import type { LeftSidebarGlobalSettings } from "./settings/utils/zodSchema"; +import type { PersonalSection } from "./settings/utils/zodSchema"; const parseReference = (text: string) => { const extracted = extractRef(text); @@ -87,36 +75,6 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { } }; -const toggleFoldedState = ({ - isOpen, - setIsOpen, - folded, - parentUid, -}: { - isOpen: boolean; - setIsOpen: 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, @@ -156,31 +114,32 @@ const SectionChildren = ({ }; const PersonalSectionItem = ({ + sectionName, section, + onToggleFolded, }: { - section: LeftSidebarPersonalSectionConfig; + sectionName: string; + section: PersonalSection; + onToggleFolded: (sectionName: string) => void; }) => { - 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 truncateAt = section.Settings?.["Truncate-result?"]; + const [isOpen, setIsOpen] = useState(section.Settings?.Folded ?? false); - const handleChevronClick = () => { - if (!section.settings) return; - - toggleFoldedState({ - isOpen, - setIsOpen, - folded: section.settings.folded, - parentUid: section.settings.uid || "", - }); - }; + useEffect(() => { + setIsOpen(section.Settings?.Folded ?? false); + }, [section.Settings?.Folded]); + + const handleChevronClick = useCallback(() => { + const newState = !isOpen; + setIsOpen(newState); + onToggleFolded(sectionName); + }, [isOpen, sectionName, onToggleFolded]); + + const childrenNodes = section.Children.map((child) => ({ + uid: child.Page, + text: child.Page, + alias: child.Alias ? { value: child.Alias } : undefined, + })); return ( <> @@ -189,14 +148,14 @@ const PersonalSectionItem = ({
{ - if ((section.children?.length || 0) > 0) { + if (section.Children.length > 0) { handleChevronClick(); } }} > - {(blockText || titleRef.display).toUpperCase()} + {sectionName.toUpperCase()}
- {(section.children?.length || 0) > 0 && ( + {section.Children.length > 0 && ( - + ); }; -const PersonalSections = ({ config }: { config: LeftSidebarConfig }) => { - const sections = config.personal.sections || []; +const PersonalSections = () => { + const personalSettings = useLeftSidebarPersonalSettings(); + const sections = Object.entries(personalSettings); + + const handleToggleFolded = useCallback( + (sectionName: string) => { + const section = personalSettings[sectionName]; + if (!section) return; + const newFolded = !section.Settings.Folded; + setPersonalSetting(["Left Sidebar", sectionName, "Settings", "Folded"], newFolded); + }, + [personalSettings], + ); if (!sections.length) return null; return (
- {sections.map((section) => ( -
- + {sections.map(([name, section]) => ( +
+
))}
@@ -284,27 +255,6 @@ const GlobalSection = () => { ); }; -export const useConfig = () => { - const [config, setConfig] = useState( - () => getFormattedConfigTree().leftSidebar, - ); - useEffect(() => { - const handleUpdate = () => { - setConfig(getFormattedConfigTree().leftSidebar); - }; - const unsubscribe = subscribe(handleUpdate); - return () => { - unsubscribe(); - }; - }, []); - return { config, setConfig }; -}; - -export const refreshAndNotify = () => { - refreshConfigTree(); - notify(); -}; - const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const menuTriggerRef = useRef(null); @@ -416,13 +366,11 @@ const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { }; const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { - const { config } = useConfig(); - return ( <> - + ); }; @@ -435,31 +383,23 @@ const migrateFavorites = async () => { const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); if (!configPageUid) return; - let leftSidebarUid = config.uid; - if (leftSidebarUid) { - const leftSidebarTree = getBasicTreeByParentUid(leftSidebarUid); - const hasAnyPersonalSection = leftSidebarTree.some((node) => - node.text.endsWith("/Personal-Section"), - ); - if (hasAnyPersonalSection) { - await createBlock({ - parentUid: leftSidebarUid, - node: { text: "Favorites Migrated" }, - }); - refreshConfigTree(); - return; - } - } - const results = window.roamAlphaAPI.q(` - [:find ?uid + [:find ?uid :where [?e :page/sidebar] [?e :block/uid ?uid]] `); - const favorites = (results as string[][]).map(([uid]) => ({ - uid, - })); + const favorites = (results as string[][]).map(([uid]) => uid); + + const currentSettings = getGlobalSetting(["Left Sidebar"]); + const existingChildren = new Set(currentSettings?.Children || []); + const newFavorites = favorites.filter((uid) => !existingChildren.has(uid)); + + if (newFavorites.length > 0) { + const mergedChildren = [...(currentSettings?.Children || []), ...newFavorites]; + setGlobalSetting(["Left Sidebar", "Children"], mergedChildren); + } + let leftSidebarUid = config.uid; if (!leftSidebarUid) { const tree = getBasicTreeByParentUid(configPageUid); const found = tree.find((n) => n.text === "Left Sidebar"); @@ -473,47 +413,6 @@ const migrateFavorites = async () => { } } - let globalSectionUid = config.global.uid; - if (!globalSectionUid) { - const tree = getBasicTreeByParentUid(leftSidebarUid); - const found = tree.find((n) => n.text === "Global-Section"); - if (found) { - globalSectionUid = found.uid; - } else { - globalSectionUid = await createBlock({ - parentUid: leftSidebarUid, - node: { text: "Global-Section" }, - }); - } - } - - let childrenUid = config.global.childrenUid; - if (!childrenUid) { - const tree = getBasicTreeByParentUid(globalSectionUid); - const found = tree.find((n) => n.text === "Children"); - if (found) { - childrenUid = found.uid; - } else { - childrenUid = await createBlock({ - parentUid: globalSectionUid, - node: { text: "Children" }, - }); - } - } - - const childrenTree = getBasicTreeByParentUid(childrenUid); - const existingTexts = new Set(childrenTree.map((c) => c.text)); - const newFavorites = favorites.filter(({ uid }) => !existingTexts.has(uid)); - - if (newFavorites.length > 0) { - await Promise.all( - newFavorites.map(({ uid }) => - createBlock({ parentUid: childrenUid, node: { text: uid } }), - ), - ); - refreshAndNotify(); - } - await createBlock({ parentUid: leftSidebarUid, node: { text: "Favorites Migrated" }, @@ -539,7 +438,6 @@ export const mountLeftSidebar = async ( let root = wrapper.querySelector(`#${id}`) as HTMLDivElement; if (!root) { await migrateFavorites(); - await migrateLeftSidebarSettings(); wrapper.innerHTML = ""; root = document.createElement("div"); root.id = id; diff --git a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx index 3a2a824c7..9bdfacd04 100644 --- a/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx +++ b/apps/roam/src/components/settings/LeftSidebarPersonalSettings.tsx @@ -1,10 +1,8 @@ -import discourseConfigRef from "~/utils/discourseConfigRef"; import React, { useCallback, - useEffect, useMemo, - useRef, useState, + memo, } from "react"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import getAllPageNames from "roamjs-components/queries/getAllPageNames"; @@ -14,311 +12,137 @@ import { Collapse, Dialog, InputGroup, + NumericInput, + Label, } from "@blueprintjs/core"; -import createBlock from "roamjs-components/writes/createBlock"; -import deleteBlock from "roamjs-components/writes/deleteBlock"; -import updateBlock from "roamjs-components/writes/updateBlock"; -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 { extractRef } 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"; +import { + getPersonalSetting, + setPersonalSetting, +} from "~/components/settings/utils/accessors"; +import { + LeftSidebarPersonalSettingsSchema, + type LeftSidebarPersonalSettings, + type PersonalSection, +} from "~/components/settings/utils/zodSchema"; + +type ChildData = { + Page: string; + Alias: string; +}; + +type SectionData = { + name: string; + Children: ChildData[]; + Settings: { + "Truncate-result?": number; + Folded: boolean; + }; +}; const SectionItem = memo( ({ section, - setSettingsDialogSectionUid, - pageNames, - setSections, index, isFirst, isLast, + pageNames, + onUpdateSection, + onRemoveSection, onMoveSection, + onOpenSettings, }: { - section: LeftSidebarPersonalSectionConfig; - setSections: Dispatch>; - setSettingsDialogSectionUid: (uid: string | null) => void; - pageNames: string[]; + section: SectionData; index: number; isFirst: boolean; isLast: boolean; + pageNames: string[]; + onUpdateSection: (name: string, updates: Partial) => void; + onRemoveSection: (name: string) => void; onMoveSection: (index: number, direction: "up" | "down") => void; + onOpenSettings: (name: string) => 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 [isExpanded, setIsExpanded] = useState(false); + const [childSettingsIndex, setChildSettingsIndex] = useState(null); - 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 hasChildren = section.Children.length > 0; + const addChild = useCallback( + (childName: string) => { + if (!childName) 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", - }); + if (section.Children.some((c) => c.Page === targetUid)) { + return; } + + const newChildren = [...section.Children, { Page: targetUid, Alias: "" }]; + onUpdateSection(section.name, { Children: newChildren }); + setChildInput(""); + setChildInputKey((prev) => prev + 1); }, - [setSections], + [section, onUpdateSection], ); + 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", - }); - } + (childIndex: number) => { + const newChildren = section.Children.filter((_, i) => i !== childIndex); + onUpdateSection(section.name, { Children: newChildren }); }, - [setSections], + [section, onUpdateSection], ); 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; + (childIndex: number, direction: "up" | "down") => { + if (direction === "up" && childIndex === 0) return; + if (direction === "down" && childIndex === section.Children.length - 1) return; - const newChildren = [...section.children]; - const [removed] = newChildren.splice(index, 1); - const newIndex = direction === "up" ? index - 1 : index + 1; + const newChildren = [...section.Children]; + const [removed] = newChildren.splice(childIndex, 1); + const newIndex = direction === "up" ? childIndex - 1 : childIndex + 1; newChildren.splice(newIndex, 0, removed); + onUpdateSection(section.name, { Children: newChildren }); + }, + [section, onUpdateSection], + ); - setSections((prev) => - prev.map((s) => { - if (s.uid === section.uid) { - return { - ...s, - children: newChildren, - }; - } - return s; - }), + const updateChildAlias = useCallback( + (childIndex: number, alias: string) => { + const newChildren = section.Children.map((c, i) => + i === childIndex ? { ...c, Alias: alias } : c, ); - - 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(); - }); - } + onUpdateSection(section.name, { Children: newChildren }); }, - [setSections], + [section, onUpdateSection], ); - 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; + const activeChild = childSettingsIndex !== null ? section.Children[childSettingsIndex] : null; return (
- {!sectionWithoutSettingsAndChildren && ( + {hasChildren && (
- {!sectionWithoutSettingsAndChildren && ( - -
-
- void handleAddChild()} - /> -
+ +
+
+ addChild(childInput)} + /> +
- {(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}) - + {section.Children.length > 0 ? ( +
+ {section.Children.map((child, childIndex) => { + const childDisplayTitle = + getPageTitleByPageUid(child.Page) || + getTextByBlockUid(extractRef(child.Page)) || + child.Page; + return ( +
+
+
+ {child.Alias ? ( + + {child.Alias} + + ({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 -
- )} +
+ ); + })} +
+ ) : ( +
No children added yet
+ )} +
+ + + {activeChild && childSettingsIndex !== null && ( + setChildSettingsIndex(null)} + title={`Settings for "${getPageTitleByPageUid(activeChild.Page) || activeChild.Page}"`} + style={{ width: "400px" }} + > +
+
- +
)}
); @@ -515,93 +289,75 @@ const SectionItem = memo( SectionItem.displayName = "SectionItem"; -const LeftSidebarPersonalSectionsContent = ({ - leftSidebar, -}: { - leftSidebar: RoamBasicNode; -}) => { - const [sections, setSections] = useState( - [], - ); - const [personalSectionUid, setPersonalSectionUid] = useState( - null, - ); +const LeftSidebarPersonalSectionsContent = () => { + const [sections, setSections] = useState(() => { + const raw = getPersonalSetting(["Left Sidebar"]); + const parsed = LeftSidebarPersonalSettingsSchema.parse(raw ?? {}); + return Object.entries(parsed).map(([name, data]) => ({ + name, + Children: data.Children, + Settings: data.Settings, + })); + }); const [newSectionInput, setNewSectionInput] = useState(""); - const [settingsDialogSectionUid, setSettingsDialogSectionUid] = useState< - string | null - >(null); - const sectionTitleUpdateTimeoutRef = useRef>(); - - useEffect(() => { - const initialize = async () => { - const userUid = window.roamAlphaAPI.user.uid(); - const personalSectionText = userUid + "/Personal-Section"; - - const personalSection = leftSidebar.children.find( - (n) => n.text === personalSectionText, - ); + const [settingsDialogSection, setSettingsDialogSection] = useState(null); - 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 pageNames = useMemo(() => getAllPageNames(), []); + + const saveToBlockProps = useCallback((newSections: SectionData[]) => { + const record: LeftSidebarPersonalSettings = {}; + for (const section of newSections) { + record[section.name] = { + Children: section.Children, + Settings: section.Settings, + }; + } + setPersonalSetting(["Left Sidebar"], record); + }, []); 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", - }); - } + (sectionName: string) => { + if (!sectionName) return; + if (sections.some((s) => s.name === sectionName)) return; + + const newSection: SectionData = { + name: sectionName, + Children: [], + Settings: { "Truncate-result?": 75, Folded: false }, + }; + const newSections = [...sections, newSection]; + setSections(newSections); + saveToBlockProps(newSections); + setNewSectionInput(""); }, - [personalSectionUid, sections], + [sections, saveToBlockProps], ); - const handleNewSectionInputChange = useCallback((value: string) => { - setNewSectionInput(value); - }, []); + const removeSection = useCallback( + (sectionName: string) => { + const newSections = sections.filter((s) => s.name !== sectionName); + setSections(newSections); + saveToBlockProps(newSections); + }, + [sections, saveToBlockProps], + ); + + const updateSection = useCallback( + (sectionName: string, updates: Partial) => { + const newSections = sections.map((s) => + s.name === sectionName + ? { + ...s, + Children: updates.Children ?? s.Children, + Settings: updates.Settings ?? s.Settings, + } + : s, + ); + setSections(newSections); + saveToBlockProps(newSections); + }, + [sections, saveToBlockProps], + ); const moveSection = useCallback( (index: number, direction: "up" | "down") => { @@ -612,35 +368,41 @@ const LeftSidebarPersonalSectionsContent = ({ const [removed] = newSections.splice(index, 1); const newIndex = direction === "up" ? index - 1 : index + 1; newSections.splice(newIndex, 0, removed); - setSections(newSections); + saveToBlockProps(newSections); + }, + [sections, saveToBlockProps], + ); + + const renameSection = useCallback( + (oldName: string, newName: string) => { + if (!newName || newName === oldName) return; + if (sections.some((s) => s.name === newName)) return; - 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(); - }); - } + const newSections = sections.map((s) => + s.name === oldName ? { ...s, name: newName } : s, + ); + setSections(newSections); + saveToBlockProps(newSections); }, - [sections, personalSectionUid], + [sections, saveToBlockProps], ); - const activeDialogSection = useMemo(() => { - return sections.find((s) => s.uid === settingsDialogSectionUid) || null; - }, [sections, settingsDialogSectionUid]); + const updateSectionSettings = useCallback( + (sectionName: string, settings: Partial) => { + const section = sections.find((s) => s.name === sectionName); + if (!section) return; - const pageNames = useMemo(() => getAllPageNames(), []); + const newSettings = { ...section.Settings, ...settings }; + updateSection(sectionName, { Settings: newSettings }); + }, + [sections, updateSection], + ); + + const activeSection = settingsDialogSection + ? sections.find((s) => s.name === settingsDialogSection) + : null; - if (!personalSectionUid) { - return null; - } return (
@@ -650,13 +412,13 @@ const LeftSidebarPersonalSectionsContent = ({
handleNewSectionInputChange(e.target.value)} + onChange={(e) => setNewSectionInput(e.target.value)} placeholder="Add section …" onKeyDown={(e) => { if (e.key === "Enter" && newSectionInput) { e.preventDefault(); e.stopPropagation(); - void addSection(newSectionInput); + addSection(newSectionInput); } }} /> @@ -664,79 +426,62 @@ const LeftSidebarPersonalSectionsContent = ({ icon="plus" small minimal - disabled={ - !newSectionInput || - sections.some((s) => s.text === newSectionInput) - } - onClick={() => void addSection(newSectionInput)} + disabled={!newSectionInput || sections.some((s) => s.name === newSectionInput)} + onClick={() => addSection(newSectionInput)} />
{sections.map((section, index) => ( -
- -
+ ))}
- {activeDialogSection && activeDialogSection.settings && ( + {activeSection && ( setSettingsDialogSectionUid(null)} - title={`Settings for "${activeDialogSection.text}"`} + onClose={() => setSettingsDialogSection(null)} + title={`Settings for "${activeSection.name}"`} style={{ width: "500px" }} >
-
- +
- + +
@@ -746,32 +491,5 @@ const LeftSidebarPersonalSectionsContent = ({ }; 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 ; + return ; }; diff --git a/apps/roam/src/components/settings/utils/hooks.ts b/apps/roam/src/components/settings/utils/hooks.ts index 42467ac20..d47b17708 100644 --- a/apps/roam/src/components/settings/utils/hooks.ts +++ b/apps/roam/src/components/settings/utils/hooks.ts @@ -1,11 +1,12 @@ import { useState, useEffect, useCallback } from "react"; -import { getFeatureFlag, getGlobalSetting } from "./accessors"; -import type { FeatureFlags, LeftSidebarGlobalSettings } from "./zodSchema"; -import { LeftSidebarGlobalSettingsSchema } from "./zodSchema"; +import { getFeatureFlag, getGlobalSetting, getPersonalSetting } from "./accessors"; +import type { FeatureFlags, LeftSidebarGlobalSettings, LeftSidebarPersonalSettings } from "./zodSchema"; +import { LeftSidebarGlobalSettingsSchema, LeftSidebarPersonalSettingsSchema } 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"; +const PERSONAL_SETTING_CHANGE_EVENT = "discourse-graph:personal-setting-change"; type FeatureFlagChangeDetail = { key: keyof FeatureFlags; @@ -56,6 +57,14 @@ export const useFeatureFlag = (key: keyof FeatureFlags): boolean => { return value; }; +export const emitPersonalSettingChange = (keys: string[], value: json): void => { + window.dispatchEvent( + new CustomEvent(PERSONAL_SETTING_CHANGE_EVENT, { + detail: { keys, value }, + }), + ); +}; + export const useLeftSidebarGlobalSettings = (): LeftSidebarGlobalSettings => { const [settings, setSettings] = useState(() => { const raw = getGlobalSetting(["Left Sidebar"]); @@ -83,3 +92,31 @@ export const useLeftSidebarGlobalSettings = (): LeftSidebarGlobalSettings => { return settings; }; + +export const useLeftSidebarPersonalSettings = (): LeftSidebarPersonalSettings => { + const [settings, setSettings] = useState(() => { + const raw = getPersonalSetting(["Left Sidebar"]); + return LeftSidebarPersonalSettingsSchema.parse(raw ?? {}); + }); + + const refreshSettings = useCallback(() => { + const raw = getPersonalSetting(["Left Sidebar"]); + setSettings(LeftSidebarPersonalSettingsSchema.parse(raw ?? {})); + }, []); + + useEffect(() => { + const handleChange = (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail.keys[0] === "Left Sidebar") { + refreshSettings(); + } + }; + + window.addEventListener(PERSONAL_SETTING_CHANGE_EVENT, handleChange); + return () => { + window.removeEventListener(PERSONAL_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 8ecab714b..3150e21a7 100644 --- a/apps/roam/src/components/settings/utils/pullWatchers.ts +++ b/apps/roam/src/components/settings/utils/pullWatchers.ts @@ -22,7 +22,11 @@ import { initializeSupabaseSync, setSyncActivity, } from "~/utils/syncDgNodesToSupabase"; -import { emitFeatureFlagChange, emitGlobalSettingChange } from "./hooks"; +import { + emitFeatureFlagChange, + emitGlobalSettingChange, + emitPersonalSettingChange, +} from "./hooks"; type PullWatchCallback = (before: unknown, after: unknown) => void; @@ -143,7 +147,13 @@ export const globalSettingsHandlers: Partial< export const personalSettingsHandlers: Partial< Record -> = {}; +> = { + "Left Sidebar": (newValue, oldValue) => { + if (JSON.stringify(newValue) !== JSON.stringify(oldValue)) { + emitPersonalSettingChange(["Left Sidebar"], newValue); + } + }, +}; export const discourseNodeHandlers: DiscourseNodeHandler[] = []; diff --git a/apps/roam/src/utils/getLeftSidebarSettings.ts b/apps/roam/src/utils/getLeftSidebarSettings.ts index 94d018690..f57dcdbaa 100644 --- a/apps/roam/src/utils/getLeftSidebarSettings.ts +++ b/apps/roam/src/utils/getLeftSidebarSettings.ts @@ -209,7 +209,6 @@ export const getLeftSidebarSettings = ( const leftSidebarChildren = leftSidebarNode?.children || []; const global = getLeftSidebarGlobalSectionConfig(leftSidebarChildren); const personal = getLeftSidebarPersonalSectionConfig(leftSidebarChildren); - // TODO: remove this on complete migration task [ENG-1171: Remove `migrateLeftSideBarSettings`](https://linear.app/discourse-graphs/issue/ENG-1171/remove-migrateleftsidebarsettings) const allPersonalSections = getAllLeftSidebarPersonalSectionConfigs(leftSidebarChildren); const favoritesMigrated = getUidAndBooleanSetting({ diff --git a/apps/roam/src/utils/migrateLeftSidebarSettings.ts b/apps/roam/src/utils/migrateLeftSidebarSettings.ts deleted file mode 100644 index 2604bb5b0..000000000 --- a/apps/roam/src/utils/migrateLeftSidebarSettings.ts +++ /dev/null @@ -1,76 +0,0 @@ -import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; -import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid"; -import updateBlock from "roamjs-components/writes/updateBlock"; -import createBlock from "roamjs-components/writes/createBlock"; -import { getFormattedConfigTree } from "./discourseConfigRef"; -import { DISCOURSE_CONFIG_PAGE_TITLE } from "./renderNodeConfigPage"; -import refreshConfigTree from "./refreshConfigTree"; - -const migrateSectionChildren = async ( - children: { uid: string; text: string }[], -) => { - const promises = children.map(async (child) => { - const currentText = child.text; - - const titleFromUid = getPageTitleByPageUid(currentText); - if (titleFromUid) { - return; - } - - const uidFromTitle = getPageUidByPageTitle(currentText); - if (uidFromTitle) { - try { - await updateBlock({ - uid: child.uid, - text: uidFromTitle, - }); - console.log( - `Migrated sidebar item "${currentText}" to UID "${uidFromTitle}"`, - ); - } catch (e) { - console.error(`Failed to migrate sidebar item "${currentText}"`, e); - } - } - }); - - await Promise.all(promises); -}; - -export const migrateLeftSidebarSettings = async () => { - const leftSidebarSettings = getFormattedConfigTree().leftSidebar; - - if (!leftSidebarSettings.uid) return; - - if (leftSidebarSettings.sidebarMigrated.value) return; - - const configPageUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); - if (!configPageUid) return; - - const globalChildren = leftSidebarSettings.global.children; - if (globalChildren.length > 0) { - await migrateSectionChildren(globalChildren); - } - - - const allPersonalSections = leftSidebarSettings.allPersonalSections; - - for (const [_, userPersonalSection] of Object.entries( - allPersonalSections, - )) { - for (const section of userPersonalSection.sections) { - const children = section.children || []; - if (children.length > 0) { - await migrateSectionChildren(children); - } - } - } - - if (leftSidebarSettings.uid) { - await createBlock({ - parentUid: leftSidebarSettings.uid, - node: { text: "Sidebar Migrated" }, - }); - } - - refreshConfigTree(); -};