From 5b04b3483bf22c3fb5ad31d6ebac00bd3f86fb81 Mon Sep 17 00:00:00 2001 From: Noooste <83548733+Noooste@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:57:48 +0200 Subject: [PATCH] fix(clipboard): implement copyText utility and refactor copy logic in components --- .../components/buckets/ObjectDetailsView.tsx | 7 ++-- .../components/layout/bucket-detail-shell.tsx | 9 ++--- .../src/components/ui/credential-field.tsx | 32 +++++++++++------ frontend/src/lib/clipboard.ts | 34 +++++++++++++++++++ frontend/src/pages/AccessControl.tsx | 22 ++++++------ 5 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 frontend/src/lib/clipboard.ts diff --git a/frontend/src/components/buckets/ObjectDetailsView.tsx b/frontend/src/components/buckets/ObjectDetailsView.tsx index d483c71..03600fc 100644 --- a/frontend/src/components/buckets/ObjectDetailsView.tsx +++ b/frontend/src/components/buckets/ObjectDetailsView.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ObjectPreview } from '@/components/buckets/ObjectPreview'; import { ArrowLeft, ChevronRight, Copy, Download, File, Loader2, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; +import { copyText } from '@/lib/clipboard'; import { downloadObject, formatBytes } from '@/lib/file-utils'; import { formatDate } from '@/lib/utils'; @@ -90,9 +91,9 @@ export function ObjectDetailsView() { const backHref = `/buckets/${bucketName}/objects${parentPath ? `?prefix=${encodeURIComponent(parentPath + '/')}` : ''}`; const pathSegments = parentPath ? parentPath.split('/').filter(Boolean) : []; - const copy = (text: string, label = 'Copied') => { - navigator.clipboard.writeText(text); - toast.success(label); + const copy = async (text: string, label = 'Copied') => { + if (await copyText(text)) toast.success(label); + else toast.error('Failed to copy'); }; const handleDownload = () => { diff --git a/frontend/src/components/layout/bucket-detail-shell.tsx b/frontend/src/components/layout/bucket-detail-shell.tsx index 7212f14..f14db53 100644 --- a/frontend/src/components/layout/bucket-detail-shell.tsx +++ b/frontend/src/components/layout/bucket-detail-shell.tsx @@ -4,6 +4,7 @@ import { IconTile } from '@/components/ui/icon-tile'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; +import { copyText } from '@/lib/clipboard'; import { useBuckets } from '@/hooks/useApi'; import { useBucketCan } from '@/hooks/usePermissions'; import { toast } from 'sonner'; @@ -43,12 +44,8 @@ export function BucketDetailShell() { const s3Url = `s3://${bucketName}`; const copyUrl = async () => { - try { - await navigator.clipboard.writeText(s3Url); - toast.success('URL copied'); - } catch { - toast.error('Failed to copy'); - } + if (await copyText(s3Url)) toast.success('URL copied'); + else toast.error('Failed to copy'); }; return ( diff --git a/frontend/src/components/ui/credential-field.tsx b/frontend/src/components/ui/credential-field.tsx index e2cefa9..2df40af 100644 --- a/frontend/src/components/ui/credential-field.tsx +++ b/frontend/src/components/ui/credential-field.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Check, Copy, Eye, EyeOff, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { copyText } from '@/lib/clipboard'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; @@ -21,30 +22,39 @@ export function CredentialField({ }) { const [copied, setCopied] = useState(false); const [revealed, setRevealed] = useState(!maskable); - const copy = () => { + const copy = async () => { if (!value) return; - navigator.clipboard.writeText(value); + if (!(await copyText(value))) { + toast.error(`Could not copy ${label}. Select it and copy by hand.`); + return; + } setCopied(true); toast.success(`${label} copied`); setTimeout(() => setCopied(false), 1600); }; + // Copying focuses a textarea on the fallback path, which would clear whatever + // the user just highlighted, so a click that ends a selection copies nothing. + const copyUnlessSelecting = () => { + if (!window.getSelection()?.toString()) copy(); + }; const display = loading ? '' : revealed || !maskable ? value : '•'.repeat(Math.min(40, value.length || 40)); + const copyable = !loading && !!value; return (
-
{maskable && (