diff --git a/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx b/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx index e86681692..cd230c3d5 100644 --- a/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx +++ b/apps/roam/src/components/settings/DiscourseRelationConfigPanel.tsx @@ -51,6 +51,10 @@ import { formatHexColor } from "./DiscourseNodeCanvasSettings"; import posthog from "posthog-js"; import { getSetting, setSetting } from "~/utils/extensionSettings"; import { USE_REIFIED_RELATIONS } from "~/data/userSettings"; +import { + setGlobalSetting, + getGlobalSettings, +} from "~/components/settings/utils/accessors"; const DEFAULT_SELECTED_RELATION = { display: "none", @@ -568,108 +572,147 @@ export const RelationEditPanel = ({ className="select-none" onClick={() => { setLoading(true); - setTimeout(async () => { - const rootUid = editingRelationInfo.uid; - setInputSetting({ - blockUid: rootUid, - key: "source", - value: source, - }); - setInputSetting({ - blockUid: rootUid, - key: "destination", - value: destination, - index: 1, - }); - setInputSetting({ - blockUid: rootUid, - key: "complement", - value: complement, - index: 2, - }); - updateBlock({ - uid: rootUid, - text: label, - }); - const ifUid = - editingRelationInfo.children.find((t) => - toFlexRegex("if").test(t.text), - )?.uid || - (await createBlock({ - node: { text: "If" }, - parentUid: rootUid, - order: 3, - })); - saveCyToElementRef(tab); - const blocks = tabs - .map((t) => elementsRef.current[t]) - .map((elements) => ({ - text: "And", - children: elements - .filter((e) => e.data.id.includes("-")) - .map((e) => { - const { source, target, relation } = e.data as { - source: string; - target: string; - relation: string; - }; - return { - text: ( - elements.find((e) => e.data.id === source)?.data as { - node: string; - } - )?.node, - children: [ - { - text: relation, + setTimeout( + () => + void (async () => { + const rootUid = editingRelationInfo.uid; + setInputSetting({ + blockUid: rootUid, + key: "source", + value: source, + }); + setInputSetting({ + blockUid: rootUid, + key: "destination", + value: destination, + index: 1, + }); + setInputSetting({ + blockUid: rootUid, + key: "complement", + value: complement, + index: 2, + }); + updateBlock({ + uid: rootUid, + text: label, + }); + const ifUid = + editingRelationInfo.children.find((t) => + toFlexRegex("if").test(t.text), + )?.uid || + (await createBlock({ + node: { text: "If" }, + parentUid: rootUid, + order: 3, + })); + saveCyToElementRef(tab); + const blocks = tabs + .map((t) => elementsRef.current[t]) + .map((elements) => ({ + text: "And", + children: elements + .filter((e) => e.data.id.includes("-")) + .map((e) => { + const { source, target, relation } = e.data as { + source: string; + target: string; + relation: string; + }; + return { + text: ( + elements.find((e) => e.data.id === source) + ?.data as { + node: string; + } + )?.node, children: [ { - text: ["source", "destination"].includes(target) - ? target - : ( - elements.find((e) => e.data.id === target) - ?.data as { node: string } - )?.node, + text: relation, + children: [ + { + text: ["source", "destination"].includes( + target, + ) + ? target + : ( + elements.find( + (e) => e.data.id === target, + )?.data as { node: string } + )?.node, + }, + ], }, ], + }; + }) + .concat([ + { + text: "node positions", + children: elements + .filter( + ( + e, + ): e is { + data: { id: string; node: unknown }; + position: { x: number; y: number }; + } => Object.keys(e).includes("position"), + ) + .map((e) => ({ + text: e.data.id, + children: [ + { text: `${e.position.x} ${e.position.y}` }, + ], + })), }, - ], - }; - }) - .concat([ - { - text: "node positions", - children: elements - .filter( - ( - e, - ): e is { - data: { id: string; node: unknown }; - position: { x: number; y: number }; - } => Object.keys(e).includes("position"), - ) - .map((e) => ({ - text: e.data.id, - children: [ - { text: `${e.position.x} ${e.position.y}` }, - ], - })), - }, - ]), - })); - await Promise.all( - getShallowTreeByParentUid(ifUid).map(({ uid }) => - deleteBlock(uid), - ), - ); - await Promise.all( - blocks.map((block, order) => - createBlock({ parentUid: ifUid, node: block, order }), - ), - ); - refreshConfigTree(); - back(); - }, 1); + ]), + })); + await Promise.all( + getShallowTreeByParentUid(ifUid).map(({ uid }) => + deleteBlock(uid), + ), + ); + await Promise.all( + blocks.map((block, order) => + createBlock({ parentUid: ifUid, node: block, order }), + ), + ); + refreshConfigTree(); + + const ifConditions = blocks.map((block) => { + const positionsChild = block.children.find( + (c) => c.text === "node positions", + ); + const triples = block.children + .filter((c) => c.text !== "node positions") + .map( + (c) => + [ + c.text, + c.children[0].text, + c.children[0].children[0].text, + ] as [string, string, string], + ); + const nodePositions = Object.fromEntries( + (positionsChild?.children ?? []).map((c) => [ + c.text, + c.children[0].text, + ]), + ); + return { triples, nodePositions }; + }); + setGlobalSetting(["Relations", rootUid], { + label, + source, + destination, + complement, + ifConditions, + }); + + back(); + })(), + 1, + ); }} /> @@ -1030,6 +1073,9 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ const handleDelete = (rel: Relation) => { deleteBlock(rel.uid); setRelations(relations.filter((r) => r.uid !== rel.uid)); + + const { [rel.uid]: _, ...remaining } = getGlobalSettings().Relations; + setGlobalSetting(["Relations"], remaining); }; const handleDuplicate = (rel: Relation) => { const baseText = rel.text @@ -1059,7 +1105,15 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ text, children: stripUid(copyTree), }, - }).then((newUid) => + }).then((newUid) => { + const originalRelation = getGlobalSettings().Relations[rel.uid]; + if (originalRelation) { + setGlobalSetting(["Relations", newUid], { + ...originalRelation, + label: text, + }); + } + setRelations([ ...relations, { @@ -1068,8 +1122,8 @@ const DiscourseRelationConfigPanel: CustomField["options"]["component"] = ({ destination: rel.destination, text, }, - ]), - ); + ]); + }); }; const handleBack = () => { setEditingRelation(""); diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index f862c3f83..645315fd9 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -3,7 +3,9 @@ import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeB import { createPage, createBlock } from "roamjs-components/writes"; import setBlockProps from "~/utils/setBlockProps"; import getBlockProps from "~/utils/getBlockProps"; +import type { json } from "~/utils/getBlockProps"; import INITIAL_NODE_VALUES from "~/data/defaultDiscourseNodes"; +import DEFAULT_RELATIONS_BLOCK_PROPS from "~/components/settings/data/defaultRelationsBlockProps"; import { getAllDiscourseNodes } from "./accessors"; import { DiscourseNodeSchema, @@ -13,6 +15,7 @@ import { DG_BLOCK_PROP_SETTINGS_PAGE_TITLE, DISCOURSE_NODE_PAGE_PREFIX, } from "./zodSchema"; +import toFlexRegex from "roamjs-components/util/toFlexRegex"; const ensurePageExists = async (pageTitle: string): Promise => { let pageUid = getPageUidByPageTitle(pageTitle); @@ -66,6 +69,7 @@ const buildBlockMap = (pageUid: string): Record => { }; const initializeSettingsBlockProps = ( + pageUid: string, blockMap: Record, ): void => { const configs = getTopLevelBlockPropsConfig(); @@ -74,10 +78,25 @@ const initializeSettingsBlockProps = ( const uid = blockMap[key]; if (uid) { const existingProps = getBlockProps(uid); + const defaults = schema.parse({}); + if (!existingProps || Object.keys(existingProps).length === 0) { - const defaults = schema.parse({}); setBlockProps(uid, defaults, false); } + + // Reconcile placeholder relation keys with real block UIDs. + // TODO: remove this when fully migrated to blockprops, as the keys won't need to match block UIDs anymore and the defaults can use any stable IDs. + if (key === "Global") { + const relations = ((existingProps as Record | null)?.[ + "Relations" + ] ?? (defaults as Record)["Relations"]) as Record< + string, + json + >; + if (relations) { + reconcileRelationKeys(pageUid, uid, relations); + } + } } } }; @@ -89,7 +108,7 @@ const initSettingsPageBlocks = async (): Promise> => { const topLevelBlocks = getTopLevelBlockPropsConfig().map(({ key }) => key); await ensureBlocksExist(pageUid, topLevelBlocks, blockMap); - initializeSettingsBlockProps(blockMap); + initializeSettingsBlockProps(pageUid, blockMap); return blockMap; }; @@ -150,6 +169,74 @@ const initDiscourseNodePages = async (): Promise> => { return nodePageUids; }; +/** + * Replace placeholder relation keys (_INFO-rel, etc.) in the Global blockprops + * with the actual block UIDs from the grammar > relations block tree. + * + * TODO: Remove this when fully migrated to blockprops. Once relations are read + * exclusively from blockprops, the keys won't need to match block UIDs anymore + * and the defaults can use any stable IDs. + */ +const reconcileRelationKeys = ( + pageUid: string, + globalBlockUid: string, + relations: Record, +): void => { + const placeholderKeys = Object.keys(DEFAULT_RELATIONS_BLOCK_PROPS); + const hasPlaceholders = placeholderKeys.some((k) => k in relations); + if (!hasPlaceholders) { + return; + } + + const pageChildren = getShallowTreeByParentUid(pageUid); + const grammarBlock = pageChildren.find((c) => + toFlexRegex("grammar").test(c.text), + ); + if (!grammarBlock) { + return; + } + + const grammarChildren = getShallowTreeByParentUid(grammarBlock.uid); + const relationsBlock = grammarChildren.find((c) => + toFlexRegex("relations").test(c.text), + ); + if (!relationsBlock) { + return; + } + + const relationBlocks = getShallowTreeByParentUid(relationsBlock.uid); + + const labelToUid: Record = {}; + for (const block of relationBlocks) { + labelToUid[block.text] = block.uid; + } + + const placeholderToLabel: Record = {}; + for (const [key, value] of Object.entries(DEFAULT_RELATIONS_BLOCK_PROPS)) { + placeholderToLabel[key] = value.label; + } + + const reconciledRelations: Record = {}; + let changed = false; + + for (const [key, value] of Object.entries(relations)) { + if (placeholderKeys.includes(key)) { + const label = placeholderToLabel[key]; + const realUid = label ? labelToUid[label] : undefined; + if (realUid) { + reconciledRelations[realUid] = value; + changed = true; + continue; + } + } + reconciledRelations[key] = value; + } + + if (changed) { + setBlockProps(globalBlockUid, { Relations: reconciledRelations }, false); + } +}; + export type InitSchemaResult = { blockUids: Record; nodePageUids: Record;