mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 10:26:33 +02:00
feat: add conflict handling for document deletion and selection based on processing state
This commit is contained in:
parent
aef59d04eb
commit
6cd3f5c1f6
4 changed files with 51 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue