From f8ce9505d5b9fe5d871cd30b2f3b2fc1e69db394 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 12 Feb 2026 00:13:06 +0530 Subject: [PATCH 1/4] ENG-1291: Port discourse node specification --- apps/roam/src/components/QueryBuilder.tsx | 3 + apps/roam/src/components/QueryEditor.tsx | 12 ++- .../settings/DiscourseNodeIndex.tsx | 4 +- .../settings/DiscourseNodeSpecification.tsx | 92 +++++++++++-------- .../settings/utils/zodSchema.example.ts | 23 +++-- .../components/settings/utils/zodSchema.ts | 8 +- 6 files changed, 88 insertions(+), 54 deletions(-) diff --git a/apps/roam/src/components/QueryBuilder.tsx b/apps/roam/src/components/QueryBuilder.tsx index 20b5cbf47..71ab28a8a 100644 --- a/apps/roam/src/components/QueryBuilder.tsx +++ b/apps/roam/src/components/QueryBuilder.tsx @@ -32,6 +32,7 @@ type QueryPageComponent = (props: { isEditBlock?: boolean; showAlias?: boolean; discourseNodeType?: string; + settingKey?: "index" | "specification"; }) => JSX.Element; type Props = Parameters[0]; @@ -41,6 +42,7 @@ const QueryBuilder = ({ isEditBlock, showAlias, discourseNodeType, + settingKey, }: Props) => { const extensionAPI = useExtensionAPI(); const hideMetadata = useMemo( @@ -165,6 +167,7 @@ const QueryBuilder = ({ { setHasResults(true); setIsEdit(false); diff --git a/apps/roam/src/components/QueryEditor.tsx b/apps/roam/src/components/QueryEditor.tsx index 4b9a6ee61..3c10fa6e9 100644 --- a/apps/roam/src/components/QueryEditor.tsx +++ b/apps/roam/src/components/QueryEditor.tsx @@ -437,6 +437,7 @@ type QueryEditorComponent = (props: { hideCustomSwitch?: boolean; showAlias?: boolean; discourseNodeType?: string; + settingKey?: "index" | "specification"; }) => JSX.Element; const QueryEditor: QueryEditorComponent = ({ @@ -446,6 +447,7 @@ const QueryEditor: QueryEditorComponent = ({ hideCustomSwitch, showAlias, discourseNodeType, + settingKey, }) => { useEffect(() => { const previewQuery = ((e: CustomEvent) => { @@ -487,7 +489,7 @@ const QueryEditor: QueryEditorComponent = ({ return () => window.clearTimeout(blockPropSyncTimeoutRef.current); }, []); useEffect(() => { - if (!discourseNodeType) return; + if (!discourseNodeType || !settingKey) return; const stripped: unknown = JSON.parse( JSON.stringify( @@ -501,18 +503,20 @@ const QueryEditor: QueryEditorComponent = ({ const result = IndexSchema.safeParse(stripped); if (!result.success) { - console.error("Index blockprop sync failed validation:", result.error); + console.error(`${settingKey} blockprop sync failed validation:`, result.error); return; } + const path = settingKey === "index" ? ["index"] : ["specification", "query"]; + window.clearTimeout(blockPropSyncTimeoutRef.current); blockPropSyncTimeoutRef.current = window.setTimeout(() => { - setDiscourseNodeSetting(discourseNodeType, ["index"], result.data); + setDiscourseNodeSetting(discourseNodeType, path, result.data); lastSyncedIndexRef.current = serialized; }, 250); return () => window.clearTimeout(blockPropSyncTimeoutRef.current); - }, [conditions, selections, custom, discourseNodeType]); + }, [conditions, selections, custom, discourseNodeType, settingKey]); const customNodeOnChange = (value: string) => { window.clearTimeout(debounceRef.current); diff --git a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx index ad1b0a40e..5014823b3 100644 --- a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx @@ -60,6 +60,8 @@ const NodeIndex = ({ }, ], selections: [], + custom: "", + returnNode: DEFAULT_RETURN_NODE, }); setShowQuery(true); @@ -69,7 +71,7 @@ const NodeIndex = ({ return ( {showQuery ? ( - + ) : ( )} diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index 5f2c7332f..5f7cb19fc 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -1,7 +1,6 @@ import React from "react"; import getSubTree from "roamjs-components/util/getSubTree"; import createBlock from "roamjs-components/writes/createBlock"; -import { Checkbox } from "@blueprintjs/core"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import deleteBlock from "roamjs-components/writes/deleteBlock"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -9,6 +8,8 @@ import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression"; import QueryEditor from "~/components/QueryEditor"; import internalError from "~/utils/internalError"; +import { setDiscourseNodeSetting } from "~/components/settings/utils/accessors"; +import { DiscourseNodeFlagPanel } from "~/components/settings/components/BlockPropSettingPanels"; const NodeSpecification = ({ parentUid, @@ -20,11 +21,14 @@ const NodeSpecification = ({ parentSetEnabled?: (enabled: boolean) => void; }) => { const [migrated, setMigrated] = React.useState(false); - const [enabled, setEnabled] = React.useState( + const enabledBlockUid = React.useMemo( () => getSubTree({ tree: getBasicTreeByParentUid(parentUid), key: "enabled" }) ?.uid, + [parentUid], ); + const [enabled, setEnabled] = React.useState(!!enabledBlockUid); + React.useEffect(() => { if (enabled) { const scratchNode = getSubTree({ parentUid, key: "scratch" }); @@ -69,7 +73,22 @@ const NodeSpecification = ({ }, }), ) - .then(() => setMigrated(true)) + .then(() => { + setDiscourseNodeSetting(node.type, ["specification", "query"], { + conditions: [ + { + type: "clause" as const, + source: node.text, + relation: "has title", + target: `/${getDiscourseNodeFormatExpression(node.format).source}/`, + }, + ], + selections: [], + custom: "", + returnNode: node.text, + }); + setMigrated(true); + }) .catch((error) => { internalError({ error }); }); @@ -77,11 +96,18 @@ const NodeSpecification = ({ } else { const tree = getBasicTreeByParentUid(parentUid); const scratchNode = getSubTree({ tree, key: "scratch" }); - Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))).catch( - (error) => { + Promise.all(scratchNode.children.map((c) => deleteBlock(c.uid))) + .then(() => { + setDiscourseNodeSetting(node.type, ["specification", "query"], { + conditions: [], + selections: [], + custom: "", + returnNode: "", + }); + }) + .catch((error) => { internalError({ error }); - }, - ); + }); } return () => { refreshConfigTree(); @@ -90,40 +116,24 @@ const NodeSpecification = ({ return (
-

- { - const flag = (e.target as HTMLInputElement).checked; - if (flag) { - createBlock({ - parentUid, - order: 2, - node: { text: "enabled" }, - }) - .then((uid: string) => { - setEnabled(uid); - if (parentSetEnabled) parentSetEnabled(true); - }) - .catch((error) => { - internalError({ error }); - }); - } else { - deleteBlock(enabled) - .then(() => { - setEnabled(""); - if (parentSetEnabled) parentSetEnabled(false); - }) - .catch((error) => { - internalError({ error }); - }); - } - }} - /> -

+ { + setEnabled(checked); + parentSetEnabled?.(checked); + }} + />
@@ -131,6 +141,8 @@ const NodeSpecification = ({ parentUid={parentUid} key={Number(migrated)} hideCustomSwitch + discourseNodeType={node.type} + settingKey="specification" />
diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index d72b6b7c0..b8c2d04f6 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -45,14 +45,22 @@ const discourseNodeSettings: DiscourseNodeSettings = { shortcut: "C", tag: "#claim", description: "A statement or assertion that can be supported or refuted", - specification: [ - { - type: "clause", - source: "Claim", - relation: "has title", - target: "/^\\[\\[CLM\\]\\]/", + specification: { + enabled: true, + query: { + conditions: [ + { + type: "clause", + source: "Claim", + relation: "has title", + target: "/^\\[\\[CLM\\]\\]/", + }, + ], + selections: [], + custom: "", + returnNode: "Claim", }, - ], + }, template: [ { text: "Summary::", heading: 2 }, { text: "Evidence::", heading: 2, children: [{ text: "" }] }, @@ -76,6 +84,7 @@ const discourseNodeSettings: DiscourseNodeSettings = { ], selections: [], custom: "", + returnNode: "node", }, suggestiveRules, backedBy: "user", diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 5e49a1bc3..35c751e85 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -53,6 +53,7 @@ export const IndexSchema = z.object({ conditions: z.array(ConditionSchema).default([]), selections: z.array(SelectionSchema).default([]), custom: z.string().default(""), + returnNode: z.string().default(""), }); type RoamNode = { @@ -103,10 +104,13 @@ export const DiscourseNodeSchema = z.object({ tag: stringWithDefault(""), description: stringWithDefault(""), specification: z - .array(ConditionSchema) + .object({ + enabled: z.boolean().default(false), + query: IndexSchema.default({}), + }) .nullable() .optional() - .transform((val) => val ?? []), + .transform((val) => val ?? { enabled: false, query: { conditions: [], selections: [], custom: "", returnNode: "" } }), template: z .array(RoamNodeSchema) .nullable() From 7ff8a965fdc00f13f9078c5d381c6ef3053c7bd6 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 12 Feb 2026 15:24:19 +0530 Subject: [PATCH 2/4] devin review --- apps/roam/src/components/QueryBuilder.tsx | 3 +++ apps/roam/src/components/QueryEditor.tsx | 16 +++++++++++++--- .../components/settings/DiscourseNodeIndex.tsx | 7 ++++++- .../settings/DiscourseNodeSpecification.tsx | 1 + .../src/components/settings/utils/zodSchema.ts | 2 +- 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/roam/src/components/QueryBuilder.tsx b/apps/roam/src/components/QueryBuilder.tsx index 71ab28a8a..ac7fd1897 100644 --- a/apps/roam/src/components/QueryBuilder.tsx +++ b/apps/roam/src/components/QueryBuilder.tsx @@ -33,6 +33,7 @@ type QueryPageComponent = (props: { showAlias?: boolean; discourseNodeType?: string; settingKey?: "index" | "specification"; + returnNode?: string; }) => JSX.Element; type Props = Parameters[0]; @@ -43,6 +44,7 @@ const QueryBuilder = ({ showAlias, discourseNodeType, settingKey, + returnNode, }: Props) => { const extensionAPI = useExtensionAPI(); const hideMetadata = useMemo( @@ -168,6 +170,7 @@ const QueryBuilder = ({ parentUid={pageUid} discourseNodeType={discourseNodeType} settingKey={settingKey} + returnNode={returnNode} onQuery={() => { setHasResults(true); setIsEdit(false); diff --git a/apps/roam/src/components/QueryEditor.tsx b/apps/roam/src/components/QueryEditor.tsx index 3c10fa6e9..b739af92a 100644 --- a/apps/roam/src/components/QueryEditor.tsx +++ b/apps/roam/src/components/QueryEditor.tsx @@ -438,6 +438,7 @@ type QueryEditorComponent = (props: { showAlias?: boolean; discourseNodeType?: string; settingKey?: "index" | "specification"; + returnNode?: string; }) => JSX.Element; const QueryEditor: QueryEditorComponent = ({ @@ -448,6 +449,7 @@ const QueryEditor: QueryEditorComponent = ({ showAlias, discourseNodeType, settingKey, + returnNode, }) => { useEffect(() => { const previewQuery = ((e: CustomEvent) => { @@ -493,7 +495,7 @@ const QueryEditor: QueryEditorComponent = ({ const stripped: unknown = JSON.parse( JSON.stringify( - { conditions, selections, custom }, + { conditions, selections, custom, returnNode }, (key, value: unknown) => (key === "uid" ? undefined : value), ), ); @@ -507,7 +509,8 @@ const QueryEditor: QueryEditorComponent = ({ return; } - const path = settingKey === "index" ? ["index"] : ["specification", "query"]; + const path = + settingKey === "index" ? ["index"] : ["specification", "query"]; window.clearTimeout(blockPropSyncTimeoutRef.current); blockPropSyncTimeoutRef.current = window.setTimeout(() => { @@ -516,7 +519,14 @@ const QueryEditor: QueryEditorComponent = ({ }, 250); return () => window.clearTimeout(blockPropSyncTimeoutRef.current); - }, [conditions, selections, custom, discourseNodeType, settingKey]); + }, [ + conditions, + selections, + custom, + discourseNodeType, + settingKey, + returnNode, + ]); const customNodeOnChange = (value: string) => { window.clearTimeout(debounceRef.current); diff --git a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx index 5014823b3..38717ade2 100644 --- a/apps/roam/src/components/settings/DiscourseNodeIndex.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeIndex.tsx @@ -71,7 +71,12 @@ const NodeIndex = ({ return ( {showQuery ? ( - + ) : ( )} diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index 5f7cb19fc..8bfac4954 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -143,6 +143,7 @@ const NodeSpecification = ({ hideCustomSwitch discourseNodeType={node.type} settingKey="specification" + returnNode={node.text} /> diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 35c751e85..50ced8e18 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -53,7 +53,7 @@ export const IndexSchema = z.object({ conditions: z.array(ConditionSchema).default([]), selections: z.array(SelectionSchema).default([]), custom: z.string().default(""), - returnNode: z.string().default(""), + returnNode: z.string().default("node"), }); type RoamNode = { From 5a95d6b2e2e22bd104f52fe00fbe9e1cf5622e90 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 17 Feb 2026 21:16:44 +0530 Subject: [PATCH 3/4] pass initialValue instead of defaultvalue --- .../roam/src/components/settings/DiscourseNodeSpecification.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx index 8bfac4954..b23404556 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSpecification.tsx @@ -125,7 +125,7 @@ const NodeSpecification = ({ title="enabled" description="" settingKeys={["specification", "enabled"]} - defaultValue={enabled} + initialValue={enabled} parentUid={parentUid} uid={enabledBlockUid} order={2} From 6ad633e4db713cddc0b495574a49593e6ee00140 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 23 Feb 2026 19:03:18 +0530 Subject: [PATCH 4/4] fix lint, prettier, address ai reviews --- apps/roam/src/components/QueryEditor.tsx | 9 ++++++--- apps/roam/src/components/settings/utils/init.ts | 15 +++++++++++++-- .../src/components/settings/utils/zodSchema.ts | 8 +++++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/roam/src/components/QueryEditor.tsx b/apps/roam/src/components/QueryEditor.tsx index b739af92a..1be46ae0a 100644 --- a/apps/roam/src/components/QueryEditor.tsx +++ b/apps/roam/src/components/QueryEditor.tsx @@ -448,8 +448,8 @@ const QueryEditor: QueryEditorComponent = ({ hideCustomSwitch, showAlias, discourseNodeType, - settingKey, - returnNode, + settingKey, // eslint-disable-line react/prop-types + returnNode, // eslint-disable-line react/prop-types }) => { useEffect(() => { const previewQuery = ((e: CustomEvent) => { @@ -505,7 +505,10 @@ const QueryEditor: QueryEditorComponent = ({ const result = IndexSchema.safeParse(stripped); if (!result.success) { - console.error(`${settingKey} blockprop sync failed validation:`, result.error); + console.error( + `${settingKey} blockprop sync failed validation:`, + result.error, + ); return; } diff --git a/apps/roam/src/components/settings/utils/init.ts b/apps/roam/src/components/settings/utils/init.ts index f862c3f83..6dba8505f 100644 --- a/apps/roam/src/components/settings/utils/init.ts +++ b/apps/roam/src/components/settings/utils/init.ts @@ -74,7 +74,13 @@ const initializeSettingsBlockProps = ( const uid = blockMap[key]; if (uid) { const existingProps = getBlockProps(uid); - if (!existingProps || Object.keys(existingProps).length === 0) { + // TODO: Overwriting on safeParse failure is a temporary fix for schema shape changes + // (e.g. specification: [] -> {enabled, query}). Replace with proper versioned migrations. + if ( + !existingProps || + Object.keys(existingProps).length === 0 || + !schema.safeParse(existingProps).success + ) { const defaults = schema.parse({}); setBlockProps(uid, defaults, false); } @@ -108,7 +114,12 @@ const initSingleDiscourseNode = async ( ); const existingProps = getBlockProps(pageUid); - if (!existingProps || Object.keys(existingProps).length === 0) { + // TODO: Same temporary fix as initializeSettingsBlockProps — replace with proper migrations. + if ( + !existingProps || + Object.keys(existingProps).length === 0 || + !DiscourseNodeSchema.safeParse(existingProps).success + ) { const nodeData = DiscourseNodeSchema.parse({ text: node.text, type: node.type, diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 50ced8e18..6d746c621 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -110,7 +110,13 @@ export const DiscourseNodeSchema = z.object({ }) .nullable() .optional() - .transform((val) => val ?? { enabled: false, query: { conditions: [], selections: [], custom: "", returnNode: "" } }), + .transform( + (val) => + val ?? { + enabled: false, + query: { conditions: [], selections: [], custom: "", returnNode: "" }, + }, + ), template: z .array(RoamNodeSchema) .nullable()