From 6cfb5dc5bb9217d620d742d9ded489ec327312f8 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 12 Feb 2026 00:13:06 +0530 Subject: [PATCH 1/2] 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 a47a3198d..a725800c7 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({ conditions, selections, custom }, (key, value: unknown) => @@ -500,18 +502,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 1c7af7ad77cd36d0a0f1ec74671b5691f81536ce Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 12 Feb 2026 15:24:19 +0530 Subject: [PATCH 2/2] devin review --- apps/roam/src/components/QueryBuilder.tsx | 3 +++ apps/roam/src/components/QueryEditor.tsx | 19 +++++++++++++++---- .../settings/DiscourseNodeIndex.tsx | 7 ++++++- .../settings/DiscourseNodeSpecification.tsx | 1 + .../components/settings/utils/zodSchema.ts | 2 +- 5 files changed, 26 insertions(+), 6 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 a725800c7..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) => { @@ -492,8 +494,9 @@ const QueryEditor: QueryEditorComponent = ({ if (!discourseNodeType || !settingKey) return; const stripped: unknown = JSON.parse( - JSON.stringify({ conditions, selections, custom }, (key, value: unknown) => - key === "uid" ? undefined : value, + JSON.stringify( + { conditions, selections, custom, returnNode }, + (key, value: unknown) => (key === "uid" ? undefined : value), ), ); @@ -506,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(() => { @@ -515,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 = {