diff --git a/apps/roam/src/components/LeftSidebarView.tsx b/apps/roam/src/components/LeftSidebarView.tsx index be9e2e528..46ca39cc5 100644 --- a/apps/roam/src/components/LeftSidebarView.tsx +++ b/apps/roam/src/components/LeftSidebarView.tsx @@ -36,6 +36,7 @@ import { getLeftSidebarSettings } from "~/utils/getLeftSidebarSettings"; import { getGlobalSetting, getPersonalSetting, + getPersonalSettings, setGlobalSetting, setPersonalSetting, } from "~/components/settings/utils/accessors"; @@ -45,10 +46,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 +110,7 @@ const openTarget = async (e: React.MouseEvent, targetUid: string) => { } }; -const toggleFoldedState = ({ +const toggleFoldedState = async ({ isOpen, setIsOpen, folded, @@ -130,16 +128,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 +146,8 @@ const toggleFoldedState = ({ folded.value = true; } + refreshConfigTree(); + if (isGlobal) { setGlobalSetting( [ @@ -157,13 +158,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 +233,7 @@ const PersonalSectionItem = ({ const handleChevronClick = () => { if (!section.settings) return; - toggleFoldedState({ + void toggleFoldedState({ isOpen, setIsOpen, folded: section.settings.folded, @@ -297,7 +305,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, @@ -333,9 +341,9 @@ const buildConfig = (): LeftSidebarConfig => { const globalValues = getGlobalSetting([ GLOBAL_KEYS.leftSidebar, ]); - const personalValues = getPersonalSetting([ - PERSONAL_KEYS.leftSidebar, - ]); + const personalValues = getPersonalSetting< + ReturnType[typeof PERSONAL_KEYS.leftSidebar] + >([PERSONAL_KEYS.leftSidebar]); // Read UIDs from old system (needed for fold CRUD during dual-write) const oldConfig = getCurrentLeftSidebarConfig(); diff --git a/apps/roam/src/components/settings/AdminPanel.tsx b/apps/roam/src/components/settings/AdminPanel.tsx index 5c4bd7317..ae9e0d7d7 100644 --- a/apps/roam/src/components/settings/AdminPanel.tsx +++ b/apps/roam/src/components/settings/AdminPanel.tsx @@ -276,7 +276,7 @@ const FeatureFlagsTab = (): React.ReactElement => { }; }, []); - const [suggestiveModeEnabled, setSuggestiveModeEnabled] = useState( + const [suggestiveModeEnabled, setSuggestiveModeEnabled] = useState(() => getFeatureFlag("Suggestive mode enabled"), ); const [suggestiveModeUid, setSuggestiveModeUid] = useState( @@ -294,12 +294,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 +324,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); diff --git a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx index 692458470..de126e065 100644 --- a/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeCanvasSettings.tsx @@ -142,13 +142,13 @@ const DiscourseNodeCanvasSettings = ({ settingKeys={[DISCOURSE_NODE_KEYS.canvasSettings, CANVAS_KEYS.keyImage]} initialValue={isKeyImage} onChange={(checked) => { - setIsKeyImage(checked); if (checked && !keyImageOption) setKeyImageOption("first-image"); void setInputSetting({ blockUid: uid, key: "key-image", value: checked ? "true" : "false", }); + setTimeout(() => setIsKeyImage(checked), 100); }} /> { if (errorRef.current) return; - setter(settingKeys, newValue); syncToBlock?.(newValue); + setTimeout(() => { + refreshConfigTree(); + setter(settingKeys, newValue); + }, 100); }, DEBOUNCE_MS); }; @@ -227,9 +231,10 @@ const BaseFlagPanel = ({ } setInternalValue(checked); - setter(settingKeys, checked); await syncFlagToBlock(checked); - onChange?.(checked); + refreshConfigTree(); + setter(settingKeys, checked); + setTimeout(() => onChange?.(checked), 100); }; return ( @@ -276,8 +281,9 @@ const BaseNumberPanel = ({ const handleChange = (valueAsNumber: number) => { if (Number.isNaN(valueAsNumber)) return; setValue(valueAsNumber); - setter(settingKeys, valueAsNumber); syncToBlock?.(valueAsNumber); + refreshConfigTree(); + setter(settingKeys, valueAsNumber); onChange?.(valueAsNumber); }; @@ -323,8 +329,9 @@ const BaseSelectPanel = ({ const handleChange = (e: ChangeEvent) => { const newValue = e.target.value; setValue(newValue); - setter(settingKeys, newValue); syncToBlock?.(newValue); + refreshConfigTree(); + setter(settingKeys, newValue); }; return ( @@ -400,6 +407,7 @@ const BaseMultiTextPanel = ({ }, }); childUidsRef.current = [...childUidsRef.current, valueUid]; + refreshConfigTree(); } } }; @@ -408,7 +416,6 @@ const BaseMultiTextPanel = ({ // eslint-disable-next-line @typescript-eslint/naming-convention const newValues = values.filter((_, i) => i !== index); setValues(newValues); - setter(settingKeys, newValues); onChange?.(newValues); if (hasBlockSync) { @@ -420,7 +427,9 @@ const BaseMultiTextPanel = ({ // eslint-disable-next-line @typescript-eslint/naming-convention (_, i) => i !== index, ); + refreshConfigTree(); } + setter(settingKeys, newValues); }; const handleKeyDown = (e: React.KeyboardEvent) => { diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index ad53253e6..808be1a25 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -45,6 +45,25 @@ import { PERSONAL_KEYS, QUERY_KEYS, GLOBAL_KEYS } from "./settingKeys"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); +const deepEqual = (a: unknown, b: unknown): boolean => { + if (a === b) return true; + const isEmpty = (v: unknown) => + v === undefined || v === null || v === "" || v === false; + if (isEmpty(a) && isEmpty(b)) return true; + if (a == null || b == null) return a === b; + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((v, i) => deepEqual(v, b[i])); + } + if (isRecord(a) && isRecord(b)) { + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + return keysA.every((k) => k in b && deepEqual(a[k], b[k])); + } + return false; +}; + const unwrapSchema = (schema: z.ZodTypeAny): z.ZodTypeAny => { let current = schema; let didUnwrap = true; @@ -444,7 +463,10 @@ const getLegacyGlobalSetting = (keys: string[]): unknown => { }; const getLegacyQuerySettingByParentUid = (parentUid: string) => { - const scratchNode = getSubTree({ parentUid, key: "scratch" }); + const scratchNode = getSubTree({ + tree: getBasicTreeByParentUid(parentUid), + key: "scratch", + }); const conditionsNode = getSubTree({ tree: scratchNode.children, key: "conditions", @@ -514,6 +536,9 @@ const getLegacyDiscourseNodeSetting = ( }).children; const indexUid = getSubTree({ tree, key: "Index" }).uid; const specificationUid = getSubTree({ tree, key: "Specification" }).uid; + const specificationQuery = specificationUid + ? getLegacyQuerySettingByParentUid(specificationUid) + : DEFAULT_LEGACY_QUERY; const legacySettings = { type: nodeUid, @@ -542,11 +567,11 @@ const getLegacyDiscourseNodeSetting = ( : DEFAULT_LEGACY_QUERY, specification: { enabled: specificationUid - ? !!getSubTree({ parentUid: specificationUid, key: "enabled" }).uid + ? getBasicTreeByParentUid(specificationUid).some( + (c) => c.text === "enabled", + ) : false, - query: specificationUid - ? getLegacyQuerySettingByParentUid(specificationUid) - : DEFAULT_LEGACY_QUERY, + query: specificationQuery, }, }; @@ -803,7 +828,7 @@ export const getGlobalSetting = ( const settings = getGlobalSettings(); const blockPropsValue = readPathValue(settings, keys); const legacyValue = getLegacyGlobalSetting(keys); - if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + if (!deepEqual(blockPropsValue, legacyValue)) { console.warn( `[DG Dual-Read] Mismatch at Global > ${formatSettingPath(keys)}`, { blockProps: blockPropsValue, legacy: legacyValue }, @@ -875,7 +900,7 @@ export const getPersonalSetting = ( const settings = getPersonalSettings(); const blockPropsValue = readPathValue(settings, keys); const legacyValue = getLegacyPersonalSetting(keys); - if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + if (!deepEqual(blockPropsValue, legacyValue)) { console.warn( `[DG Dual-Read] Mismatch at Personal > ${formatSettingPath(keys)}`, { blockProps: blockPropsValue, legacy: legacyValue }, @@ -912,11 +937,13 @@ export const setPersonalSetting = (keys: string[], value: json): void => { }); }; -const getRawDiscourseNodeBlockProps = (nodeType: string): json | undefined => { +const getRawDiscourseNodeBlockProps = ( + nodeType: string, +): Record | undefined => { let pageUid = nodeType; let blockProps = getBlockPropsByUid(pageUid, []); - if (!blockProps || Object.keys(blockProps).length === 0) { + if (!isRecord(blockProps) || Object.keys(blockProps).length === 0) { const lookedUpUid = getPageUidByPageTitle( `${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`, ); @@ -926,7 +953,9 @@ const getRawDiscourseNodeBlockProps = (nodeType: string): json | undefined => { } } - return blockProps; + return isRecord(blockProps) && Object.keys(blockProps).length > 0 + ? (blockProps as Record) + : undefined; }; export const getDiscourseNodeSettings = ( @@ -959,7 +988,7 @@ export const getDiscourseNodeSetting = ( const settings = getDiscourseNodeSettings(nodeType); const blockPropsValue = settings ? readPathValue(settings, keys) : undefined; const legacyValue = getLegacyDiscourseNodeSetting(nodeType, keys); - if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + if (!deepEqual(blockPropsValue, legacyValue)) { console.warn( `[DG Dual-Read] Mismatch at Discourse Node (${nodeType}) > ${formatSettingPath(keys)}`, { blockProps: blockPropsValue, legacy: legacyValue }, diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 86e19ed82..38c2731e8 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -5,6 +5,7 @@ import setBlockProps from "~/utils/setBlockProps"; import getBlockProps from "~/utils/getBlockProps"; import type { json } from "~/utils/getBlockProps"; import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; +import DEFAULT_RELATION_VALUES from "~/data/defaultDiscourseRelations"; import DEFAULT_RELATIONS_BLOCK_PROPS from "~/components/settings/data/defaultRelationsBlockProps"; import { getAllDiscourseNodes } from "./accessors"; import { @@ -72,6 +73,70 @@ const buildBlockMap = (pageUid: string): Record => { return blockMap; }; +const ensureLegacyConfigBlocks = async (pageUid: string): Promise => { + const pageBlockMap = buildBlockMap(pageUid); + + await ensureBlocksExist( + pageUid, + ["trigger", "grammar", "export", "Suggestive Mode", "Left Sidebar"], + pageBlockMap, + ); + + const triggerMap = buildBlockMap(pageBlockMap["trigger"]); + if (Object.keys(triggerMap).length === 0) { + await createBlock({ + parentUid: pageBlockMap["trigger"], + node: { text: "\\" }, + }); + } + + const grammarMap = buildBlockMap(pageBlockMap["grammar"]); + await ensureBlocksExist(pageBlockMap["grammar"], ["relations"], grammarMap); + const relationsChildren = getShallowTreeByParentUid(grammarMap["relations"]); + if (relationsChildren.length === 0) { + for (const relation of DEFAULT_RELATION_VALUES) { + await createBlock({ + parentUid: grammarMap["relations"], + node: relation, + }); + } + } + + const suggestiveMap = buildBlockMap(pageBlockMap["Suggestive Mode"]); + await ensureBlocksExist( + pageBlockMap["Suggestive Mode"], + ["Page Groups"], + suggestiveMap, + ); + + const leftSidebarMap = buildBlockMap(pageBlockMap["Left Sidebar"]); + await ensureBlocksExist( + pageBlockMap["Left Sidebar"], + ["Global-Section"], + leftSidebarMap, + ); + const globalSectionMap = buildBlockMap(leftSidebarMap["Global-Section"]); + await ensureBlocksExist( + leftSidebarMap["Global-Section"], + ["Children", "Settings"], + globalSectionMap, + ); + + const exportMap = buildBlockMap(pageBlockMap["export"]); + await ensureBlocksExist( + pageBlockMap["export"], + ["max filename length"], + exportMap, + ); + const maxFilenameMap = buildBlockMap(exportMap["max filename length"]); + if (Object.keys(maxFilenameMap).length === 0) { + await createBlock({ + parentUid: exportMap["max filename length"], + node: { text: "64" }, + }); + } +}; + const initializeSettingsBlockProps = ( pageUid: string, blockMap: Record, @@ -118,6 +183,8 @@ const initSettingsPageBlocks = async (): Promise> => { const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key); await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); + await ensureLegacyConfigBlocks(pageUid); + initializeSettingsBlockProps(pageUid, blockMap); return blockMap; diff --git a/apps/roam/src/utils/storedRelations.ts b/apps/roam/src/utils/storedRelations.ts index c32522c43..4397b4c0f 100644 --- a/apps/roam/src/utils/storedRelations.ts +++ b/apps/roam/src/utils/storedRelations.ts @@ -3,8 +3,8 @@ // ENG-1521: Update internal terminology to use "stored" instead of "reified" import { USE_STORED_RELATIONS } from "~/data/userSettings"; +import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/data/constants"; import { getSetting, setSetting } from "./extensionSettings"; -import { DISCOURSE_CONFIG_PAGE_TITLE } from "./renderNodeConfigPage"; const INSTALL_CUTOFF = Date.parse("2026-03-01T00:00:00.000Z");