Skip to content

Commit d19ebdd

Browse files
feat: add copy buttons with toast notifications to OAuth dialog
- Add CopyButton component onCopy callback for custom actions - Add device code display and copy button in OAuthCallbackDialog - Add URL copy button next to Open Authorization Page button - Add authorization code input copy button - Show toast notifications: "Code copied to clipboard" and "URL copied to clipboard" - Extract device/user code from instructions using regex pattern
1 parent 0836e32 commit d19ebdd

2 files changed

Lines changed: 58 additions & 11 deletions

File tree

frontend/src/components/settings/OAuthCallbackDialog.tsx

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { Input } from '@/components/ui/input'
44
import { Label } from '@/components/ui/label'
55
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
66
import { Loader2, ExternalLink, CheckCircle } from 'lucide-react'
7+
import { CopyButton } from '@/components/ui/copy-button'
8+
import { showToast } from '@/lib/toast'
79
import { oauthApi, type OAuthAuthorizeResponse } from '@/api/oauth'
810
import { mapOAuthError } from '@/lib/oauthErrors'
911

@@ -66,6 +68,10 @@ export function OAuthCallbackDialog({
6668

6769
const isAutoMethod = authResponse.method === 'auto'
6870

71+
// Extract device/user code from instructions (e.g., "Enter code: 596A-E304")
72+
const codeMatch = authResponse.instructions.match(/(?:Enter code|User code|Device code)[:\s]+([A-Z0-9-]+)/i)
73+
const deviceCode = codeMatch ? codeMatch[1] : ''
74+
6975
return (
7076
<Dialog open={open} onOpenChange={handleClose}>
7177
<DialogContent className="bg-card border-border max-w-lg">
@@ -89,20 +95,57 @@ export function OAuthCallbackDialog({
8995
<div className="space-y-3">
9096
<div className="bg-muted p-3 rounded-md">
9197
<p className="text-sm mb-2">{authResponse.instructions}</p>
92-
<Button
93-
onClick={handleOpenAuthUrl}
94-
variant="outline"
95-
size="sm"
96-
className="w-full"
97-
>
98-
<ExternalLink className="h-4 w-4 mr-2" />
99-
Open Authorization Page
100-
</Button>
98+
99+
{deviceCode && (
100+
<div className="flex items-center gap-2 mb-3">
101+
<code className="flex-1 bg-background px-3 py-2 rounded text-sm font-mono">
102+
{deviceCode}
103+
</code>
104+
<CopyButton
105+
content={deviceCode}
106+
title="Copy device code"
107+
variant="ghost"
108+
iconSize="sm"
109+
className="flex-shrink-0"
110+
onCopy={() => showToast.success('Code copied to clipboard')}
111+
/>
112+
</div>
113+
)}
114+
115+
<div className="flex gap-2">
116+
<Button
117+
onClick={handleOpenAuthUrl}
118+
variant="outline"
119+
size="sm"
120+
className="flex-1"
121+
>
122+
<ExternalLink className="h-4 w-4 mr-2" />
123+
Open Authorization Page
124+
</Button>
125+
<CopyButton
126+
content={authResponse.url}
127+
title="Copy authorization URL"
128+
variant="ghost"
129+
iconSize="sm"
130+
className="flex-shrink-0"
131+
onCopy={() => showToast.success('URL copied to clipboard')}
132+
/>
133+
</div>
101134
</div>
102135

103136
{!isAutoMethod && (
104137
<div className="space-y-2">
105-
<Label htmlFor="authCode">Authorization Code</Label>
138+
<div className="flex items-center gap-2">
139+
<Label htmlFor="authCode">Authorization Code</Label>
140+
<CopyButton
141+
content={authCode}
142+
title="Copy authorization code"
143+
variant="ghost"
144+
iconSize="sm"
145+
className="flex-shrink-0"
146+
onCopy={() => showToast.success('Code copied to clipboard')}
147+
/>
148+
</div>
106149
<Input
107150
id="authCode"
108151
value={authCode}

frontend/src/components/ui/copy-button.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@ interface CopyButtonProps {
88
className?: string
99
iconSize?: 'sm' | 'md'
1010
variant?: 'default' | 'ghost'
11+
onCopy?: () => void
1112
}
1213

1314
export function CopyButton({
1415
content,
1516
title = 'Copy',
1617
className = '',
1718
iconSize = 'md',
18-
variant = 'default'
19+
variant = 'default',
20+
onCopy
1921
}: CopyButtonProps) {
2022
const [copied, setCopied] = useState(false)
2123

@@ -25,6 +27,7 @@ export function CopyButton({
2527
await navigator.clipboard.writeText(content)
2628
setCopied(true)
2729
setTimeout(() => setCopied(false), 2000)
30+
onCopy?.()
2831
} catch {
2932
const textArea = document.createElement('textarea')
3033
textArea.value = content
@@ -37,6 +40,7 @@ export function CopyButton({
3740
if (successful) {
3841
setCopied(true)
3942
setTimeout(() => setCopied(false), 2000)
43+
onCopy?.()
4044
}
4145
} finally {
4246
document.body.removeChild(textArea)

0 commit comments

Comments
 (0)