Add delete button for draft files in case drafts panel
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 45s
- Add DELETE /api/cases/{case_number}/exports/{filename} endpoint
- Add useDeleteDraft hook in exports API
- Add trash icon + confirmation dialog in drafts panel UI
- Final files (סופי-) cannot be deleted as a safety measure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
useExportDocx,
|
useExportDocx,
|
||||||
useUploadDraft,
|
useUploadDraft,
|
||||||
useMarkFinal,
|
useMarkFinal,
|
||||||
|
useDeleteDraft,
|
||||||
} from "@/lib/api/exports";
|
} from "@/lib/api/exports";
|
||||||
import {
|
import {
|
||||||
useCaseFeedback,
|
useCaseFeedback,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
FileOutput,
|
FileOutput,
|
||||||
Plus,
|
Plus,
|
||||||
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/* Statuses at which a draft is considered ready */
|
/* Statuses at which a draft is considered ready */
|
||||||
@@ -79,9 +81,11 @@ export function DraftsPanel({
|
|||||||
const exportDocx = useExportDocx(caseNumber);
|
const exportDocx = useExportDocx(caseNumber);
|
||||||
const uploadDraft = useUploadDraft(caseNumber);
|
const uploadDraft = useUploadDraft(caseNumber);
|
||||||
const markFinal = useMarkFinal(caseNumber);
|
const markFinal = useMarkFinal(caseNumber);
|
||||||
|
const deleteDraft = useDeleteDraft(caseNumber);
|
||||||
const resolveMutation = useResolveFeedback();
|
const resolveMutation = useResolveFeedback();
|
||||||
|
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
|
||||||
|
|
||||||
const isDraftReady = status && DRAFT_READY.includes(status);
|
const isDraftReady = status && DRAFT_READY.includes(status);
|
||||||
const openFeedbacks = feedbacks?.filter((f) => !f.resolved) ?? [];
|
const openFeedbacks = feedbacks?.filter((f) => !f.resolved) ?? [];
|
||||||
@@ -233,6 +237,7 @@ export function DraftsPanel({
|
|||||||
הורד
|
הורד
|
||||||
</Button>
|
</Button>
|
||||||
{!file.is_final && (
|
{!file.is_final && (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -243,6 +248,15 @@ export function DraftsPanel({
|
|||||||
<Award className="w-3.5 h-3.5 me-1" />
|
<Award className="w-3.5 h-3.5 me-1" />
|
||||||
סמן כסופי
|
סמן כסופי
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||||
|
onClick={() => setDeleteTarget(file.filename)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -252,6 +266,55 @@ export function DraftsPanel({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Delete confirmation dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={deleteTarget !== null}
|
||||||
|
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-sm" dir="rtl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>מחיקת טיוטה</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<p className="text-sm text-ink-muted">
|
||||||
|
למחוק את הקובץ{" "}
|
||||||
|
<span className="font-medium text-ink">{deleteTarget}</span>?
|
||||||
|
<br />
|
||||||
|
פעולה זו לא ניתנת לביטול.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDeleteTarget(null)}
|
||||||
|
>
|
||||||
|
ביטול
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={deleteDraft.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
deleteDraft.mutate(deleteTarget, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("הקובץ נמחק");
|
||||||
|
setDeleteTarget(null);
|
||||||
|
},
|
||||||
|
onError: () => toast.error("שגיאה במחיקה"),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deleteDraft.isPending ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin me-1" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="w-4 h-4 me-1" />
|
||||||
|
)}
|
||||||
|
מחק
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Chair feedback ── */}
|
{/* ── Chair feedback ── */}
|
||||||
|
|||||||
@@ -71,6 +71,20 @@ export function useUploadDraft(caseNumber: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useDeleteDraft(caseNumber: string) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (filename: string) =>
|
||||||
|
apiRequest<{ deleted: boolean; filename: string }>(
|
||||||
|
`/api/cases/${caseNumber}/exports/${filename}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: exportsKeys.list(caseNumber) });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useMarkFinal(caseNumber: string) {
|
export function useMarkFinal(caseNumber: string) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
11
web/app.py
11
web/app.py
@@ -1907,6 +1907,17 @@ async def api_download_export(case_number: str, filename: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/cases/{case_number}/exports/{filename}")
|
||||||
|
async def api_delete_export(case_number: str, filename: str):
|
||||||
|
"""Delete an exported draft file."""
|
||||||
|
export_dir = config.find_case_dir(case_number) / "exports"
|
||||||
|
path = export_dir / filename
|
||||||
|
if not path.exists() or not path.parent.samefile(export_dir):
|
||||||
|
raise HTTPException(404, "קובץ לא נמצא")
|
||||||
|
path.unlink()
|
||||||
|
return {"deleted": True, "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/cases/{case_number}/exports/upload")
|
@app.post("/api/cases/{case_number}/exports/upload")
|
||||||
async def api_upload_export(case_number: str, file: UploadFile = File(...)):
|
async def api_upload_export(case_number: str, file: UploadFile = File(...)):
|
||||||
"""Upload a revised version of a draft."""
|
"""Upload a revised version of a draft."""
|
||||||
|
|||||||
Reference in New Issue
Block a user