Skip to content

Commit e7a72fb

Browse files
committed
Add multi-select delete with consolidated toast notifications
Replace per-file toast spam with single summary toast for batch deletions. Track success/failure per file and show detailed feedback including filenames for any failures. Single file deletions retain individual toast behavior.
1 parent c2f9aee commit e7a72fb

File tree

3 files changed

+362
-41
lines changed

3 files changed

+362
-41
lines changed

components/DataSources/DataSourcesTable.tsx

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, {useContext, useEffect, useMemo, useRef, useState} from 'react';
2-
import {IconDownload, IconTrash, IconRefresh, IconLoader2} from "@tabler/icons-react";
2+
import {IconDownload, IconTrash, IconRefresh, IconLoader2, IconCheck, IconX} from "@tabler/icons-react";
3+
import toast from 'react-hot-toast';
4+
import {Checkbox} from '@/components/ReusableComponents/CheckBox';
35
import {
46
MantineReactTable,
57
useMantineReactTable,
@@ -82,6 +84,11 @@ const DataSourcesTable = () => {
8284
const fetchedStatusKeys = useRef<Set<string>>(new Set());
8385
// Track files being reprocessed/polled
8486
const [pollingFiles, setPollingFiles] = useState<Set<string>>(new Set());
87+
88+
// Multi-select delete state
89+
const [isDeleteMode, setIsDeleteMode] = useState(false);
90+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
91+
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
8592

8693
const handleRefresh = () => {
8794
setRefreshKey(prevKey => prevKey + 1); // Triggers re-fetch
@@ -349,6 +356,61 @@ const DataSourcesTable = () => {
349356
setLoadingMessage("");
350357
}
351358
}
359+
360+
const deleteBatch = async () => {
361+
if (selectedIds.size === 0) return;
362+
363+
const totalFiles = selectedIds.size;
364+
setLoadingMessage(`Deleting ${totalFiles} file(s)...`);
365+
366+
try {
367+
const results = await Promise.all(
368+
Array.from(selectedIds).map(id => {
369+
const file = data.find(f => f.id === id);
370+
return deleteDatasourceFile({id, name: file?.name}, false);
371+
})
372+
);
373+
374+
const failures = results.filter(r => !r.success);
375+
const successCount = results.length - failures.length;
376+
377+
if (failures.length === 0) {
378+
toast.success(`Successfully deleted ${successCount} file(s)`);
379+
} else if (successCount === 0) {
380+
toast.error(`Failed to delete all ${failures.length} file(s)`);
381+
} else {
382+
toast.error(`Deleted ${successCount} file(s), but ${failures.length} failed: ${failures.map(f => f.fileName).join(', ')}`);
383+
}
384+
385+
setSelectedIds(new Set());
386+
setShowDeleteConfirmation(false);
387+
setIsDeleteMode(false);
388+
handleRefresh();
389+
} catch (error) {
390+
console.error('Error during batch delete:', error);
391+
toast.error('An unexpected error occurred during batch deletion');
392+
} finally {
393+
setLoadingMessage("");
394+
}
395+
}
396+
397+
const toggleSelectAll = () => {
398+
if (selectedIds.size === data.length) {
399+
setSelectedIds(new Set());
400+
} else {
401+
setSelectedIds(new Set(data.map(file => file.id)));
402+
}
403+
}
404+
405+
const toggleSelectId = (id: string) => {
406+
const newSelected = new Set(selectedIds);
407+
if (newSelected.has(id)) {
408+
newSelected.delete(id);
409+
} else {
410+
newSelected.add(id);
411+
}
412+
setSelectedIds(newSelected);
413+
}
352414

353415
const fileReprocessing = async (key: string) => {
354416
// Find the document that will be reprocessed to get its type
@@ -523,20 +585,38 @@ const DataSourcesTable = () => {
523585
enableSorting: false,
524586
enableColumnActions: false,
525587
enableColumnFilter: false,
526-
Cell: ({cell}) => (
527-
<ActionButton
528-
title='Delete File'
529-
handleClick={(e) => {
530-
e.preventDefault();
531-
e.stopPropagation();
532-
deleteFile(cell.row.original.id);
533-
}}>
534-
<IconTrash/>
535-
</ActionButton>
536-
),
588+
Cell: ({cell}) => {
589+
if (isDeleteMode) {
590+
return (
591+
<div onClick={(e) => {
592+
e.preventDefault();
593+
e.stopPropagation();
594+
toggleSelectId(cell.row.original.id);
595+
}}>
596+
<Checkbox
597+
id={`delete-checkbox-${cell.row.original.id}`}
598+
label=""
599+
checked={selectedIds.has(cell.row.original.id)}
600+
onChange={() => toggleSelectId(cell.row.original.id)}
601+
/>
602+
</div>
603+
);
604+
}
605+
return (
606+
<ActionButton
607+
title='Delete File'
608+
handleClick={(e) => {
609+
e.preventDefault();
610+
e.stopPropagation();
611+
deleteFile(cell.row.original.id);
612+
}}>
613+
<IconTrash/>
614+
</ActionButton>
615+
);
616+
},
537617
},
538618
],
539-
[embeddingStatus, fetchedStatusKeys],
619+
[embeddingStatus, fetchedStatusKeys, isDeleteMode, selectedIds, pollingFiles],
540620
);
541621

542622
const myTailwindColors = {
@@ -587,13 +667,71 @@ const DataSourcesTable = () => {
587667
: undefined,
588668
renderToolbarInternalActions: ({ table }) => (
589669
<>
590-
<div className="ml-[10px] rounded p-1 hover:bg-gray-600 dark:hover:bg-black">
591-
<IconRefresh
592-
593-
onClick={handleRefresh}
594-
style={{ cursor: 'pointer' }}
595-
/>
596-
</div>
670+
671+
{isDeleteMode && !showDeleteConfirmation && (
672+
<>
673+
<button
674+
onClick={toggleSelectAll}
675+
className="ml-2 text-xs px-2 py-1 rounded hover:bg-gray-600 dark:hover:bg-black"
676+
>
677+
{selectedIds.size === data.length ? 'Deselect All' : 'Select All'}
678+
</button>
679+
<span className="ml-2 text-xs text-gray-400">
680+
{selectedIds.size} selected
681+
</span>
682+
<button
683+
onClick={() => setShowDeleteConfirmation(true)}
684+
disabled={selectedIds.size === 0}
685+
className="ml-2 px-2 py-1 rounded bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-xs"
686+
>
687+
Delete
688+
</button>
689+
</>
690+
)}
691+
692+
{showDeleteConfirmation && (
693+
<div className="ml-2 flex items-center gap-2 bg-red-500/10 px-2 py-1 rounded">
694+
<span className="text-xs">
695+
Delete {selectedIds.size} file(s)?
696+
</span>
697+
<button
698+
onClick={deleteBatch}
699+
className="p-1 rounded hover:bg-green-500/20"
700+
title="Confirm"
701+
>
702+
<IconCheck size={16} className="text-green-500" />
703+
</button>
704+
<button
705+
onClick={() => setShowDeleteConfirmation(false)}
706+
className="p-1 rounded hover:bg-red-500/20"
707+
title="Cancel"
708+
>
709+
<IconX size={16} className="text-red-500" />
710+
</button>
711+
</div>
712+
)}
713+
714+
<button
715+
onClick={() => {
716+
if (isDeleteMode) {
717+
setIsDeleteMode(false);
718+
setSelectedIds(new Set());
719+
} else {
720+
setIsDeleteMode(true);
721+
}
722+
}}
723+
className="ml-[10px] rounded p-1 hover:bg-gray-600 dark:hover:bg-black flex items-center gap-1"
724+
title={isDeleteMode ? 'Cancel Delete Mode' : 'Delete Multiple'}
725+
>
726+
{isDeleteMode ? <IconX size={18} /> : <IconTrash size={18} />}
727+
</button>
728+
729+
<div className="ml-[10px] rounded p-1 hover:bg-gray-600 dark:hover:bg-black">
730+
<IconRefresh
731+
onClick={handleRefresh}
732+
style={{ cursor: 'pointer' }}
733+
/>
734+
</div>
597735
<MRT_ToggleGlobalFilterButton table={table} />
598736
<MRT_ToggleFiltersButton table={table} />
599737
<MRT_ShowHideColumnsButton table={table} />

0 commit comments

Comments
 (0)