From a976b32780106df0a2f06aba71d8040b898c512a Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 10 Sep 2026 19:50:46 -0400 Subject: [PATCH] ENG-1884 Offer Convert To on canvas shapes that carry text The canvas Convert To submenu and its ?C dialog only accepted text and image shapes, so sticky notes and labelled rectangles fell through to a no-op. Both gates now call one helper, getConvertibleShapeText, which returns the dialog's initial text or null when the shape cannot convert. Geo and note shapes qualify only when they carry text. Co-Authored-By: Claude Opus 5 --- .../src/components/canvas/ConvertToDialog.tsx | 6 +++--- .../canvas/convertShapeToDiscourseNode.ts | 14 ++++++++++++++ apps/roam/src/components/canvas/uiOverrides.tsx | 16 +++++++--------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/apps/roam/src/components/canvas/ConvertToDialog.tsx b/apps/roam/src/components/canvas/ConvertToDialog.tsx index 93298b983..f4f91e1d4 100644 --- a/apps/roam/src/components/canvas/ConvertToDialog.tsx +++ b/apps/roam/src/components/canvas/ConvertToDialog.tsx @@ -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"; @@ -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 ( diff --git a/apps/roam/src/components/canvas/convertShapeToDiscourseNode.ts b/apps/roam/src/components/canvas/convertShapeToDiscourseNode.ts index 13bd704ba..7083267f7 100644 --- a/apps/roam/src/components/canvas/convertShapeToDiscourseNode.ts +++ b/apps/roam/src/components/canvas/convertShapeToDiscourseNode.ts @@ -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(); + // Geo and note shapes convert only when they carry text. + return shape.type === "text" || text ? text : null; +}; + export const uploadImageShapeToRoam = async ({ editor, shape, diff --git a/apps/roam/src/components/canvas/uiOverrides.tsx b/apps/roam/src/components/canvas/uiOverrides.tsx index e009160d3..957c31eb4 100644 --- a/apps/roam/src/components/canvas/uiOverrides.tsx +++ b/apps/roam/src/components/canvas/uiOverrides.tsx @@ -5,7 +5,6 @@ import { TLImageShape, TLShape, TLShapeId, - TLTextShape, TLUiDialogProps, TLUiOverrides, TLUiTranslationKey, @@ -57,6 +56,7 @@ import { import { replaceShapeWithDiscourseNode, uploadImageShapeToRoam, + getConvertibleShapeText, } from "./convertShapeToDiscourseNode"; import { AddReferencedNodeType } from "./DiscourseRelationShape/DiscourseRelationTool"; import { @@ -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({ @@ -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 }); }; type ArrowBoundNodeInfo = { @@ -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", @@ -454,7 +452,7 @@ export const CustomContextMenu = ({ /> )} - {(isTextSelected || isImageSelected) && ( + {selectedShape && convertibleText !== null && ( {allNodes