From abe3c5967a93c68cf3fb0fd6d062c2ed281582bd Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:43:12 +0800 Subject: [PATCH] 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) --- .../issues/components/issue-detail.tsx | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/apps/web/features/issues/components/issue-detail.tsx b/apps/web/features/issues/components/issue-detail.tsx index f770639c..e7509aa9 100644 --- a/apps/web/features/issues/components/issue-detail.tsx +++ b/apps/web/features/issues/components/issue-detail.tsx @@ -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) { )} - - {issue.id.slice(0, 8)} - - {issue.title}
@@ -720,33 +721,27 @@ export function IssueDetail({ issueId, onDelete }: IssueDetailProps) { {/* Content — scrollable */}
- {editingTitle ? ( - 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" - /> - ) : ( -

{ setTitleDraft(issue.title); setEditingTitle(true); }} - > - {issue.title} -

- )} + 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" + />