1import { useState, useRef, useEffect } from 'react'
2import { Pencil } from 'lucide-react'
3import { Button } from '@/components/ui/button'
4import { Input } from '@/components/ui/input'
5import { useAuth } from '@/lib/auth'
6import { useBugSetTitleMutation, BugDetailDocument } from '@/__generated__/graphql'
7
8interface TitleEditorProps {
9 bugPrefix: string
10 title: string
11 humanId: string
12 /** Current repo slug, passed as `ref` in refetch query variables. */
13 ref_?: string | null
14}
15
16// Inline title editor in BugDetailPage. Shows the title as plain text with a
17// pencil icon on hover (auth-gated). Enter saves, Escape cancels.
18export function TitleEditor({ bugPrefix, title, humanId, ref_ }: TitleEditorProps) {
19 const { user } = useAuth()
20 const [editing, setEditing] = useState(false)
21 const [value, setValue] = useState(title)
22 const inputRef = useRef<HTMLInputElement>(null)
23
24 const [setTitle, { loading }] = useBugSetTitleMutation({
25 refetchQueries: [{ query: BugDetailDocument, variables: { ref: ref_, prefix: bugPrefix } }],
26 })
27
28 useEffect(() => {
29 if (editing) inputRef.current?.focus()
30 }, [editing])
31
32 // Keep local value in sync if title prop changes (e.g. after refetch)
33 useEffect(() => {
34 if (!editing) setValue(title)
35 }, [title, editing])
36
37 async function handleSave() {
38 const trimmed = value.trim()
39 if (trimmed && trimmed !== title) {
40 await setTitle({ variables: { input: { prefix: bugPrefix, title: trimmed } } })
41 }
42 setEditing(false)
43 }
44
45 function handleKeyDown(e: React.KeyboardEvent) {
46 if (e.key === 'Enter') handleSave()
47 if (e.key === 'Escape') {
48 setValue(title)
49 setEditing(false)
50 }
51 }
52
53 if (editing) {
54 return (
55 <div className="flex items-start gap-2">
56 <Input
57 ref={inputRef}
58 value={value}
59 onChange={(e) => setValue(e.target.value)}
60 onKeyDown={handleKeyDown}
61 className="text-xl font-semibold"
62 disabled={loading}
63 />
64 <Button size="sm" onClick={handleSave} disabled={loading || !value.trim()}>
65 Save
66 </Button>
67 <Button
68 size="sm"
69 variant="ghost"
70 onClick={() => {
71 setValue(title)
72 setEditing(false)
73 }}
74 >
75 Cancel
76 </Button>
77 </div>
78 )
79 }
80
81 return (
82 <div className="group flex items-start gap-2">
83 <h1 className="flex-1 text-2xl font-semibold leading-tight text-foreground">
84 {title}
85 <span className="ml-2 text-xl font-normal text-muted-foreground">#{humanId}</span>
86 </h1>
87 {user && (
88 <button
89 onClick={() => setEditing(true)}
90 className="mt-1 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
91 title="Edit title"
92 >
93 <Pencil className="size-4" />
94 </button>
95 )}
96 </div>
97 )
98}