diff --git a/apps/roam/src/components/DiscourseFloatingMenu.tsx b/apps/roam/src/components/DiscourseFloatingMenu.tsx index 3a7e337b9..a96c33f1b 100644 --- a/apps/roam/src/components/DiscourseFloatingMenu.tsx +++ b/apps/roam/src/components/DiscourseFloatingMenu.tsx @@ -13,7 +13,7 @@ import { import { FeedbackWidget } from "./BirdEatsBugs"; import { render as renderSettings } from "~/components/settings/Settings"; import posthog from "posthog-js"; -import { getPersonalSetting } from "./settings/utils/accessors"; +import { type SettingsSnapshot } from "./settings/utils/accessors"; import { PERSONAL_KEYS } from "./settings/utils/settingKeys"; type DiscourseFloatingMenuProps = { @@ -118,11 +118,7 @@ export const showDiscourseFloatingMenu = () => { export const installDiscourseFloatingMenu = ( onLoadArgs: OnloadArgs, - props: DiscourseFloatingMenuProps = { - position: "bottom-right", - theme: "bp3-light", - buttonTheme: "bp3-light", - }, + snapshot: SettingsSnapshot, ) => { let floatingMenuAnchor = document.getElementById(ANCHOR_ID); if (!floatingMenuAnchor) { @@ -130,14 +126,15 @@ export const installDiscourseFloatingMenu = ( floatingMenuAnchor.id = ANCHOR_ID; document.getElementById("app")?.appendChild(floatingMenuAnchor); } - if (getPersonalSetting([PERSONAL_KEYS.hideFeedbackButton])) { + if (snapshot.personalSettings[PERSONAL_KEYS.hideFeedbackButton]) { floatingMenuAnchor.classList.add("hidden"); } + // eslint-disable-next-line react/no-deprecated ReactDOM.render( , floatingMenuAnchor, @@ -148,6 +145,7 @@ export const removeDiscourseFloatingMenu = () => { const anchor = document.getElementById(ANCHOR_ID); if (anchor) { try { + // eslint-disable-next-line react/no-deprecated ReactDOM.unmountComponentAtNode(anchor); } catch (e) { // no-op: unmount best-effort diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index c7f2e2b96..e25ca7e61 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -27,11 +27,9 @@ import { getNewDiscourseNodeText } from "~/utils/formatUtils"; import { OnloadArgs } from "roamjs-components/types"; import { formatHexColor } from "./settings/DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; -import { - getPersonalSetting, - setPersonalSetting, -} from "~/components/settings/utils/accessors"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; +import type { PersonalSettings } from "~/components/settings/utils/zodSchema"; type Props = { textarea?: HTMLTextAreaElement; @@ -420,19 +418,15 @@ export const comboToString = (combo: IKeyCombo): string => { export const NodeMenuTriggerComponent = ({ extensionAPI, + initialValue, }: { extensionAPI: OnloadArgs["extensionAPI"]; + initialValue: PersonalSettings["Personal node menu trigger"]; }) => { const inputRef = useRef(null); const [isActive, setIsActive] = useState(false); - const [comboKey, setComboKey] = useState( - () => - getPersonalSetting([ - PERSONAL_KEYS.personalNodeMenuTrigger, - ]) || { - modifiers: 0, - key: "", - }, + const [comboKey, setComboKey] = useState(() => + typeof initialValue === "object" ? initialValue : { modifiers: 0, key: "" }, ); const handleKeyDown = useCallback( diff --git a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx index 03fdfc82d..8058d869e 100644 --- a/apps/roam/src/components/DiscourseNodeSearchMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeSearchMenu.tsx @@ -25,10 +25,7 @@ import getDiscourseNodes, { DiscourseNode } from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import { Result } from "~/utils/types"; import MiniSearch from "minisearch"; -import { - getPersonalSetting, - setPersonalSetting, -} from "~/components/settings/utils/accessors"; +import { setPersonalSetting } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; type Props = { @@ -709,12 +706,14 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => { export const NodeSearchMenuTriggerSetting = ({ onloadArgs, + initialValue, }: { onloadArgs: OnloadArgs; + initialValue: string; }) => { const extensionAPI = onloadArgs.extensionAPI; const [nodeSearchTrigger, setNodeSearchTrigger] = useState( - getPersonalSetting([PERSONAL_KEYS.nodeSearchMenuTrigger]) ?? "@", + () => initialValue, ); const handleNodeSearchTriggerChange = ( diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index be9e2e528..b5793707c 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -36,8 +36,10 @@ import { getLeftSidebarSettings } from "~/utils/getLeftSidebarSettings"; import { getGlobalSetting, getPersonalSetting, + getPersonalSettings, setGlobalSetting, setPersonalSetting, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS, @@ -45,10 +47,7 @@ import { LEFT_SIDEBAR_KEYS, LEFT_SIDEBAR_SETTINGS_KEYS, } from "~/components/settings/utils/settingKeys"; -import type { - LeftSidebarGlobalSettings, - PersonalSection, -} from "~/components/settings/utils/zodSchema"; +import type { LeftSidebarGlobalSettings } from "~/components/settings/utils/zodSchema"; import { createBlock } from "roamjs-components/writes"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import getTextByBlockUid from "roamjs-components/queries/getTextByBlockUid"; @@ -112,7 +111,7 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { } }; -const toggleFoldedState = ({ +const toggleFoldedState = async ({ isOpen, setIsOpen, folded, @@ -130,16 +129,17 @@ const toggleFoldedState = ({ const newFolded = !isOpen; if (isOpen) { - setIsOpen(false); - if (folded.uid) { - void deleteBlock(folded.uid); - folded.uid = undefined; - folded.value = false; - } + const children = getBasicTreeByParentUid(parentUid); + await Promise.all( + children + .filter((c) => c.text === "Folded") + .map((c) => deleteBlock(c.uid)), + ); + folded.uid = undefined; + folded.value = false; } else { - setIsOpen(true); const newUid = window.roamAlphaAPI.util.generateUID(); - void createBlock({ + await createBlock({ parentUid, node: { text: "Folded", uid: newUid }, }); @@ -147,6 +147,8 @@ const toggleFoldedState = ({ folded.value = true; } + refreshConfigTree(); + if (isGlobal) { setGlobalSetting( [ @@ -157,13 +159,20 @@ const toggleFoldedState = ({ newFolded, ); } else if (sectionIndex !== undefined) { - const sections = - getPersonalSetting([PERSONAL_KEYS.leftSidebar]) || []; + const sections = [...getPersonalSettings()[PERSONAL_KEYS.leftSidebar]]; if (sections[sectionIndex]) { - sections[sectionIndex].Settings.Folded = newFolded; + sections[sectionIndex] = { + ...sections[sectionIndex], + Settings: { + ...sections[sectionIndex].Settings, + Folded: newFolded, + }, + }; setPersonalSetting([PERSONAL_KEYS.leftSidebar], sections); } } + + setIsOpen(newFolded); }; const SectionChildren = ({ @@ -225,7 +234,7 @@ const PersonalSectionItem = ({ const handleChevronClick = () => { if (!section.settings) return; - toggleFoldedState({ + void toggleFoldedState({ isOpen, setIsOpen, folded: section.settings.folded, @@ -297,7 +306,7 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { className="sidebar-title-button flex w-full items-center border-none bg-transparent py-1 pl-6 pr-2.5 font-semibold outline-none" onClick={() => { if (!isCollapsable || !config.settings) return; - toggleFoldedState({ + void toggleFoldedState({ isOpen, setIsOpen, folded: config.settings.folded, @@ -328,14 +337,16 @@ const GlobalSection = ({ config }: { config: LeftSidebarConfig["global"] }) => { // TODO(ENG-1471): Remove old-system merge when migration complete — just use accessor values directly. // See mergeGlobalSectionWithAccessor/mergePersonalSectionsWithAccessor for why the merge exists. -const buildConfig = (): LeftSidebarConfig => { +const buildConfig = (snapshot?: SettingsSnapshot): LeftSidebarConfig => { // Read VALUES from accessor (handles flag routing + mismatch detection) - const globalValues = getGlobalSetting([ - GLOBAL_KEYS.leftSidebar, - ]); - const personalValues = getPersonalSetting([ - PERSONAL_KEYS.leftSidebar, - ]); + const globalValues = snapshot + ? snapshot.globalSettings[GLOBAL_KEYS.leftSidebar] + : getGlobalSetting([GLOBAL_KEYS.leftSidebar]); + const personalValues = snapshot + ? snapshot.personalSettings[PERSONAL_KEYS.leftSidebar] + : getPersonalSetting< + ReturnType[typeof PERSONAL_KEYS.leftSidebar] + >([PERSONAL_KEYS.leftSidebar]); // Read UIDs from old system (needed for fold CRUD during dual-write) const oldConfig = getCurrentLeftSidebarConfig(); @@ -356,8 +367,8 @@ const buildConfig = (): LeftSidebarConfig => { }; }; -export const useConfig = () => { - const [config, setConfig] = useState(() => buildConfig()); +export const useConfig = (initialSnapshot?: SettingsSnapshot) => { + const [config, setConfig] = useState(() => buildConfig(initialSnapshot)); useEffect(() => { const handleUpdate = () => { setConfig(buildConfig()); @@ -496,8 +507,14 @@ const FavoritesPopover = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { ); }; -const LeftSidebarView = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => { - const { config } = useConfig(); +const LeftSidebarView = ({ + onloadArgs, + initialSnapshot, +}: { + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}) => { + const { config } = useConfig(initialSnapshot); return ( <> @@ -602,10 +619,15 @@ const migrateFavorites = async () => { refreshConfigTree(); }; -export const mountLeftSidebar = async ( - wrapper: HTMLElement, - onloadArgs: OnloadArgs, -): Promise => { +export const mountLeftSidebar = async ({ + wrapper, + onloadArgs, + initialSnapshot, +}: { + wrapper: HTMLElement; + onloadArgs: OnloadArgs; + initialSnapshot?: SettingsSnapshot; +}): Promise => { if (!wrapper) return; const id = "dg-left-sidebar-root"; @@ -622,7 +644,14 @@ export const mountLeftSidebar = async ( } else { root.className = "starred-pages"; } - ReactDOM.render(, root); + // eslint-disable-next-line react/no-deprecated + ReactDOM.render( + , + root, + ); }; export default LeftSidebarView; diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 5c4bd7317..aeb7f5b47 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -15,8 +15,8 @@ import { import Description from "roamjs-components/components/Description"; import { Select } from "@blueprintjs/select"; import { - getFeatureFlag, setFeatureFlag, + type SettingsSnapshot, } from "~/components/settings/utils/accessors"; import { onSettingChange, @@ -264,7 +264,11 @@ const NodeListTab = (): React.ReactElement => { ); }; -const FeatureFlagsTab = (): React.ReactElement => { +const FeatureFlagsTab = ({ + featureFlags, +}: { + featureFlags: SettingsSnapshot["featureFlags"]; +}): React.ReactElement => { const legacySuggestiveModeMeta = useMemo(() => { refreshConfigTree(); return { @@ -277,7 +281,7 @@ const FeatureFlagsTab = (): React.ReactElement => { }, []); const [suggestiveModeEnabled, setSuggestiveModeEnabled] = useState( - getFeatureFlag("Suggestive mode enabled"), + featureFlags["Suggestive mode enabled"], ); const [suggestiveModeUid, setSuggestiveModeUid] = useState( legacySuggestiveModeMeta.suggestiveModeEnabledUid, @@ -294,12 +298,15 @@ const FeatureFlagsTab = (): React.ReactElement => { if (checked) { setIsAlertOpen(true); } else { - if (suggestiveModeUid) { - void deleteBlock(suggestiveModeUid); - setSuggestiveModeUid(undefined); - } - setSuggestiveModeEnabled(false); - setFeatureFlag("Suggestive mode enabled", false); + void (async () => { + if (suggestiveModeUid) { + await deleteBlock(suggestiveModeUid); + setSuggestiveModeUid(undefined); + } + refreshConfigTree(); + setSuggestiveModeEnabled(false); + setFeatureFlag("Suggestive mode enabled", false); + })(); } }} labelElement={ @@ -321,6 +328,7 @@ const FeatureFlagsTab = (): React.ReactElement => { node: { text: "(BETA) Suggestive Mode Enabled" }, }).then((uid) => { setSuggestiveModeUid(uid); + refreshConfigTree(); setSuggestiveModeEnabled(true); setFeatureFlag("Suggestive mode enabled", true); setIsAlertOpen(false); @@ -361,6 +369,7 @@ const FeatureFlagsTab = (): React.ReactElement => { title="Use new settings store" description="When enabled, accessor getters read from block props instead of the old system. Surfaces dual-write gaps during development." featureKey="Use new settings store" + initialValue={featureFlags["Use new settings store"]} />