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") 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 size="sm" onClick={handleSave} disabled={loading || !value.trim()}>
66 Save
67 </Button>
68 <Button
69 size="sm"
70 variant="ghost"
71 onClick={() => {
72 setValue(title);
73 setEditing(false);
74 }}
75 >
76 Cancel
77 </Button>
78 </div>
79 );
80 }
81
82 return (
83 <div className="group flex items-start gap-2">
84 <h1 className="flex-1 text-2xl font-semibold leading-tight text-foreground">
85 {title}
86 <span className="ml-2 text-xl font-normal text-muted-foreground">#{humanId}</span>
87 </h1>
88 {user && (
89 <button
90 onClick={() => setEditing(true)}
91 className="mt-1 shrink-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground group-hover:opacity-100"
92 title="Edit title"
93 >
94 <Pencil className="size-4" />
95 </button>
96 )}
97 </div>
98 );
99}