feat: add conflict handling for document deletion and selection based on processing state

This commit is contained in:
Anish Sarkar 2026-02-05 22:16:23 +05:30
parent aef59d04eb
commit 6cd3f5c1f6
4 changed files with 51 additions and 14 deletions

View file

@ -294,13 +294,22 @@ export function DocumentsTableShell({
[documents, sortKey, sortDesc]
);
const allSelectedOnPage = sorted.length > 0 && sorted.every((d) => selectedIds.has(d.id));
const someSelectedOnPage = sorted.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
// Helper: check if document can be selected (not processing/pending)
const isSelectable = (doc: Document) => {
const state = doc.status?.state;
return state !== "pending" && state !== "processing";
};
// Only consider selectable documents for "select all" logic
const selectableDocs = sorted.filter(isSelectable);
const allSelectedOnPage = selectableDocs.length > 0 && selectableDocs.every((d) => selectedIds.has(d.id));
const someSelectedOnPage = selectableDocs.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
const toggleAll = (checked: boolean) => {
const next = new Set(selectedIds);
if (checked)
sorted.forEach((d) => {
// Only select documents that are not processing/pending
selectableDocs.forEach((d) => {
next.add(d.id);
});
else
@ -547,6 +556,7 @@ export function DocumentsTableShell({
{sorted.map((doc, index) => {
const title = doc.title;
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<motion.tr
key={doc.id}
@ -568,9 +578,10 @@ export function DocumentsTableShell({
<div className="flex items-center justify-center h-full">
<Checkbox
checked={isSelected}
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
aria-label="Select row"
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
disabled={!canSelect}
aria-label={canSelect ? "Select row" : "Cannot select while processing"}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
/>
</div>
</TableCell>
@ -649,6 +660,7 @@ export function DocumentsTableShell({
<div className="md:hidden divide-y divide-border/40 h-[50vh] overflow-auto">
{sorted.map((doc, index) => {
const isSelected = selectedIds.has(doc.id);
const canSelect = isSelectable(doc);
return (
<motion.div
key={doc.id}
@ -661,9 +673,10 @@ export function DocumentsTableShell({
<div className="flex items-center gap-3">
<Checkbox
checked={isSelected}
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
aria-label="Select row"
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
disabled={!canSelect}
aria-label={canSelect ? "Select row" : "Cannot select while processing"}
className={`border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary ${!canSelect ? "opacity-40 cursor-not-allowed" : ""}`}
/>
<div className="flex-1 min-w-0 space-y-1.5">
<button

View file

@ -63,9 +63,16 @@ export function RowActions({
if (!ok) toast.error("Failed to delete document");
// Note: Success toast is handled by the mutation atom's onSuccess callback
// Cache is updated optimistically by the mutation, no need to refresh
} catch (error) {
} catch (error: unknown) {
console.error("Error deleting document:", error);
toast.error("Failed to delete document");
// Check for 409 Conflict (document started processing after UI loaded)
const status = (error as { response?: { status?: number } })?.response?.status
?? (error as { status?: number })?.status;
if (status === 409) {
toast.error("Document is now being processed. Please try again later.");
} else {
toast.error("Failed to delete document");
}
} finally {
setIsDeleting(false);
setIsDeleteOpen(false);

View file

@ -188,20 +188,29 @@ export default function DocumentsTable() {
try {
// Delete documents one by one using the mutation
// Track 409 conflicts separately (document started processing after UI loaded)
let conflictCount = 0;
const results = await Promise.all(
deletableIds.map(async (id) => {
try {
await deleteDocumentMutation({ id });
return true;
} catch {
} catch (error: unknown) {
const status = (error as { response?: { status?: number } })?.response?.status
?? (error as { status?: number })?.status;
if (status === 409) conflictCount++;
return false;
}
})
);
const okCount = results.filter((r) => r === true).length;
if (okCount === deletableIds.length)
if (okCount === deletableIds.length) {
toast.success(t("delete_success_count", { count: okCount }));
else toast.error(t("delete_partial_failed"));
} else if (conflictCount > 0) {
toast.error(`${conflictCount} document(s) started processing. Please try again later.`);
} else {
toast.error(t("delete_partial_failed"));
}
// If in search mode, refetch search results to reflect deletion
if (isSearchMode) {