feat(jobs-list): add per-row Edit (rename) and Delete buttons

- Edit button opens inline rename modal with Enter/Escape support
- Delete button shows confirmation modal with clear warning about
  permanent removal from storage and database
- Both actions available for admin/production/project_manager roles
- Delete uses existing single-job DELETE endpoint (GCS + MongoDB)
- Rename uses existing PATCH endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-04-30 17:49:51 +01:00
parent 5db01248b6
commit 0badae9e5d

View file

@ -3,7 +3,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { formatDistanceToNow, subDays, isAfter, isPast, format } from 'date-fns';
import { useAuthStore } from '../../lib/auth';
import { JOB_STATUS_LABELS, getJobStatusLabel } from '../../utils/jobStatusMessages';
import { useJobs, useBulkDeleteJobs, useReprocessJob, useBulkReturnToQC, useCloneJob } from '../../hooks/useJob';
import { useJobs, useBulkDeleteJobs, useReprocessJob, useBulkReturnToQC, useCloneJob, useDeleteJob, useUpdateJob } from '../../hooks/useJob';
import { StatusBadge } from '../../components/StatusBadge';
import { useToastContext } from '../../contexts/ToastContext';
import { useGlobalWebSocket } from '../../contexts/GlobalWebSocketContext';
@ -95,6 +95,9 @@ export function JobsList() {
const [showDownloadConfirm, setShowDownloadConfirm] = useState(false);
const [showReturnToQCConfirm, setShowReturnToQCConfirm] = useState(false);
const [returnToQCNotes, setReturnToQCNotes] = useState('');
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [editTarget, setEditTarget] = useState<{ id: string; title: string } | null>(null);
const [editTitle, setEditTitle] = useState('');
const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number } | null>(null);
// Update filters from URL params
@ -115,6 +118,8 @@ export function JobsList() {
});
const bulkDeleteMutation = useBulkDeleteJobs();
const deleteJobMutation = useDeleteJob();
const updateJobMutation = useUpdateJob();
const reprocessMutation = useReprocessJob();
const bulkReturnToQCMutation = useBulkReturnToQC();
const cloneJobMutation = useCloneJob();
@ -260,6 +265,30 @@ export function JobsList() {
setBulkAction('');
};
const handleSingleDelete = async () => {
if (!deleteTargetId) return;
try {
await deleteJobMutation.mutateAsync(deleteTargetId);
toast.toastOnly.success('Job deleted');
} catch {
toast.toastOnly.error('Failed to delete job');
} finally {
setDeleteTargetId(null);
}
};
const handleEditSave = async () => {
if (!editTarget || !editTitle.trim()) return;
try {
await updateJobMutation.mutateAsync({ id: editTarget.id, data: { title: editTitle.trim() } as never });
toast.toastOnly.success('Job renamed');
} catch {
toast.toastOnly.error('Failed to rename job');
} finally {
setEditTarget(null);
}
};
const handleDeleteConfirm = async () => {
if (selectedJobs.size === 0) return;
@ -878,14 +907,30 @@ export function JobsList() {
<div className="flex items-center justify-end gap-2">
{getActionButton(job)}
{canManageJobs && (
<button
onClick={() => handleCloneJob(job.id)}
disabled={cloneJobMutation.isPending}
title="Clone job"
className="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded text-gray-600 hover:bg-gray-50 disabled:opacity-50"
>
Clone
</button>
<>
<button
onClick={() => { setEditTarget({ id: job.id, title: job.title }); setEditTitle(job.title); }}
title="Rename job"
className="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded text-gray-600 hover:bg-gray-50"
>
Edit
</button>
<button
onClick={() => handleCloneJob(job.id)}
disabled={cloneJobMutation.isPending}
title="Clone job"
className="inline-flex items-center px-2 py-1 text-xs border border-gray-300 rounded text-gray-600 hover:bg-gray-50 disabled:opacity-50"
>
Clone
</button>
<button
onClick={() => setDeleteTargetId(job.id)}
title="Delete job"
className="inline-flex items-center px-2 py-1 text-xs border border-red-200 rounded text-red-600 hover:bg-red-50"
>
Delete
</button>
</>
)}
</div>
</td>
@ -1230,6 +1275,65 @@ export function JobsList() {
);
})()}
{/* Single-job delete confirmation */}
{deleteTargetId && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-sm w-full mx-4 space-y-4">
<h3 className="text-base font-semibold text-gray-900">Delete job?</h3>
<p className="text-sm text-gray-600">
This will permanently delete the job, all generated files, and source video from storage. This cannot be undone.
</p>
<div className="flex gap-3 justify-end pt-1">
<button
onClick={() => setDeleteTargetId(null)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleSingleDelete}
disabled={deleteJobMutation.isPending}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700 disabled:opacity-50"
>
{deleteJobMutation.isPending ? 'Deleting…' : 'Delete'}
</button>
</div>
</div>
</div>
)}
{/* Rename job modal */}
{editTarget && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl p-6 max-w-sm w-full mx-4 space-y-4">
<h3 className="text-base font-semibold text-gray-900">Rename job</h3>
<input
type="text"
value={editTitle}
onChange={e => setEditTitle(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleEditSave(); if (e.key === 'Escape') setEditTarget(null); }}
className="w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
autoFocus
/>
<div className="flex gap-3 justify-end pt-1">
<button
onClick={() => setEditTarget(null)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleEditSave}
disabled={updateJobMutation.isPending || !editTitle.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 disabled:opacity-50"
>
{updateJobMutation.isPending ? 'Saving…' : 'Save'}
</button>
</div>
</div>
</div>
)}
{/* Download Progress Indicator */}
{downloadProgress && (
<div className="fixed bottom-4 right-4 bg-white rounded-lg shadow-lg p-4 z-50 border border-gray-200">