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