From 3fe104af368e7f168c370c59409998be9b08b1cc Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 28 Dec 2025 17:46:36 +0530 Subject: [PATCH 1/2] zod schema for dg node --- .../data/defaultDiscourseNodeValues.ts | 48 ++++++++++++++++ .../settings/block-prop/utils/zodSchema.ts | 55 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 apps/roam/src/components/settings/block-prop/data/defaultDiscourseNodeValues.ts diff --git a/apps/roam/src/components/settings/block-prop/data/defaultDiscourseNodeValues.ts b/apps/roam/src/components/settings/block-prop/data/defaultDiscourseNodeValues.ts new file mode 100644 index 000000000..cec808386 --- /dev/null +++ b/apps/roam/src/components/settings/block-prop/data/defaultDiscourseNodeValues.ts @@ -0,0 +1,48 @@ +import { DiscourseNode } from "~/utils/getDiscourseNodes"; + +const INITIAL_NODE_VALUES: Partial[] = [ + { + type: "_CLM-node", + format: "[[CLM]] - {content}", + text: "Claim", + shortcut: "C", + tag: "#clm-candidate", + graphOverview: true, + canvasSettings: { + color: "7DA13E", + }, + }, + { + type: "_QUE-node", + format: "[[QUE]] - {content}", + text: "Question", + shortcut: "Q", + graphOverview: true, + canvasSettings: { + color: "99890e", + }, + }, + { + type: "_EVD-node", + format: "[[EVD]] - {content} - {Source}", + text: "Evidence", + shortcut: "E", + tag: "#evd-candidate", + graphOverview: true, + canvasSettings: { + color: "DB134A", + }, + }, + { + type: "_SRC-node", + format: "@{content}", + text: "Source", + shortcut: "S", + graphOverview: true, + canvasSettings: { + color: "9E9E9E", + }, + }, +]; + +export default INITIAL_NODE_VALUES; diff --git a/apps/roam/src/components/settings/block-prop/utils/zodSchema.ts b/apps/roam/src/components/settings/block-prop/utils/zodSchema.ts index 109dfb767..72866e9ad 100644 --- a/apps/roam/src/components/settings/block-prop/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/block-prop/utils/zodSchema.ts @@ -1,6 +1,58 @@ import { z } from "zod"; /* eslint-disable @typescript-eslint/naming-convention */ + +export const CanvasSettingsSchema = z.object({ + color: z.string().default(""), + alias: z.string().default(""), + "key-image": z.boolean().default(false), + "key-image-option": z + .enum(["first-image", "query-builder"]) + .default("first-image"), + "query-builder-alias": z.string().default(""), +}); + +export const SuggestiveRulesSchema = z.object({ + template: z.array(z.unknown()).default([]), + embeddingRef: z.string().optional(), + embeddingRefUid: z.string().optional(), + isFirstChild: z + .object({ + uid: z.string(), + value: z.boolean(), + }) + .optional(), +}); + +export const AttributesSchema = z.record(z.string(), z.string()).default({}); + +export const DiscourseNodeSchema = z.object({ + text: z.string(), + type: z.string(), + format: z.string().default(""), + shortcut: z.string().default(""), + tag: z.string().optional(), + description: z.string().optional(), + specification: z.array(z.unknown()).default([]), + template: z.array(z.unknown()).optional(), + canvasSettings: z.record(z.string(), z.string()).default({}), + graphOverview: z.boolean().optional(), + attributes: AttributesSchema, + overlay: z.string().optional(), + index: z.unknown().optional(), + suggestiveRules: SuggestiveRulesSchema.optional(), + embeddingRef: z.string().optional(), + embeddingRefUid: z.string().optional(), + isFirstChild: z + .object({ + uid: z.string(), + value: z.boolean(), + }) + .optional(), + backedBy: z.enum(["user", "default", "relation"]).default("user"), +}); + + export const FeatureFlagsSchema = z.object({ "Enable Left Sidebar": z.boolean().default(false), }); @@ -41,6 +93,9 @@ export const PersonalSettingsSchema = z.object({ }); /* eslint-enable @typescript-eslint/naming-convention */ +export type CanvasSettings = z.infer; +export type SuggestiveRules = z.infer; +export type DiscourseNodeSettings = z.infer; export type FeatureFlags = z.infer; export type GlobalSettings = z.infer; export type PersonalSection = z.infer; From 1041e3096157a1fe4e5a674c72fd19d716692ed0 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 28 Dec 2025 20:35:09 +0530 Subject: [PATCH 2/2] add init accessors and pullwatch --- .../settings/block-prop/utils/accessors.ts | 181 +++++++++++------- .../settings/block-prop/utils/init.ts | 57 ++++++ .../settings/block-prop/utils/pullWatch.ts | 58 +++++- 3 files changed, 224 insertions(+), 72 deletions(-) diff --git a/apps/roam/src/components/settings/block-prop/utils/accessors.ts b/apps/roam/src/components/settings/block-prop/utils/accessors.ts index a558c4b89..ecfcbac77 100644 --- a/apps/roam/src/components/settings/block-prop/utils/accessors.ts +++ b/apps/roam/src/components/settings/block-prop/utils/accessors.ts @@ -11,12 +11,80 @@ import { FeatureFlagsSchema, GlobalSettingsSchema, PersonalSettingsSchema, + DiscourseNodeSchema, + DiscourseNodeSettings, } from "~/components/settings/block-prop/utils/zodSchema"; -import { getPersonalSettingsKey } from "~/components/settings/block-prop/utils/init"; +import { + getPersonalSettingsKey, + getDiscourseNodePageUid, +} from "~/components/settings/block-prop/utils/init"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); +export const getBlockPropsByUid = ( + blockUid: string, + keys: string[], +): json | undefined => { + const allBlockProps = getBlockProps(blockUid); + + if (keys.length === 0) { + return allBlockProps; + } + + const targetValue = keys.reduce((currentContext: json, currentKey) => { + if ( + currentContext && + typeof currentContext === "object" && + !Array.isArray(currentContext) + ) { + const value = (currentContext as Record)[currentKey]; + return value === undefined ? null : value; + } + return null; + }, allBlockProps); + + return targetValue === null ? undefined : targetValue; +}; + +export const setBlockPropsByUid = ( + blockUid: string, + keys: string[], + value: json, +): void => { + if (keys.length === 0) { + setBlockProps(blockUid, value as Record, false); + return; + } + + const currentProps = getBlockProps(blockUid); + const updatedProps = JSON.parse(JSON.stringify(currentProps || {})) as Record< + string, + json + >; + + const lastKeyIndex = keys.length - 1; + + keys.reduce>((currentContext, currentKey, index) => { + if (index === lastKeyIndex) { + currentContext[currentKey] = value; + return currentContext; + } + + if ( + !currentContext[currentKey] || + typeof currentContext[currentKey] !== "object" || + Array.isArray(currentContext[currentKey]) + ) { + currentContext[currentKey] = {}; + } + + return currentContext[currentKey] as Record; + }, updatedProps); + + setBlockProps(blockUid, updatedProps, false); +}; + export const getBlockPropBasedSettings = ({ keys, }: { @@ -27,38 +95,14 @@ export const getBlockPropBasedSettings = ({ return { blockProps: undefined, blockUid: "" }; } - const sectionKey = keys[0]; - const blockUid = getBlockUidByTextOnPage({ - text: sectionKey, + text: keys[0], title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, }); - const allBlockPropsForSection = getBlockProps(blockUid); - - if (keys.length > 1) { - const propertyPath = keys.slice(1); - - const targetValue = propertyPath.reduce( - (currentContext: json, currentKey) => { - if ( - currentContext && - typeof currentContext === "object" && - !Array.isArray(currentContext) - ) { - const value = (currentContext as Record)[currentKey]; - return value === undefined ? null : value; - } - return null; - }, - allBlockPropsForSection, - ); - return { - blockProps: targetValue === null ? undefined : targetValue, - blockUid, - }; - } - return { blockProps: allBlockPropsForSection, blockUid }; + const blockProps = getBlockPropsByUid(blockUid, keys.slice(1)); + + return { blockProps, blockUid }; }; export const setBlockPropBasedSettings = ({ @@ -78,41 +122,7 @@ export const setBlockPropBasedSettings = ({ title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, }); - if (keys.length === 1) { - setBlockProps(blockUid, value as Record, false); - return; - } - - const currentProps = getBlockProps(blockUid); - const updatedProps = JSON.parse(JSON.stringify(currentProps || {})) as Record< - string, - json - >; - - const propertyPath = keys.slice(1); - const lastKeyIndex = propertyPath.length - 1; - - propertyPath.reduce>( - (currentContext, currentKey, index) => { - if (index === lastKeyIndex) { - currentContext[currentKey] = value; - return currentContext; - } - - if ( - !currentContext[currentKey] || - typeof currentContext[currentKey] !== "object" || - Array.isArray(currentContext[currentKey]) - ) { - currentContext[currentKey] = {}; - } - - return currentContext[currentKey] as Record; - }, - updatedProps, - ); - - setBlockProps(blockUid, updatedProps, false); + setBlockPropsByUid(blockUid, keys.slice(1), value); }; export const getFeatureFlag = (key: keyof FeatureFlags): boolean => { @@ -188,3 +198,46 @@ export const setPersonalSetting = (keys: string[], value: json): void => { value, }); }; + +export const getDiscourseNodeSettings = ( + nodeType: string, +): DiscourseNodeSettings | undefined => { + const pageUid = getDiscourseNodePageUid(nodeType); + + if (!pageUid) return undefined; + + const blockProps = getBlockPropsByUid(pageUid, []); + + if (!blockProps) return undefined; + + return DiscourseNodeSchema.parse(blockProps); +}; + +export const getDiscourseNodeSetting = ( + nodeType: string, + keys: string[], +): unknown => { + const settings = getDiscourseNodeSettings(nodeType); + + if (!settings) return undefined; + + return keys.reduce((current, key) => { + if (!isRecord(current) || !(key in current)) return undefined; + return current[key]; + }, settings); +}; + +export const setDiscourseNodeSetting = ( + nodeType: string, + keys: string[], + value: json, +): void => { + const pageUid = getDiscourseNodePageUid(nodeType); + + if (!pageUid) { + console.warn(`Discourse node page not found for type: ${nodeType}`); + return; + } + + setBlockPropsByUid(pageUid, keys, value); +}; diff --git a/apps/roam/src/components/settings/block-prop/utils/init.ts b/apps/roam/src/components/settings/block-prop/utils/init.ts index 5b57a3fab..bccd928b5 100644 --- a/apps/roam/src/components/settings/block-prop/utils/init.ts +++ b/apps/roam/src/components/settings/block-prop/utils/init.ts @@ -2,9 +2,15 @@ import { TOP_LEVEL_BLOCK_PROP_KEYS, DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; +import INITIAL_NODE_VALUES from "~/components/settings/block-prop/data/defaultDiscourseNodeValues"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeByParentUid"; import { createPage, createBlock } from "roamjs-components/writes"; +import setBlockProps from "~/utils/setBlockProps"; +import getBlockProps, { type json } from "~/utils/getBlockProps"; +import { DiscourseNodeSchema } from "~/components/settings/block-prop/utils/zodSchema"; + +export const DISCOURSE_NODE_PAGE_PREFIX = "discourse-graph/nodes/"; const ensurePageExists = async (pageTitle: string): Promise => { let pageUid = getPageUidByPageTitle(pageTitle); @@ -87,3 +93,54 @@ export const initSchema = async (): Promise> => { return blockMap; }; + +export const getDiscourseNodePageTitle = (nodeType: string): string => { + return `${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`; +}; + +export const getDiscourseNodePageUid = ( + nodeType: string, +): string | undefined => { + const pageTitle = getDiscourseNodePageTitle(nodeType); + const pageUid = getPageUidByPageTitle(pageTitle); + return pageUid || undefined; +}; + +const ensureDiscourseNodePageExists = async ( + nodeType: string, +): Promise => { + const pageTitle = getDiscourseNodePageTitle(nodeType); + return ensurePageExists(pageTitle); +}; + +export const initDiscourseNodes = async (): Promise< + Record +> => { + const nodePageUids: Record = {}; + + for (const node of INITIAL_NODE_VALUES) { + if (!node.type) continue; + + const pageUid = await ensureDiscourseNodePageExists(node.type); + nodePageUids[node.type] = pageUid; + + const existingProps = getBlockProps(pageUid); + + if (!existingProps || Object.keys(existingProps).length === 0) { + const nodeData = DiscourseNodeSchema.parse({ + text: node.text || "", + type: node.type, + format: node.format || "", + shortcut: node.shortcut || "", + tag: node.tag, + graphOverview: node.graphOverview, + canvasSettings: node.canvasSettings || {}, + backedBy: "user", + }); + + setBlockProps(pageUid, nodeData as Record, false); + } + } + + return nodePageUids; +}; diff --git a/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts b/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts index ad19c4c71..6b154d410 100644 --- a/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts +++ b/apps/roam/src/components/settings/block-prop/utils/pullWatch.ts @@ -1,20 +1,27 @@ import { TOP_LEVEL_BLOCK_PROP_KEYS } from "~/components/settings/block-prop/data/blockPropsSettingsConfig"; import { type json, normalizeProps } from "~/utils/getBlockProps"; import { getPersonalSettingsKey } from "~/components/settings/block-prop/utils/init"; +import { DiscourseNodeSchema } from "~/components/settings/block-prop/utils/zodSchema"; + +const getNormalizedProps = (data: unknown): Record => { + return normalizeProps( + ((data as Record)?.[":block/props"] || {}) as json, + ) as Record; +}; const hasPropChanged = ( before: unknown, after: unknown, - key: string, + key?: string, ): boolean => { - const beforeProps = normalizeProps( - ((before as Record)?.[":block/props"] || {}) as json, - ) as Record; - const afterProps = normalizeProps( - ((after as Record)?.[":block/props"] || {}) as json, - ) as Record; + const beforeProps = getNormalizedProps(before); + const afterProps = getNormalizedProps(after); + + if (key) { + return JSON.stringify(beforeProps[key]) !== JSON.stringify(afterProps[key]); + } - return JSON.stringify(beforeProps[key]) !== JSON.stringify(afterProps[key]); + return JSON.stringify(beforeProps) !== JSON.stringify(afterProps); }; export const setupPullWatchBlockPropsBasedSettings = ( @@ -66,3 +73,38 @@ export const setupPullWatchBlockPropsBasedSettings = ( ); } }; + +export type DiscourseNodeChangeCallback = ( + nodeType: string, + settings: ReturnType, +) => void; + +type PullWatchCallback = (before: unknown, after: unknown) => void; + +export const setupPullWatchDiscourseNodes = ( + nodePageUids: Record, + onNodeChange: DiscourseNodeChangeCallback, +): (() => void) => { + const watches: Array<{ pattern: string; entityId: string; callback: PullWatchCallback }> = []; + + Object.entries(nodePageUids).forEach(([nodeType, pageUid]) => { + const pattern = "[:block/props]"; + const entityId = `[:block/uid "${pageUid}"]`; + const callback: PullWatchCallback = (before, after) => { + if (hasPropChanged(before, after)) { + const afterProps = getNormalizedProps(after); + const settings = DiscourseNodeSchema.parse(afterProps); + onNodeChange(nodeType, settings); + } + }; + + window.roamAlphaAPI.data.addPullWatch(pattern, entityId, callback); + watches.push({ pattern, entityId, callback }); + }); + + return () => { + watches.forEach(({ pattern, entityId, callback }) => { + window.roamAlphaAPI.data.removePullWatch(pattern, entityId, callback); + }); + }; +};