diff --git a/apps/web/src/components/DiffFilePathCopyButton.tsx b/apps/web/src/components/DiffFilePathCopyButton.tsx new file mode 100644 index 00000000000..0c8a273e61a --- /dev/null +++ b/apps/web/src/components/DiffFilePathCopyButton.tsx @@ -0,0 +1,41 @@ +import { CheckIcon, CopyIcon } from "lucide-react"; +import { useRef } from "react"; +import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; +import { Button } from "./ui/button"; +import { + ANCHORED_COPY_TOAST_TIMEOUT_MS, + showAnchoredCopyErrorToast, + showAnchoredCopySuccessToast, +} from "./ui/anchoredCopyToast"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; + +export function DiffFilePathCopyButton({ filePath }: { filePath: string }) { + const ref = useRef(null); + const { copyToClipboard, isCopied } = useCopyToClipboard({ + onCopy: () => showAnchoredCopySuccessToast(ref), + onError: (error) => showAnchoredCopyErrorToast(ref, error), + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, + }); + + return ( + + copyToClipboard(filePath, undefined)} + /> + } + > + {isCopied ? : } + + +

{isCopied ? "Copied" : "Copy path"}

+
+
+ ); +} diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx index e41ba9c2440..97807ae4721 100644 --- a/apps/web/src/components/DiffPanel.tsx +++ b/apps/web/src/components/DiffPanel.tsx @@ -37,6 +37,7 @@ import { useProject, useThread } from "../state/entities"; import { resolveThreadRouteRef } from "../threadRoutes"; import { useClientSettings } from "../hooks/useSettings"; import { formatShortTimestamp } from "../timestampFormat"; +import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton"; import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView"; import { ToggleGroup, Toggle } from "./ui/toggle-group"; @@ -808,6 +809,9 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff sectionId={reviewSectionId} sectionTitle={reviewSectionTitle} composerDraftTarget={composerDraftTarget} + renderHeaderMetadata={(fileDiff) => ( + + )} renderHeaderPrefix={(fileDiff, fileKey, collapsed) => { const filePath = resolveFileDiffPath(fileDiff); return ( diff --git a/apps/web/src/components/chat/MessageCopyButton.tsx b/apps/web/src/components/chat/MessageCopyButton.tsx index e6a6a491bb3..86e1a5d3c8d 100644 --- a/apps/web/src/components/chat/MessageCopyButton.tsx +++ b/apps/web/src/components/chat/MessageCopyButton.tsx @@ -3,41 +3,13 @@ import { CopyIcon, CheckIcon } from "lucide-react"; import { Button } from "../ui/button"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { cn } from "~/lib/utils"; -import { anchoredToastManager } from "../ui/toast"; +import { + ANCHORED_COPY_TOAST_TIMEOUT_MS, + showAnchoredCopyErrorToast, + showAnchoredCopySuccessToast, +} from "../ui/anchoredCopyToast"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; -const ANCHORED_TOAST_TIMEOUT_MS = 1000; -const onCopy = (ref: React.RefObject) => { - if (ref.current) { - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Copied!", - }); - } -}; - -const onCopyError = (ref: React.RefObject, error: Error) => { - if (ref.current) { - anchoredToastManager.add({ - data: { - tooltipStyle: true, - }, - positionerProps: { - anchor: ref.current, - }, - timeout: ANCHORED_TOAST_TIMEOUT_MS, - title: "Failed to copy", - description: error.message, - }); - } -}; - export const MessageCopyButton = memo(function MessageCopyButton({ text, size = "xs", @@ -51,9 +23,9 @@ export const MessageCopyButton = memo(function MessageCopyButton({ }) { const ref = useRef(null); const { copyToClipboard, isCopied } = useCopyToClipboard({ - onCopy: () => onCopy(ref), - onError: (error: Error) => onCopyError(ref, error), - timeout: ANCHORED_TOAST_TIMEOUT_MS, + onCopy: () => showAnchoredCopySuccessToast(ref), + onError: (error: Error) => showAnchoredCopyErrorToast(ref, error), + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, }); return ( diff --git a/apps/web/src/components/diffs/AnnotatableCodeView.tsx b/apps/web/src/components/diffs/AnnotatableCodeView.tsx index 6cea64fb570..fd5793341cf 100644 --- a/apps/web/src/components/diffs/AnnotatableCodeView.tsx +++ b/apps/web/src/components/diffs/AnnotatableCodeView.tsx @@ -83,6 +83,7 @@ interface AnnotatableCodeViewProps { options: NonNullable["options"]>; viewerRef?: Ref; className?: string; + renderHeaderMetadata: (fileDiff: FileDiffMetadata) => ReactNode; renderHeaderPrefix: ( fileDiff: FileDiffMetadata, fileKey: string, @@ -102,6 +103,7 @@ export function AnnotatableCodeView({ options, viewerRef, className, + renderHeaderMetadata, renderHeaderPrefix, }: AnnotatableCodeViewProps) { const addReviewComment = useComposerDraftStore((store) => store.addReviewComment); @@ -243,6 +245,9 @@ export function AnnotatableCodeView({ enableLineSelection: !hasOpenComment, onLineSelectionEnd: beginComment, }} + renderHeaderMetadata={(item) => + item.type === "diff" ? renderHeaderMetadata(item.fileDiff) : null + } renderHeaderPrefix={(item) => item.type === "diff" ? renderHeaderPrefix(item.fileDiff, item.id, item.collapsed === true) diff --git a/apps/web/src/components/ui/anchoredCopyToast.ts b/apps/web/src/components/ui/anchoredCopyToast.ts new file mode 100644 index 00000000000..df1ac579c89 --- /dev/null +++ b/apps/web/src/components/ui/anchoredCopyToast.ts @@ -0,0 +1,33 @@ +import type { RefObject } from "react"; +import { anchoredToastManager } from "./toast"; + +export const ANCHORED_COPY_TOAST_TIMEOUT_MS = 1000; + +export function showAnchoredCopySuccessToast(ref: RefObject) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, + title: "Copied!", + }); +} + +export function showAnchoredCopyErrorToast(ref: RefObject, error: Error) { + if (!ref.current) return; + anchoredToastManager.add({ + data: { + tooltipStyle: true, + }, + positionerProps: { + anchor: ref.current, + }, + timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, + title: "Failed to copy", + description: error.message, + }); +}