From 2ba0b5837d4d1cffd7c0e92a9b06efbbb04a2f8d Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 10 Jul 2026 23:23:42 -0700 Subject: [PATCH] fix(rich-markdown-editor): reliable image selection + serialization/paste polish (#5590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(rich-markdown-editor): reliable image selection + resize and broken-image polish - Reactive editability. The editor runs with shouldRerenderOnTransaction:false, so a node view that read editor.isEditable once at render kept a stale value after setEditable() toggled (e.g. an agent stream settling into the doc), leaving a pasted image showing read-only affordances and code blocks stuck on their read-only label until a full refresh. A shared useEditorEditable hook subscribes to the editor's update/transaction events so both node views track editability reactively. - Deterministic click-to-select. A handleClickOn plugin sets the image's NodeSelection on a plain click so selecting never depends on ProseMirror's click-vs-drag arbitration; grab-anywhere drag-reorder is kept, and modified clicks (Cmd/Ctrl to follow a linked badge) fall through. - Resize commits once. The width previews in local state during the drag and commits to the node once on pointer-up (or pointer-cancel), so a resize is a single undo step and an interrupted drag isn't lost. - Broken-image placeholder. A src that fails to load renders as a visible box with its alt text and stays selectable, instead of collapsing to a bare broken-icon. * fix(rich-markdown-editor): keep bare URLs and autolinks bare on serialize The normalizing serializer rewrote a bare URL or / autolink to [url](url) / [a@b.com](mailto:a@b.com) on every save, churning every README's links. postProcessSerializedMarkdown now collapses a link back to its bare form when the visible text already equals the destination (a plain http(s) URL, or an email behind mailto:) — GFM re-autolinks it, so the round-trip is identical with a far quieter diff. Titled links, explicit links, and any link inside a fenced/inline code region are left untouched. Idempotent. * feat(rich-markdown-editor): linkify a selection when a URL is pasted over it Pasting a single URL (or a bare www. host / email) over a non-empty text selection within one block now wraps the selection in a link, keeping the visible text. www. gets https://, an email gets mailto:, and the href is scheme-sanitized (javascript:/data: rejected; mailto: requires a real user@host address). Collapsed carets, cross-block selections, multi-word pastes, node selections, and code contexts fall through to normal paste. * chore(rich-markdown-editor): drop useless String.raw in highlight.ts biome 2.0's noUselessStringRaw flags HIGHLIGHT_BODY — its pattern has no escape sequences, so String.raw is equivalent to a plain template literal (byte-identical value; interpolated into the other String.raw regexes unchanged). Pre-existing on staging; the repo-wide lint gate blocks CI on it. --- .../rich-markdown-editor/code-block.tsx | 14 ++-- .../rich-markdown-editor/highlight.ts | 2 +- .../rich-markdown-editor/image.tsx | 73 +++++++++++++++---- .../rich-markdown-editor/markdown-fidelity.ts | 32 +++++++- .../markdown-paste.test.ts | 60 +++++++++++++++ .../rich-markdown-editor/markdown-paste.ts | 35 ++++++++- .../rich-markdown-editor/round-trip.test.ts | 34 +++++++++ .../use-editor-editable.ts | 27 +++++++ 8 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/use-editor-editable.ts diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx index c17577c917..7db9b1481b 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx @@ -15,6 +15,7 @@ import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap import { Check, ChevronDown, Code, Copy, Eye, WrapText } from 'lucide-react' import { looksLikeMermaid, MermaidDiagram } from '../mermaid-diagram' import { detectLanguage } from './detect-language' +import { useEditorEditable } from './use-editor-editable' const PLAIN = 'plain' const MERMAID = 'mermaid' @@ -59,6 +60,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView const [editingInline, setEditingInline] = useState(false) const [peekSource, setPeekSource] = useState(false) const { copied, copy } = useCopyToClipboard({ resetMs: 1500 }) + const editable = useEditorEditable(editor) const explicitLanguage = node.attrs.language as string | null const text = node.textContent @@ -68,7 +70,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView // diagram on blur (the Linear/GitHub model). The Show source / Show diagram control drives this by // focusing into / blurring the block; read-only uses {@link peekSource} since there is no caret. useEffect(() => { - if (!isMermaid || !editor.isEditable) { + if (!isMermaid || !editable) { setEditingInline(false) return } @@ -91,9 +93,9 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView editor.off('focus', sync) editor.off('blur', sync) } - }, [editor, getPos, isMermaid]) + }, [editor, getPos, isMermaid, editable]) - const showSource = editor.isEditable ? editingInline : peekSource + const showSource = editable ? editingInline : peekSource const showDiagram = isMermaid && text.trim().length > 0 && !showSource // Skip language detection on the mermaid path — the picker/label never render there. @@ -104,7 +106,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView 'Plain text' const toggleSource = () => { - if (!editor.isEditable) { + if (!editable) { setPeekSource((value) => !value) return } @@ -144,7 +146,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView )} {!isMermaid && - (editor.isEditable ? ( + (editable ? ( // Editable: a language picker. Read-only: a static label — selecting a language calls // updateAttributes, which would mutate a doc that must not change. @@ -179,7 +181,7 @@ function CodeBlockView({ node, updateAttributes, editor, getPos }: ReactNodeView {label} ))} - {!isMermaid && editor.isEditable && ( + {!isMermaid && editable && (