@@ -27,6 +27,7 @@ import { submitApproval, unapproveVersion } from "@/actions/approvals";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { TaskStatus, TaskType } from "@prisma/client";
|
||||
import { formatDistanceToNow, format } from "date-fns";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
ArrowLeft,
|
||||
CalendarDays,
|
||||
@@ -46,6 +47,7 @@ import {
|
||||
Trash2,
|
||||
XCircle,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
} from "lucide-react";
|
||||
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
||||
import { VersionUpload } from "@/components/versions/VersionUpload";
|
||||
@@ -133,6 +135,9 @@ export function TaskDetailClient({
|
||||
const [estimatedHoursValue, setEstimatedHoursValue] = useState(
|
||||
task.estimatedHours != null ? String(task.estimatedHours) : ""
|
||||
);
|
||||
const [editingTitle, setEditingTitle] = useState(false);
|
||||
const [titleValue, setTitleValue] = useState(task.title);
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const statusCfg = TASK_STATUS_CONFIG[task.status];
|
||||
const StatusIcon = statusCfg.icon;
|
||||
@@ -178,6 +183,33 @@ export function TaskDetailClient({
|
||||
}
|
||||
};
|
||||
|
||||
const handleTitleSave = async () => {
|
||||
const trimmed = titleValue.trim();
|
||||
if (!trimmed || trimmed === task.title) {
|
||||
setTitleValue(task.title);
|
||||
setEditingTitle(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateTask(task.id, { title: trimmed });
|
||||
router.refresh();
|
||||
} catch {
|
||||
toast({ title: "Failed to update title", variant: "destructive" });
|
||||
setTitleValue(task.title);
|
||||
} finally {
|
||||
setEditingTitle(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
e.currentTarget.blur();
|
||||
} else if (e.key === "Escape") {
|
||||
setTitleValue(task.title);
|
||||
setEditingTitle(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssigneeChange = async (artistId: string) => {
|
||||
try {
|
||||
await updateTask(task.id, { assignedArtistId: artistId === "__none__" ? null : artistId });
|
||||
@@ -241,7 +273,7 @@ export function TaskDetailClient({
|
||||
</>
|
||||
)}
|
||||
<span>/</span>
|
||||
<span className="text-zinc-300">{task.title}</span>
|
||||
<span className="text-zinc-300">{titleValue}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
@@ -251,7 +283,33 @@ export function TaskDetailClient({
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-bold text-white">{task.title}</h1>
|
||||
{editingTitle && canManage ? (
|
||||
<Input
|
||||
ref={titleInputRef}
|
||||
value={titleValue}
|
||||
onChange={(e) => setTitleValue(e.target.value)}
|
||||
onBlur={handleTitleSave}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
className="text-2xl font-bold h-auto py-0.5 px-1 bg-transparent border-zinc-700 focus-visible:ring-amber-500/50"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 group">
|
||||
<h1 className="text-2xl font-bold text-white">{titleValue}</h1>
|
||||
{canManage && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingTitle(true);
|
||||
setTimeout(() => titleInputRef.current?.select(), 0);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity text-zinc-500 hover:text-zinc-300"
|
||||
title="Edit title"
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||
<span className="font-mono">{TASK_TYPE_LABELS[task.type]}</span>
|
||||
{parentLink && (
|
||||
|
||||
Reference in New Issue
Block a user