diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index 0c55f7d8c..af07e05ce 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -42,6 +42,7 @@ type Props = { isShift?: boolean; menuMaxHeight?: number; settingsSnapshot?: SettingsSnapshot; + onTagAdded?: (newText: string) => void; }; const NodeMenu = ({ @@ -53,6 +54,7 @@ const NodeMenu = ({ isShift, menuMaxHeight, settingsSnapshot, + onTagAdded, }: { onClose: () => void } & Props) => { const isInitialTextSelected = !!textarea && textarea.selectionStart !== textarea.selectionEnd; @@ -161,7 +163,7 @@ const NodeMenu = ({ const tag = menuItem.getAttribute("data-tag") || ""; if (!tag) return; - const addTagToBlock = () => { + const addTagToBlock = async () => { const textToInsert = `${ selectionStart === 0 ? "" : " " }#${tag.replace(/^#/, "")}`; @@ -171,10 +173,11 @@ const NodeMenu = ({ selectionStart, )}${textToInsert}${currentText.substring(selectionStart)}`; - void updateBlock({ text: newText, uid: targetBlockUid }); + await updateBlock({ text: newText, uid: targetBlockUid }); posthog.capture("Discourse Tag: Created via Node Menu", { tag, }); + onTagAdded?.(newText); }; // timeout required to ensure the block is updated setTimeout(() => void addTagToBlock(), 100); @@ -184,7 +187,15 @@ const NodeMenu = ({ } onClose(); }, - [menuRef, targetBlockUid, onClose, textarea, extensionAPI, showNodeTypes], + [ + menuRef, + targetBlockUid, + onClose, + textarea, + extensionAPI, + showNodeTypes, + onTagAdded, + ], ); const keydownListener = useCallback( diff --git a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx index 6a5733800..fe5d9bd90 100644 --- a/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx +++ b/apps/roam/src/components/canvas/DiscourseNodeUtil.tsx @@ -42,6 +42,7 @@ import { getRelationColor } from "./DiscourseRelationShape/DiscourseRelationUtil import { getPersonalSetting } from "~/components/settings/utils/accessors"; import { PERSONAL_KEYS } from "~/components/settings/utils/settingKeys"; import DiscourseContextOverlay from "~/components/DiscourseContextOverlay"; +import NodeMenu from "~/components/DiscourseNodeMenu"; import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors"; import { render as renderToast } from "roamjs-components/components/Toast"; import { RenderRoamBlockString } from "~/utils/roamReactComponents"; @@ -459,6 +460,8 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil { // eslint-disable-next-line react-hooks/rules-of-hooks const [overlayMounted, setOverlayMounted] = useState(false); // eslint-disable-next-line react-hooks/rules-of-hooks + const [addTagMenuKey, setAddTagMenuKey] = useState(0); + // eslint-disable-next-line react-hooks/rules-of-hooks const dialogRenderedRef = useRef(false); // Detect discourse node tags in block text for blck-node shapes @@ -487,6 +490,32 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil { return null; }, [shape]); + const showAddTagButton = + getDiscourseNodeTypeId({ shape }) === "blck-node" && + isLiveBlock(shape.props.uid) && + Object.values(discourseContext.nodes).some( + (n) => n.backedBy === "user" && n.tag, + ); + + const handleTagAdded = (newText: string) => { + const updateShape = async () => { + if (!extensionAPI) return; + const { h, w, imageUrl } = await calcCanvasNodeSizeAndImg({ + nodeText: newText, + uid: shape.props.uid, + nodeType: getDiscourseNodeTypeId({ shape }), + extensionAPI, + }); + this.updateProps(shape.id, shape.type, { + title: newText, + h, + w, + imageUrl, + }); + }; + void updateShape(); + }; + const { backgroundColor, textColor } = this.getColors(shape); const showEmbeddedRoamBlock = !isPageUid(shape.props.uid) && isLiveBlock(shape.props.uid); @@ -619,114 +648,145 @@ export class DiscourseNodeUtil extends BaseBoxShapeUtil { className="relative flex h-full min-h-0 w-full min-w-0 flex-col" style={{ pointerEvents: "all" }} > - {/* Open in Sidebar Button */} - + } + /> + )} + + {/* Convert to Node Type Button */} + {matchedNodeForConversion && ( + - )} + + Convert to {matchedNodeForConversion.node.text} + + + )} + {shape.props.imageUrl && isKeyImage === "true" ? (