=> {
@@ -237,14 +297,14 @@ export function ObjectBrowserView({
- {onDeleteMultipleObjects && selectedFileKeys.size > 0 && (
+ {onDeleteMultipleObjects && selectedCount > 0 && (
)}
{onUploadFiles && (
@@ -387,6 +447,7 @@ export function ObjectBrowserView({
filterQuery={filterQuery}
deepSearch={deepSearch}
selectedFileKeys={selectedFileKeys}
+ selectedFolderKeys={selectedFolderKeys}
isDragActive={isDragActive}
isLoading={isLoading && !isRefreshing && !isNavigating}
isTruncated={isTruncated}
@@ -397,8 +458,10 @@ export function ObjectBrowserView({
setSelectedObject(obj);
setDeleteObjectDialogOpen(true);
} : undefined}
+ onDeleteFolder={onDeleteMultipleObjects ? (obj) => handleDeleteFolder(obj.key) : undefined}
onToggleFileSelection={handleToggleFileSelection}
- onSelectAllFiles={handleSelectAllFiles}
+ onToggleFolderSelection={handleToggleFolderSelection}
+ onSelectAll={handleSelectAll}
onPageChange={onPageChange}
onItemsPerPageChange={onItemsPerPageChange}
initialPageToken={initialPageToken}
@@ -424,6 +487,53 @@ export function ObjectBrowserView({
object={selectedObject}
onDeleteObject={handleDeleteObject}
/>
+
+ {/* Bulk / Folder Delete Confirmation */}
+ {
+ if (!open && !bulkDeleting) setPendingDelete(null);
+ }}
+ title={getBulkDeleteTitle(pendingDelete)}
+ description={getBulkDeleteDescription(pendingDelete)}
+ confirmLabel="Delete"
+ loading={bulkDeleting}
+ onConfirm={handleConfirmBulkDelete}
+ />
);
}
+
+// Builds a concise title summarising what the bulk-delete dialog will remove.
+function getBulkDeleteTitle(pending: { keys: string[]; prefixes: string[] } | null): string {
+ if (!pending) return 'Delete items?';
+ const { keys, prefixes } = pending;
+ const total = keys.length + prefixes.length;
+ if (keys.length === 0 && prefixes.length === 1) {
+ return 'Delete folder?';
+ }
+ return `Delete ${total} item${total !== 1 ? 's' : ''}?`;
+}
+
+// Spells out the file/folder counts and warns that folders are removed recursively.
+function getBulkDeleteDescription(
+ pending: { keys: string[]; prefixes: string[] } | null,
+): string {
+ if (!pending) return '';
+ const { keys, prefixes } = pending;
+ const parts: string[] = [];
+ if (keys.length > 0) {
+ parts.push(`${keys.length} file${keys.length !== 1 ? 's' : ''}`);
+ }
+ if (prefixes.length > 0) {
+ parts.push(`${prefixes.length} folder${prefixes.length !== 1 ? 's' : ''}`);
+ }
+ const summary = parts.join(' and ');
+
+ if (prefixes.length > 0) {
+ return `This will permanently delete ${summary}. Every object stored inside the selected folder${
+ prefixes.length !== 1 ? 's' : ''
+ } will be removed recursively.`;
+ }
+ return `This will permanently delete ${summary}.`;
+}
diff --git a/frontend/src/components/buckets/ObjectsTable.tsx b/frontend/src/components/buckets/ObjectsTable.tsx
index 07362e6..641addd 100644
--- a/frontend/src/components/buckets/ObjectsTable.tsx
+++ b/frontend/src/components/buckets/ObjectsTable.tsx
@@ -25,15 +25,22 @@ interface ObjectsTableProps {
filterQuery: string;
deepSearch: boolean;
selectedFileKeys: Set;
+ selectedFolderKeys: Set;
isDragActive: boolean;
isLoading?: boolean;
isTruncated?: boolean;
nextContinuationToken?: string;
itemsPerPage: number;
onNavigateToFolder: (key: string) => void;
+ // Optional so the parent can withhold them when the user lacks delete
+ // permission; canDelete (below) is derived from onDeleteObject.
onDeleteObject?: (object: S3Object) => void;
+ onDeleteFolder?: (object: S3Object) => void;
onToggleFileSelection: (key: string) => void;
- onSelectAllFiles: () => void;
+ onToggleFolderSelection: (key: string) => void;
+ // Receives the keys of the currently *visible* (filtered) rows so selection
+ // stays aligned with what the search is actually showing.
+ onSelectAll: (fileKeys: string[], folderKeys: string[]) => void;
onPageChange: (token?: string) => void;
onItemsPerPageChange: (count: number) => void;
initialPageToken?: string;
@@ -51,6 +58,7 @@ export function ObjectsTable({
filterQuery,
deepSearch,
selectedFileKeys,
+ selectedFolderKeys,
isDragActive,
isLoading = false,
isTruncated = false,
@@ -58,8 +66,10 @@ export function ObjectsTable({
itemsPerPage,
onNavigateToFolder,
onDeleteObject,
+ onDeleteFolder,
onToggleFileSelection,
- onSelectAllFiles,
+ onToggleFolderSelection,
+ onSelectAll,
onPageChange,
onItemsPerPageChange,
initialPageToken,
@@ -218,12 +228,23 @@ export function ObjectsTable({
{canDelete && (
!obj.isFolder).length > 0 &&
- selectedFileKeys.size === filteredObjects.filter(obj => !obj.isFolder).length
+ pageObjects.length > 0 &&
+ pageObjects.every(obj =>
+ obj.isFolder ? selectedFolderKeys.has(obj.key) : selectedFileKeys.has(obj.key),
+ )
}
- onCheckedChange={onSelectAllFiles}
- aria-label="Select all files"
+ onCheckedChange={() =>
+ onSelectAll(
+ pageObjects.filter(obj => !obj.isFolder).map(obj => obj.key),
+ pageObjects.filter(obj => obj.isFolder).map(obj => obj.key),
+ )
+ }
+ aria-label="Select all objects"
/>
)}
@@ -277,10 +298,9 @@ export function ObjectsTable({
{obj.isFolder ? (
onToggleFolderSelection(obj.key)}
+ aria-label={`Select folder ${obj.key} (deletes its contents recursively)`}
/>
) : (
- {!obj.isFolder && (
+ {obj.isFolder ? (
+
+
+
+
+
+ onNavigateToFolder(obj.key)}>
+
+ Open
+
+ {onDeleteFolder && (
+ <>
+
+ onDeleteFolder(obj)}
+ >
+
+ Delete folder
+
+ >
+ )}
+
+
+ ) : (