|
1 | 1 | 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'; |
3 | 5 | import { |
4 | 6 | MantineReactTable, |
5 | 7 | useMantineReactTable, |
@@ -82,6 +84,11 @@ const DataSourcesTable = () => { |
82 | 84 | const fetchedStatusKeys = useRef<Set<string>>(new Set()); |
83 | 85 | // Track files being reprocessed/polled |
84 | 86 | 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); |
85 | 92 |
|
86 | 93 | const handleRefresh = () => { |
87 | 94 | setRefreshKey(prevKey => prevKey + 1); // Triggers re-fetch |
@@ -349,6 +356,61 @@ const DataSourcesTable = () => { |
349 | 356 | setLoadingMessage(""); |
350 | 357 | } |
351 | 358 | } |
| 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 | + } |
352 | 414 |
|
353 | 415 | const fileReprocessing = async (key: string) => { |
354 | 416 | // Find the document that will be reprocessed to get its type |
@@ -523,20 +585,38 @@ const DataSourcesTable = () => { |
523 | 585 | enableSorting: false, |
524 | 586 | enableColumnActions: false, |
525 | 587 | 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 | + }, |
537 | 617 | }, |
538 | 618 | ], |
539 | | - [embeddingStatus, fetchedStatusKeys], |
| 619 | + [embeddingStatus, fetchedStatusKeys, isDeleteMode, selectedIds, pollingFiles], |
540 | 620 | ); |
541 | 621 |
|
542 | 622 | const myTailwindColors = { |
@@ -587,13 +667,71 @@ const DataSourcesTable = () => { |
587 | 667 | : undefined, |
588 | 668 | renderToolbarInternalActions: ({ table }) => ( |
589 | 669 | <> |
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> |
597 | 735 | <MRT_ToggleGlobalFilterButton table={table} /> |
598 | 736 | <MRT_ToggleFiltersButton table={table} /> |
599 | 737 | <MRT_ShowHideColumnsButton table={table} /> |
|
0 commit comments