diff --git a/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts b/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts new file mode 100644 index 000000000..3f02ae700 --- /dev/null +++ b/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts @@ -0,0 +1,142 @@ +import type { DiscourseRelationSettings } from "~/components/settings/utils/zodSchema"; +/* eslint-disable @typescript-eslint/naming-convention */ // This is for nodePosition keys + +// TODO: Delete the original default relations in data/defaultRelations.ts when fully migrated. +const DEFAULT_RELATIONS_BLOCK_PROPS: DiscourseRelationSettings[] = [ + { + id: "informs", + label: "Informs", + source: "_EVD-node", + destination: "_QUE-node", + complement: "Informed By", + ifConditions: [ + { + triples: [ + ["Page", "is a", "source"], + ["Block", "references", "Page"], + ["Block", "is in page", "ParentPage"], + ["ParentPage", "is a", "destination"], + ], + nodePositions: { + "0": "100 57", + "1": "100 208", + "2": "100 345", + source: "281 57", + destination: "281 345", + }, + }, + ], + }, + { + id: "supports", + label: "Supports", + source: "_EVD-node", + destination: "_CLM-node", + complement: "Supported By", + ifConditions: [ + { + triples: [ + ["Page", "is a", "source"], + ["Block", "references", "Page"], + ["SBlock", "references", "SPage"], + ["SPage", "has title", "SupportedBy"], + ["SBlock", "has child", "Block"], + ["PBlock", "references", "ParentPage"], + ["PBlock", "has child", "SBlock"], + ["ParentPage", "is a", "destination"], + ], + nodePositions: { + "0": "250 325", + "1": "100 325", + "2": "100 200", + "3": "250 200", + "4": "400 200", + "5": "100 75", + "6": "250 75", + source: "400 325", + destination: "400 75", + }, + }, + { + triples: [ + ["Page", "is a", "destination"], + ["Block", "references", "Page"], + ["SBlock", "references", "SPage"], + ["SPage", "has title", "Supports"], + ["SBlock", "has child", "Block"], + ["PBlock", "references", "ParentPage"], + ["PBlock", "has child", "SBlock"], + ["ParentPage", "is a", "source"], + ], + nodePositions: { + "7": "250 325", + "8": "100 325", + "9": "100 200", + "10": "250 200", + "11": "400 200", + "12": "100 75", + "13": "250 75", + source: "400 75", + destination: "400 325", + }, + }, + ], + }, + { + id: "opposes", + label: "Opposes", + source: "_EVD-node", + destination: "_CLM-node", + complement: "Opposed By", + ifConditions: [ + { + triples: [ + ["Page", "is a", "source"], + ["Block", "references", "Page"], + ["SBlock", "references", "SPage"], + ["SPage", "has title", "OpposedBy"], + ["SBlock", "has child", "Block"], + ["PBlock", "references", "ParentPage"], + ["PBlock", "has child", "SBlock"], + ["ParentPage", "is a", "destination"], + ], + nodePositions: { + "0": "250 325", + "1": "100 325", + "2": "100 200", + "3": "250 200", + "4": "400 200", + "5": "100 75", + "6": "250 75", + source: "400 325", + destination: "400 75", + }, + }, + { + triples: [ + ["Page", "is a", "destination"], + ["Block", "references", "Page"], + ["SBlock", "references", "SPage"], + ["SPage", "has title", "Opposes"], + ["SBlock", "has child", "Block"], + ["PBlock", "references", "ParentPage"], + ["PBlock", "has child", "SBlock"], + ["ParentPage", "is a", "source"], + ], + nodePositions: { + "7": "250 325", + "8": "100 325", + "9": "100 200", + "10": "250 200", + "11": "400 200", + "12": "100 75", + "13": "250 75", + source: "400 75", + destination: "400 325", + }, + }, + ], + }, +]; + +export default DEFAULT_RELATIONS_BLOCK_PROPS; diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts new file mode 100644 index 000000000..fad732604 --- /dev/null +++ b/apps/roam/src/components/settings/utils/init.ts @@ -0,0 +1,224 @@ +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 from "~/utils/getBlockProps"; +import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; +import { + DiscourseNodeSchema, + getTopLevelBlockPropsConfig, + getPersonalSettingsKey, +} from "~/components/settings/utils/zodSchema"; +import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, DISCOURSE_NODE_PAGE_PREFIX } from "./zodSchema"; + +const ensurePageExists = async (pageTitle: string): Promise => { + let pageUid = getPageUidByPageTitle(pageTitle); + + if (!pageUid) { + pageUid = window.roamAlphaAPI.util.generateUID(); + await createPage({ + title: pageTitle, + uid: pageUid, + }); + } + + return pageUid; +}; + +const ensureBlocksExist = async ( + pageUid: string, + blockTexts: string[], + existingBlockMap: Record, +): Promise> => { + const missingBlocks = blockTexts.filter( + (blockText) => !existingBlockMap[blockText], + ); + + if (missingBlocks.length > 0) { + const createdBlocks = await Promise.all( + missingBlocks.map(async (blockText) => { + const uid = await createBlock({ + parentUid: pageUid, + node: { text: blockText }, + }); + return { text: blockText, uid }; + }), + ); + + createdBlocks.forEach((block) => { + existingBlockMap[block.text] = block.uid; + }); + } + + return existingBlockMap; +}; + +const buildBlockMap = (pageUid: string): Record => { + const existingChildren = getShallowTreeByParentUid(pageUid); + const blockMap: Record = {}; + existingChildren.forEach((child) => { + blockMap[child.text] = child.uid; + }); + return blockMap; +}; + +const initializeSettingsBlockProps = ( + blockMap: Record, +): void => { + const configs = getTopLevelBlockPropsConfig(); + + for (const { key, schema } of configs) { + const uid = blockMap[key]; + if (uid) { + const existingProps = getBlockProps(uid); + if (!existingProps || Object.keys(existingProps).length === 0) { + const defaults = schema.parse({}); + setBlockProps(uid, defaults, false); + } + } + } +}; + +const initSettingsPageBlocks = async (): Promise> => { + const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); + const blockMap = buildBlockMap(pageUid); + + const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key); + await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); + + initializeSettingsBlockProps(blockMap); + + return blockMap; +}; + +const hasNonDefaultNodes = (): boolean => { + 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][]; + + for (const [pageUid] of results) { + const blockProps = getBlockProps(pageUid); + if (!blockProps) continue; + + const parsed = DiscourseNodeSchema.safeParse(blockProps); + if (!parsed.success) continue; + + if (parsed.data.backedBy !== "default") { + return true; + } + } + + return false; +}; + +const initSingleDiscourseNode = async ( + node: (typeof INITIAL_NODE_VALUES)[number], +): Promise<{ label: string; pageUid: string } | null> => { + if (!node.text) return null; + + const pageUid = await ensurePageExists( + `${DISCOURSE_NODE_PAGE_PREFIX}${node.text}`, + ); + 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 ?? false, + canvasSettings: node.canvasSettings || {}, + backedBy: "default", + }); + + setBlockProps(pageUid, nodeData, false); + } + + return { label: node.text, pageUid }; +}; + +const initDiscourseNodePages = async (): Promise> => { + if (hasNonDefaultNodes()) { + return {}; + } + + const results = await Promise.all( + INITIAL_NODE_VALUES.map((node) => initSingleDiscourseNode(node)), + ); + + const nodePageUids: Record = {}; + for (const result of results) { + if (result) { + nodePageUids[result.label] = result.pageUid; + } + } + + return nodePageUids; +}; + +const printAllSettings = ( + blockMap: Record, + nodePageUids: Record, +): void => { + const configs = getTopLevelBlockPropsConfig(); + const featureFlagsUid = blockMap[configs.find(({ key }) => key === "Feature Flags")?.key ?? ""]; + const globalUid = blockMap[configs.find(({ key }) => key === "Global")?.key ?? ""]; + const personalKey = getPersonalSettingsKey(); + const personalUid = blockMap[personalKey]; + + const featureFlags = featureFlagsUid ? getBlockProps(featureFlagsUid) : null; + const globalSettings = globalUid ? getBlockProps(globalUid) : null; + const personalSettings = personalUid ? getBlockProps(personalUid) : null; + + console.group("🔧 Discourse Graph Settings Initialized (RAW DATA)"); + + console.group(`🚩 Feature Flags (uid: ${featureFlagsUid})`); + console.log("Raw block props:", JSON.stringify(featureFlags, null, 2)); + console.groupEnd(); + + console.group(`🌍 Global Settings (uid: ${globalUid})`); + console.log("Raw block props:", JSON.stringify(globalSettings, null, 2)); + console.groupEnd(); + + console.group(`👤 Personal Settings (uid: ${personalUid})`); + console.log("Raw block props:", JSON.stringify(personalSettings, null, 2)); + console.groupEnd(); + + console.group("📝 Discourse Nodes"); + for (const [nodeLabel, pageUid] of Object.entries(nodePageUids)) { + const nodeProps = getBlockProps(pageUid); + console.group(`${nodeLabel} (uid: ${pageUid})`); + console.log("Raw block props:", JSON.stringify(nodeProps, null, 2)); + console.groupEnd(); + } + console.groupEnd(); + + const relations = (globalSettings as Record)?.Relations; + console.group("🔗 Discourse Relations"); + console.log("Relations:", JSON.stringify(relations, null, 2)); + console.groupEnd(); + + console.groupEnd(); +}; + +export type InitSchemaResult = { + blockUids: Record; + nodePageUids: Record; +}; + +export const initSchema = async (): Promise => { + const blockUids = await initSettingsPageBlocks(); + const nodePageUids = await initDiscourseNodePages(); + + setTimeout(() => { + printAllSettings(blockUids, nodePageUids); + }, 2000); + + return { blockUids, nodePageUids }; +}; diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index fe6cbff69..5a5890367 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -71,13 +71,17 @@ const discourseNodeSettings: DiscourseNodeSettings = { Confidence: "confidence-attr-uid", }, overlay: "Status", - index: [ - { - type: "filter", - condition: "has attribute", - attribute: "Status", - }, - ], + index: { + conditions: [ + { + type: "clause", + source: "Claim", + relation: "has attribute", + target: "Status", + }, + ], + selections: [], + }, suggestiveRules: { template: [], embeddingRef: "((embed-ref))", diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index fc7a2d06d..e82093a19 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import DEFAULT_RELATIONS_BLOCK_PROPS from "~/components/settings/data/defaultRelationsBlockProps"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -61,6 +62,11 @@ export const SelectionSchema = z.object({ label: z.string(), }); +export const IndexSchema = z.object({ + conditions: z.array(ConditionSchema).default([]), + selections: z.array(SelectionSchema).default([]), +}); + type RoamNode = { text: string; children?: RoamNode[]; @@ -120,7 +126,7 @@ export const DiscourseNodeSchema = z.object({ .optional() .transform((val) => val ?? {}), overlay: stringWithDefault(""), - index: z.unknown().nullable().optional(), + index: IndexSchema.nullable().optional(), suggestiveRules: SuggestiveRulesSchema.nullable().optional(), embeddingRef: stringWithDefault(""), isFirstChild: z @@ -190,12 +196,12 @@ export const LeftSidebarGlobalSettingsSchema = z.object({ }); export const GlobalSettingsSchema = z.object({ - Trigger: z.string().default(""), - "Canvas Page Format": z.string().default(""), + Trigger: z.string().default("\\"), + "Canvas Page Format": z.string().default("Canvas/*"), "Left Sidebar": LeftSidebarGlobalSettingsSchema.default({}), Export: ExportSettingsSchema.default({}), "Suggestive Mode": SuggestiveModeGlobalSettingsSchema.default({}), - Relations: z.array(DiscourseRelationSchema).default([]), + Relations: z.array(DiscourseRelationSchema).default(DEFAULT_RELATIONS_BLOCK_PROPS), }); export const PersonalSectionSchema = z.object({ @@ -234,7 +240,7 @@ export const QuerySettingsSchema = z.object({ export const PersonalSettingsSchema = z.object({ "Left Sidebar": LeftSidebarPersonalSettingsSchema, "Personal Node Menu Trigger": z.string().default(""), - "Node Search Menu Trigger": z.string().default(""), + "Node Search Menu Trigger": z.string().default("@"), "Discourse Tool Shortcut": z.string().default(""), "Discourse Context Overlay": z.boolean().default(false), "Suggestive Mode Overlay": z.boolean().default(false), @@ -254,7 +260,22 @@ export const GithubSettingsSchema = z.object({ "selected-repo": z.string().optional(), }); -/* eslint-enable @typescript-eslint/naming-convention */ +let cachedPersonalSettingsKey: string | null = null; +export const getPersonalSettingsKey = (): string => { + if (cachedPersonalSettingsKey !== null) return cachedPersonalSettingsKey; + cachedPersonalSettingsKey = window.roamAlphaAPI.user.uid() || ""; + return cachedPersonalSettingsKey; +}; + +export const getTopLevelBlockPropsConfig = () => [ + { key: "Feature Flags", schema: FeatureFlagsSchema }, + { key: "Global", schema: GlobalSettingsSchema }, + { key: getPersonalSettingsKey(), schema: PersonalSettingsSchema }, +]; + +export const DG_BLOCK_PROP_SETTINGS_PAGE_TITLE = + "roam/js/discourse-graph"; +export const DISCOURSE_NODE_PAGE_PREFIX = "discourse-graph/nodes/"; export type CanvasSettings = z.infer; export type SuggestiveRules = z.infer; @@ -283,3 +304,4 @@ export type GithubSettings = z.infer; export type QueryCondition = z.infer; export type QuerySelection = z.infer; export type RoamNodeType = z.infer; +export type Index = z.infer; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 5b877173c..b44469c95 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -41,6 +41,7 @@ import { STREAMLINE_STYLING_KEY, DISALLOW_DIAGNOSTICS, } from "./data/userSettings"; +import { initSchema } from "./components/settings/utils/init"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; @@ -78,6 +79,8 @@ export default runExtension(async (onloadArgs) => { await initializeDiscourseNodes(); refreshConfigTree(); + // For testing purposes + await initSchema(); addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); createSettingsPanel(onloadArgs);