| author | |
| committer | |
| log | b57726ca842624b713572529109ac49b17c93fb3 |
| tree | c00462bfe2f0cd8e8acce8be960fb5f22b17699c |
| parent | 8d5b712de3cbd84118327808c403756e8894014a |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>6 files changed, 92 insertions(+), 36 deletions(-)
apps/web/src/components/DiffFilePathCopyButton.tsx created+41| ... | ... | @@ -0,0 +1,41 @@ |
| 1 | import { CheckIcon, CopyIcon } from "lucide-react"; | |
| 2 | import { useRef } from "react"; | |
| 3 | import { useCopyToClipboard } from "../hooks/useCopyToClipboard"; | |
| 4 | import { | |
| 5 | ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 6 | showAnchoredCopyErrorToast, | |
| 7 | showAnchoredCopySuccessToast, | |
| 8 | } from "./ui/anchoredCopyToast"; | |
| 9 | import { Button } from "./ui/button"; | |
| 10 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | |
| 11 | ||
| 12 | export function DiffFilePathCopyButton({ filePath }: { filePath: string }) { | |
| 13 | const ref = useRef<HTMLButtonElement>(null); | |
| 14 | const { copyToClipboard, isCopied } = useCopyToClipboard<void>({ | |
| 15 | onCopy: () => showAnchoredCopySuccessToast(ref), | |
| 16 | onError: (error) => showAnchoredCopyErrorToast(ref, error), | |
| 17 | timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 18 | }); | |
| 19 | ||
| 20 | return ( | |
| 21 | <Tooltip> | |
| 22 | <TooltipTrigger | |
| 23 | render={ | |
| 24 | <Button | |
| 25 | ref={ref} | |
| 26 | size="icon-micro" | |
| 27 | variant="ghost" | |
| 28 | className="text-muted-foreground [:hover,[data-pressed]]:bg-transparent" | |
| 29 | aria-label="Copy file path" | |
| 30 | onClick={() => copyToClipboard(filePath, undefined)} | |
| 31 | /> | |
| 32 | } | |
| 33 | > | |
| 34 | {isCopied ? <CheckIcon className="size-3 text-success" /> : <CopyIcon className="size-3" />} | |
| 35 | </TooltipTrigger> | |
| 36 | <TooltipPopup> | |
| 37 | <p>{isCopied ? "Copied" : "Copy path"}</p> | |
| 38 | </TooltipPopup> | |
| 39 | </Tooltip> | |
| 40 | ); | |
| 41 | } |
apps/web/src/components/DiffPanel.tsx+4| ... | ... | @@ -46,6 +46,7 @@ import { useProject, useThread } from "../state/entities"; |
| 46 | 46 | import { resolveThreadRouteRef } from "../threadRoutes"; |
| 47 | 47 | import { useClientSettings } from "../hooks/useSettings"; |
| 48 | 48 | import { formatShortTimestamp } from "../timestampFormat"; |
| 49 | import { DiffFilePathCopyButton } from "./DiffFilePathCopyButton"; | |
| 49 | 50 | import { DiffPanelLoadingState, DiffPanelShell, type DiffPanelMode } from "./DiffPanelShell"; |
| 50 | 51 | import { DiffStatLabel } from "./chat/DiffStatLabel"; |
| 51 | 52 | import { AnnotatableCodeView, type AnnotatableCodeViewHandle } from "./diffs/AnnotatableCodeView"; |
| ... | ... | @@ -920,6 +921,9 @@ export default function DiffPanel({ |
| 920 | 921 | sectionId={reviewSectionId} |
| 921 | 922 | sectionTitle={reviewSectionTitle} |
| 922 | 923 | composerDraftTarget={composerDraftTarget} |
| 924 | renderHeaderFilenameSuffix={(fileDiff) => ( | |
| 925 | <DiffFilePathCopyButton filePath={resolveFileDiffPath(fileDiff)} /> | |
| 926 | )} | |
| 923 | 927 | renderHeaderPrefix={(fileDiff, fileKey, collapsed) => { |
| 924 | 928 | const filePath = resolveFileDiffPath(fileDiff); |
| 925 | 929 | return ( |
apps/web/src/components/chat/MessageCopyButton.tsx+8-36| ... | ... | @@ -3,41 +3,13 @@ import { CopyIcon, CheckIcon } from "lucide-react"; |
| 3 | 3 | import { Button } from "../ui/button"; |
| 4 | 4 | import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; |
| 5 | 5 | import { cn } from "~/lib/utils"; |
| 6 | import { anchoredToastManager } from "../ui/toast"; | |
| 6 | import { | |
| 7 | ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 8 | showAnchoredCopyErrorToast, | |
| 9 | showAnchoredCopySuccessToast, | |
| 10 | } from "../ui/anchoredCopyToast"; | |
| 7 | 11 | import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; |
| 8 | 12 | |
| 9 | const ANCHORED_TOAST_TIMEOUT_MS = 1000; | |
| 10 | const onCopy = (ref: React.RefObject<HTMLButtonElement | null>) => { | |
| 11 | if (ref.current) { | |
| 12 | anchoredToastManager.add({ | |
| 13 | data: { | |
| 14 | tooltipStyle: true, | |
| 15 | }, | |
| 16 | positionerProps: { | |
| 17 | anchor: ref.current, | |
| 18 | }, | |
| 19 | timeout: ANCHORED_TOAST_TIMEOUT_MS, | |
| 20 | title: "Copied!", | |
| 21 | }); | |
| 22 | } | |
| 23 | }; | |
| 24 | ||
| 25 | const onCopyError = (ref: React.RefObject<HTMLButtonElement | null>, error: Error) => { | |
| 26 | if (ref.current) { | |
| 27 | anchoredToastManager.add({ | |
| 28 | data: { | |
| 29 | tooltipStyle: true, | |
| 30 | }, | |
| 31 | positionerProps: { | |
| 32 | anchor: ref.current, | |
| 33 | }, | |
| 34 | timeout: ANCHORED_TOAST_TIMEOUT_MS, | |
| 35 | title: "Failed to copy", | |
| 36 | description: error.message, | |
| 37 | }); | |
| 38 | } | |
| 39 | }; | |
| 40 | ||
| 41 | 13 | export const MessageCopyButton = memo(function MessageCopyButton({ |
| 42 | 14 | text, |
| 43 | 15 | size = "xs", |
| ... | ... | @@ -51,9 +23,9 @@ export const MessageCopyButton = memo(function MessageCopyButton({ |
| 51 | 23 | }) { |
| 52 | 24 | const ref = useRef<HTMLButtonElement>(null); |
| 53 | 25 | const { copyToClipboard, isCopied } = useCopyToClipboard<void>({ |
| 54 | onCopy: () => onCopy(ref), | |
| 55 | onError: (error: Error) => onCopyError(ref, error), | |
| 56 | timeout: ANCHORED_TOAST_TIMEOUT_MS, | |
| 26 | onCopy: () => showAnchoredCopySuccessToast(ref), | |
| 27 | onError: (error: Error) => showAnchoredCopyErrorToast(ref, error), | |
| 28 | timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 57 | 29 | }); |
| 58 | 30 | |
| 59 | 31 | return ( |
apps/web/src/components/diffs/AnnotatableCodeView.test.tsx+1| ... | ... | @@ -46,6 +46,7 @@ describe("AnnotatableCodeView", () => { |
| 46 | 46 | composerDraftTarget={"draft-test" as never} |
| 47 | 47 | options={{}} |
| 48 | 48 | renderHeaderPrefix={() => null} |
| 49 | renderHeaderFilenameSuffix={() => null} | |
| 49 | 50 | />, |
| 50 | 51 | ); |
| 51 | 52 |
apps/web/src/components/diffs/AnnotatableCodeView.tsx+5| ... | ... | @@ -86,6 +86,7 @@ interface AnnotatableCodeViewProps { |
| 86 | 86 | options: StyledDiffCodeViewOptions<DiffCommentAnnotationGroup>; |
| 87 | 87 | viewerRef?: Ref<AnnotatableCodeViewHandle>; |
| 88 | 88 | className?: string; |
| 89 | renderHeaderFilenameSuffix: (fileDiff: FileDiffMetadata) => ReactNode; | |
| 89 | 90 | renderHeaderPrefix: ( |
| 90 | 91 | fileDiff: FileDiffMetadata, |
| 91 | 92 | fileKey: string, |
| ... | ... | @@ -106,6 +107,7 @@ export function AnnotatableCodeView({ |
| 106 | 107 | options, |
| 107 | 108 | viewerRef, |
| 108 | 109 | className, |
| 110 | renderHeaderFilenameSuffix, | |
| 109 | 111 | renderHeaderPrefix, |
| 110 | 112 | }: AnnotatableCodeViewProps) { |
| 111 | 113 | const addReviewComment = useComposerDraftStore((store) => store.addReviewComment); |
| ... | ... | @@ -252,6 +254,9 @@ export function AnnotatableCodeView({ |
| 252 | 254 | enableLineSelection: !hasOpenComment, |
| 253 | 255 | onGutterUtilityClick: beginComment, |
| 254 | 256 | }} |
| 257 | renderHeaderFilenameSuffix={(item) => | |
| 258 | item.type === "diff" ? renderHeaderFilenameSuffix(item.fileDiff) : null | |
| 259 | } | |
| 255 | 260 | renderHeaderPrefix={(item) => |
| 256 | 261 | item.type === "diff" |
| 257 | 262 | ? renderHeaderPrefix(item.fileDiff, item.id, item.collapsed === true) |
apps/web/src/components/ui/anchoredCopyToast.ts created+33| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | import type { RefObject } from "react"; | |
| 2 | import { anchoredToastManager } from "./toast"; | |
| 3 | ||
| 4 | export const ANCHORED_COPY_TOAST_TIMEOUT_MS = 1000; | |
| 5 | ||
| 6 | export function showAnchoredCopySuccessToast(ref: RefObject<HTMLButtonElement | null>) { | |
| 7 | if (!ref.current) return; | |
| 8 | anchoredToastManager.add({ | |
| 9 | data: { | |
| 10 | tooltipStyle: true, | |
| 11 | }, | |
| 12 | positionerProps: { | |
| 13 | anchor: ref.current, | |
| 14 | }, | |
| 15 | timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 16 | title: "Copied!", | |
| 17 | }); | |
| 18 | } | |
| 19 | ||
| 20 | export function showAnchoredCopyErrorToast(ref: RefObject<HTMLButtonElement | null>, error: Error) { | |
| 21 | if (!ref.current) return; | |
| 22 | anchoredToastManager.add({ | |
| 23 | data: { | |
| 24 | tooltipStyle: true, | |
| 25 | }, | |
| 26 | positionerProps: { | |
| 27 | anchor: ref.current, | |
| 28 | }, | |
| 29 | timeout: ANCHORED_COPY_TOAST_TIMEOUT_MS, | |
| 30 | title: "Failed to copy", | |
| 31 | description: error.message, | |
| 32 | }); | |
| 33 | } |