mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): add smart chips for GitHub issue and commit URLs
Extend smart link rendering to convert GitHub issue URLs (CircleDot icon) and commit URLs (GitCommitHorizontal icon, 7-char short SHA) into inline chips, matching the existing PR chip pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a3784fc00a
commit
8d624903e9
@@ -1,4 +1,4 @@
|
||||
import { GitPullRequest } from "lucide-react";
|
||||
import { CircleDot, GitCommitHorizontal, GitPullRequest } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import ReactMarkdown, { type Components } from "react-markdown";
|
||||
import remarkBreaks from "remark-breaks";
|
||||
@@ -15,6 +15,12 @@ import remarkMentions from "@/shared/lib/remarkMentions";
|
||||
const GITHUB_PR_RE =
|
||||
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/;
|
||||
|
||||
const GITHUB_ISSUE_RE =
|
||||
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/issues\/(\d+)\/?$/;
|
||||
|
||||
const GITHUB_COMMIT_RE =
|
||||
/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/commit\/([0-9a-f]{7,40})\/?$/;
|
||||
|
||||
type MarkdownProps = {
|
||||
channelNames?: string[];
|
||||
className?: string;
|
||||
@@ -46,25 +52,50 @@ function createMarkdownComponents(
|
||||
|
||||
return {
|
||||
a: ({ children, href, ...props }) => {
|
||||
const prMatch = href ? GITHUB_PR_RE.exec(href) : null;
|
||||
if (prMatch) {
|
||||
const [, owner, repo, number] = prMatch;
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className="relative inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-sm font-medium text-primary no-underline transition-colors hover:bg-primary/20"
|
||||
href={href}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="pointer-events-none select-none inline-flex items-center gap-1" aria-hidden="true">
|
||||
<GitPullRequest className="size-3.5" />
|
||||
{owner}/{repo}#{number}
|
||||
</span>
|
||||
<span className="absolute w-0 overflow-hidden whitespace-nowrap">{href}</span>
|
||||
</a>
|
||||
);
|
||||
if (href) {
|
||||
let Icon: React.ComponentType<{ className?: string }> | null = null;
|
||||
let label: string | null = null;
|
||||
|
||||
const prMatch = GITHUB_PR_RE.exec(href);
|
||||
if (prMatch) {
|
||||
const [, owner, repo, number] = prMatch;
|
||||
Icon = GitPullRequest;
|
||||
label = `${owner}/${repo}#${number}`;
|
||||
}
|
||||
|
||||
const issueMatch = !Icon ? GITHUB_ISSUE_RE.exec(href) : null;
|
||||
if (issueMatch) {
|
||||
const [, owner, repo, number] = issueMatch;
|
||||
Icon = CircleDot;
|
||||
label = `${owner}/${repo}#${number}`;
|
||||
}
|
||||
|
||||
const commitMatch = !Icon ? GITHUB_COMMIT_RE.exec(href) : null;
|
||||
if (commitMatch) {
|
||||
const [, owner, repo, sha] = commitMatch;
|
||||
Icon = GitCommitHorizontal;
|
||||
label = `${owner}/${repo}@${sha.slice(0, 7)}`;
|
||||
}
|
||||
|
||||
if (Icon && label) {
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
className="relative inline-flex items-center gap-1 rounded-md bg-primary/10 px-1.5 py-0.5 text-sm font-medium text-primary no-underline transition-colors hover:bg-primary/20"
|
||||
href={href}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="pointer-events-none select-none inline-flex items-center gap-1" aria-hidden="true">
|
||||
<Icon className="size-3.5" />
|
||||
{label}
|
||||
</span>
|
||||
<span className="absolute w-0 overflow-hidden whitespace-nowrap">{href}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
|
||||
@@ -72,6 +72,50 @@ test("selecting a PR chip copies the full URL, not the chip label", async ({
|
||||
await expect(visibleLabel).toBeVisible();
|
||||
});
|
||||
|
||||
test("GitHub issue URL renders as an inline smart chip", async ({ page }) => {
|
||||
const issueUrl = "https://github.com/block/sprout/issues/99";
|
||||
const message = `See ${issueUrl} for context`;
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.fill(message);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastRow = page.getByTestId("message-row").last();
|
||||
|
||||
const issueChip = lastRow.locator("a", { hasText: "block/sprout#99" });
|
||||
await expect(issueChip).toBeVisible();
|
||||
await expect(issueChip).toHaveAttribute("href", issueUrl);
|
||||
await expect(issueChip.locator("svg")).toBeVisible();
|
||||
});
|
||||
|
||||
test("GitHub commit URL renders as an inline smart chip", async ({ page }) => {
|
||||
const commitUrl =
|
||||
"https://github.com/block/sprout/commit/abc1234def5678901234567890abcdef12345678";
|
||||
const message = `Reverted in ${commitUrl}`;
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const input = page.getByTestId("message-input");
|
||||
await input.fill(message);
|
||||
await page.getByTestId("send-message").click();
|
||||
|
||||
const lastRow = page.getByTestId("message-row").last();
|
||||
|
||||
// Should show short SHA
|
||||
const commitChip = lastRow.locator("a", {
|
||||
hasText: "block/sprout@abc1234",
|
||||
});
|
||||
await expect(commitChip).toBeVisible();
|
||||
await expect(commitChip).toHaveAttribute("href", commitUrl);
|
||||
await expect(commitChip.locator("svg")).toBeVisible();
|
||||
});
|
||||
|
||||
test("non-PR GitHub links render as regular links", async ({ page }) => {
|
||||
const repoUrl = "https://github.com/block/sprout";
|
||||
const message = `Check out ${repoUrl}`;
|
||||
|
||||
Reference in New Issue
Block a user