diff --git a/apps/roam/src/components/ModifyNodeDialog.tsx b/apps/roam/src/components/ModifyNodeDialog.tsx index 7b6f94e28..c388a6d0f 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) => { @@ -413,13 +418,15 @@ const ModifyNodeDialog = ({ return; } - // Create new discourse node - const newPageUid = await createDiscourseNode({ - text: formattedTitle, - configPageUid: selectedNodeType?.type ?? "", - extensionAPI, - imageUrl, - }); + const configPageUid = selectedNodeType?.type ?? ""; + const newPageUid = createOverride + ? await createOverride({ formattedTitle, configPageUid }) + : 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 3bfd9c037..d0c737c88 100644 --- a/apps/roam/src/utils/createDiscourseNode.ts +++ b/apps/roam/src/utils/createDiscourseNode.ts @@ -40,6 +40,147 @@ const stripTemplateUids = (nodes: InputTextNode[]): InputTextNode[] => }; }); +const handleImageCreation = async ({ + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, +}: { + pageUid: string; + discourseNodes: ReturnType; + configPageUid: string; + imageUrl?: string; + extensionAPI?: OnloadArgs["extensionAPI"]; + text: 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 }); + 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; + await createOrUpdateImageBlock(imagePlaceholderUid); + } else { + await createOrUpdateImageBlock(); + } + } +}; + +export const createBlocksFromTemplate = async ({ + templateChildren, + pageUid, + order = 0, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, +}: { + templateChildren: InputTextNode[]; + pageUid: string; + order?: number; + discourseNodes: ReturnType; + configPageUid: string; + imageUrl?: string; + extensionAPI?: OnloadArgs["extensionAPI"]; + text: string; +}) => { + const createBlocks = async () => { + await Promise.all( + stripTemplateUids(templateChildren).map((node, templateOrder) => + createBlock({ + node, + order: order + templateOrder, + parentUid: pageUid, + }), + ), + ); + + await handleImageCreation({ + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, + }); + }; + + const hasSmartBlockSyntax = (node: InputTextNode): boolean => { + if (node.text.includes("<%")) return true; + if (node.children) return node.children.some(hasSmartBlockSyntax); + return false; + }; + const useSmartBlocks = templateChildren.some(hasSmartBlockSyntax); + const legacyTemplateNode = getSubTree({ + tree: getFullTreeByParentUid(configPageUid).children, + key: "template", + }); + const canUseLegacySmartBlock = !!legacyTemplateNode.uid; + + 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 createBlocks(); + } else if ( + useSmartBlocks && + canUseLegacySmartBlock && + window.roamjs?.extension?.smartblocks + ) { + void window.roamjs.extension.smartblocks?.triggerSmartblock({ + srcUid: legacyTemplateNode.uid, + targetUid: pageUid, + }); + await handleImageCreation({ + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, + }); + } else { + await createBlocks(); + } +}; + const createDiscourseNode = async ({ text, configPageUid, @@ -69,49 +210,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 }); - 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( @@ -163,54 +261,15 @@ const createDiscourseNode = async ({ DISCOURSE_NODE_KEYS.template, ]) ?? []; - const createBlocksFromTemplate = async () => { - await Promise.all( - stripTemplateUids(templateChildren).map((node, order) => - createBlock({ - node, - order, - parentUid: pageUid, - }), - ), - ); - - // Add image to page if imageUrl is provided - await handleImageCreation(pageUid); - }; - - const hasSmartBlockSyntax = (node: InputTextNode): boolean => { - if (node.text.includes("<%")) return true; - if (node.children) return node.children.some(hasSmartBlockSyntax); - return false; - }; - const useSmartBlocks = templateChildren.some(hasSmartBlockSyntax); - const legacyTemplateNode = getSubTree({ - tree: getFullTreeByParentUid(configPageUid).children, - key: "template", + await createBlocksFromTemplate({ + templateChildren, + pageUid, + discourseNodes, + configPageUid, + imageUrl, + extensionAPI, + text, }); - const canUseLegacySmartBlock = !!legacyTemplateNode.uid; - - 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 && - canUseLegacySmartBlock && - window.roamjs?.extension?.smartblocks - ) { - void window.roamjs.extension.smartblocks?.triggerSmartblock({ - srcUid: legacyTemplateNode.uid, - targetUid: pageUid, - }); - await handleImageCreation(pageUid); - } else { - await createBlocksFromTemplate(); - } handleOpenInSidebar(pageUid); return pageUid; }; diff --git a/apps/roam/src/utils/registerCommandPaletteCommands.ts b/apps/roam/src/utils/registerCommandPaletteCommands.ts index 6f0834773..00ec7c610 100644 --- a/apps/roam/src/utils/registerCommandPaletteCommands.ts +++ b/apps/roam/src/utils/registerCommandPaletteCommands.ts @@ -19,17 +19,23 @@ import { getOverlayHandler, onPageRefObserverChange, } from "./pageRefObserverHandlers"; +import findDiscourseNode from "~/utils/findDiscourseNode"; +import { createBlocksFromTemplate } from "~/utils/createDiscourseNode"; +import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid"; import { HIDE_METADATA_KEY } from "~/data/userSettings"; import posthog from "posthog-js"; import { + getDiscourseNodeSetting, getPersonalSetting, setPersonalSetting, setGlobalSetting, } from "~/components/settings/utils/accessors"; import { + DISCOURSE_NODE_KEYS, PERSONAL_KEYS, QUERY_KEYS, } from "~/components/settings/utils/settingKeys"; +import { InputTextNode } from "roamjs-components/types"; import { extractRef } from "roamjs-components/util"; import discourseConfigRef from "~/utils/discourseConfigRef"; import { @@ -132,6 +138,71 @@ 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 templateChildren = + getDiscourseNodeSetting(configPageUid, [ + DISCOURSE_NODE_KEYS.template, + ]) ?? []; + if (templateChildren.length > 0) { + const existingChildren = getFullTreeByParentUid(pageUid).children || []; + await createBlocksFromTemplate({ + templateChildren, + pageUid, + order: existingChildren.length, + discourseNodes: getDiscourseNodes(), + configPageUid, + extensionAPI, + text: formattedTitle, + }); + } + + return pageUid; + }, + onSuccess: async () => {}, + onClose: () => {}, + }); +}; + export const registerCommandPaletteCommands = (onloadArgs: OnloadArgs) => { const { extensionAPI } = onloadArgs; @@ -332,6 +403,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), );