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
6 changes: 3 additions & 3 deletions apps/roam/src/components/canvas/ConvertToDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OnloadArgs } from "roamjs-components/types";
import { Editor } from "tldraw";
import { DiscourseNode } from "~/utils/getDiscourseNodes";
import { getOnSelectForShape } from "./uiOverrides";
import { getConvertibleShapeText } from "./convertShapeToDiscourseNode";
import { Dialog, Button, Classes } from "@blueprintjs/core";
import posthog from "posthog-js";

Expand All @@ -21,13 +22,12 @@ const ConvertToDialog = ({
}) => {
if (!editor) return null;
const selectedShapes = editor.getSelectedShapes();
const isTextSelected = selectedShapes[0]?.type === "text";
const isImageSelected = selectedShapes[0]?.type === "image";
const oneShapeSelected = selectedShapes.length === 1;
const isNodeSelected =
(isTextSelected || isImageSelected) && oneShapeSelected;
oneShapeSelected && getConvertibleShapeText(selectedShapes[0]) !== null;

let errorMessage = "Please select a text or image shape";
let errorMessage = "Please select an image, or a shape with text in it";
if (!oneShapeSelected) errorMessage = "Please select only one shape";

return (
Expand Down
14 changes: 14 additions & 0 deletions apps/roam/src/components/canvas/convertShapeToDiscourseNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ import type { OnloadArgs } from "roamjs-components/types";
import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg";
import { DISCOURSE_NODE_SHAPE_TYPE } from "./DiscourseNodeUtil";

const TEXT_SHAPE_TYPES = ["text", "geo", "note"];

export const getConvertibleShapeText = (
shape?: TLShape | null,
): string | null => {
if (!shape || shape.isLocked) return null;
if (shape.type === "image") return "";
if (!TEXT_SHAPE_TYPES.includes(shape.type)) return null;
if (!("text" in shape.props)) return null;
const text = shape.props.text.trim();

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 the original shape text after the emptiness check

When a text, note, or geo shape begins or ends with whitespace—for example, an intentionally indented multiline snippet—this trimmed value is passed to the node dialog, so conversion silently changes the shape's content and existing text shapes no longer behave as before. Use the trimmed value only to decide whether a note or geo shape is empty, while returning the original shape.props.text.

Useful? React with 👍 / 👎.

// Geo and note shapes convert only when they carry text.
return shape.type === "text" || text ? text : null;
};

export const uploadImageShapeToRoam = async ({
editor,
shape,
Expand Down
16 changes: 7 additions & 9 deletions apps/roam/src/components/canvas/uiOverrides.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
TLImageShape,
TLShape,
TLShapeId,
TLTextShape,
TLUiDialogProps,
TLUiOverrides,
TLUiTranslationKey,
Expand Down Expand Up @@ -57,6 +56,7 @@ import {
import {
replaceShapeWithDiscourseNode,
uploadImageShapeToRoam,
getConvertibleShapeText,
} from "./convertShapeToDiscourseNode";
import { AddReferencedNodeType } from "./DiscourseRelationShape/DiscourseRelationTool";
import {
Expand Down Expand Up @@ -201,6 +201,9 @@ export const getOnSelectForShape = ({
});
};

const shapeText = getConvertibleShapeText(shape);
if (shapeText === null) return () => {};

if (shape.type === "image") {
return async () => {
const src = await uploadImageShapeToRoam({
Expand All @@ -212,13 +215,8 @@ export const getOnSelectForShape = ({

openDialogAndCreateShape({ initialText, imageUrl: src });
};
} else if (shape.type === "text") {
return () => {
const { text } = (shape as TLTextShape).props;
openDialogAndCreateShape({ initialText: text });
};
}
return () => {};
return () => openDialogAndCreateShape({ initialText: shapeText });

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 the parent when converting nested shapes

When one of the newly supported geo or note shapes is inside a frame or group, this route eventually calls replaceShapeWithDiscourseNode, which copies only the shape's local x/y and creates the replacement without its parentId. Tldraw then interprets those coordinates in page or newly inferred parent space, so the replacement can move far from the original or leave its container; pass the original parent and parent-relative transform through the replacement.

Useful? React with 👍 / 👎.

};

type ArrowBoundNodeInfo = {
Expand Down Expand Up @@ -407,7 +405,7 @@ export const CustomContextMenu = ({
const shareableResults = getShareableCanvasSelectionResults({
shapes: selectedShapes,
});
const isTextSelected = selectedShape?.type === "text";
const convertibleText = getConvertibleShapeText(selectedShape);
const isImageSelected = selectedShape?.type === "image";
const arrowRelationOptions = useValue(
"arrowRelationOptions",
Expand Down Expand Up @@ -454,7 +452,7 @@ export const CustomContextMenu = ({
/>
</TldrawUiMenuGroup>
)}
{(isTextSelected || isImageSelected) && (
{selectedShape && convertibleText !== null && (
<TldrawUiMenuGroup id="convert-to-group">
<TldrawUiMenuSubmenu id="convert-to-submenu" label="Convert To">
{allNodes
Expand Down