fix(ui): switch issue title to inline editable input

Replace the click-to-edit h1/Input toggle with a persistent inline
input that saves on blur/enter. Removes duplicate issue ID from
breadcrumbs. Uses a ref to prevent realtime updates from clobbering
in-progress edits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-29 17:43:12 +08:00
parent b6119878ec
commit abe3c5967a

View file

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import { useDefaultLayout, usePanelRef } from "react-resizable-panels";
import Link from "next/link";
import { useRouter } from "next/navigation";
@ -198,8 +198,8 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
const [loading, setLoading] = useState(true);
const [submitting, setSubmitting] = useState(false);
const [deleting, setDeleting] = useState(false);
const [editingTitle, setEditingTitle] = useState(false);
const [titleDraft, setTitleDraft] = useState("");
const titleFocusedRef = useRef(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [propertiesOpen, setPropertiesOpen] = useState(true);
const [detailsOpen, setDetailsOpen] = useState(true);
@ -210,17 +210,22 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
useEffect(() => {
if (storeIssue) {
setIssue(storeIssue);
if (!titleFocusedRef.current) {
setTitleDraft(storeIssue.title);
}
}
}, [storeIssue]);
useEffect(() => {
setIssue(null);
setTitleDraft("");
setTimeline([]);
setSubscribers([]);
setLoading(true);
Promise.all([api.getIssue(id), api.listTimeline(id), api.listIssueSubscribers(id)])
.then(([iss, entries, subs]) => {
setIssue(iss);
setTitleDraft(iss.title);
setTimeline(entries);
setSubscribers(subs);
})
@ -479,10 +484,6 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
<ChevronRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
</>
)}
<span className="truncate text-muted-foreground">
{issue.id.slice(0, 8)}
</span>
<ChevronRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
<span className="truncate">{issue.title}</span>
</div>
<div className="flex items-center gap-1 shrink-0">
@ -720,33 +721,27 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) {
{/* Content — scrollable */}
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-3xl px-8 py-8">
{editingTitle ? (
<Input
autoFocus
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onBlur={() => {
if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
setEditingTitle(false);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (titleDraft.trim()) handleUpdateField({ title: titleDraft.trim() });
setEditingTitle(false);
} else if (e.key === "Escape") {
setEditingTitle(false);
}
}}
className="text-2xl font-bold leading-snug tracking-tight"
/>
) : (
<h1
className="text-2xl font-bold leading-snug tracking-tight cursor-pointer hover:bg-accent/50 rounded px-1 -mx-1"
onClick={() => { setTitleDraft(issue.title); setEditingTitle(true); }}
>
{issue.title}
</h1>
)}
<input
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onFocus={() => { titleFocusedRef.current = true; }}
onBlur={() => {
titleFocusedRef.current = false;
const trimmed = titleDraft.trim();
if (trimmed && trimmed !== issue.title) handleUpdateField({ title: trimmed });
else setTitleDraft(issue.title);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
(e.target as HTMLInputElement).blur();
} else if (e.key === "Escape") {
setTitleDraft(issue.title);
(e.target as HTMLInputElement).blur();
}
}}
className="w-full bg-transparent text-2xl font-bold leading-snug tracking-tight outline-none placeholder:text-muted-foreground"
/>
<RichTextEditor
defaultValue={issue.description || ""}