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:
parent
5db01248b6
commit
0badae9e5d
1 changed files with 113 additions and 9 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue