refactor(ui): reuse CopyButton and lift TooltipProvider in CopyableBadge

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-07-22 23:39:10 +00:00
parent aa8c64555a
commit 60dbafa255
3 changed files with 30 additions and 51 deletions

View file

@ -27,6 +27,7 @@ import ObjectPermissionsView from "../object_permissions_view";
import { CopyableBadge } from "../shared/copyable_badge";
import NumericalInput from "../shared/numerical_input";
import MemberModal from "../team/EditMembership";
import { TooltipProvider } from "../ui/tooltip";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
interface OrganizationInfoProps {
@ -302,11 +303,13 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
</Card>
<Card>
<Text>Teams</Text>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.teams?.map((team, index) => (
<CopyableBadge key={index} value={teamAliasMap[team.team_id] || team.team_id} />
))}
</div>
<TooltipProvider delay={300}>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.teams?.map((team, index) => (
<CopyableBadge key={index} value={teamAliasMap[team.team_id] || team.team_id} />
))}
</div>
</TooltipProvider>
</Card>
<ObjectPermissionsView

View file

@ -1,15 +1,17 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CopyableBadge } from "./copyable_badge";
const { copyToClipboardMock } = vi.hoisted(() => ({ copyToClipboardMock: vi.fn() }));
const writeText = vi.fn().mockResolvedValue(undefined);
vi.mock("@/utils/dataUtils", async (importOriginal) => ({
...(await importOriginal<typeof import("@/utils/dataUtils")>()),
copyToClipboard: copyToClipboardMock,
}));
beforeEach(() => {
writeText.mockClear();
Object.defineProperty(navigator, "clipboard", {
configurable: true,
value: { writeText },
});
});
describe("CopyableBadge", () => {
it("renders the value and truncates it", () => {
@ -19,14 +21,12 @@ describe("CopyableBadge", () => {
expect(label.className).toContain("max-w-[220px]");
});
it("copies the full value when clicked", async () => {
copyToClipboardMock.mockResolvedValue(true);
const user = userEvent.setup();
render(<CopyableBadge value="team-alias-long-name" dataTestId="team-badge" />);
it("copies the full value when the copy button is clicked", async () => {
render(<CopyableBadge value="team-alias-long-name" />);
await user.click(screen.getByTestId("team-badge"));
fireEvent.click(screen.getByRole("button", { name: "Copy team-alias-long-name" }));
expect(copyToClipboardMock).toHaveBeenCalledWith("team-alias-long-name");
await waitFor(() => expect(writeText).toHaveBeenCalledWith("team-alias-long-name"));
});
it("exposes a copy affordance via accessible label", () => {

View file

@ -1,12 +1,10 @@
"use client";
import { Tag } from "antd";
import { Check, Copy } from "lucide-react";
import * as React from "react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import CopyButton from "@/components/shared/CopyButton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/cva.config";
import { copyToClipboard } from "@/utils/dataUtils";
interface CopyableBadgeProps {
value: string;
@ -21,37 +19,15 @@ export function CopyableBadge({
maxWidthClassName = "max-w-[220px]",
dataTestId,
}: CopyableBadgeProps) {
const [copied, setCopied] = React.useState(false);
const handleCopy = async () => {
const success = await copyToClipboard(value);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<TooltipProvider delay={300}>
<Tag color={color} className="m-0 inline-flex max-w-full items-center gap-1" data-testid={dataTestId}>
<Tooltip>
<TooltipTrigger
aria-label={`Copy ${value}`}
data-testid={dataTestId}
onClick={handleCopy}
className="inline-flex max-w-full cursor-pointer border-0 bg-transparent p-0"
>
<Tag color={color} className="m-0 max-w-full">
<span className={cn("block truncate", maxWidthClassName)}>{value}</span>
</Tag>
<TooltipTrigger className="max-w-full cursor-default border-0 bg-transparent p-0">
<span className={cn("block truncate", maxWidthClassName)}>{value}</span>
</TooltipTrigger>
<TooltipContent>
<span className="inline-flex items-center gap-1.5">
{copied ? <Check className="size-3 shrink-0" /> : <Copy className="size-3 shrink-0" />}
<span className="font-mono break-all">{value}</span>
<span className="opacity-70">{copied ? "Copied" : "Click to copy"}</span>
</span>
</TooltipContent>
<TooltipContent>{value}</TooltipContent>
</Tooltip>
</TooltipProvider>
<CopyButton value={value} label={`Copy ${value}`} iconClassName="size-3" />
</Tag>
);
}