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:
parent
b6119878ec
commit
abe3c5967a
1 changed files with 28 additions and 33 deletions
|
|
@ -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 || ""}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue