diff --git a/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts b/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts index 3f02ae700..c58c3eef9 100644 --- a/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts +++ b/apps/roam/src/components/settings/data/defaultRelationsBlockProps.ts @@ -1,142 +1,140 @@ -import type { DiscourseRelationSettings } from "~/components/settings/utils/zodSchema"; /* eslint-disable @typescript-eslint/naming-convention */ // This is for nodePosition keys +import type { DiscourseRelationSettings } from "~/components/settings/utils/zodSchema"; // TODO: Delete the original default relations in data/defaultRelations.ts when fully migrated. -const DEFAULT_RELATIONS_BLOCK_PROPS: DiscourseRelationSettings[] = [ +const DEFAULT_RELATIONS_BLOCK_PROPS: Record = { - 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", + "_INFO-rel": { + 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", + ], + }, + "_SUPP-rel": { + 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", + { + 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", + ], + }, + "_OPPO-rel": { + 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", + { + 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/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts new file mode 100644 index 000000000..3f116ad63 --- /dev/null +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -0,0 +1,537 @@ +import getBlockProps, { + normalizeProps, + type json, +} from "~/utils/getBlockProps"; +import setBlockProps from "~/utils/setBlockProps"; +import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import internalError from "~/utils/internalError"; +import { z } from "zod"; + +import { + DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + DISCOURSE_NODE_PAGE_PREFIX, + TOP_LEVEL_BLOCK_PROP_KEYS, + FeatureFlagsSchema, + GlobalSettingsSchema, + PersonalSettingsSchema, + DiscourseNodeSchema, + getPersonalSettingsKey, + type FeatureFlags, + type GlobalSettings, + type PersonalSettings, + type DiscourseNodeSettings, + type DiscourseRelationSettings, +} from "./zodSchema"; + +const isRecord = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const unwrapSchema = (schema: z.ZodTypeAny): z.ZodTypeAny => { + let current = schema; + let didUnwrap = true; + + while (didUnwrap) { + didUnwrap = false; + + if (current instanceof z.ZodDefault) { + const defaultSchema = current as z.ZodDefault; + current = defaultSchema._def.innerType; + didUnwrap = true; + continue; + } + + if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) { + current = current.unwrap() as z.ZodTypeAny; + didUnwrap = true; + continue; + } + + if (current instanceof z.ZodEffects) { + const effectsSchema = current as z.ZodEffects; + current = effectsSchema._def.schema; + didUnwrap = true; + continue; + } + + if (current instanceof z.ZodCatch) { + const catchSchema = current as z.ZodCatch; + current = catchSchema._def.innerType; + didUnwrap = true; + continue; + } + + if (current instanceof z.ZodLazy) { + const lazySchema = current as z.ZodLazy; + current = lazySchema._def.getter(); + didUnwrap = true; + } + } + + return current; +}; + +const getSchemaAtPath = ( + schema: z.ZodTypeAny, + keys: string[], +): z.ZodTypeAny | null => { + let current = unwrapSchema(schema); + + for (const key of keys) { + current = unwrapSchema(current); + + if (current instanceof z.ZodObject) { + const shape = current.shape as Record; + if (!(key in shape)) return null; + current = shape[key]; + continue; + } + + if (current instanceof z.ZodRecord) { + current = current.valueSchema as z.ZodTypeAny; + continue; + } + + if (current instanceof z.ZodArray) { + current = current.element as z.ZodTypeAny; + continue; + } + + return null; + } + + return current; +}; + +const formatSettingPath = (keys: string[]): string => + keys.length === 0 ? "(root)" : keys.join(" > "); + +const validateSettingValue = ({ + schema, + keys, + value, + context, +}: { + schema: z.ZodTypeAny; + keys: string[]; + value: json; + context: string; +}): boolean => { + const targetSchema = getSchemaAtPath(schema, keys); + + if (!targetSchema) { + internalError({ + error: `Unknown ${context} setting path: ${formatSettingPath(keys)}`, + type: "DG Accessor", + context: { keys }, + }); + return false; + } + + const result = targetSchema.safeParse(value); + + if (!result.success) { + internalError({ + error: `Invalid ${context} setting value at path: ${formatSettingPath(keys)}`, + type: "DG Accessor", + context: { keys, zodError: result.error.message }, + }); + return false; + } + + return true; +}; + +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[currentKey]; + return value === undefined ? null : value; + } + return null; + }, allBlockProps); + + return targetValue === null ? undefined : targetValue; +}; + +const setBlockPropAtPath = ( + blockUid: string, + keys: string[], + value: json, +): void => { + if (!blockUid) { + internalError({ + error: "setBlockPropAtPath called with empty blockUid", + type: "DG Accessor", + }); + return; + } + + if (keys.length === 0) { + internalError({ + error: "setBlockPropAtPath called with empty keys array", + type: "DG Accessor", + }); + return; + } + + const currentProps = getBlockProps(blockUid); + const updatedProps: Record = currentProps || {}; + 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]; + }, updatedProps); + + setBlockProps(blockUid, updatedProps, false); +}; + +const getBlockPropBasedSettings = ({ + keys, +}: { + keys: string[]; +}): { blockProps: json | undefined; blockUid: string } => { + if (keys.length === 0) { + internalError({ + error: "getBlockPropBasedSettings called with no keys", + type: "DG Accessor", + }); + 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 }; +}; + +const setBlockPropBasedSettings = ({ + keys, + value, +}: { + keys: string[]; + value: json; +}): void => { + if (keys.length === 0) { + internalError({ + error: "setBlockPropBasedSettings called with no keys", + type: "DG Accessor", + }); + return; + } + + const blockUid = getBlockUidByTextOnPage({ + text: keys[0], + title: DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + }); + + if (!blockUid) { + internalError({ + error: `Block not found for key "${keys[0]}" on settings page`, + type: "DG Accessor", + }); + return; + } + + setBlockPropAtPath(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 => { + if (keys.length === 0) { + internalError({ + error: "setGlobalSetting called with empty keys array", + type: "DG Accessor", + }); + return; + } + + if ( + !validateSettingValue({ + schema: GlobalSettingsSchema, + keys, + value, + context: "Global", + }) + ) { + return; + } + + setBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global, ...keys], + value, + }); +}; + +export const getAllRelations = (): DiscourseRelationSettings[] => { + const settings = getGlobalSettings(); + + return Object.entries(settings.Relations).map(([id, relation]) => ({ + ...relation, + id, + })); +}; + +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 => { + if (keys.length === 0) { + internalError({ + error: "setPersonalSetting called with empty keys array", + type: "DG Accessor", + }); + return; + } + + const personalKey = getPersonalSettingsKey(); + + if ( + !validateSettingValue({ + schema: PersonalSettingsSchema, + keys, + value, + context: "Personal", + }) + ) { + return; + } + + 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 = getPageUidByPageTitle( + `${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`, + ); + if (lookedUpUid) { + pageUid = lookedUpUid; + blockProps = getBlockPropsByUid(pageUid, []); + } + } + + if (!blockProps) return undefined; + + const result = DiscourseNodeSchema.safeParse(blockProps); + if (!result.success) { + internalError({ + error: `Failed to parse discourse node settings for ${nodeType}`, + type: "DG Accessor", + context: { zodError: result.error.message }, + }); + 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 => { + if (keys.length === 0) { + internalError({ + error: "setDiscourseNodeSetting called with empty keys array", + type: "DG Accessor", + }); + return; + } + + if ( + !validateSettingValue({ + schema: DiscourseNodeSchema, + keys, + value, + context: "Discourse Node", + }) + ) { + return; + } + + let pageUid = nodeType; + const blockProps = getBlockPropsByUid(pageUid, []); + + if (!blockProps || Object.keys(blockProps).length === 0) { + const lookedUpUid = getPageUidByPageTitle( + `${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`, + ); + if (lookedUpUid) { + pageUid = lookedUpUid; + } + } + + if (!pageUid) { + internalError({ + error: `setDiscourseNodeSetting - could not find page for: ${nodeType}`, + type: "DG Accessor", + }); + return; + } + + setBlockPropAtPath(pageUid, keys, value); +}; + +export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => { + const results = window.roamAlphaAPI.data.fast.q(` + [:find ?uid ?title (pull ?page [:block/props]) + :where + [?page :node/title ?title] + [?page :block/uid ?uid] + [(clojure.string/starts-with? ?title "${DISCOURSE_NODE_PAGE_PREFIX}")]] + `) as [string, string, Record | null][]; + + const nodes: DiscourseNodeSettings[] = []; + + for (const [pageUid, title, rawProps] of results) { + if (typeof pageUid !== "string" || typeof title !== "string") continue; + const rawBlockProps = rawProps?.[":block/props"]; + const blockProps = rawBlockProps + ? normalizeProps(rawBlockProps) + : undefined; + if ( + !blockProps || + !isRecord(blockProps) || + Object.keys(blockProps).length === 0 + ) + continue; + + const result = DiscourseNodeSchema.safeParse(blockProps); + if (result.success) { + nodes.push({ + ...result.data, + type: pageUid, + text: title.replace(DISCOURSE_NODE_PAGE_PREFIX, ""), + }); + } else { + internalError({ + error: result.error, + type: "DG Discourse Node Parse", + context: { pageUid, title }, + sendEmail: false, + }); + } + } + + return nodes; +}; diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index 31a83813b..d50d0027e 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -2,13 +2,17 @@ import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTit 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 getBlockProps from "~/utils/getBlockProps"; import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; +import { getAllDiscourseNodes } from "./accessors"; import { DiscourseNodeSchema, getTopLevelBlockPropsConfig, } from "~/components/settings/utils/zodSchema"; -import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, DISCOURSE_NODE_PAGE_PREFIX } from "./zodSchema"; +import { + DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, + DISCOURSE_NODE_PAGE_PREFIX, +} from "./zodSchema"; const ensurePageExists = async (pageTitle: string): Promise => { let pageUid = getPageUidByPageTitle(pageTitle); @@ -91,27 +95,7 @@ const initSettingsPageBlocks = async (): Promise> => { }; 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; + return getAllDiscourseNodes().some((node) => node.backedBy !== "default"); }; const initSingleDiscourseNode = async ( @@ -161,8 +145,6 @@ const initDiscourseNodePages = async (): Promise> => { return nodePageUids; }; - - export type InitSchemaResult = { blockUids: Record; nodePageUids: Record; diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index 5a5890367..3474ebaf0 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -182,9 +182,8 @@ const globalSettings: GlobalSettings = { }, ], }, - Relations: [ - { - id: "relation-uid-1", + Relations: { + "_INFO-rel": { label: "Informs", source: "_EVD-node", destination: "_QUE-node", @@ -204,8 +203,7 @@ const globalSettings: GlobalSettings = { }, ], }, - { - id: "relation-uid-2", + "_SUPP-rel": { label: "Supports", source: "_EVD-node", destination: "_CLM-node", @@ -259,7 +257,7 @@ const globalSettings: GlobalSettings = { }, ], }, - ], + }, }; const defaultGlobalSettings: GlobalSettings = { @@ -286,7 +284,7 @@ const defaultGlobalSettings: GlobalSettings = { "Include Parent And Child Blocks": false, "Page Groups": [], }, - Relations: [], + Relations: {}, }; const personalSection: PersonalSection = { diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index e82093a19..a971205f7 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -150,7 +150,7 @@ export const RelationConditionSchema = z.object({ }); export const DiscourseRelationSchema = z.object({ - id: z.string(), + id: z.string().optional(), label: z.string(), source: z.string(), destination: z.string(), @@ -201,7 +201,9 @@ export const GlobalSettingsSchema = z.object({ "Left Sidebar": LeftSidebarGlobalSettingsSchema.default({}), Export: ExportSettingsSchema.default({}), "Suggestive Mode": SuggestiveModeGlobalSettingsSchema.default({}), - Relations: z.array(DiscourseRelationSchema).default(DEFAULT_RELATIONS_BLOCK_PROPS), + Relations: z + .record(z.string(), DiscourseRelationSchema) + .default(DEFAULT_RELATIONS_BLOCK_PROPS), }); export const PersonalSectionSchema = z.object({ @@ -267,14 +269,30 @@ export const getPersonalSettingsKey = (): string => { return cachedPersonalSettingsKey; }; +const staticTopLevelEntries = [ + { + propKey: "featureFlags" as const, + key: "Feature Flags", + schema: FeatureFlagsSchema, + }, + { + propKey: "global" as const, + key: "Global", + schema: GlobalSettingsSchema, + }, +]; + +export const TOP_LEVEL_BLOCK_PROP_KEYS = { + featureFlags: "Feature Flags", + global: "Global", +} as const; + export const getTopLevelBlockPropsConfig = () => [ - { key: "Feature Flags", schema: FeatureFlagsSchema }, - { key: "Global", schema: GlobalSettingsSchema }, + ...staticTopLevelEntries, { key: getPersonalSettingsKey(), schema: PersonalSettingsSchema }, ]; -export const DG_BLOCK_PROP_SETTINGS_PAGE_TITLE = - "roam/js/discourse-graph"; +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;