diff --git a/apps/web/components/common/rich-text-editor.tsx b/apps/web/components/common/rich-text-editor.tsx index 58485ab9..0cd67ec9 100644 --- a/apps/web/components/common/rich-text-editor.tsx +++ b/apps/web/components/common/rich-text-editor.tsx @@ -14,7 +14,6 @@ import Typography from "@tiptap/extension-typography"; import Mention from "@tiptap/extension-mention"; import { Markdown } from "@tiptap/markdown"; import { Extension } from "@tiptap/core"; -import type { JSONContent, MarkdownParseHelpers, MarkdownToken } from "@tiptap/core"; import { cn } from "@/lib/utils"; import { createMentionSuggestion } from "./mention-suggestion"; import "./rich-text-editor.css"; @@ -83,9 +82,6 @@ const LinkExtension = Link.configure({ }, }); -const MENTION_LINK_RE = - /^\[(@?[^\]]*)\]\(mention:\/\/(member|agent|issue)\/([^)]+)\)/; - const MentionExtension = Mention.configure({ HTMLAttributes: { class: "mention" }, suggestion: createMentionSuggestion(), @@ -109,59 +105,22 @@ const MentionExtension = Mention.configure({ ...this.parent?.(), type: { default: "member", - parseHTML: (el: HTMLElement) => - el.getAttribute("data-mention-type") ?? "member", + parseHTML: (el: HTMLElement) => el.getAttribute("data-mention-type") ?? "member", }, description: { default: null, - parseHTML: (el: HTMLElement) => - el.getAttribute("data-mention-description"), + parseHTML: (el: HTMLElement) => el.getAttribute("data-mention-description"), }, }; }, - - // -- Markdown serialization: [@Label](mention://type/id) -- - renderMarkdown(node: JSONContent) { - const type = (node.attrs?.type as string) ?? "member"; - const label = (node.attrs?.label as string) ?? node.attrs?.id; + // @tiptap/markdown 3.x uses renderMarkdown as a top-level extension field + // eslint-disable-next-line @typescript-eslint/no-explicit-any + renderMarkdown(node: any) { + const type = node.attrs?.type ?? "member"; + const label = node.attrs?.label ?? node.attrs?.id; const display = type === "issue" ? label : `@${label}`; return `[${display}](mention://${type}/${node.attrs?.id})`; }, - - // -- Markdown parsing: turn the link back into a mention node -- - parseMarkdown(token: MarkdownToken, h: MarkdownParseHelpers) { - return h.createNode("mention", { - id: token.attributes?.id, - label: token.attributes?.label, - type: token.attributes?.type ?? "member", - }); - }, - - markdownTokenizer: { - name: "mention", - level: "inline" as const, - start(src: string) { - // Find [@ or [ followed by ](mention:// - const idx = src.indexOf("](mention://"); - if (idx === -1) return -1; - // Walk back to find the opening [ - const bracketIdx = src.lastIndexOf("[", idx); - return bracketIdx === -1 ? -1 : bracketIdx; - }, - tokenize(src: string) { - const match = MENTION_LINK_RE.exec(src); - if (!match) return undefined; - const [raw, displayLabel = "", type, id] = match; - const label = - displayLabel.startsWith("@") ? displayLabel.slice(1) : displayLabel; - return { - type: "mention", - raw, - content: "", - attributes: { id, label, type }, - }; - }, - }, }); // --------------------------------------------------------------------------- diff --git a/apps/web/components/markdown/Markdown.tsx b/apps/web/components/markdown/Markdown.tsx index c10fccb3..6ddff630 100644 --- a/apps/web/components/markdown/Markdown.tsx +++ b/apps/web/components/markdown/Markdown.tsx @@ -1,11 +1,11 @@ import * as React from 'react' -import Link from 'next/link' import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown' import rehypeRaw from 'rehype-raw' import remarkGfm from 'remark-gfm' import { cn } from '@/lib/utils' import { CodeBlock, InlineCode } from './CodeBlock' import { preprocessLinks } from './linkify' +import { IssueMentionCard } from '@/features/issues/components/issue-mention-card' /** * Render modes for markdown content: @@ -71,17 +71,9 @@ function createComponents( // Mention links: mention://member/id, mention://agent/id, mention://issue/id if (href?.startsWith('mention://')) { const mentionMatch = href.match(/^mention:\/\/(member|agent|issue)\/(.+)$/) - if (mentionMatch?.[1] === 'issue') { - const issueId = mentionMatch[2] - return ( - - {children} - - ) + if (mentionMatch?.[1] === 'issue' && mentionMatch[2]) { + const label = typeof children === 'string' ? children : Array.isArray(children) ? children.join('') : undefined + return } return ( s.issues.find((i) => i.id === issueId)); + + if (!issue) { + return ( + + {fallbackLabel ?? issueId.slice(0, 8)} + + ); + } + + return ( + + + {issue.identifier} + {issue.title} + + ); +}