mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-29 19:06:24 +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
|
|
@ -230,6 +230,14 @@ async def delete_note(
|
||||||
if not document:
|
if not document:
|
||||||
raise HTTPException(status_code=404, detail="Note not found")
|
raise HTTPException(status_code=404, detail="Note not found")
|
||||||
|
|
||||||
|
# Check if note is pending or currently being processed
|
||||||
|
doc_state = document.status.get("state") if document.status else None
|
||||||
|
if doc_state in ("pending", "processing"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="Cannot delete note while it is pending or being processed. Please wait for processing to complete.",
|
||||||
|
)
|
||||||
|
|
||||||
# Delete document (chunks will be cascade deleted)
|
# Delete document (chunks will be cascade deleted)
|
||||||
await session.delete(document)
|
await session.delete(document)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
|
||||||
|
|
@ -294,13 +294,22 @@ export function DocumentsTableShell({
|
||||||
[documents, sortKey, sortDesc]
|
[documents, sortKey, sortDesc]
|
||||||
);
|
);
|
||||||
|
|
||||||
const allSelectedOnPage = sorted.length > 0 && sorted.every((d) => selectedIds.has(d.id));
|
// Helper: check if document can be selected (not processing/pending)
|
||||||
const someSelectedOnPage = sorted.some((d) => selectedIds.has(d.id)) && !allSelectedOnPage;
|
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 toggleAll = (checked: boolean) => {
|
||||||
const next = new Set(selectedIds);
|
const next = new Set(selectedIds);
|
||||||
if (checked)
|
if (checked)
|
||||||
sorted.forEach((d) => {
|
// Only select documents that are not processing/pending
|
||||||
|
selectableDocs.forEach((d) => {
|
||||||
next.add(d.id);
|
next.add(d.id);
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
|
|
@ -547,6 +556,7 @@ export function DocumentsTableShell({
|
||||||
{sorted.map((doc, index) => {
|
{sorted.map((doc, index) => {
|
||||||
const title = doc.title;
|
const title = doc.title;
|
||||||
const isSelected = selectedIds.has(doc.id);
|
const isSelected = selectedIds.has(doc.id);
|
||||||
|
const canSelect = isSelectable(doc);
|
||||||
return (
|
return (
|
||||||
<motion.tr
|
<motion.tr
|
||||||
key={doc.id}
|
key={doc.id}
|
||||||
|
|
@ -568,9 +578,10 @@ export function DocumentsTableShell({
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
|
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
|
||||||
aria-label="Select row"
|
disabled={!canSelect}
|
||||||
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
@ -649,6 +660,7 @@ export function DocumentsTableShell({
|
||||||
<div className="md:hidden divide-y divide-border/40 h-[50vh] overflow-auto">
|
<div className="md:hidden divide-y divide-border/40 h-[50vh] overflow-auto">
|
||||||
{sorted.map((doc, index) => {
|
{sorted.map((doc, index) => {
|
||||||
const isSelected = selectedIds.has(doc.id);
|
const isSelected = selectedIds.has(doc.id);
|
||||||
|
const canSelect = isSelectable(doc);
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={doc.id}
|
key={doc.id}
|
||||||
|
|
@ -661,9 +673,10 @@ export function DocumentsTableShell({
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={(v) => toggleOne(doc.id, !!v)}
|
onCheckedChange={(v) => canSelect && toggleOne(doc.id, !!v)}
|
||||||
aria-label="Select row"
|
disabled={!canSelect}
|
||||||
className="border-foreground data-[state=checked]:bg-primary data-[state=checked]:border-primary"
|
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">
|
<div className="flex-1 min-w-0 space-y-1.5">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,16 @@ export function RowActions({
|
||||||
if (!ok) toast.error("Failed to delete document");
|
if (!ok) toast.error("Failed to delete document");
|
||||||
// Note: Success toast is handled by the mutation atom's onSuccess callback
|
// Note: Success toast is handled by the mutation atom's onSuccess callback
|
||||||
// Cache is updated optimistically by the mutation, no need to refresh
|
// Cache is updated optimistically by the mutation, no need to refresh
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error("Error deleting document:", error);
|
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 {
|
} finally {
|
||||||
setIsDeleting(false);
|
setIsDeleting(false);
|
||||||
setIsDeleteOpen(false);
|
setIsDeleteOpen(false);
|
||||||
|
|
|
||||||
|
|
@ -188,20 +188,29 @@ export default function DocumentsTable() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete documents one by one using the mutation
|
// 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(
|
const results = await Promise.all(
|
||||||
deletableIds.map(async (id) => {
|
deletableIds.map(async (id) => {
|
||||||
try {
|
try {
|
||||||
await deleteDocumentMutation({ id });
|
await deleteDocumentMutation({ id });
|
||||||
return true;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const okCount = results.filter((r) => r === true).length;
|
const okCount = results.filter((r) => r === true).length;
|
||||||
if (okCount === deletableIds.length)
|
if (okCount === deletableIds.length) {
|
||||||
toast.success(t("delete_success_count", { count: okCount }));
|
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 in search mode, refetch search results to reflect deletion
|
||||||
if (isSearchMode) {
|
if (isSearchMode) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue