From a8875db351f74a1b5b2f7b79baebd6e7e11de657 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Apr 2026 17:43:44 +0530 Subject: [PATCH 01/10] ENG-1549 Add convert existing page to discourse node command --- apps/roam/src/components/ModifyNodeDialog.tsx | 26 +++++-- .../utils/registerCommandPaletteCommands.ts | 67 +++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/components/ModifyNodeDialog.tsx b/apps/roam/src/components/ModifyNodeDialog.tsx index 39181ea6c..b0b354cdb 100644 --- a/apps/roam/src/components/ModifyNodeDialog.tsx +++ b/apps/roam/src/components/ModifyNodeDialog.tsx @@ -50,6 +50,10 @@ export type ModifyNodeDialogProps = { includeDefaultNodes?: boolean; // Include default nodes (Page, Block) in node type selector imageUrl?: string; // For image conversion from canvas disableNodeTypeChange?: boolean; // Disable node type selector (e.g. canvas contexts) + createOverride?: (args: { + formattedTitle: string; + configPageUid: string; + }) => Promise; onSuccess: (result: { text: string; uid: string; @@ -69,6 +73,7 @@ const ModifyNodeDialog = ({ includeDefaultNodes = false, imageUrl, disableNodeTypeChange = false, + createOverride, onSuccess, onClose, }: RoamOverlayProps) => { @@ -416,13 +421,20 @@ const ModifyNodeDialog = ({ return; } - // Create new discourse node - const newPageUid = await createDiscourseNode({ - text: formattedTitle, - configPageUid: selectedNodeType?.type ?? "", - extensionAPI, - imageUrl, - }); + const configPageUid = selectedNodeType?.type ?? ""; + const overrideUid = createOverride + ? await createOverride({ formattedTitle, configPageUid }) + : undefined; + + const newPageUid = + overrideUid !== undefined + ? overrideUid + : await createDiscourseNode({ + text: formattedTitle, + configPageUid, + extensionAPI, + imageUrl, + }); if (sourceBlockUid) { const pageRef = `[[${formattedTitle}]]`; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 26b780084..9e3eb7bbf 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -19,6 +19,10 @@ import { getOverlayHandler, onPageRefObserverChange, } from "./pageRefObserverHandlers"; +import findDiscourseNode from "~/utils/findDiscourseNode"; +import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; +import getSubTree from "roamjs-components/util/getSubTree"; +import stripUid from "roamjs-components/util/stripUid"; import { HIDE_METADATA_KEY } from "~/data/userSettings"; import posthog from "posthog-js"; import { extractRef } from "roamjs-components/util"; @@ -121,6 +125,66 @@ export const createDiscourseNodeFromCommand = ( }); }; +export const convertPageToNodeFromCommand = ( + extensionAPI: OnloadArgs["extensionAPI"], +) => { + posthog.capture("Discourse Node: Convert Command Triggered"); + const pageUid = getCurrentPageUid(); + if (!pageUid) { + renderToast({ + id: "convert-page-no-page", + content: "Navigate to a page to convert it to a discourse node.", + }); + return; + } + + const pageTitle = getPageTitleByPageUid(pageUid); + if (!pageTitle) { + renderToast({ + id: "convert-page-no-title", + content: "Could not determine the current page title.", + }); + return; + } + + const existingNode = findDiscourseNode({ uid: pageUid, title: pageTitle }); + if (existingNode && existingNode.backedBy !== "default") { + renderToast({ + id: "convert-page-already-node", + content: `This page is already a ${existingNode.text} node.`, + }); + return; + } + + renderModifyNodeDialog({ + mode: "create", + nodeType: "", + initialValue: { text: pageTitle, uid: "" }, + extensionAPI, + createOverride: async ({ formattedTitle, configPageUid }) => { + await window.roamAlphaAPI.data.page.update({ + page: { uid: pageUid, title: formattedTitle }, + }); + + const nodeTree = getFullTreeByParentUid(configPageUid).children; + const templateNode = getSubTree({ tree: nodeTree, key: "template" }); + if (templateNode.children.length > 0) { + const existingChildren = getFullTreeByParentUid(pageUid).children || []; + const lastOrder = existingChildren.length; + await Promise.all( + stripUid(templateNode.children).map(({ uid, ...node }, order) => + createBlock({ node, order: lastOrder + order, parentUid: pageUid }), + ), + ); + } + + return pageUid; + }, + onSuccess: async () => {}, + onClose: () => {}, + }); +}; + export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const { extensionAPI } = onloadArgs; @@ -314,6 +378,9 @@ export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { }; // Roam organizes commands alphabetically + void addCommand("DG: Convert current page to discourse node", () => + convertPageToNodeFromCommand(extensionAPI), + ); void addCommand("DG: Create/Insert discourse node", () => createDiscourseNodeFromCommand(extensionAPI), ); From 26330ad4517199e273556b20907deb8984380307 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Apr 2026 17:54:48 +0530 Subject: [PATCH 02/10] Add SmartBlocks support and fix unused uid destructuring --- .../utils/registerCommandPaletteCommands.ts | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 9e3eb7bbf..026c2eae1 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -169,13 +169,42 @@ export const convertPageToNodeFromCommand = ( const nodeTree = getFullTreeByParentUid(configPageUid).children; const templateNode = getSubTree({ tree: nodeTree, key: "template" }); if (templateNode.children.length > 0) { - const existingChildren = getFullTreeByParentUid(pageUid).children || []; - const lastOrder = existingChildren.length; - await Promise.all( - stripUid(templateNode.children).map(({ uid, ...node }, order) => - createBlock({ node, order: lastOrder + order, parentUid: pageUid }), - ), - ); + const hasSmartBlockSyntax = ( + node: Parameters[0][0], + ): boolean => { + if (node.text.includes("<%")) return true; + if (node.children) return node.children.some(hasSmartBlockSyntax); + return false; + }; + const useSmartBlocks = hasSmartBlockSyntax(templateNode); + + if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + void window.roamjs.extension.smartblocks?.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: pageUid, + }); + } else { + if (useSmartBlocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + } + const existingChildren = + getFullTreeByParentUid(pageUid).children || []; + const lastOrder = existingChildren.length; + await Promise.all( + stripUid(templateNode.children).map((node, order) => + createBlock({ + node, + order: lastOrder + order, + parentUid: pageUid, + }), + ), + ); + } } return pageUid; From 81b0227148af8efed0012036c4f04001f192e3fc Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 11:15:02 +0530 Subject: [PATCH 03/10] Extract insertTemplateBlocks helper and tighten createOverride type Address review comments on PR #987: - DRY the SmartBlocks template-handling logic by extracting insertTemplateBlocks into createDiscourseNode.ts and reusing it from convertPageToNodeFromCommand. - Tighten createOverride return type from Promise to Promise so the dialog can collapse the override fallback into a single const. --- apps/roam/src/components/ModifyNodeDialog.tsx | 21 ++-- apps/roam/src/utils/createDiscourseNode.ts | 96 ++++++++++--------- .../utils/registerCommandPaletteCommands.ts | 47 +-------- 3 files changed, 61 insertions(+), 103 deletions(-) diff --git a/apps/roam/src/components/ModifyNodeDialog.tsx b/apps/roam/src/components/ModifyNodeDialog.tsx index b0b354cdb..02fb62dd3 100644 --- a/apps/roam/src/components/ModifyNodeDialog.tsx +++ b/apps/roam/src/components/ModifyNodeDialog.tsx @@ -53,7 +53,7 @@ export type ModifyNodeDialogProps = { createOverride?: (args: { formattedTitle: string; configPageUid: string; - }) => Promise; + }) => Promise; onSuccess: (result: { text: string; uid: string; @@ -422,19 +422,14 @@ const ModifyNodeDialog = ({ } const configPageUid = selectedNodeType?.type ?? ""; - const overrideUid = createOverride + const newPageUid = createOverride ? await createOverride({ formattedTitle, configPageUid }) - : undefined; - - const newPageUid = - overrideUid !== undefined - ? overrideUid - : await createDiscourseNode({ - text: formattedTitle, - configPageUid, - extensionAPI, - imageUrl, - }); + : await createDiscourseNode({ + text: formattedTitle, + configPageUid, + extensionAPI, + imageUrl, + }); if (sourceBlockUid) { const pageRef = `[[${formattedTitle}]]`; diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 851cba630..5b561ad2d 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -13,6 +13,55 @@ import runQuery from "./runQuery"; import updateBlock from "roamjs-components/writes/updateBlock"; import posthog from "posthog-js"; +const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { + if (node.text.includes("<%")) return true; + if (node.children) return node.children.some(hasSmartBlockSyntax); + return false; +}; + +export const insertTemplateBlocks = async ({ + configPageUid, + parentUid, +}: { + configPageUid: string; + parentUid: string; +}) => { + const nodeTree = getFullTreeByParentUid(configPageUid).children; + const templateNode = getSubTree({ tree: nodeTree, key: "template" }); + if (!templateNode.children.length) return; + + const useSmartBlocks = hasSmartBlockSyntax(templateNode); + + if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + void window.roamjs.extension.smartblocks.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: parentUid, + }); + return; + } + + if (useSmartBlocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + } + + const existingChildren = getFullTreeByParentUid(parentUid).children || []; + const orderOffset = existingChildren.length; + await Promise.all( + stripUid(templateNode.children).map((node, order) => + createBlock({ + node, + order: orderOffset + order, + parentUid, + }), + ), + ); +}; + type Props = { text: string; configPageUid: string; @@ -142,51 +191,8 @@ const createDiscourseNode = async ({ return pageUid; } - const nodeTree = getFullTreeByParentUid(configPageUid).children; - const templateNode = getSubTree({ - tree: nodeTree, - key: "template", - }); - - const createBlocksFromTemplate = async () => { - await Promise.all( - stripUid(templateNode.children).map(({ uid, ...node }, order) => - createBlock({ - node, - order, - parentUid: pageUid, - }), - ), - ); - - // Add image to page if imageUrl is provided - await handleImageCreation(pageUid); - }; - - const hasSmartBlockSyntax = (node: RoamBasicNode) => { - if (node.text.includes("<%")) return true; - if (node.children) return node.children.some(hasSmartBlockSyntax); - return false; - }; - const useSmartBlocks = hasSmartBlockSyntax(templateNode); - - if (useSmartBlocks && !window.roamjs?.extension?.smartblocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", - }); - await createBlocksFromTemplate(); - } else if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - window.roamjs.extension.smartblocks?.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: pageUid, - }); - await handleImageCreation(pageUid); - } else { - await createBlocksFromTemplate(); - } + await insertTemplateBlocks({ configPageUid, parentUid: pageUid }); + await handleImageCreation(pageUid); handleOpenInSidebar(pageUid); return pageUid; }; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 026c2eae1..632a6f371 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -20,9 +20,7 @@ import { onPageRefObserverChange, } from "./pageRefObserverHandlers"; import findDiscourseNode from "~/utils/findDiscourseNode"; -import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; -import getSubTree from "roamjs-components/util/getSubTree"; -import stripUid from "roamjs-components/util/stripUid"; +import { insertTemplateBlocks } from "~/utils/createDiscourseNode"; import { HIDE_METADATA_KEY } from "~/data/userSettings"; import posthog from "posthog-js"; import { extractRef } from "roamjs-components/util"; @@ -165,48 +163,7 @@ export const convertPageToNodeFromCommand = ( await window.roamAlphaAPI.data.page.update({ page: { uid: pageUid, title: formattedTitle }, }); - - const nodeTree = getFullTreeByParentUid(configPageUid).children; - const templateNode = getSubTree({ tree: nodeTree, key: "template" }); - if (templateNode.children.length > 0) { - const hasSmartBlockSyntax = ( - node: Parameters[0][0], - ): boolean => { - if (node.text.includes("<%")) return true; - if (node.children) return node.children.some(hasSmartBlockSyntax); - return false; - }; - const useSmartBlocks = hasSmartBlockSyntax(templateNode); - - if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - void window.roamjs.extension.smartblocks?.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: pageUid, - }); - } else { - if (useSmartBlocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", - }); - } - const existingChildren = - getFullTreeByParentUid(pageUid).children || []; - const lastOrder = existingChildren.length; - await Promise.all( - stripUid(templateNode.children).map((node, order) => - createBlock({ - node, - order: lastOrder + order, - parentUid: pageUid, - }), - ), - ); - } - } - + await insertTemplateBlocks({ configPageUid, parentUid: pageUid }); return pageUid; }, onSuccess: async () => {}, From 44ebe151c2dd31a826465e3964a882b3fc5ac662 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 11:43:20 +0530 Subject: [PATCH 04/10] Fold image creation into insertTemplateBlocks helper Image insertion belongs with template insertion (matches the original pre-refactor coupling). Extract handleImageCreation to a module-level insertCanvasKeyImage and call it from the helper after template work. createDiscourseNode no longer needs a separate image step; convert flow's image branch is a no-op via undefined imageUrl. --- apps/roam/src/utils/createDiscourseNode.ts | 179 ++++++++++++--------- 1 file changed, 104 insertions(+), 75 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 5b561ad2d..27ef3ddbb 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -19,47 +19,118 @@ const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { return false; }; +const insertCanvasKeyImage = async ({ + pageUid, + configPageUid, + imageUrl, + extensionAPI, + text, +}: { + pageUid: string; + configPageUid: string; + imageUrl: string; + extensionAPI?: OnloadArgs["extensionAPI"]; + text: string; +}) => { + const discourseNodes = getDiscourseNodes(); + const canvasSettings = Object.fromEntries( + discourseNodes.map((n) => [n.type, { ...n.canvasSettings }]), + ); + const { + "query-builder-alias": qbAlias = "", + "key-image": isKeyImage = "", + "key-image-option": keyImageOption = "", + } = canvasSettings[configPageUid] || {}; + + if (!isKeyImage) return; + + const createOrUpdateImageBlock = async (imagePlaceholderUid?: string) => { + const imageMarkdown = `![](${imageUrl})`; + if (imagePlaceholderUid) { + await updateBlock({ uid: imagePlaceholderUid, text: imageMarkdown }); + } else { + await createBlock({ + node: { text: imageMarkdown }, + order: 0, + parentUid: pageUid, + }); + } + }; + + if (keyImageOption === "query-builder") { + if (!extensionAPI) return; + const parentUid = resolveQueryBuilderRef({ + queryRef: qbAlias, + extensionAPI, + }); + const results = await runQuery({ + extensionAPI, + parentUid, + inputs: { NODETEXT: text, NODEUID: pageUid }, + }); + const imagePlaceholderUid = results.allProcessedResults[0]?.uid; + await createOrUpdateImageBlock(imagePlaceholderUid); + } else { + await createOrUpdateImageBlock(); + } +}; + export const insertTemplateBlocks = async ({ configPageUid, parentUid, + imageUrl, + extensionAPI, + text, }: { configPageUid: string; parentUid: string; + imageUrl?: string; + extensionAPI?: OnloadArgs["extensionAPI"]; + text?: string; }) => { const nodeTree = getFullTreeByParentUid(configPageUid).children; const templateNode = getSubTree({ tree: nodeTree, key: "template" }); - if (!templateNode.children.length) return; - const useSmartBlocks = hasSmartBlockSyntax(templateNode); + if (templateNode.children.length) { + const useSmartBlocks = hasSmartBlockSyntax(templateNode); - if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - void window.roamjs.extension.smartblocks.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: parentUid, - }); - return; + if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + void window.roamjs.extension.smartblocks.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: parentUid, + }); + } else { + if (useSmartBlocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + } + const existingChildren = getFullTreeByParentUid(parentUid).children || []; + const orderOffset = existingChildren.length; + await Promise.all( + stripUid(templateNode.children).map((node, order) => + createBlock({ + node, + order: orderOffset + order, + parentUid, + }), + ), + ); + } } - if (useSmartBlocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", + if (imageUrl) { + await insertCanvasKeyImage({ + pageUid: parentUid, + configPageUid, + imageUrl, + extensionAPI, + text: text ?? "", }); } - - const existingChildren = getFullTreeByParentUid(parentUid).children || []; - const orderOffset = existingChildren.length; - await Promise.all( - stripUid(templateNode.children).map((node, order) => - createBlock({ - node, - order: orderOffset + order, - parentUid, - }), - ), - ); }; type Props = { @@ -99,53 +170,6 @@ const createDiscourseNode = async ({ }, 1); }, 100); }; - const handleImageCreation = async (pageUid: string) => { - const canvasSettings = Object.fromEntries( - discourseNodes.map((n) => [n.type, { ...n.canvasSettings }]), - ); - const { - "query-builder-alias": qbAlias = "", - "key-image": isKeyImage = "", - "key-image-option": keyImageOption = "", - } = canvasSettings[configPageUid] || {}; - - if (isKeyImage && imageUrl) { - const createOrUpdateImageBlock = async (imagePlaceholderUid?: string) => { - const imageMarkdown = `![](${imageUrl})`; - if (imagePlaceholderUid) { - await updateBlock({ - uid: imagePlaceholderUid, - text: imageMarkdown, - }); - } else { - await createBlock({ - node: { text: imageMarkdown }, - order: 0, - parentUid: pageUid, - }); - } - }; - - if (keyImageOption === "query-builder") { - if (!extensionAPI) return; - - const parentUid = resolveQueryBuilderRef({ - queryRef: qbAlias, - extensionAPI, - }); - const results = await runQuery({ - extensionAPI, - parentUid, - inputs: { NODETEXT: text, NODEUID: pageUid }, - }); - const imagePlaceholderUid = results.allProcessedResults[0]?.uid; - await createOrUpdateImageBlock(imagePlaceholderUid); - } else { - await createOrUpdateImageBlock(); - } - } - }; - const discourseNodes = getDiscourseNodes(); const specification = discourseNodes?.find( (n) => n.type === configPageUid, @@ -191,8 +215,13 @@ const createDiscourseNode = async ({ return pageUid; } - await insertTemplateBlocks({ configPageUid, parentUid: pageUid }); - await handleImageCreation(pageUid); + await insertTemplateBlocks({ + configPageUid, + parentUid: pageUid, + imageUrl, + extensionAPI, + text, + }); handleOpenInSidebar(pageUid); return pageUid; }; From 6f37b49bbd408d3ed9996c09baa5fd5a9fa4a1a4 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 15:40:12 +0530 Subject: [PATCH 05/10] Restore handleImageCreation body unchanged at module scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure extraction — the function body is identical to the original closure. The signature now takes pageUid, discourseNodes, configPageUid, imageUrl, extensionAPI, and text as params instead of picking them up via closure. createDiscourseNode passes its already-computed discourseNodes through (no duplicate fetch). --- apps/roam/src/utils/createDiscourseNode.ts | 81 ++++++++++++---------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 27ef3ddbb..d41e05a14 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -19,20 +19,21 @@ const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { return false; }; -const insertCanvasKeyImage = async ({ +const handleImageCreation = async ({ pageUid, + discourseNodes, configPageUid, imageUrl, extensionAPI, text, }: { pageUid: string; + discourseNodes: ReturnType; configPageUid: string; - imageUrl: string; + imageUrl?: string; extensionAPI?: OnloadArgs["extensionAPI"]; text: string; }) => { - const discourseNodes = getDiscourseNodes(); const canvasSettings = Object.fromEntries( discourseNodes.map((n) => [n.type, { ...n.canvasSettings }]), ); @@ -42,51 +43,57 @@ const insertCanvasKeyImage = async ({ "key-image-option": keyImageOption = "", } = canvasSettings[configPageUid] || {}; - if (!isKeyImage) return; + if (isKeyImage && imageUrl) { + const createOrUpdateImageBlock = async (imagePlaceholderUid?: string) => { + const imageMarkdown = `![](${imageUrl})`; + if (imagePlaceholderUid) { + await updateBlock({ + uid: imagePlaceholderUid, + text: imageMarkdown, + }); + } else { + await createBlock({ + node: { text: imageMarkdown }, + order: 0, + parentUid: pageUid, + }); + } + }; - const createOrUpdateImageBlock = async (imagePlaceholderUid?: string) => { - const imageMarkdown = `![](${imageUrl})`; - if (imagePlaceholderUid) { - await updateBlock({ uid: imagePlaceholderUid, text: imageMarkdown }); - } else { - await createBlock({ - node: { text: imageMarkdown }, - order: 0, - parentUid: pageUid, + if (keyImageOption === "query-builder") { + if (!extensionAPI) return; + + const parentUid = resolveQueryBuilderRef({ + queryRef: qbAlias, + extensionAPI, + }); + const results = await runQuery({ + extensionAPI, + parentUid, + inputs: { NODETEXT: text, NODEUID: pageUid }, }); + const imagePlaceholderUid = results.allProcessedResults[0]?.uid; + await createOrUpdateImageBlock(imagePlaceholderUid); + } else { + await createOrUpdateImageBlock(); } - }; - - if (keyImageOption === "query-builder") { - if (!extensionAPI) return; - const parentUid = resolveQueryBuilderRef({ - queryRef: qbAlias, - extensionAPI, - }); - const results = await runQuery({ - extensionAPI, - parentUid, - inputs: { NODETEXT: text, NODEUID: pageUid }, - }); - const imagePlaceholderUid = results.allProcessedResults[0]?.uid; - await createOrUpdateImageBlock(imagePlaceholderUid); - } else { - await createOrUpdateImageBlock(); } }; export const insertTemplateBlocks = async ({ configPageUid, parentUid, + discourseNodes, + text, imageUrl, extensionAPI, - text, }: { configPageUid: string; parentUid: string; + discourseNodes?: ReturnType; + text?: string; imageUrl?: string; extensionAPI?: OnloadArgs["extensionAPI"]; - text?: string; }) => { const nodeTree = getFullTreeByParentUid(configPageUid).children; const templateNode = getSubTree({ tree: nodeTree, key: "template" }); @@ -122,13 +129,14 @@ export const insertTemplateBlocks = async ({ } } - if (imageUrl) { - await insertCanvasKeyImage({ + if (discourseNodes && text !== undefined) { + await handleImageCreation({ pageUid: parentUid, + discourseNodes, configPageUid, imageUrl, extensionAPI, - text: text ?? "", + text, }); } }; @@ -218,9 +226,10 @@ const createDiscourseNode = async ({ await insertTemplateBlocks({ configPageUid, parentUid: pageUid, + discourseNodes, + text, imageUrl, extensionAPI, - text, }); handleOpenInSidebar(pageUid); return pageUid; From d79a736d7c428c0c7096af7d312b5440f3ec04c3 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 15:51:19 +0530 Subject: [PATCH 06/10] Simplify: just extract helpers, drop the wrapper Step back from the insertTemplateBlocks orchestrator. The PR is just two function extractions: - hasSmartBlockSyntax and createBlocksFromTemplate are exported from createDiscourseNode.ts at module scope. - handleImageCreation stays at module scope (used only by createDiscourseNode). createDiscourseNode keeps its original if/else SmartBlocks orchestration; convertPageToNodeFromCommand keeps its inline orchestration too. Both sites now call the shared helpers. --- apps/roam/src/utils/createDiscourseNode.ts | 103 ++++++++---------- .../utils/registerCommandPaletteCommands.ts | 35 +++++- 2 files changed, 76 insertions(+), 62 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index d41e05a14..25651b9d1 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -13,7 +13,7 @@ import runQuery from "./runQuery"; import updateBlock from "roamjs-components/writes/updateBlock"; import posthog from "posthog-js"; -const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { +export const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { if (node.text.includes("<%")) return true; if (node.children) return node.children.some(hasSmartBlockSyntax); return false; @@ -80,65 +80,24 @@ const handleImageCreation = async ({ } }; -export const insertTemplateBlocks = async ({ - configPageUid, +export const createBlocksFromTemplate = async ({ + templateNode, parentUid, - discourseNodes, - text, - imageUrl, - extensionAPI, }: { - configPageUid: string; + templateNode: RoamBasicNode; parentUid: string; - discourseNodes?: ReturnType; - text?: string; - imageUrl?: string; - extensionAPI?: OnloadArgs["extensionAPI"]; }) => { - const nodeTree = getFullTreeByParentUid(configPageUid).children; - const templateNode = getSubTree({ tree: nodeTree, key: "template" }); - - if (templateNode.children.length) { - const useSmartBlocks = hasSmartBlockSyntax(templateNode); - - if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - void window.roamjs.extension.smartblocks.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: parentUid, - }); - } else { - if (useSmartBlocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", - }); - } - const existingChildren = getFullTreeByParentUid(parentUid).children || []; - const orderOffset = existingChildren.length; - await Promise.all( - stripUid(templateNode.children).map((node, order) => - createBlock({ - node, - order: orderOffset + order, - parentUid, - }), - ), - ); - } - } - - if (discourseNodes && text !== undefined) { - await handleImageCreation({ - pageUid: parentUid, - discourseNodes, - configPageUid, - imageUrl, - extensionAPI, - text, - }); - } + const existingChildren = getFullTreeByParentUid(parentUid).children || []; + const lastOrder = existingChildren.length; + await Promise.all( + stripUid(templateNode.children).map((node, order) => + createBlock({ + node, + order: lastOrder + order, + parentUid, + }), + ), + ); }; type Props = { @@ -223,13 +182,37 @@ const createDiscourseNode = async ({ return pageUid; } - await insertTemplateBlocks({ - configPageUid, - parentUid: pageUid, + const nodeTree = getFullTreeByParentUid(configPageUid).children; + const templateNode = getSubTree({ + tree: nodeTree, + key: "template", + }); + + const useSmartBlocks = hasSmartBlockSyntax(templateNode); + + if (useSmartBlocks && !window.roamjs?.extension?.smartblocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + await createBlocksFromTemplate({ templateNode, parentUid: pageUid }); + } else if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + window.roamjs.extension.smartblocks?.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: pageUid, + }); + } else { + await createBlocksFromTemplate({ templateNode, parentUid: pageUid }); + } + await handleImageCreation({ + pageUid, discourseNodes, - text, + configPageUid, imageUrl, extensionAPI, + text, }); handleOpenInSidebar(pageUid); return pageUid; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 632a6f371..ad6c36d32 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -20,7 +20,12 @@ import { onPageRefObserverChange, } from "./pageRefObserverHandlers"; import findDiscourseNode from "~/utils/findDiscourseNode"; -import { insertTemplateBlocks } from "~/utils/createDiscourseNode"; +import { + hasSmartBlockSyntax, + createBlocksFromTemplate, +} from "~/utils/createDiscourseNode"; +import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; +import getSubTree from "roamjs-components/util/getSubTree"; import { HIDE_METADATA_KEY } from "~/data/userSettings"; import posthog from "posthog-js"; import { extractRef } from "roamjs-components/util"; @@ -163,7 +168,33 @@ export const convertPageToNodeFromCommand = ( await window.roamAlphaAPI.data.page.update({ page: { uid: pageUid, title: formattedTitle }, }); - await insertTemplateBlocks({ configPageUid, parentUid: pageUid }); + + const nodeTree = getFullTreeByParentUid(configPageUid).children; + const templateNode = getSubTree({ tree: nodeTree, key: "template" }); + if (templateNode.children.length > 0) { + const useSmartBlocks = hasSmartBlockSyntax(templateNode); + + if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + void window.roamjs.extension.smartblocks?.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: pageUid, + }); + } else { + if (useSmartBlocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + } + await createBlocksFromTemplate({ + templateNode, + parentUid: pageUid, + }); + } + } + return pageUid; }, onSuccess: async () => {}, From d5deabbc82468e06a41236f3d6de8c46f13aa1f1 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 16:21:32 +0530 Subject: [PATCH 07/10] Fix shared template creation flow --- apps/roam/src/utils/createDiscourseNode.ts | 105 +++++++++++------- .../utils/registerCommandPaletteCommands.ts | 34 ++---- 2 files changed, 76 insertions(+), 63 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 25651b9d1..1c0e3c14a 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -13,12 +13,6 @@ import runQuery from "./runQuery"; import updateBlock from "roamjs-components/writes/updateBlock"; import posthog from "posthog-js"; -export const hasSmartBlockSyntax = (node: RoamBasicNode): boolean => { - if (node.text.includes("<%")) return true; - if (node.children) return node.children.some(hasSmartBlockSyntax); - return false; -}; - const handleImageCreation = async ({ pageUid, discourseNodes, @@ -82,22 +76,74 @@ const handleImageCreation = async ({ export const createBlocksFromTemplate = async ({ templateNode, - parentUid, + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, }: { templateNode: RoamBasicNode; - parentUid: string; + pageUid: string; + discourseNodes: ReturnType; + configPageUid: string; + imageUrl?: string; + extensionAPI?: OnloadArgs["extensionAPI"]; + text: string; }) => { - const existingChildren = getFullTreeByParentUid(parentUid).children || []; - const lastOrder = existingChildren.length; - await Promise.all( - stripUid(templateNode.children).map((node, order) => - createBlock({ - node, - order: lastOrder + order, - parentUid, - }), - ), - ); + const createBlocksFromTemplate = async () => { + await Promise.all( + stripUid(templateNode.children).map(({ uid, ...node }, order) => + createBlock({ + node, + order, + parentUid: pageUid, + }), + ), + ); + + // Add image to page if imageUrl is provided + await handleImageCreation({ + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, + }); + }; + + const hasSmartBlockSyntax = (node: RoamBasicNode) => { + if (node.text.includes("<%")) return true; + if (node.children) return node.children.some(hasSmartBlockSyntax); + return false; + }; + const useSmartBlocks = hasSmartBlockSyntax(templateNode); + + if (useSmartBlocks && !window.roamjs?.extension?.smartblocks) { + renderToast({ + content: + "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", + id: "smartblocks-extension-disabled", + intent: "warning", + }); + await createBlocksFromTemplate(); + } else if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { + window.roamjs.extension.smartblocks?.triggerSmartblock({ + srcUid: templateNode.uid, + targetUid: pageUid, + }); + await handleImageCreation({ + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, + }); + } else { + await createBlocksFromTemplate(); + } }; type Props = { @@ -188,25 +234,8 @@ const createDiscourseNode = async ({ key: "template", }); - const useSmartBlocks = hasSmartBlockSyntax(templateNode); - - if (useSmartBlocks && !window.roamjs?.extension?.smartblocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", - }); - await createBlocksFromTemplate({ templateNode, parentUid: pageUid }); - } else if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - window.roamjs.extension.smartblocks?.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: pageUid, - }); - } else { - await createBlocksFromTemplate({ templateNode, parentUid: pageUid }); - } - await handleImageCreation({ + await createBlocksFromTemplate({ + templateNode, pageUid, discourseNodes, configPageUid, diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index ad6c36d32..d46502442 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -20,10 +20,7 @@ import { onPageRefObserverChange, } from "./pageRefObserverHandlers"; import findDiscourseNode from "~/utils/findDiscourseNode"; -import { - hasSmartBlockSyntax, - createBlocksFromTemplate, -} from "~/utils/createDiscourseNode"; +import { createBlocksFromTemplate } from "~/utils/createDiscourseNode"; import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; import getSubTree from "roamjs-components/util/getSubTree"; import { HIDE_METADATA_KEY } from "~/data/userSettings"; @@ -172,27 +169,14 @@ export const convertPageToNodeFromCommand = ( const nodeTree = getFullTreeByParentUid(configPageUid).children; const templateNode = getSubTree({ tree: nodeTree, key: "template" }); if (templateNode.children.length > 0) { - const useSmartBlocks = hasSmartBlockSyntax(templateNode); - - if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - void window.roamjs.extension.smartblocks?.triggerSmartblock({ - srcUid: templateNode.uid, - targetUid: pageUid, - }); - } else { - if (useSmartBlocks) { - renderToast({ - content: - "This template requires SmartBlocks. Enable SmartBlocks in Roam Depot to use this template.", - id: "smartblocks-extension-disabled", - intent: "warning", - }); - } - await createBlocksFromTemplate({ - templateNode, - parentUid: pageUid, - }); - } + await createBlocksFromTemplate({ + templateNode, + pageUid, + discourseNodes: getDiscourseNodes(), + configPageUid, + extensionAPI, + text: formattedTitle, + }); } return pageUid; From bc2ffcd3596ceeab83b0a7936d2a5575d903e9e9 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 16:30:32 +0530 Subject: [PATCH 08/10] Fix template helper lint warnings --- apps/roam/src/utils/createDiscourseNode.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 1c0e3c14a..73322f686 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -64,6 +64,7 @@ const handleImageCreation = async ({ const results = await runQuery({ extensionAPI, parentUid, + // eslint-disable-next-line @typescript-eslint/naming-convention inputs: { NODETEXT: text, NODEUID: pageUid }, }); const imagePlaceholderUid = results.allProcessedResults[0]?.uid; @@ -93,7 +94,7 @@ export const createBlocksFromTemplate = async ({ }) => { const createBlocksFromTemplate = async () => { await Promise.all( - stripUid(templateNode.children).map(({ uid, ...node }, order) => + stripUid(templateNode.children).map((node, order) => createBlock({ node, order, @@ -129,7 +130,7 @@ export const createBlocksFromTemplate = async ({ }); await createBlocksFromTemplate(); } else if (useSmartBlocks && window.roamjs?.extension?.smartblocks) { - window.roamjs.extension.smartblocks?.triggerSmartblock({ + void window.roamjs.extension.smartblocks?.triggerSmartblock({ srcUid: templateNode.uid, targetUid: pageUid, }); @@ -166,7 +167,7 @@ const createDiscourseNode = async ({ }); const handleOpenInSidebar = (uid: string) => { if (extensionAPI?.settings.get("disable-sidebar-open")) return; - openBlockInSidebar(uid); + void openBlockInSidebar(uid); setTimeout(() => { const sidebarTitle = document.querySelector( ".rm-sidebar-outline .rm-title-display", From 993a866b8ae4320dc97d7dfbddfaf64075bf0fe2 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 12 May 2026 16:57:29 +0530 Subject: [PATCH 09/10] Append converted page templates after existing blocks --- apps/roam/src/utils/createDiscourseNode.ts | 6 ++++-- apps/roam/src/utils/registerCommandPaletteCommands.ts | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index 73322f686..a96ee9202 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -78,6 +78,7 @@ const handleImageCreation = async ({ export const createBlocksFromTemplate = async ({ templateNode, pageUid, + order = 0, discourseNodes, configPageUid, imageUrl, @@ -86,6 +87,7 @@ export const createBlocksFromTemplate = async ({ }: { templateNode: RoamBasicNode; pageUid: string; + order?: number; discourseNodes: ReturnType; configPageUid: string; imageUrl?: string; @@ -94,10 +96,10 @@ export const createBlocksFromTemplate = async ({ }) => { const createBlocksFromTemplate = async () => { await Promise.all( - stripUid(templateNode.children).map((node, order) => + stripUid(templateNode.children).map((node, templateOrder) => createBlock({ node, - order, + order: order + templateOrder, parentUid: pageUid, }), ), diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index d46502442..2be691c49 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -169,9 +169,11 @@ export const convertPageToNodeFromCommand = ( const nodeTree = getFullTreeByParentUid(configPageUid).children; const templateNode = getSubTree({ tree: nodeTree, key: "template" }); if (templateNode.children.length > 0) { + const existingChildren = getFullTreeByParentUid(pageUid).children || []; await createBlocksFromTemplate({ templateNode, pageUid, + order: existingChildren.length, discourseNodes: getDiscourseNodes(), configPageUid, extensionAPI, From 2e433279745da2dd14e2d011ae9190c3562cef60 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 20 May 2026 18:20:41 +0530 Subject: [PATCH 10/10] Restore eslint-disable for NODETEXT/NODEUID input keys --- apps/roam/src/utils/createDiscourseNode.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/roam/src/utils/createDiscourseNode.ts b/apps/roam/src/utils/createDiscourseNode.ts index ba01147f7..d0c737c88 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -88,6 +88,7 @@ const handleImageCreation = async ({ const results = await runQuery({ extensionAPI, parentUid, + // eslint-disable-next-line @typescript-eslint/naming-convention inputs: { NODETEXT: text, NODEUID: pageUid }, }); const imagePlaceholderUid = results.allProcessedResults[0]?.uid;