@@ -27,6 +27,7 @@ import { submitApproval, unapproveVersion } from "@/actions/approvals";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { TaskStatus, TaskType } from "@prisma/client";
|
import { TaskStatus, TaskType } from "@prisma/client";
|
||||||
import { formatDistanceToNow, format } from "date-fns";
|
import { formatDistanceToNow, format } from "date-fns";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
CalendarDays,
|
CalendarDays,
|
||||||
@@ -46,6 +47,7 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
XCircle,
|
XCircle,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
import { TASK_STATUS_CONFIG, TASK_TYPE_LABELS } from "@/components/tasks/TaskCard";
|
||||||
import { VersionUpload } from "@/components/versions/VersionUpload";
|
import { VersionUpload } from "@/components/versions/VersionUpload";
|
||||||
@@ -133,6 +135,9 @@ export function TaskDetailClient({
|
|||||||
const [estimatedHoursValue, setEstimatedHoursValue] = useState(
|
const [estimatedHoursValue, setEstimatedHoursValue] = useState(
|
||||||
task.estimatedHours != null ? String(task.estimatedHours) : ""
|
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 statusCfg = TASK_STATUS_CONFIG[task.status];
|
||||||
const StatusIcon = statusCfg.icon;
|
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) => {
|
const handleAssigneeChange = async (artistId: string) => {
|
||||||
try {
|
try {
|
||||||
await updateTask(task.id, { assignedArtistId: artistId === "__none__" ? null : artistId });
|
await updateTask(task.id, { assignedArtistId: artistId === "__none__" ? null : artistId });
|
||||||
@@ -241,7 +273,7 @@ export function TaskDetailClient({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-zinc-300">{task.title}</span>
|
<span className="text-zinc-300">{titleValue}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<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="space-y-3">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<div className="space-y-1">
|
<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">
|
<div className="flex items-center gap-2 text-sm text-zinc-500">
|
||||||
<span className="font-mono">{TASK_TYPE_LABELS[task.type]}</span>
|
<span className="font-mono">{TASK_TYPE_LABELS[task.type]}</span>
|
||||||
{parentLink && (
|
{parentLink && (
|
||||||
|
|||||||
Reference in New Issue
Block a user