Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions apps/roam/src/components/ModifyNodeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
onSuccess: (result: {
text: string;
uid: string;
Expand All @@ -69,6 +73,7 @@ const ModifyNodeDialog = ({
includeDefaultNodes = false,
imageUrl,
disableNodeTypeChange = false,
createOverride,
onSuccess,
onClose,
}: RoamOverlayProps<ModifyNodeDialogProps>) => {
Expand Down Expand Up @@ -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}]]`;
Expand Down
239 changes: 149 additions & 90 deletions apps/roam/src/utils/createDiscourseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,147 @@ const stripTemplateUids = (nodes: InputTextNode[]): InputTextNode[] =>
};
});

const handleImageCreation = async ({

@sid597 sid597 May 12, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled out function from line 53, nothing changed in the implementation of this.

pageUid,
discourseNodes,
configPageUid,
imageUrl,
extensionAPI,
text,
}: {
pageUid: string;
discourseNodes: ReturnType<typeof getDiscourseNodes>;
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<typeof getDiscourseNodes>;
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({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void That is only to mark the promise as intentionally not awaited. Runtime intent is the same

srcUid: legacyTemplateNode.uid,
targetUid: pageUid,
});
await handleImageCreation({
pageUid,
discourseNodes,
configPageUid,
imageUrl,
extensionAPI,
text,
});
} else {
await createBlocks();
}
};

const createDiscourseNode = async ({
text,
configPageUid,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
};
Expand Down
Loading
Loading