diff --git a/ui/src/components/MarkdownBody.test.tsx b/ui/src/components/MarkdownBody.test.tsx index f30db99101c..814d83b05c7 100644 --- a/ui/src/components/MarkdownBody.test.tsx +++ b/ui/src/components/MarkdownBody.test.tsx @@ -146,6 +146,20 @@ describe("MarkdownBody", () => { expect(html).toContain(">http://localhost:3100/PAP/issues/PAP-1179<"); }); + it("rewrites issue scheme links to internal issue links", () => { + const html = renderMarkdown("See issue://PAP-1310 and issue://:PAP-1311.", [ + { identifier: "PAP-1310", status: "done" }, + { identifier: "PAP-1311", status: "blocked" }, + ]); + + expect(html).toContain('href="/issues/PAP-1310"'); + expect(html).toContain('href="/issues/PAP-1311"'); + expect(html).toContain(">issue://PAP-1310<"); + expect(html).toContain(">issue://:PAP-1311<"); + expect(html).toContain("text-green-600"); + expect(html).toContain("text-red-600"); + }); + it("linkifies issue identifiers inside inline code spans", () => { const html = renderMarkdown("Reference `PAP-1271` here.", [ { identifier: "PAP-1271", status: "done" }, diff --git a/ui/src/lib/issue-reference.test.ts b/ui/src/lib/issue-reference.test.ts index 9c334b56b19..a7c7d4c768f 100644 --- a/ui/src/lib/issue-reference.test.ts +++ b/ui/src/lib/issue-reference.test.ts @@ -11,7 +11,7 @@ describe("issue-reference", () => { expect(parseIssuePathIdFromPath("http://localhost:3100/PAP/issues/PAP-1179")).toBe("PAP-1179"); }); - it("normalizes bare identifiers and issue URLs into internal links", () => { + it("normalizes bare identifiers, issue URLs, and issue scheme links into internal links", () => { expect(parseIssueReferenceFromHref("pap-1271")).toEqual({ issuePathId: "PAP-1271", href: "/issues/PAP-1271", @@ -20,6 +20,14 @@ describe("issue-reference", () => { issuePathId: "PAP-1179", href: "/issues/PAP-1179", }); + expect(parseIssueReferenceFromHref("issue://PAP-1310")).toEqual({ + issuePathId: "PAP-1310", + href: "/issues/PAP-1310", + }); + expect(parseIssueReferenceFromHref("issue://:PAP-1311")).toEqual({ + issuePathId: "PAP-1311", + href: "/issues/PAP-1311", + }); }); it("normalizes exact inline-code-like issue identifiers", () => { diff --git a/ui/src/lib/issue-reference.ts b/ui/src/lib/issue-reference.ts index 3c6d1109bfa..18de5a32440 100644 --- a/ui/src/lib/issue-reference.ts +++ b/ui/src/lib/issue-reference.ts @@ -6,7 +6,8 @@ type MarkdownNode = { }; const BARE_ISSUE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]+-\d+$/i; -const ISSUE_REFERENCE_TOKEN_RE = /https?:\/\/[^\s<>()]+|\b[A-Z][A-Z0-9]+-\d+\b/gi; +const ISSUE_SCHEME_RE = /^issue:\/\/:?([^?#\s]+)(?:[?#].*)?$/i; +const ISSUE_REFERENCE_TOKEN_RE = /issue:\/\/:?[^\s<>()]+|https?:\/\/[^\s<>()]+|\b[A-Z][A-Z0-9]+-\d+\b/gi; export function parseIssuePathIdFromPath(pathOrUrl: string | null | undefined): string | null { if (!pathOrUrl) return null; @@ -29,6 +30,16 @@ export function parseIssuePathIdFromPath(pathOrUrl: string | null | undefined): export function parseIssueReferenceFromHref(href: string | null | undefined) { if (!href) return null; + const trimmed = href.trim(); + const issueSchemeMatch = trimmed.match(ISSUE_SCHEME_RE); + if (issueSchemeMatch?.[1]) { + const issuePathId = decodeURIComponent(issueSchemeMatch[1]); + return { + issuePathId, + href: `/issues/${encodeURIComponent(issuePathId)}`, + }; + } + const pathId = parseIssuePathIdFromPath(href); if (pathId) { return { @@ -37,7 +48,6 @@ export function parseIssueReferenceFromHref(href: string | null | undefined) { }; } - const trimmed = href.trim(); if (!BARE_ISSUE_IDENTIFIER_RE.test(trimmed)) return null; const normalized = trimmed.toUpperCase(); return {