Skip to content
Open
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
16 changes: 11 additions & 5 deletions apps/obsidian/src/components/canvas/CustomContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
} from "tldraw";
import type { TFile } from "obsidian";
import { usePlugin } from "~/components/PluginContext";
import { convertToDiscourseNode } from "./utils/convertToDiscourseNode";
import {
canConvertShapeToNode,
convertToDiscourseNode,
} from "./utils/convertToDiscourseNode";
import {
convertArrowToDiscourseRelation,
getValidRelationTypesForArrow,
Expand All @@ -34,9 +37,11 @@ export const CustomContextMenu = ({
[editor],
);

const shouldShowConvertTo =
selectedShape &&
(selectedShape.type === "text" || selectedShape.type === "image");
const shouldShowConvertTo = useValue(
"shouldShowConvertTo",
() => canConvertShapeToNode(editor, editor.getOnlySelectedShape()),
[editor],
);

const isReadonly = useValue(
"isReadonly",
Expand Down Expand Up @@ -86,7 +91,7 @@ export const CustomContextMenu = ({
</TldrawUiMenuSubmenu>
</TldrawUiMenuGroup>
)}
{shouldShowConvertTo && (
{shouldShowConvertTo && selectedShape && (
<TldrawUiMenuGroup id="convert-to">
<TldrawUiMenuSubmenu id="convert-to-submenu" label="Convert To">
{plugin.settings.nodeTypes.map((nodeType) => (
Expand All @@ -95,6 +100,7 @@ export const CustomContextMenu = ({
id={`convert-to-${nodeType.id}`}
label={"Convert to " + nodeType.name}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's take this opportunity to drop the duplicate "Convert to" from the submenu

We did this already in Roam:
ENG-1336: Remove redundant "Convert To" in tldraw context menu

Image

icon="file-type"
disabled={isReadonly}
onSelect={() => {
void convertToDiscourseNode({
editor,
Expand Down
47 changes: 32 additions & 15 deletions apps/obsidian/src/components/canvas/utils/convertToDiscourseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
TLShape,
createShapeId,
TLAssetId,
TLTextShape,
TLRichText,
TLShapeId,
renderPlaintextFromRichText,
} from "tldraw";
Expand All @@ -20,6 +20,26 @@ import { showToast } from "./toastUtils";
import ModifyNodeModal from "~/components/ModifyNodeModal";
import { calcDiscourseNodeSize } from "~/utils/calcDiscourseNodeSize";

// Only shapes storing a richText prop. Arrow is not one (it uses props.text) and
// owns the "Relation" submenu instead.
const RICH_TEXT_SHAPE_TYPES: readonly string[] = ["text", "geo", "note"];

const getShapeText = (editor: Editor, shape: TLShape): string => {
if (!RICH_TEXT_SHAPE_TYPES.includes(shape.type)) return "";
const { richText } = shape.props as { richText?: TLRichText };
if (!richText) return "";
return renderPlaintextFromRichText(editor, richText).trim();
};

export const canConvertShapeToNode = (
editor: Editor,
shape: TLShape | null,
): boolean => {
if (!shape) return false;
// Images are gated at conversion time, not here: the asset may not resolve to a vault file.
return shape.type === "image" || getShapeText(editor, shape) !== "";
};

type ConvertToDiscourseNodeArgs = {
editor: Editor;
shape: TLShape;
Expand All @@ -34,15 +54,15 @@ export const convertToDiscourseNode = async (
try {
const { shape } = args;

if (shape.type === "text") {
return await convertTextShapeToNode(args);
} else if (shape.type === "image") {
if (shape.type === "image") {
return await convertImageShapeToNode(args);
} else if (RICH_TEXT_SHAPE_TYPES.includes(shape.type)) {
return convertTextBearingShapeToNode(args);
Comment on lines +59 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve frame placement when converting new shapes

When a newly supported geo or note is a child of a frame, this dispatch eventually passes its parent-relative shape.x/shape.y to editor.createShape without preserving parentId or converting the coordinates to page space. The replacement node can therefore jump elsewhere on the canvas or be assigned to the wrong container. Preserve the original parent and transform, or derive page coordinates before creating the node.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@trangdoan982 might be worth looking into

Comment thread
mdroidian marked this conversation as resolved.
} else {
showToast({
severity: "warning",
title: "Cannot Convert",
description: "Only text and image shapes can be converted",
description: "Only shapes with text or images can be converted",
targetCanvasId: args.canvasFile.path,
});
}
Expand All @@ -57,23 +77,20 @@ export const convertToDiscourseNode = async (
}
};

const convertTextShapeToNode = ({
const convertTextBearingShapeToNode = ({
editor,
shape,
nodeType,
plugin,
canvasFile,
}: ConvertToDiscourseNodeArgs): TLShapeId | undefined => {
const text = renderPlaintextFromRichText(
editor,
(shape as TLTextShape).props.richText,
);
const text = getShapeText(editor, shape);

if (!text.trim()) {
if (!text) {
showToast({
severity: "warning",
title: "Cannot Convert",
description: "Text shape has no content to convert",
description: "Shape has no text to convert",
targetCanvasId: canvasFile.path,
});
return undefined;
Expand All @@ -85,7 +102,7 @@ const convertTextShapeToNode = ({
nodeTypes: plugin.settings.nodeTypes,
plugin,
initialNodeType: nodeType,
initialTitle: text.trim(),
initialTitle: text,
onSubmit: async ({
nodeType: selectedNodeType,
title,
Expand Down Expand Up @@ -116,11 +133,11 @@ const convertTextShapeToNode = ({
showToast({
severity: "success",
title: "Shape Converted",
description: `Converted text to ${selectedNodeType.name}`,
description: `Converted shape to ${selectedNodeType.name}`,
targetCanvasId: canvasFile.path,
});
} catch (error) {
console.error("Error creating node from text:", error);
console.error("Error creating node from shape text:", error);
throw error;
}
},
Expand Down