From c979a07c5c05dc0595072f2b3300a6eeef1eee64 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 28 Oct 2025 16:49:29 -0400 Subject: [PATCH 1/4] curr progress --- .../components/canvas/CustomContextMenu.tsx | 61 ++++ .../components/canvas/TldrawViewComponent.tsx | 3 + .../canvas/utils/convertToDiscourseNode.ts | 311 ++++++++++++++++++ apps/obsidian/tsconfig.json | 2 +- 4 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 apps/obsidian/src/components/canvas/CustomContextMenu.tsx create mode 100644 apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts diff --git a/apps/obsidian/src/components/canvas/CustomContextMenu.tsx b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx new file mode 100644 index 000000000..def4b8c87 --- /dev/null +++ b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx @@ -0,0 +1,61 @@ +import { + DefaultContextMenu, + TldrawUiMenuGroup, + TldrawUiMenuSubmenu, + TldrawUiMenuItem, + useEditor, + TLUiContextMenuProps, + DefaultContextMenuContent, +} from "tldraw"; +import type { TFile } from "obsidian"; +import { usePlugin } from "~/components/PluginContext"; +import { convertToDiscourseNode } from "./utils/convertToDiscourseNode"; + +type CustomContextMenuProps = { + canvasFile: TFile; + props: TLUiContextMenuProps; +}; + +export const CustomContextMenu = ({ canvasFile, props }: CustomContextMenuProps) => { + const editor = useEditor(); + const plugin = usePlugin(); + + // Get selected shapes + const selectedShapes = editor.getSelectedShapes(); + + // Check if we have exactly one text or image shape selected + const shouldShowConvertTo = + selectedShapes.length === 1 && + selectedShapes[0] && + (selectedShapes[0].type === "text" || selectedShapes[0].type === "image"); + + return ( + + + {shouldShowConvertTo && selectedShapes[0] && ( + + + {plugin.settings.nodeTypes.map((nodeType) => ( + { + void convertToDiscourseNode({ + editor, + shape: selectedShapes[0]!, + nodeType, + plugin, + canvasFile, + }); + }} + /> + ))} + + + )} + + ); +} + diff --git a/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx b/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx index 9149baf44..c31d2f1cb 100644 --- a/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx @@ -50,6 +50,7 @@ import ToastListener from "./ToastListener"; import { RelationsOverlay } from "./overlays/RelationOverlay"; import { showToast } from "./utils/toastUtils"; import { WHITE_LOGO_SVG } from "~/icons"; +import { CustomContextMenu } from "./CustomContextMenu"; type TldrawPreviewProps = { store: TLStore; @@ -337,6 +338,8 @@ export const TldrawPreviewComponent = ({ }, }} components={{ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + ContextMenu: (props) => , /* eslint-disable-next-line @typescript-eslint/naming-convention */ StylePanel: () => { const tools = useTools(); diff --git a/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts new file mode 100644 index 000000000..41b8b6691 --- /dev/null +++ b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts @@ -0,0 +1,311 @@ +import { Editor, TLShape, createShapeId, TLAssetId, TLTextShape } from "tldraw"; +import type { TFile } from "obsidian"; +import { DiscourseNode } from "~/types"; +import DiscourseGraphPlugin from "~/index"; +import { createDiscourseNode } from "~/utils/createNode"; +import { + addWikilinkBlockrefForFile, + extractBlockRefId, + resolveLinkedTFileByBlockRef, +} from "~/components/canvas/stores/assetStore"; +import { showToast } from "./toastUtils"; +import { CreateNodeModal } from "~/components/CreateNodeModal"; + +type ConvertToDiscourseNodeArgs = { + editor: Editor; + shape: TLShape; + nodeType: DiscourseNode; + plugin: DiscourseGraphPlugin; + canvasFile: TFile; +}; + +/** + * Extracts text content from a text shape + */ +const getTextShapeContent = (shape: TLTextShape): string => { + console.log("shape", shape); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return shape.props.richText.content[0].content[0].text; +}; + +/** + * Gets the image file from an image shape + */ +const getImageFileFromShape = async ({ + shape, + editor, + plugin, + canvasFile, +}: { + shape: TLShape; + editor: Editor; + plugin: DiscourseGraphPlugin; + canvasFile: TFile; +}): Promise => { + if (shape.type !== "image") return null; + + try { + // Get the asset ID from the image shape + const assetId = "assetId" in shape.props ? (shape.props.assetId as TLAssetId) : null; + if (!assetId) return null; + + // Get the asset from the editor + const asset = editor.getAsset(assetId); + if (!asset) return null; + + // Extract the blockref from the asset src + const src = asset.props.src; + if (!src) return null; + + const blockRefId = extractBlockRefId(src); + if (!blockRefId) return null; + + // Resolve the linked file + const canvasFileCache = plugin.app.metadataCache.getFileCache(canvasFile); + if (!canvasFileCache) return null; + + return await resolveLinkedTFileByBlockRef({ + app: plugin.app, + canvasFile, + blockRefId, + canvasFileCache, + }); + } catch (error) { + console.error("Error getting image file from shape:", error); + return null; + } +}; + +/** + * Converts a text shape to a discourse node + */ +const convertTextShapeToNode = async ({ + editor, + shape, + nodeType, + plugin, + canvasFile, +}: ConvertToDiscourseNodeArgs): Promise => { + const text = getTextShapeContent(shape as TLTextShape); + + if (!text.trim()) { + showToast({ + severity: "warning", + title: "Cannot Convert", + description: "Text shape has no content to convert", + targetCanvasId: canvasFile.path, + }); + return; + } + + // Create the discourse node file + const createdFile = await createDiscourseNode({ + plugin, + nodeType, + text: text.trim(), + }); + + if (!createdFile) { + throw new Error("Failed to create discourse node file"); + } + + // Create the discourse node shape + await createDiscourseNodeShape({ + editor, + shape, + createdFile, + nodeType, + plugin, + canvasFile, + }); + + showToast({ + severity: "success", + title: "Shape Converted", + description: `Converted text to ${nodeType.name}`, + targetCanvasId: canvasFile.path, + }); +}; + +/** + * Converts an image shape to a discourse node + */ +const convertImageShapeToNode = async ({ + editor, + shape, + nodeType, + plugin, + canvasFile, +}: ConvertToDiscourseNodeArgs): Promise => { + // Get the image file from the shape + const imageFile = await getImageFileFromShape({ shape, editor, plugin, canvasFile }); + + // Open modal for user to input the node name + const modal = new CreateNodeModal(plugin.app, { + nodeTypes: plugin.settings.nodeTypes, + plugin, + initialNodeType: nodeType, + initialTitle: imageFile?.basename || "Image", + onNodeCreate: async (selectedNodeType: DiscourseNode, title: string) => { + try { + // Create the discourse node file + const createdFile = await createDiscourseNode({ + plugin, + nodeType: selectedNodeType, + text: title, + }); + + if (!createdFile) { + throw new Error("Failed to create discourse node file"); + } + + // If we have an image file, embed it in the new node + if (imageFile) { + await embedImageInNode(createdFile, imageFile, plugin); + } + + // Create the discourse node shape + await createDiscourseNodeShape({ + editor, + shape, + createdFile, + nodeType: selectedNodeType, + plugin, + canvasFile, + }); + + showToast({ + severity: "success", + title: "Shape Converted", + description: `Converted image to ${selectedNodeType.name}`, + targetCanvasId: canvasFile.path, + }); + } catch (error) { + console.error("Error creating node from image:", error); + throw error; + } + }, + }); + + modal.open(); +}; + +/** + * Embeds an image in a discourse node file + */ +const embedImageInNode = async ( + nodeFile: TFile, + imageFile: TFile, + plugin: DiscourseGraphPlugin, +): Promise => { + const imageLink = plugin.app.metadataCache.fileToLinktext( + imageFile, + nodeFile.path, + ); + const imageEmbed = `![[${imageLink}]]`; + + // Add image after the frontmatter + await plugin.app.vault.process(nodeFile, (data: string) => { + const fileCache = plugin.app.metadataCache.getFileCache(nodeFile); + const { start, end } = fileCache?.frontmatterPosition ?? { + start: { offset: 0 }, + end: { offset: 0 }, + }; + + const frontmatter = data.slice(start.offset, end.offset); + const rest = data.slice(end.offset); + + return `${frontmatter}\n\n${imageEmbed}\n${rest}`; + }); +}; + +/** + * Creates a discourse node shape in the canvas + */ +const createDiscourseNodeShape = async ({ + editor, + shape, + createdFile, + nodeType, + plugin, + canvasFile, +}: { + editor: Editor; + shape: TLShape; + createdFile: TFile; + nodeType: DiscourseNode; + plugin: DiscourseGraphPlugin; + canvasFile: TFile; +}): Promise => { + // Create the blockref link + const src = await addWikilinkBlockrefForFile({ + app: plugin.app, + canvasFile, + linkedFile: createdFile, + }); + + // Get the position and size of the original shape + const { x, y } = shape; + const width = "w" in shape.props ? Number(shape.props.w) : 200; + const height = "h" in shape.props ? Number(shape.props.h) : 100; + + // Create the new discourse node shape + const shapeId = createShapeId(); + editor.createShape({ + id: shapeId, + type: "discourse-node", + x, + y, + props: { + w: Math.max(width, 200), + h: Math.max(height, 100), + src: src ?? "", + title: createdFile.basename, + nodeTypeId: nodeType.id, + }, + }); + + // Delete the original shape + editor.deleteShape(shape.id); + + // Select the new shape + editor.setSelectedShapes([shapeId]); + + // Mark history point + editor.markHistoryStoppingPoint( + `convert ${shape.type} to discourse node`, + ); +}; + +/** + * Converts a text or image shape to a discourse node + */ +export const convertToDiscourseNode = async ( + args: ConvertToDiscourseNodeArgs, +): Promise => { + try { + const { shape } = args; + + if (shape.type === "text") { + await convertTextShapeToNode(args); + } else if (shape.type === "image") { + await convertImageShapeToNode(args); + } else { + showToast({ + severity: "warning", + title: "Cannot Convert", + description: "Only text and image shapes can be converted", + targetCanvasId: args.canvasFile.path, + }); + } + } catch (error) { + console.error("Error converting shape to discourse node:", error); + showToast({ + severity: "error", + title: "Conversion Failed", + description: `Could not convert shape: ${error instanceof Error ? error.message : "Unknown error"}`, + targetCanvasId: args.canvasFile.path, + }); + } +}; + diff --git a/apps/obsidian/tsconfig.json b/apps/obsidian/tsconfig.json index 56146f6ec..e0ca17ce5 100644 --- a/apps/obsidian/tsconfig.json +++ b/apps/obsidian/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@repo/typescript-config/react-library.json", - "include": ["**/*.ts"], + "include": ["**/*.ts", "**/*.tsx"], "compilerOptions": { "baseUrl": ".", "rootDir": ".", From 4de4f2a3a09aa2b1df6a68b447b2d1c134d09f17 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 28 Oct 2025 20:18:31 -0400 Subject: [PATCH 2/4] feature complete --- .../components/canvas/CustomContextMenu.tsx | 25 +- .../components/canvas/TldrawViewComponent.tsx | 11 +- .../canvas/utils/convertToDiscourseNode.ts | 256 ++++++++---------- 3 files changed, 132 insertions(+), 160 deletions(-) diff --git a/apps/obsidian/src/components/canvas/CustomContextMenu.tsx b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx index def4b8c87..32b16b428 100644 --- a/apps/obsidian/src/components/canvas/CustomContextMenu.tsx +++ b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx @@ -6,6 +6,7 @@ import { useEditor, TLUiContextMenuProps, DefaultContextMenuContent, + useValue, } from "tldraw"; import type { TFile } from "obsidian"; import { usePlugin } from "~/components/PluginContext"; @@ -16,23 +17,27 @@ type CustomContextMenuProps = { props: TLUiContextMenuProps; }; -export const CustomContextMenu = ({ canvasFile, props }: CustomContextMenuProps) => { +export const CustomContextMenu = ({ + canvasFile, + props, +}: CustomContextMenuProps) => { const editor = useEditor(); const plugin = usePlugin(); - // Get selected shapes - const selectedShapes = editor.getSelectedShapes(); + const selectedShape = useValue( + "selectedShape", + () => editor.getOnlySelectedShape(), + [editor], + ); - // Check if we have exactly one text or image shape selected const shouldShowConvertTo = - selectedShapes.length === 1 && - selectedShapes[0] && - (selectedShapes[0].type === "text" || selectedShapes[0].type === "image"); + selectedShape && + (selectedShape?.type === "text" || selectedShape?.type === "image"); return ( - {shouldShowConvertTo && selectedShapes[0] && ( + {shouldShowConvertTo && ( {plugin.settings.nodeTypes.map((nodeType) => ( @@ -44,7 +49,7 @@ export const CustomContextMenu = ({ canvasFile, props }: CustomContextMenuProps) onSelect={() => { void convertToDiscourseNode({ editor, - shape: selectedShapes[0]!, + shape: selectedShape, nodeType, plugin, canvasFile, @@ -57,5 +62,5 @@ export const CustomContextMenu = ({ canvasFile, props }: CustomContextMenuProps) )} ); -} +}; diff --git a/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx b/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx index c31d2f1cb..46c153f0c 100644 --- a/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx +++ b/apps/obsidian/src/components/canvas/TldrawViewComponent.tsx @@ -338,9 +338,11 @@ export const TldrawPreviewComponent = ({ }, }} components={{ - /* eslint-disable-next-line @typescript-eslint/naming-convention */ - ContextMenu: (props) => , - /* eslint-disable-next-line @typescript-eslint/naming-convention */ + /* eslint-disable @typescript-eslint/naming-convention */ + ContextMenu: (props) => ( + + ), + StylePanel: () => { const tools = useTools(); const isDiscourseNodeSelected = useIsToolSelected( @@ -356,9 +358,7 @@ export const TldrawPreviewComponent = ({ return ; }, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ OnTheCanvas: () => , - /* eslint-disable-next-line @typescript-eslint/naming-convention */ Toolbar: (props) => { const tools = useTools(); const isDiscourseNodeSelected = useIsToolSelected( @@ -381,7 +381,6 @@ export const TldrawPreviewComponent = ({ ); }, - /* eslint-disable-next-line @typescript-eslint/naming-convention */ InFrontOfTheCanvas: () => ( ), diff --git a/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts index 41b8b6691..0604b43c4 100644 --- a/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts +++ b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts @@ -1,8 +1,16 @@ -import { Editor, TLShape, createShapeId, TLAssetId, TLTextShape } from "tldraw"; +import { + Editor, + TLShape, + createShapeId, + TLAssetId, + TLTextShape, + TLShapeId, + renderPlaintextFromRichText, +} from "tldraw"; import type { TFile } from "obsidian"; import { DiscourseNode } from "~/types"; import DiscourseGraphPlugin from "~/index"; -import { createDiscourseNode } from "~/utils/createNode"; +import { createDiscourseNode as createDiscourseNodeFile } from "~/utils/createNode"; import { addWikilinkBlockrefForFile, extractBlockRefId, @@ -19,75 +27,48 @@ type ConvertToDiscourseNodeArgs = { canvasFile: TFile; }; -/** - * Extracts text content from a text shape - */ -const getTextShapeContent = (shape: TLTextShape): string => { - console.log("shape", shape); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return shape.props.richText.content[0].content[0].text; -}; - -/** - * Gets the image file from an image shape - */ -const getImageFileFromShape = async ({ - shape, - editor, - plugin, - canvasFile, -}: { - shape: TLShape; - editor: Editor; - plugin: DiscourseGraphPlugin; - canvasFile: TFile; -}): Promise => { - if (shape.type !== "image") return null; - +export const convertToDiscourseNode = async ( + args: ConvertToDiscourseNodeArgs, +): Promise => { try { - // Get the asset ID from the image shape - const assetId = "assetId" in shape.props ? (shape.props.assetId as TLAssetId) : null; - if (!assetId) return null; - - // Get the asset from the editor - const asset = editor.getAsset(assetId); - if (!asset) return null; - - // Extract the blockref from the asset src - const src = asset.props.src; - if (!src) return null; - - const blockRefId = extractBlockRefId(src); - if (!blockRefId) return null; - - // Resolve the linked file - const canvasFileCache = plugin.app.metadataCache.getFileCache(canvasFile); - if (!canvasFileCache) return null; + const { shape } = args; - return await resolveLinkedTFileByBlockRef({ - app: plugin.app, - canvasFile, - blockRefId, - canvasFileCache, - }); + if (shape.type === "text") { + return await convertTextShapeToNode(args); + } else if (shape.type === "image") { + return await convertImageShapeToNode(args); + } else { + showToast({ + severity: "warning", + title: "Cannot Convert", + description: "Only text and image shapes can be converted", + targetCanvasId: args.canvasFile.path, + }); + } } catch (error) { - console.error("Error getting image file from shape:", error); - return null; + console.error("Error converting shape to discourse node:", error); + showToast({ + severity: "error", + title: "Conversion Failed", + description: `Could not convert shape: ${error instanceof Error ? error.message : "Unknown error"}`, + targetCanvasId: args.canvasFile.path, + }); } }; -/** - * Converts a text shape to a discourse node - */ const convertTextShapeToNode = async ({ editor, shape, nodeType, plugin, canvasFile, -}: ConvertToDiscourseNodeArgs): Promise => { - const text = getTextShapeContent(shape as TLTextShape); - +}: ConvertToDiscourseNodeArgs): Promise => { + const text = renderPlaintextFromRichText( + editor, + (shape as TLTextShape).props.richText, + ); + + console.log("text", text); if (!text.trim()) { showToast({ severity: "warning", @@ -95,11 +76,10 @@ const convertTextShapeToNode = async ({ description: "Text shape has no content to convert", targetCanvasId: canvasFile.path, }); - return; + return undefined; } - // Create the discourse node file - const createdFile = await createDiscourseNode({ + const createdFile = await createDiscourseNodeFile({ plugin, nodeType, text: text.trim(), @@ -109,8 +89,7 @@ const convertTextShapeToNode = async ({ throw new Error("Failed to create discourse node file"); } - // Create the discourse node shape - await createDiscourseNodeShape({ + const shapeId = await createDiscourseNodeShape({ editor, shape, createdFile, @@ -125,31 +104,34 @@ const convertTextShapeToNode = async ({ description: `Converted text to ${nodeType.name}`, targetCanvasId: canvasFile.path, }); + + return shapeId; }; -/** - * Converts an image shape to a discourse node - */ const convertImageShapeToNode = async ({ editor, shape, nodeType, plugin, canvasFile, -}: ConvertToDiscourseNodeArgs): Promise => { - // Get the image file from the shape - const imageFile = await getImageFileFromShape({ shape, editor, plugin, canvasFile }); +}: ConvertToDiscourseNodeArgs): Promise => { + const imageFile = await getImageFileFromShape({ + shape, + editor, + plugin, + canvasFile, + }); + + let shapeId: TLShapeId | undefined; - // Open modal for user to input the node name const modal = new CreateNodeModal(plugin.app, { nodeTypes: plugin.settings.nodeTypes, plugin, initialNodeType: nodeType, - initialTitle: imageFile?.basename || "Image", + initialTitle: "", onNodeCreate: async (selectedNodeType: DiscourseNode, title: string) => { try { - // Create the discourse node file - const createdFile = await createDiscourseNode({ + const createdFile = await createDiscourseNodeFile({ plugin, nodeType: selectedNodeType, text: title, @@ -159,13 +141,11 @@ const convertImageShapeToNode = async ({ throw new Error("Failed to create discourse node file"); } - // If we have an image file, embed it in the new node if (imageFile) { await embedImageInNode(createdFile, imageFile, plugin); } - // Create the discourse node shape - await createDiscourseNodeShape({ + shapeId = await createDiscourseNodeShape({ editor, shape, createdFile, @@ -188,40 +168,10 @@ const convertImageShapeToNode = async ({ }); modal.open(); -}; - -/** - * Embeds an image in a discourse node file - */ -const embedImageInNode = async ( - nodeFile: TFile, - imageFile: TFile, - plugin: DiscourseGraphPlugin, -): Promise => { - const imageLink = plugin.app.metadataCache.fileToLinktext( - imageFile, - nodeFile.path, - ); - const imageEmbed = `![[${imageLink}]]`; - - // Add image after the frontmatter - await plugin.app.vault.process(nodeFile, (data: string) => { - const fileCache = plugin.app.metadataCache.getFileCache(nodeFile); - const { start, end } = fileCache?.frontmatterPosition ?? { - start: { offset: 0 }, - end: { offset: 0 }, - }; - - const frontmatter = data.slice(start.offset, end.offset); - const rest = data.slice(end.offset); - return `${frontmatter}\n\n${imageEmbed}\n${rest}`; - }); + return shapeId; }; -/** - * Creates a discourse node shape in the canvas - */ const createDiscourseNodeShape = async ({ editor, shape, @@ -236,8 +186,7 @@ const createDiscourseNodeShape = async ({ nodeType: DiscourseNode; plugin: DiscourseGraphPlugin; canvasFile: TFile; -}): Promise => { - // Create the blockref link +}): Promise => { const src = await addWikilinkBlockrefForFile({ app: plugin.app, canvasFile, @@ -249,7 +198,6 @@ const createDiscourseNodeShape = async ({ const width = "w" in shape.props ? Number(shape.props.w) : 200; const height = "h" in shape.props ? Number(shape.props.h) : 100; - // Create the new discourse node shape const shapeId = createShapeId(); editor.createShape({ id: shapeId, @@ -265,47 +213,67 @@ const createDiscourseNodeShape = async ({ }, }); - // Delete the original shape editor.deleteShape(shape.id); - - // Select the new shape editor.setSelectedShapes([shapeId]); - // Mark history point - editor.markHistoryStoppingPoint( - `convert ${shape.type} to discourse node`, - ); + editor.markHistoryStoppingPoint(`convert ${shape.type} to discourse node`); + + return shapeId; }; -/** - * Converts a text or image shape to a discourse node - */ -export const convertToDiscourseNode = async ( - args: ConvertToDiscourseNodeArgs, -): Promise => { +const getImageFileFromShape = async ({ + shape, + editor, + plugin, + canvasFile, +}: { + shape: TLShape; + editor: Editor; + plugin: DiscourseGraphPlugin; + canvasFile: TFile; +}): Promise => { + if (shape.type !== "image") return null; + try { - const { shape } = args; + const assetId = + "assetId" in shape.props ? (shape.props.assetId as TLAssetId) : null; + if (!assetId) return null; - if (shape.type === "text") { - await convertTextShapeToNode(args); - } else if (shape.type === "image") { - await convertImageShapeToNode(args); - } else { - showToast({ - severity: "warning", - title: "Cannot Convert", - description: "Only text and image shapes can be converted", - targetCanvasId: args.canvasFile.path, - }); - } - } catch (error) { - console.error("Error converting shape to discourse node:", error); - showToast({ - severity: "error", - title: "Conversion Failed", - description: `Could not convert shape: ${error instanceof Error ? error.message : "Unknown error"}`, - targetCanvasId: args.canvasFile.path, + const asset = editor.getAsset(assetId); + if (!asset) return null; + + const src = asset.props.src; + if (!src) return null; + + const blockRefId = extractBlockRefId(src); + if (!blockRefId) return null; + + const canvasFileCache = plugin.app.metadataCache.getFileCache(canvasFile); + if (!canvasFileCache) return null; + + return await resolveLinkedTFileByBlockRef({ + app: plugin.app, + canvasFile, + blockRefId, + canvasFileCache, }); + } catch (error) { + console.error("Error getting image file from shape:", error); + return null; } }; +const embedImageInNode = async ( + nodeFile: TFile, + imageFile: TFile, + plugin: DiscourseGraphPlugin, +): Promise => { + const imageLink = plugin.app.metadataCache.fileToLinktext( + imageFile, + nodeFile.path, + ); + const imageEmbed = `![[${imageLink}]]`; + await plugin.app.vault.process(nodeFile, (data: string) => { + return `${data}\n${imageEmbed} \n`; + }); +}; From 425e6b21aade3befe6154719a33bc045a0a24f0e Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 28 Oct 2025 20:26:02 -0400 Subject: [PATCH 3/4] address PR comments --- apps/obsidian/src/components/canvas/CustomContextMenu.tsx | 2 +- .../src/components/canvas/utils/convertToDiscourseNode.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/obsidian/src/components/canvas/CustomContextMenu.tsx b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx index 32b16b428..bc557cbe1 100644 --- a/apps/obsidian/src/components/canvas/CustomContextMenu.tsx +++ b/apps/obsidian/src/components/canvas/CustomContextMenu.tsx @@ -32,7 +32,7 @@ export const CustomContextMenu = ({ const shouldShowConvertTo = selectedShape && - (selectedShape?.type === "text" || selectedShape?.type === "image"); + (selectedShape.type === "text" || selectedShape.type === "image"); return ( diff --git a/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts index 0604b43c4..686b2d3e0 100644 --- a/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts +++ b/apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts @@ -68,7 +68,6 @@ const convertTextShapeToNode = async ({ (shape as TLTextShape).props.richText, ); - console.log("text", text); if (!text.trim()) { showToast({ severity: "warning", @@ -199,6 +198,7 @@ const createDiscourseNodeShape = async ({ const height = "h" in shape.props ? Number(shape.props.h) : 100; const shapeId = createShapeId(); + // TODO: Update the imageSrc, width and height of the shape after the key figure is merged editor.createShape({ id: shapeId, type: "discourse-node", @@ -274,6 +274,6 @@ const embedImageInNode = async ( const imageEmbed = `![[${imageLink}]]`; await plugin.app.vault.process(nodeFile, (data: string) => { - return `${data}\n${imageEmbed} \n`; + return `${data}\n${imageEmbed}\n`; }); }; From 3f7341e27b0b5bd990d2ba3aac8de6d9229ababd Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 3 Nov 2025 16:42:10 -0500 Subject: [PATCH 4/4] revert change --- apps/obsidian/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/obsidian/tsconfig.json b/apps/obsidian/tsconfig.json index e0ca17ce5..56146f6ec 100644 --- a/apps/obsidian/tsconfig.json +++ b/apps/obsidian/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "@repo/typescript-config/react-library.json", - "include": ["**/*.ts", "**/*.tsx"], + "include": ["**/*.ts"], "compilerOptions": { "baseUrl": ".", "rootDir": ".",