feat: inline title rename in JobDetail and QCDetail
Click the pencil icon next to the job title to rename it inline. Enter saves, Escape or blur cancels. Available for admin/production/PM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0badae9e5d
commit
efa2395527
2 changed files with 85 additions and 6 deletions
|
|
@ -308,6 +308,8 @@ export function QCDetail() {
|
|||
const [showDownloads, setShowDownloads] = useState(false);
|
||||
const [showRetranslateDialog, setShowRetranslateDialog] = useState(false);
|
||||
const [pendingVttSave, setPendingVttSave] = useState<{ captions?: string; ad?: string } | null>(null);
|
||||
const [editingTitle, setEditingTitle] = useState(false);
|
||||
const [titleDraft, setTitleDraft] = useState('');
|
||||
const [adVttUploaded, setAdVttUploaded] = useState(false);
|
||||
const [renderJustCompleted, setRenderJustCompleted] = useState(false);
|
||||
const captionsFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -532,6 +534,18 @@ export function QCDetail() {
|
|||
};
|
||||
|
||||
// Cmd+S full-VTT save (hotkey handler)
|
||||
const handleTitleSave = async () => {
|
||||
if (!id || !titleDraft.trim() || titleDraft.trim() === job?.title) { setEditingTitle(false); return; }
|
||||
try {
|
||||
await updateJobMutation.mutateAsync({ id, data: { title: titleDraft.trim() } as never });
|
||||
toast.toastOnly.success('Renamed');
|
||||
} catch {
|
||||
toast.toastOnly.error('Failed to rename');
|
||||
} finally {
|
||||
setEditingTitle(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _doSaveVtt = async (retranslate: boolean, captions?: string, ad?: string) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
|
|
@ -890,7 +904,32 @@ export function QCDetail() {
|
|||
{/* Header */}
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">{job.title}</h1>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{editingTitle ? (
|
||||
<input
|
||||
type="text"
|
||||
value={titleDraft}
|
||||
onChange={e => setTitleDraft(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleTitleSave(); if (e.key === 'Escape') setEditingTitle(false); }}
|
||||
onBlur={handleTitleSave}
|
||||
className="text-3xl font-bold text-gray-900 border-b-2 border-indigo-500 focus:outline-none bg-transparent w-full"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h1 className="text-3xl font-bold text-gray-900">{job.title}</h1>
|
||||
)}
|
||||
{canAssign && !editingTitle && (
|
||||
<button
|
||||
onClick={() => { setTitleDraft(job.title); setEditingTitle(true); }}
|
||||
className="text-gray-400 hover:text-gray-600 flex-shrink-0"
|
||||
title="Rename"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-500">
|
||||
<StatusBadge status={job.status} />
|
||||
<span>Source: {job.source.filename}</span>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useJob, useJobDownloads, useJobVttContent, useRetryJob, useReturnToQC } from '../../hooks/useJob';
|
||||
import { useJob, useJobDownloads, useJobVttContent, useRetryJob, useReturnToQC, useUpdateJob } from '../../hooks/useJob';
|
||||
import { useAuthStore } from '../../lib/auth';
|
||||
import type { JobFailure } from '../../types/api';
|
||||
import { StatusBadge } from '../../components/StatusBadge';
|
||||
|
|
@ -136,7 +136,10 @@ export function JobDetail() {
|
|||
|
||||
// Retry mutations
|
||||
const retryJobMutation = useRetryJob();
|
||||
const updateJobMutation = useUpdateJob();
|
||||
const toast = useToastContext();
|
||||
const [editingTitle, setEditingTitle] = useState(false);
|
||||
const [titleDraft, setTitleDraft] = useState('');
|
||||
|
||||
// Return to QC
|
||||
const { user } = useAuthStore();
|
||||
|
|
@ -148,6 +151,20 @@ export function JobDetail() {
|
|||
(user.role === 'production' || user.role === 'admin') &&
|
||||
job?.status && RETURN_TO_QC_ELIGIBLE_STATUSES.includes(job.status);
|
||||
|
||||
const canEditTitle = user && ['admin', 'production', 'project_manager'].includes(user.role || '');
|
||||
|
||||
const handleTitleSave = async () => {
|
||||
if (!id || !titleDraft.trim() || titleDraft.trim() === job?.title) { setEditingTitle(false); return; }
|
||||
try {
|
||||
await updateJobMutation.mutateAsync({ id, data: { title: titleDraft.trim() } as never });
|
||||
toast.toastOnly.success('Renamed');
|
||||
} catch {
|
||||
toast.toastOnly.error('Failed to rename');
|
||||
} finally {
|
||||
setEditingTitle(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReturnToQC = async () => {
|
||||
if (!id || !returnToQCNotes.trim()) return;
|
||||
try {
|
||||
|
|
@ -436,11 +453,34 @@ export function JobDetail() {
|
|||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{job.title}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{editingTitle ? (
|
||||
<input
|
||||
type="text"
|
||||
value={titleDraft}
|
||||
onChange={e => setTitleDraft(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') handleTitleSave(); if (e.key === 'Escape') setEditingTitle(false); }}
|
||||
onBlur={handleTitleSave}
|
||||
className="text-3xl font-bold text-gray-900 border-b-2 border-indigo-500 focus:outline-none bg-transparent w-full"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h1 className="text-3xl font-bold text-gray-900">{job.title}</h1>
|
||||
)}
|
||||
{canEditTitle && !editingTitle && (
|
||||
<button
|
||||
onClick={() => { setTitleDraft(job.title); setEditingTitle(true); }}
|
||||
className="text-gray-400 hover:text-gray-600 flex-shrink-0"
|
||||
title="Rename"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
{job.source?.original_filename} •
|
||||
{job.source?.original_filename} •
|
||||
Created {formatDistanceToNow(new Date(job.created_at))} ago
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue