diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts new file mode 100644 index 000000000..a2335b519 --- /dev/null +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -0,0 +1,321 @@ +import getBlockProps, { type json } from "~/utils/getBlockProps"; +import setBlockProps from "~/utils/setBlockProps"; +import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; +import { z } from "zod"; +import { + DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + TOP_LEVEL_BLOCK_PROP_KEYS, + DISCOURSE_NODE_PAGE_PREFIX, +} from "../data/blockPropsSettingsConfig"; +import { + FeatureFlagsSchema, + GlobalSettingsSchema, + PersonalSettingsSchema, + DiscourseNodeSchema, + type FeatureFlags, + type GlobalSettings, + type PersonalSettings, + type DiscourseNodeSettings, +} from "./zodSchema"; +import { + getPersonalSettingsKey, + getDiscourseNodePageUid, +} from "./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 => { + if (!blockUid) return 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 (!blockUid) { + console.warn("[DG:accessor] setBlockPropsByUid called with empty blockUid"); + return; + } + + 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, +}: { + keys: string[]; +}): { blockProps: json | undefined; blockUid: string } => { + if (keys.length === 0) { + console.warn("[DG:accessor] getBlockPropBasedSettings called with no keys"); + return { blockProps: undefined, blockUid: "" }; + } + + const blockUid = getBlockUidByTextOnPage({ + text: keys[0], + title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + }); + + if (!blockUid) { + return { blockProps: undefined, blockUid: "" }; + } + + const blockProps = getBlockPropsByUid(blockUid, keys.slice(1)); + + return { blockProps, blockUid }; +}; + +export const setBlockPropBasedSettings = ({ + keys, + value, +}: { + keys: string[]; + value: json; +}): void => { + if (keys.length === 0) { + console.warn("[DG:accessor] setBlockPropBasedSettings called with no keys"); + return; + } + + const blockUid = getBlockUidByTextOnPage({ + text: keys[0], + title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + }); + + if (!blockUid) { + console.warn( + `[DG:accessor] Block not found for key "${keys[0]}" on settings page`, + ); + return; + } + + setBlockPropsByUid(blockUid, keys.slice(1), value); +}; + +export const getFeatureFlags = (): FeatureFlags => { + const { blockProps } = getBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags], + }); + + return FeatureFlagsSchema.parse(blockProps || {}); +}; + +export const getFeatureFlag = (key: keyof FeatureFlags): boolean => { + const flags = getFeatureFlags(); + return flags[key]; +}; + +export const setFeatureFlag = ( + key: keyof FeatureFlags, + value: boolean, +): void => { + const validatedValue = z.boolean().parse(value); + + setBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.featureFlags, key], + value: validatedValue, + }); +}; + +export const getGlobalSettings = (): GlobalSettings => { + const { blockProps } = getBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global], + }); + + return GlobalSettingsSchema.parse(blockProps || {}); +}; + +export const getGlobalSetting = (keys: string[]): T | undefined => { + const settings = getGlobalSettings(); + + return keys.reduce((current, key) => { + if (!isRecord(current) || !(key in current)) return undefined; + return current[key]; + }, settings) as T | undefined; +}; + +export const setGlobalSetting = (keys: string[], value: json): void => { + setBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global, ...keys], + value, + }); +}; + +export const getPersonalSettings = (): PersonalSettings => { + const personalKey = getPersonalSettingsKey(); + + const { blockProps } = getBlockPropBasedSettings({ + keys: [personalKey], + }); + + return PersonalSettingsSchema.parse(blockProps || {}); +}; + +export const getPersonalSetting = ( + keys: string[], +): T | undefined => { + const settings = getPersonalSettings(); + + return keys.reduce((current, key) => { + if (!isRecord(current) || !(key in current)) return undefined; + return current[key]; + }, settings) as T | undefined; +}; + +export const setPersonalSetting = (keys: string[], value: json): void => { + const personalKey = getPersonalSettingsKey(); + + setBlockPropBasedSettings({ + keys: [personalKey, ...keys], + value, + }); +}; + +export const getDiscourseNodeSettings = ( + nodeType: string, +): DiscourseNodeSettings | undefined => { + let pageUid = nodeType; + let blockProps = getBlockPropsByUid(pageUid, []); + + if (!blockProps || Object.keys(blockProps).length === 0) { + const lookedUpUid = getDiscourseNodePageUid(nodeType); + if (lookedUpUid) { + pageUid = lookedUpUid; + blockProps = getBlockPropsByUid(pageUid, []); + } + } + + if (!blockProps) return undefined; + + const result = DiscourseNodeSchema.safeParse(blockProps); + if (!result.success) { + console.warn( + `[DG:accessor] Failed to parse discourse node settings for ${nodeType}:`, + result.error, + ); + return undefined; + } + + return result.data; +}; + +export const getDiscourseNodeSetting = ( + nodeType: string, + keys: string[], +): T | undefined => { + 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) as T | undefined; +}; + +export const setDiscourseNodeSetting = ( + nodeType: string, + keys: string[], + value: json, +): void => { + let pageUid = nodeType; + let blockProps = getBlockPropsByUid(pageUid, []); + + if (!blockProps || Object.keys(blockProps).length === 0) { + const lookedUpUid = getDiscourseNodePageUid(nodeType); + if (lookedUpUid) { + pageUid = lookedUpUid; + } + } + + if (!pageUid) { + console.warn( + `[DG:accessor] setDiscourseNodeSetting - could not find page for: ${nodeType}`, + ); + return; + } + + setBlockPropsByUid(pageUid, keys, value); +}; + +export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => { + 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 nodes: DiscourseNodeSettings[] = []; + + for (const [pageUid, title] of results) { + const blockProps = getBlockPropsByUid(pageUid, []); + if (!blockProps) continue; + + const result = DiscourseNodeSchema.safeParse(blockProps); + if (result.success) { + nodes.push({ + ...result.data, + type: pageUid, + text: title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""), + }); + } + } + + return nodes; +}; diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 5c3a4c809..8f3105045 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -21,7 +21,7 @@ import { let cachedPersonalSettingsKey: string | null = null; -const getPersonalSettingsKey = (): string => { +export const getPersonalSettingsKey = (): string => { if (cachedPersonalSettingsKey !== null) { return cachedPersonalSettingsKey; } @@ -29,10 +29,15 @@ const getPersonalSettingsKey = (): string => { return cachedPersonalSettingsKey; }; -const getDiscourseNodePageTitle = (nodeLabel: string): string => { +export const getDiscourseNodePageTitle = (nodeLabel: string): string => { return `${DISCOURSE_NODE_PAGE_PREFIX}${nodeLabel}`; }; +export const getDiscourseNodePageUid = (nodeLabel: string): string => { + const pageTitle = getDiscourseNodePageTitle(nodeLabel); + return getPageUidByPageTitle(pageTitle); +}; + const ensurePageExists = async (pageTitle: string): Promise => { let pageUid = getPageUidByPageTitle(pageTitle);