diff --git a/apps/roam/src/components/settings/data/blockPropsSettingsConfig.ts b/apps/roam/src/components/settings/data/blockPropsSettingsConfig.ts new file mode 100644 index 000000000..e79c88475 --- /dev/null +++ b/apps/roam/src/components/settings/data/blockPropsSettingsConfig.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +export const DG_BLOCK_PROP_SETTINGS_PAGE_TITLE = + "roam/js/discourse-graph/block-prop-settings"; + +export const DISCOURSE_NODE_PAGE_PREFIX = "discourse-graph/nodes/"; + +export const TOP_LEVEL_BLOCK_PROP_KEYS = { + featureFlags: "Feature Flags", + global: "Global", +} as const; + +export const DISCOURSE_NODE_BLOCK_KEYS = { + template: "Template", + index: "Index", + specification: "Specification", +} as const; + +/* eslint-enable @typescript-eslint/naming-convention */ 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..5c3a4c809 --- /dev/null +++ b/apps/roam/src/components/settings/utils/init.ts @@ -0,0 +1,327 @@ +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"; +// eslint-disable-next-line @typescript-eslint/naming-convention +import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; +import { + DiscourseNodeSchema, + FeatureFlagsSchema, + GlobalSettingsSchema, + PersonalSettingsSchema, +} from "./zodSchema"; +import type { ZodSchema } from "zod"; +import { + DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + DISCOURSE_NODE_PAGE_PREFIX, + TOP_LEVEL_BLOCK_PROP_KEYS, + DISCOURSE_NODE_BLOCK_KEYS, +} from "../data/blockPropsSettingsConfig"; + +let cachedPersonalSettingsKey: string | null = null; + +const getPersonalSettingsKey = (): string => { + if (cachedPersonalSettingsKey !== null) { + return cachedPersonalSettingsKey; + } + cachedPersonalSettingsKey = window.roamAlphaAPI.user.uid() || ""; + return cachedPersonalSettingsKey; +}; + +const getDiscourseNodePageTitle = (nodeLabel: string): string => { + return `${DISCOURSE_NODE_PAGE_PREFIX}${nodeLabel}`; +}; + +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 ensurePersonalBlockExists = async ( + pageUid: string, + existingBlockMap: Record, +): Promise<{ key: string; uid: string }> => { + const personalKey = getPersonalSettingsKey(); + + if (existingBlockMap[personalKey]) { + return { key: personalKey, uid: existingBlockMap[personalKey] }; + } + + const uid = await createBlock({ + parentUid: pageUid, + node: { text: personalKey }, + }); + + return { key: personalKey, uid }; +}; + +const buildBlockMap = (pageUid: string): Record => { + const existingChildren = getShallowTreeByParentUid(pageUid); + const blockMap: Record = {}; + existingChildren.forEach((child) => { + blockMap[child.text] = child.uid; + }); + return blockMap; +}; + +const initBlockPropsIfEmpty = (uid: string, schema: ZodSchema): void => { + const existingProps = getBlockProps(uid); + if (!existingProps || Object.keys(existingProps).length === 0) { + const defaults = schema.parse({}) as Record; + setBlockProps(uid, defaults, false); + } +}; + +const initializeSettingsBlockProps = ( + blockMap: Record, +): void => { + const configs = [ + { key: TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags, schema: FeatureFlagsSchema }, + { key: TOP_LEVEL_BLOCK_PROP_KEYS.global, schema: GlobalSettingsSchema }, + { key: getPersonalSettingsKey(), schema: PersonalSettingsSchema }, + ]; + + for (const { key, schema } of configs) { + const uid = blockMap[key]; + if (uid) { + initBlockPropsIfEmpty(uid, schema); + } + } +}; + +const initSettingsPageBlocks = async (): Promise> => { + const pageUid = await ensurePageExists(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); + const blockMap = buildBlockMap(pageUid); + + const topLevelBlocks = Object.values(TOP_LEVEL_BLOCK_PROP_KEYS); + await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); + + const personalBlock = await ensurePersonalBlockExists(pageUid, blockMap); + blockMap[personalBlock.key] = personalBlock.uid; + + initializeSettingsBlockProps(blockMap); + + return blockMap; +}; + +const ensureDiscourseNodePageExists = async ( + nodeLabel: string, +): Promise => { + const pageTitle = getDiscourseNodePageTitle(nodeLabel); + return ensurePageExists(pageTitle); +}; + +const initSingleDiscourseNode = async ( + node: (typeof INITIAL_NODE_VALUES)[number], +): Promise<{ label: string; pageUid: string } | null> => { + if (!node.text) return null; + + const pageUid = await ensureDiscourseNodePageExists(node.text); + const existingProps = getBlockProps(pageUid); + const blockMap = buildBlockMap(pageUid); + + for (const key of Object.values(DISCOURSE_NODE_BLOCK_KEYS)) { + if (!blockMap[key]) { + blockMap[key] = await createBlock({ + parentUid: pageUid, + node: { text: key }, + }); + } + } + + const templateUid = blockMap[DISCOURSE_NODE_BLOCK_KEYS.template]; + const indexUid = blockMap[DISCOURSE_NODE_BLOCK_KEYS.index]; + const specificationUid = blockMap[DISCOURSE_NODE_BLOCK_KEYS.specification]; + + if (!existingProps || Object.keys(existingProps).length === 0) { + const nodeData = DiscourseNodeSchema.parse({ + text: node.text, + uid: pageUid, + format: node.format || "", + shortcut: node.shortcut || "", + tag: node.tag || "", + graphOverview: node.graphOverview ?? false, + canvasSettings: node.canvasSettings || {}, + backedBy: "user", + }); + + setBlockProps(pageUid, nodeData as Record, false); + } + + return { label: node.text, pageUid }; +}; + +const initDiscourseNodePages = async (): Promise> => { + 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): void => { + const featureFlagsUid = blockMap[TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags]; + const globalUid = blockMap[TOP_LEVEL_BLOCK_PROP_KEYS.global]; + 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"); + + if (featureFlags) { + console.group("🚩 Feature Flags"); + console.table(featureFlags); + console.groupEnd(); + } + + if (globalSettings) { + console.group("🌍 Global Settings"); + console.log("Trigger:", globalSettings?.Trigger || "(empty)"); + console.log( + "Canvas Page Format:", + globalSettings?.["Canvas Page Format"] || "(empty)", + ); + + if (globalSettings?.["Left Sidebar"]) { + console.group("📂 Left Sidebar"); + console.log(globalSettings["Left Sidebar"]); + console.groupEnd(); + } + + if (globalSettings?.Export) { + console.group("📤 Export Settings"); + console.table(globalSettings.Export); + console.groupEnd(); + } + + if (globalSettings?.["Suggestive Mode"]) { + console.group("💡 Suggestive Mode"); + console.log(globalSettings["Suggestive Mode"]); + console.groupEnd(); + } + + console.groupEnd(); + } + + if (personalSettings) { + console.group("👤 Personal Settings"); + console.log( + "Personal Node Menu Trigger:", + personalSettings?.["Personal Node Menu Trigger"] || "(empty)", + ); + console.log( + "Node Search Menu Trigger:", + personalSettings?.["Node Search Menu Trigger"] || "(empty)", + ); + console.log( + "Discourse Tool Shortcut:", + personalSettings?.["Discourse Tool Shortcut"] || "(empty)", + ); + + console.group("🎛️ Toggles"); + const toggles = { + // eslint-disable-next-line @typescript-eslint/naming-convention + "Discourse Context Overlay": + personalSettings?.["Discourse Context Overlay"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Suggestive Mode Overlay": personalSettings?.["Suggestive Mode Overlay"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Overlay in Canvas": personalSettings?.["Overlay in Canvas"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Text Selection Popup": personalSettings?.["Text Selection Popup"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Disable Sidebar Open": personalSettings?.["Disable Sidebar Open"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Page Preview": personalSettings?.["Page Preview"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Hide Feedback Button": personalSettings?.["Hide Feedback Button"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Streamline Styling": personalSettings?.["Streamline Styling"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Auto Canvas Relations": personalSettings?.["Auto Canvas Relations"], + // eslint-disable-next-line @typescript-eslint/naming-convention + "Disable Product Diagnostics": + personalSettings?.["Disable Product Diagnostics"], + }; + console.table(toggles); + console.groupEnd(); + + if (personalSettings?.["Left Sidebar"]) { + console.group("📂 Personal Left Sidebar"); + console.log(personalSettings["Left Sidebar"]); + console.groupEnd(); + } + + if (personalSettings?.Query) { + console.group("🔍 Query Settings"); + console.table(personalSettings.Query); + console.groupEnd(); + } + + console.groupEnd(); + } + + console.groupEnd(); +}; + +export type InitSchemaResult = { + blockUids: Record; + nodePageUids: Record; +}; + +export const initSchema = async (): Promise => { + const blockUids = await initSettingsPageBlocks(); + const nodePageUids = await initDiscourseNodePages(); + + printAllSettings(blockUids); + + return { blockUids, nodePageUids }; +}; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 5b877173c..b48e079de 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/*"; @@ -75,9 +76,8 @@ export default runExtension(async (onloadArgs) => { initPluginTimer(); - await initializeDiscourseNodes(); - refreshConfigTree(); - + // For testing purposes + await initSchema(); addGraphViewNodeStyling(); registerCommandPaletteCommands(onloadArgs); createSettingsPanel(onloadArgs);