From 36c683140eeb5f9495cefc7218d8ba399e50efc5 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 11 Jan 2026 12:01:02 +0530 Subject: [PATCH 1/4] pull watchers for prop settings --- .../components/settings/utils/pullWatchers.ts | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 apps/roam/src/components/settings/utils/pullWatchers.ts diff --git a/apps/roam/src/components/settings/utils/pullWatchers.ts b/apps/roam/src/components/settings/utils/pullWatchers.ts new file mode 100644 index 000000000..9666392b0 --- /dev/null +++ b/apps/roam/src/components/settings/utils/pullWatchers.ts @@ -0,0 +1,276 @@ +import { type json, normalizeProps } from "~/utils/getBlockProps"; +import { + TOP_LEVEL_BLOCK_PROP_KEYS, + DISCOURSE_NODE_PAGE_PREFIX, +} from "../data/blockPropsSettingsConfig"; +import { getPersonalSettingsKey } from "./init"; +import { + FeatureFlagsSchema, + GlobalSettingsSchema, + PersonalSettingsSchema, + DiscourseNodeSchema, + type FeatureFlags, + type GlobalSettings, + type PersonalSettings, + type DiscourseNodeSettings, +} from "./zodSchema"; + +type PullWatchCallback = (before: unknown, after: unknown) => void; + +type PullWatchEntry = { + pattern: string; + entityId: string; + callback: PullWatchCallback; +}; + +const getNormalizedProps = (data: unknown): Record => { + return normalizeProps( + ((data as Record)?.[":block/props"] || {}) as json, + ) as Record; +}; + +const hasPropChanged = ( + before: unknown, + after: unknown, + key?: string, +): boolean => { + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + + if (key) { + return JSON.stringify(beforeProps[key]) !== JSON.stringify(afterProps[key]); + } + + return JSON.stringify(beforeProps) !== JSON.stringify(afterProps); +}; + +const createCleanupFn = (watches: PullWatchEntry[]): (() => void) => { + return () => { + watches.forEach(({ pattern, entityId, callback }) => { + window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback); + }); + }; +}; + +const addPullWatch = ( + watches: PullWatchEntry[], + blockUid: string, + callback: PullWatchCallback, +): void => { + const pattern = "[:block/props]"; + const entityId = `[:block/uid "${blockUid}"]`; + + window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback); + watches.push({ pattern, entityId, callback }); +}; + +type FeatureFlagHandler = ( + newValue: boolean, + oldValue: boolean, + allSettings: FeatureFlags, +) => void; + +type GlobalSettingHandler = ( + newValue: GlobalSettings[K], + oldValue: GlobalSettings[K], + allSettings: GlobalSettings, +) => void; + +type PersonalSettingHandler = ( + newValue: PersonalSettings[K], + oldValue: PersonalSettings[K], + allSettings: PersonalSettings, +) => void; + +type DiscourseNodeHandler = ( + nodeType: string, + newSettings: DiscourseNodeSettings, + oldSettings: DiscourseNodeSettings | null, +) => void; + +export const featureFlagHandlers: Partial< + Record +> = { + // Add handlers as needed: + // "Enable Left Sidebar": (newValue) => { ... }, + // "Suggestive Mode Enabled": (newValue) => { ... }, + // "Reified Relation Triples": (newValue) => { ... }, +}; + +export const globalSettingsHandlers: Partial< + Record +> = { + // Add handlers as needed: + // "Trigger": (newValue) => { ... }, + // "Canvas Page Format": (newValue) => { ... }, + // "Left Sidebar": (newValue) => { ... }, + // "Export": (newValue) => { ... }, + // "Suggestive Mode": (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 setupPullWatchSettings = ( + blockUids: Record, +): (() => void) => { + const watches: PullWatchEntry[] = []; + + const featureFlagsBlockUid = + blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; + const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global]; + const personalSettingsKey = getPersonalSettingsKey(); + const personalSettingsBlockUid = blockUids[personalSettingsKey]; + + if (featureFlagsBlockUid && Object.keys(featureFlagHandlers).length > 0) { + addPullWatch(watches, featureFlagsBlockUid, (before, after) => { + if (!hasPropChanged(before, after)) return; + + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + const beforeResult = FeatureFlagsSchema.safeParse(beforeProps); + const afterResult = FeatureFlagsSchema.safeParse(afterProps); + + if (!afterResult.success) return; + + const oldSettings = beforeResult.success ? beforeResult.data : null; + const newSettings = afterResult.data; + + for (const [key, handler] of Object.entries(featureFlagHandlers)) { + const typedKey = key as keyof FeatureFlags; + if (hasPropChanged(before, after, key) && handler) { + handler( + newSettings[typedKey], + oldSettings?.[typedKey] ?? false, + newSettings, + ); + } + } + }); + } + + if (globalSettingsBlockUid && Object.keys(globalSettingsHandlers).length > 0) { + addPullWatch(watches, globalSettingsBlockUid, (before, after) => { + if (!hasPropChanged(before, after)) return; + + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + const beforeResult = GlobalSettingsSchema.safeParse(beforeProps); + const afterResult = GlobalSettingsSchema.safeParse(afterProps); + + if (!afterResult.success) return; + + const oldSettings = beforeResult.success ? beforeResult.data : null; + const newSettings = afterResult.data; + + for (const [key, handler] of Object.entries(globalSettingsHandlers)) { + const typedKey = key as keyof GlobalSettings; + if (hasPropChanged(before, after, key) && handler) { + handler( + newSettings[typedKey], + oldSettings?.[typedKey] as GlobalSettings[typeof typedKey], + newSettings, + ); + } + } + }); + } + + if (personalSettingsBlockUid && Object.keys(personalSettingsHandlers).length > 0) { + addPullWatch(watches, personalSettingsBlockUid, (before, after) => { + if (!hasPropChanged(before, after)) return; + + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + const beforeResult = PersonalSettingsSchema.safeParse(beforeProps); + const afterResult = PersonalSettingsSchema.safeParse(afterProps); + + if (!afterResult.success) return; + + const oldSettings = beforeResult.success ? beforeResult.data : null; + const newSettings = afterResult.data; + + for (const [key, handler] of Object.entries(personalSettingsHandlers)) { + const typedKey = key as keyof PersonalSettings; + if (hasPropChanged(before, after, key) && handler) { + handler( + newSettings[typedKey], + oldSettings?.[typedKey] as PersonalSettings[typeof typedKey], + newSettings, + ); + } + } + }); + } + + return createCleanupFn(watches); +}; + + +export const setupPullWatchDiscourseNodes = ( + nodePageUids: Record, +): (() => void) => { + const watches: PullWatchEntry[] = []; + + if (discourseNodeHandlers.length === 0) { + return () => {}; + } + + Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => { + addPullWatch(watches, pageUid, (before, after) => { + if (!hasPropChanged(before, after)) return; + + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + const beforeResult = DiscourseNodeSchema.safeParse(beforeProps); + const afterResult = DiscourseNodeSchema.safeParse(afterProps); + + if (!afterResult.success) return; + + const oldSettings = beforeResult.success ? beforeResult.data : null; + const newSettings = afterResult.data; + + for (const handler of discourseNodeHandlers) { + handler(nodeType, newSettings, oldSettings); + } + }); + }); + + return createCleanupFn(watches); +}; + + +export const queryAllDiscourseNodePageUids = (): Record => { + const results = window.roamAlphaAPI.q(` + [:find ?uid ?title + :where + [?page :node/title ?title] + [?page :block/uid ?uid] + [(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]] + `) as [string, string][]; + + const nodePageUids: Record = {}; + + for (const [pageUid, title] of results) { + const nodeLabel = title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""); + nodePageUids[nodeLabel] = pageUid; + } + + return nodePageUids; +}; + +export { hasPropChanged, getNormalizedProps }; From 974c926f40d771596e8cfc8cf5dfc5a7a9b7e1ef Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 11 Jan 2026 12:52:24 +0530 Subject: [PATCH 2/4] address review --- .../components/settings/utils/accessors.ts | 2 +- .../src/components/settings/utils/init.ts | 8 +- .../components/settings/utils/pullWatchers.ts | 300 ++++++++---------- apps/roam/src/index.ts | 1 - 4 files changed, 143 insertions(+), 168 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 3f116ad63..ec0cf3ddc 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -534,4 +534,4 @@ export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => { } return nodes; -}; +}; \ No newline at end of file diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index d50d0027e..f862c3f83 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -128,7 +128,12 @@ const initSingleDiscourseNode = async ( const initDiscourseNodePages = async (): Promise> => { if (hasNonDefaultNodes()) { - return {}; + const existingNodes = getAllDiscourseNodes(); + const nodePageUids: Record = {}; + for (const node of existingNodes) { + nodePageUids[node.text] = node.type; + } + return nodePageUids; } const results = await Promise.all( @@ -153,6 +158,5 @@ export type InitSchemaResult = { export const initSchema = async (): Promise => { const blockUids = await initSettingsPageBlocks(); const nodePageUids = await initDiscourseNodePages(); - return { blockUids, nodePageUids }; }; diff --git a/apps/roam/src/components/settings/utils/pullWatchers.ts b/apps/roam/src/components/settings/utils/pullWatchers.ts index 9666392b0..9e84404fa 100644 --- a/apps/roam/src/components/settings/utils/pullWatchers.ts +++ b/apps/roam/src/components/settings/utils/pullWatchers.ts @@ -1,10 +1,8 @@ import { type json, normalizeProps } from "~/utils/getBlockProps"; +import type { AddPullWatch, PullBlock } from "roamjs-components/types"; import { TOP_LEVEL_BLOCK_PROP_KEYS, - DISCOURSE_NODE_PAGE_PREFIX, -} from "../data/blockPropsSettingsConfig"; -import { getPersonalSettingsKey } from "./init"; -import { + getPersonalSettingsKey, FeatureFlagsSchema, GlobalSettingsSchema, PersonalSettingsSchema, @@ -15,23 +13,17 @@ import { type DiscourseNodeSettings, } from "./zodSchema"; -type PullWatchCallback = (before: unknown, after: unknown) => void; +type PullWatchCallback = Parameters[2]; -type PullWatchEntry = { - pattern: string; - entityId: string; - callback: PullWatchCallback; -}; -const getNormalizedProps = (data: unknown): Record => { - return normalizeProps( - ((data as Record)?.[":block/props"] || {}) as json, - ) as Record; +// Need assertions to bridge type defs between the (roamjs-components) and json type (getBlockProps.ts) +const getNormalizedProps = (data: PullBlock | null): Record => { + return normalizeProps((data?.[":block/props"] || {}) as json) as Record; }; const hasPropChanged = ( - before: unknown, - after: unknown, + before: PullBlock | null, + after: PullBlock | null, key?: string, ): boolean => { const beforeProps = getNormalizedProps(before); @@ -44,16 +36,42 @@ const hasPropChanged = ( return JSON.stringify(beforeProps) !== JSON.stringify(afterProps); }; -const createCleanupFn = (watches: PullWatchEntry[]): (() => void) => { +const createCleanupFn = (watches: Parameters[]): (() => void) => { return () => { - watches.forEach(({ pattern, entityId, callback }) => { + watches.forEach(([pattern, entityId, callback]) => { window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback); }); }; }; +const createSettingsWatchCallback = ( + schema: { safeParse: (data: unknown) => { success: boolean; data?: T } }, + onSettingsChange: ( + newSettings: T, + oldSettings: T | null, + before: PullBlock | null, + after: PullBlock | null + ) => void +): PullWatchCallback => { + return (before, after) => { + if (!hasPropChanged(before, after)) return; + + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + const beforeResult = schema.safeParse(beforeProps); + const afterResult = schema.safeParse(afterProps); + + if (!afterResult.success) return; + + const oldSettings = beforeResult.success ? beforeResult.data ?? null : null; + const newSettings = afterResult.data as T; + + onSettingsChange(newSettings, oldSettings, before, after); + }; +}; + const addPullWatch = ( - watches: PullWatchEntry[], + watches: Parameters[], blockUid: string, callback: PullWatchCallback, ): void => { @@ -61,35 +79,12 @@ const addPullWatch = ( const entityId = `[:block/uid "${blockUid}"]`; window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback); - watches.push({ pattern, entityId, callback }); + watches.push([pattern, entityId, callback]); }; -type FeatureFlagHandler = ( - newValue: boolean, - oldValue: boolean, - allSettings: FeatureFlags, -) => void; - -type GlobalSettingHandler = ( - newValue: GlobalSettings[K], - oldValue: GlobalSettings[K], - allSettings: GlobalSettings, -) => void; - -type PersonalSettingHandler = ( - newValue: PersonalSettings[K], - oldValue: PersonalSettings[K], - allSettings: PersonalSettings, -) => void; - -type DiscourseNodeHandler = ( - nodeType: string, - newSettings: DiscourseNodeSettings, - oldSettings: DiscourseNodeSettings | null, -) => void; export const featureFlagHandlers: Partial< - Record + Record void> > = { // Add handlers as needed: // "Enable Left Sidebar": (newValue) => { ... }, @@ -97,9 +92,15 @@ export const featureFlagHandlers: Partial< // "Reified Relation Triples": (newValue) => { ... }, }; -export const globalSettingsHandlers: Partial< - Record -> = { +type GlobalSettingsHandlers = { + [K in keyof GlobalSettings]?: ( + newValue: GlobalSettings[K], + oldValue: GlobalSettings[K], + allSettings: GlobalSettings + ) => void; +}; + +export const globalSettingsHandlers: GlobalSettingsHandlers = { // Add handlers as needed: // "Trigger": (newValue) => { ... }, // "Canvas Page Format": (newValue) => { ... }, @@ -108,113 +109,113 @@ export const globalSettingsHandlers: Partial< // "Suggestive Mode": (newValue) => { ... }, }; -export const personalSettingsHandlers: Partial< - Record -> = { - // Add handlers as needed: - // "Left Sidebar": (newValue) => { ... }, - // "Discourse Context Overlay": (newValue) => { ... }, - // "Page Preview": (newValue) => { ... }, - // etc. +type PersonalSettingsHandlers = { + [K in keyof PersonalSettings]?: ( + newValue: PersonalSettings[K], + oldValue: PersonalSettings[K], + allSettings: PersonalSettings + ) => void; }; +export const personalSettingsHandlers: PersonalSettingsHandlers = { + // "Left Sidebar" stub for testing with stubSetLeftSidebarPersonalSections() in accessors.ts + /* eslint-disable @typescript-eslint/naming-convention */ + "Left Sidebar": (newValue, oldValue) => { + const oldSections = Object.keys(oldValue || {}); + const newSections = Object.keys(newValue || {}); + + if (newSections.length === 0 && oldSections.length === 0) return; + + console.group("👤 [PullWatch] Personal Settings Changed: Left Sidebar"); + console.log("Old value:", JSON.stringify(oldValue, null, 2)); + console.log("New value:", JSON.stringify(newValue, null, 2)); + + const addedSections = newSections.filter((s) => !oldSections.includes(s)); + const removedSections = oldSections.filter((s) => !newSections.includes(s)); + + if (addedSections.length > 0) { + console.log(" → Sections added:", addedSections); + } + if (removedSections.length > 0) { + console.log(" → Sections removed:", removedSections); + } + console.groupEnd(); + }, + /* eslint-enable @typescript-eslint/naming-convention */ +}; -export const discourseNodeHandlers: DiscourseNodeHandler[] = [ +export const discourseNodeHandlers: Array< + (nodeType: string, newSettings: DiscourseNodeSettings, oldSettings: DiscourseNodeSettings | null) => void +> = [ // Add handlers as needed: // (nodeType, newSettings, oldSettings) => { ... }, ]; -export const setupPullWatchSettings = ( +export const setupPullWatchOnSettingsPage = ( blockUids: Record, ): (() => void) => { - const watches: PullWatchEntry[] = []; + const watches: Parameters[] = []; - const featureFlagsBlockUid = - blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; + const featureFlagsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global]; const personalSettingsKey = getPersonalSettingsKey(); const personalSettingsBlockUid = blockUids[personalSettingsKey]; if (featureFlagsBlockUid && Object.keys(featureFlagHandlers).length > 0) { - addPullWatch(watches, featureFlagsBlockUid, (before, after) => { - if (!hasPropChanged(before, after)) return; - - const beforeProps = getNormalizedProps(before); - const afterProps = getNormalizedProps(after); - const beforeResult = FeatureFlagsSchema.safeParse(beforeProps); - const afterResult = FeatureFlagsSchema.safeParse(afterProps); - - if (!afterResult.success) return; - - const oldSettings = beforeResult.success ? beforeResult.data : null; - const newSettings = afterResult.data; - - for (const [key, handler] of Object.entries(featureFlagHandlers)) { - const typedKey = key as keyof FeatureFlags; - if (hasPropChanged(before, after, key) && handler) { - handler( - newSettings[typedKey], - oldSettings?.[typedKey] ?? false, - newSettings, - ); + addPullWatch(watches, featureFlagsBlockUid, createSettingsWatchCallback( + FeatureFlagsSchema, + (newSettings, oldSettings, before, after) => { + for (const [key, handler] of Object.entries(featureFlagHandlers)) { + const typedKey = key as keyof FeatureFlags; + if (hasPropChanged(before, after, key) && handler) { + handler( + newSettings[typedKey], + oldSettings?.[typedKey] ?? false, + newSettings, + ); + } } } - }); + )); } if (globalSettingsBlockUid && Object.keys(globalSettingsHandlers).length > 0) { - addPullWatch(watches, globalSettingsBlockUid, (before, after) => { - if (!hasPropChanged(before, after)) return; - - const beforeProps = getNormalizedProps(before); - const afterProps = getNormalizedProps(after); - const beforeResult = GlobalSettingsSchema.safeParse(beforeProps); - const afterResult = GlobalSettingsSchema.safeParse(afterProps); - - if (!afterResult.success) return; - - const oldSettings = beforeResult.success ? beforeResult.data : null; - const newSettings = afterResult.data; - - for (const [key, handler] of Object.entries(globalSettingsHandlers)) { - const typedKey = key as keyof GlobalSettings; - if (hasPropChanged(before, after, key) && handler) { - handler( - newSettings[typedKey], - oldSettings?.[typedKey] as GlobalSettings[typeof typedKey], - newSettings, - ); + addPullWatch(watches, globalSettingsBlockUid, createSettingsWatchCallback( + GlobalSettingsSchema, + (newSettings, oldSettings, before, after) => { + for (const [key, handler] of Object.entries(globalSettingsHandlers)) { + const typedKey = key as keyof GlobalSettings; + if (hasPropChanged(before, after, key) && handler) { + // Object.entries loses key-handler correlation, but data is Zod-validated + (handler as (newValue: unknown, oldValue: unknown, allSettings: GlobalSettings) => void)( + newSettings[typedKey], + oldSettings?.[typedKey], + newSettings, + ); + } } } - }); + )); } if (personalSettingsBlockUid && Object.keys(personalSettingsHandlers).length > 0) { - addPullWatch(watches, personalSettingsBlockUid, (before, after) => { - if (!hasPropChanged(before, after)) return; - - const beforeProps = getNormalizedProps(before); - const afterProps = getNormalizedProps(after); - const beforeResult = PersonalSettingsSchema.safeParse(beforeProps); - const afterResult = PersonalSettingsSchema.safeParse(afterProps); - - if (!afterResult.success) return; - - const oldSettings = beforeResult.success ? beforeResult.data : null; - const newSettings = afterResult.data; - - for (const [key, handler] of Object.entries(personalSettingsHandlers)) { - const typedKey = key as keyof PersonalSettings; - if (hasPropChanged(before, after, key) && handler) { - handler( - newSettings[typedKey], - oldSettings?.[typedKey] as PersonalSettings[typeof typedKey], - newSettings, - ); + addPullWatch(watches, personalSettingsBlockUid, createSettingsWatchCallback( + PersonalSettingsSchema, + (newSettings, oldSettings, before, after) => { + for (const [key, handler] of Object.entries(personalSettingsHandlers)) { + const typedKey = key as keyof PersonalSettings; + if (hasPropChanged(before, after, key) && handler) { + // Object.entries loses key-handler correlation, but data is Zod-validated + (handler as (newValue: unknown, oldValue: unknown, allSettings: PersonalSettings) => void)( + newSettings[typedKey], + oldSettings?.[typedKey], + newSettings, + ); + } } } - }); + )); } return createCleanupFn(watches); @@ -224,53 +225,24 @@ export const setupPullWatchSettings = ( export const setupPullWatchDiscourseNodes = ( nodePageUids: Record, ): (() => void) => { - const watches: PullWatchEntry[] = []; + const watches: Parameters[] = []; if (discourseNodeHandlers.length === 0) { return () => {}; } Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => { - addPullWatch(watches, pageUid, (before, after) => { - if (!hasPropChanged(before, after)) return; - - const beforeProps = getNormalizedProps(before); - const afterProps = getNormalizedProps(after); - const beforeResult = DiscourseNodeSchema.safeParse(beforeProps); - const afterResult = DiscourseNodeSchema.safeParse(afterProps); - - if (!afterResult.success) return; - - const oldSettings = beforeResult.success ? beforeResult.data : null; - const newSettings = afterResult.data; - - for (const handler of discourseNodeHandlers) { - handler(nodeType, newSettings, oldSettings); + addPullWatch(watches, pageUid, createSettingsWatchCallback( + DiscourseNodeSchema, + (newSettings, oldSettings) => { + for (const handler of discourseNodeHandlers) { + handler(nodeType, newSettings, oldSettings); + } } - }); + )); }); return createCleanupFn(watches); }; - -export const queryAllDiscourseNodePageUids = (): Record => { - const results = window.roamAlphaAPI.q(` - [:find ?uid ?title - :where - [?page :node/title ?title] - [?page :block/uid ?uid] - [(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]] - `) as [string, string][]; - - const nodePageUids: Record = {}; - - for (const [pageUid, title] of results) { - const nodeLabel = title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""); - nodePageUids[nodeLabel] = pageUid; - } - - return nodePageUids; -}; - -export { hasPropChanged, getNormalizedProps }; +export { getNormalizedProps, hasPropChanged }; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 11426c546..1bed828d4 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -41,7 +41,6 @@ import { STREAMLINE_STYLING_KEY, DISALLOW_DIAGNOSTICS, } from "./data/userSettings"; -import { initSchema } from "./components/settings/utils/init"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; From e39022a1b40c104b0e7947f09a80073c414cccd4 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 5 Feb 2026 23:43:27 +0530 Subject: [PATCH 3/4] address review --- .../components/settings/utils/pullWatchers.ts | 183 +++++++++++------- 1 file changed, 109 insertions(+), 74 deletions(-) diff --git a/apps/roam/src/components/settings/utils/pullWatchers.ts b/apps/roam/src/components/settings/utils/pullWatchers.ts index 9e84404fa..5b3c097e8 100644 --- a/apps/roam/src/components/settings/utils/pullWatchers.ts +++ b/apps/roam/src/components/settings/utils/pullWatchers.ts @@ -15,10 +15,12 @@ import { type PullWatchCallback = Parameters[2]; - // Need assertions to bridge type defs between the (roamjs-components) and json type (getBlockProps.ts) const getNormalizedProps = (data: PullBlock | null): Record => { - return normalizeProps((data?.[":block/props"] || {}) as json) as Record; + return normalizeProps((data?.[":block/props"] || {}) as json) as Record< + string, + json + >; }; const hasPropChanged = ( @@ -46,16 +48,14 @@ const createCleanupFn = (watches: Parameters[]): (() => void) => { const createSettingsWatchCallback = ( schema: { safeParse: (data: unknown) => { success: boolean; data?: T } }, - onSettingsChange: ( - newSettings: T, - oldSettings: T | null, - before: PullBlock | null, - after: PullBlock | null - ) => void + onSettingsChange: (context: { + newSettings: T; + oldSettings: T | null; + before: PullBlock | null; + after: PullBlock | null; + }) => void, ): PullWatchCallback => { return (before, after) => { - if (!hasPropChanged(before, after)) return; - const beforeProps = getNormalizedProps(before); const afterProps = getNormalizedProps(after); const beforeResult = schema.safeParse(beforeProps); @@ -63,10 +63,12 @@ const createSettingsWatchCallback = ( if (!afterResult.success) return; - const oldSettings = beforeResult.success ? beforeResult.data ?? null : null; + const oldSettings = beforeResult.success + ? (beforeResult.data ?? null) + : null; const newSettings = afterResult.data as T; - onSettingsChange(newSettings, oldSettings, before, after); + onSettingsChange({ newSettings, oldSettings, before, after }); }; }; @@ -82,9 +84,11 @@ const addPullWatch = ( watches.push([pattern, entityId, callback]); }; - export const featureFlagHandlers: Partial< - Record void> + Record< + keyof FeatureFlags, + (newValue: boolean, oldValue: boolean, allFlags: FeatureFlags) => void + > > = { // Add handlers as needed: // "Enable Left Sidebar": (newValue) => { ... }, @@ -96,7 +100,7 @@ type GlobalSettingsHandlers = { [K in keyof GlobalSettings]?: ( newValue: GlobalSettings[K], oldValue: GlobalSettings[K], - allSettings: GlobalSettings + allSettings: GlobalSettings, ) => void; }; @@ -113,7 +117,7 @@ type PersonalSettingsHandlers = { [K in keyof PersonalSettings]?: ( newValue: PersonalSettings[K], oldValue: PersonalSettings[K], - allSettings: PersonalSettings + allSettings: PersonalSettings, ) => void; }; @@ -145,83 +149,110 @@ export const personalSettingsHandlers: PersonalSettingsHandlers = { }; export const discourseNodeHandlers: Array< - (nodeType: string, newSettings: DiscourseNodeSettings, oldSettings: DiscourseNodeSettings | null) => void + ( + nodeType: string, + newSettings: DiscourseNodeSettings, + oldSettings: DiscourseNodeSettings | null, + ) => void > = [ // Add handlers as needed: // (nodeType, newSettings, oldSettings) => { ... }, ]; - export const setupPullWatchOnSettingsPage = ( blockUids: Record, ): (() => void) => { const watches: Parameters[] = []; - const featureFlagsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; + const featureFlagsBlockUid = + blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; const globalSettingsBlockUid = blockUids[TOP_LEVEL_BLOCK_PROP_KEYS.global]; const personalSettingsKey = getPersonalSettingsKey(); const personalSettingsBlockUid = blockUids[personalSettingsKey]; if (featureFlagsBlockUid && Object.keys(featureFlagHandlers).length > 0) { - addPullWatch(watches, featureFlagsBlockUid, createSettingsWatchCallback( - FeatureFlagsSchema, - (newSettings, oldSettings, before, after) => { - for (const [key, handler] of Object.entries(featureFlagHandlers)) { - const typedKey = key as keyof FeatureFlags; - if (hasPropChanged(before, after, key) && handler) { - handler( - newSettings[typedKey], - oldSettings?.[typedKey] ?? false, - newSettings, - ); + addPullWatch( + watches, + featureFlagsBlockUid, + createSettingsWatchCallback( + FeatureFlagsSchema, + ({ newSettings, oldSettings, before, after }) => { + for (const [key, handler] of Object.entries(featureFlagHandlers)) { + const typedKey = key as keyof FeatureFlags; + if (hasPropChanged(before, after, key) && handler) { + handler( + newSettings[typedKey], + oldSettings?.[typedKey] ?? false, + newSettings, + ); + } } - } - } - )); + }, + ), + ); } - if (globalSettingsBlockUid && Object.keys(globalSettingsHandlers).length > 0) { - addPullWatch(watches, globalSettingsBlockUid, createSettingsWatchCallback( - GlobalSettingsSchema, - (newSettings, oldSettings, before, after) => { - for (const [key, handler] of Object.entries(globalSettingsHandlers)) { - const typedKey = key as keyof GlobalSettings; - if (hasPropChanged(before, after, key) && handler) { - // Object.entries loses key-handler correlation, but data is Zod-validated - (handler as (newValue: unknown, oldValue: unknown, allSettings: GlobalSettings) => void)( - newSettings[typedKey], - oldSettings?.[typedKey], - newSettings, - ); + if ( + globalSettingsBlockUid && + Object.keys(globalSettingsHandlers).length > 0 + ) { + addPullWatch( + watches, + globalSettingsBlockUid, + createSettingsWatchCallback( + GlobalSettingsSchema, + ({ newSettings, oldSettings, before, after }) => { + for (const [key, handler] of Object.entries(globalSettingsHandlers)) { + const typedKey = key as keyof GlobalSettings; + if (hasPropChanged(before, after, key) && handler) { + // Object.entries loses key-handler correlation, but data is Zod-validated + ( + handler as ( + newValue: unknown, + oldValue: unknown, + allSettings: GlobalSettings, + ) => void + )(newSettings[typedKey], oldSettings?.[typedKey], newSettings); + } } - } - } - )); + }, + ), + ); } - if (personalSettingsBlockUid && Object.keys(personalSettingsHandlers).length > 0) { - addPullWatch(watches, personalSettingsBlockUid, createSettingsWatchCallback( - PersonalSettingsSchema, - (newSettings, oldSettings, before, after) => { - for (const [key, handler] of Object.entries(personalSettingsHandlers)) { - const typedKey = key as keyof PersonalSettings; - if (hasPropChanged(before, after, key) && handler) { - // Object.entries loses key-handler correlation, but data is Zod-validated - (handler as (newValue: unknown, oldValue: unknown, allSettings: PersonalSettings) => void)( - newSettings[typedKey], - oldSettings?.[typedKey], - newSettings, - ); + if ( + personalSettingsBlockUid && + Object.keys(personalSettingsHandlers).length > 0 + ) { + addPullWatch( + watches, + personalSettingsBlockUid, + createSettingsWatchCallback( + PersonalSettingsSchema, + ({ newSettings, oldSettings, before, after }) => { + for (const [key, handler] of Object.entries( + personalSettingsHandlers, + )) { + const typedKey = key as keyof PersonalSettings; + if (hasPropChanged(before, after, key) && handler) { + // Object.entries loses key-handler correlation, but data is Zod-validated + ( + handler as ( + newValue: unknown, + oldValue: unknown, + allSettings: PersonalSettings, + ) => void + )(newSettings[typedKey], oldSettings?.[typedKey], newSettings); + } } - } - } - )); + }, + ), + ); } return createCleanupFn(watches); }; - export const setupPullWatchDiscourseNodes = ( nodePageUids: Record, ): (() => void) => { @@ -232,14 +263,18 @@ export const setupPullWatchDiscourseNodes = ( } Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => { - addPullWatch(watches, pageUid, createSettingsWatchCallback( - DiscourseNodeSchema, - (newSettings, oldSettings) => { - for (const handler of discourseNodeHandlers) { - handler(nodeType, newSettings, oldSettings); - } - } - )); + addPullWatch( + watches, + pageUid, + createSettingsWatchCallback( + DiscourseNodeSchema, + ({ newSettings, oldSettings }) => { + for (const handler of discourseNodeHandlers) { + handler(nodeType, newSettings, oldSettings); + } + }, + ), + ); }); return createCleanupFn(watches); From 36fe06000563a5c780ef2b95bb34c4f7af2fd689 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 6 Feb 2026 17:52:47 +0530 Subject: [PATCH 4/4] address devin review from accessors pr --- apps/roam/src/components/settings/utils/accessors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index ec0cf3ddc..ec1fdc4c2 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -470,7 +470,7 @@ export const setDiscourseNodeSetting = ( } let pageUid = nodeType; - const blockProps = getBlockPropsByUid(pageUid, []); + let blockProps = getBlockPropsByUid(pageUid, []); if (!blockProps || Object.keys(blockProps).length === 0) { const lookedUpUid = getPageUidByPageTitle( @@ -478,10 +478,11 @@ export const setDiscourseNodeSetting = ( ); if (lookedUpUid) { pageUid = lookedUpUid; + blockProps = getBlockPropsByUid(pageUid, []); } } - if (!pageUid) { + if (!blockProps || Object.keys(blockProps).length === 0) { internalError({ error: `setDiscourseNodeSetting - could not find page for: ${nodeType}`, type: "DG Accessor",