diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
index 0c250c4a90b..14c778913d0 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
@@ -220,6 +220,18 @@ const FileExplorerColumn = ({
// We're over column background (not over a specific item)
if (isInternalFileDrag) {
+ // Check if this is a drag from the same column (same location)
+ try {
+ const draggedItem = JSON.parse(event.dataTransfer.getData('application/json'))
+ if (draggedItem && draggedItem.columnIndex === index) {
+ // Same column drag - don't highlight or allow drop
+ console.log('Same column drag detected, ignoring')
+ return
+ }
+ } catch (error) {
+ // Can't parse data during dragover, continue with normal logic
+ }
+
// Internal file drag over column background - show blue ring to indicate valid drop zone
event.preventDefault()
if (event.type === 'dragover') {
@@ -241,21 +253,10 @@ const FileExplorerColumn = ({
)
}
} else {
- // External file drag - only show overlay for empty columns
- const hasVisibleItems =
- column.items.length > 0 && column.status !== STORAGE_ROW_STATUS.LOADING
-
- if (!hasVisibleItems) {
- // Column is empty - show overlay for external file uploads
- event.preventDefault()
- if (event.type === 'dragover' && !isDraggedOver) {
- setIsDraggedOver(true)
- }
- } else {
- // Column has items - don't show overlay for external files
- if (isDraggedOver) {
- setIsDraggedOver(false)
- }
+ // External file drag - show overlay for file uploads (both empty and non-empty columns)
+ event.preventDefault()
+ if (event.type === 'dragover' && !isDraggedOver) {
+ setIsDraggedOver(true)
}
}
}
@@ -287,6 +288,12 @@ const FileExplorerColumn = ({
return
}
+ // Check if this is a drop to the same column (same location)
+ if (draggedItem.columnIndex === index) {
+ console.log('Same column drop detected, ignoring move operation')
+ return
+ }
+
// This is a column background drop
// Use the pre-calculated path from drag over
const targetDirectory = moveToPath
@@ -311,12 +318,8 @@ const FileExplorerColumn = ({
target.closest('[data-item-type]') || target.closest('.storage-row')
)
- // Set the selected items for moving (required by moveFiles)
- snap.setSelectedItemsToMove([draggedItem])
-
- // Trigger the move operation directly without opening the modal
- // moveFiles expects the directory path, not the full file path
- snap.moveFiles(targetDirectory)
+ // Use the new drag & drop function that doesn't interfere with the modal
+ snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
// Reset drag states after successful drop
setIsDraggedOver(false)
@@ -350,7 +353,7 @@ const FileExplorerColumn = ({
fullWidth ? 'w-full' : 'w-64 border-r border-overlay',
snap.view === STORAGE_VIEWS.LIST && 'h-full',
'hide-scrollbar relative flex flex-shrink-0 flex-col overflow-auto',
- (isDraggedOver || isInternalDragOver) && 'ring-2 ring-blue-500 ring-opacity-50'
+ (isDraggedOver || isInternalDragOver) && 'bg-selection/10'
)}
onContextMenu={displayMenu}
onDragOver={onDragOver}
@@ -433,6 +436,67 @@ const FileExplorerColumn = ({
onLoadNextPage={() => onColumnLoadMore(index, column)}
/>
+ {/* List View Background Drag & Drop (separate from content) */}
+ {snap.view === STORAGE_VIEWS.LIST && (
+
{
+ // Handle list view background drag over for internal file moves
+ if (event.dataTransfer.types.includes('application/json')) {
+ event.preventDefault()
+ if (!isInternalDragOver) {
+ setIsInternalDragOver(true)
+ // For list view, the target is the current folder level
+ const currentPath = snap.openedFolders.map((folder) => folder.name).join('/')
+ setMoveToPath(currentPath)
+ console.log('List view background drag over - Target path:', currentPath)
+ }
+ }
+ }}
+ onDrop={(event) => {
+ // Handle list view background drop for internal file moves
+ if (event.dataTransfer.types.includes('application/json')) {
+ event.preventDefault()
+ try {
+ const draggedItem = JSON.parse(event.dataTransfer.getData('application/json'))
+ if (draggedItem && draggedItem.type === STORAGE_ROW_TYPES.FILE) {
+ // Check if this is a drop to the same location (same opened folders)
+ const draggedItemPath = snap.openedFolders
+ .slice(0, draggedItem.columnIndex)
+ .map((folder) => folder.name)
+ .join('/')
+ const currentPath = snap.openedFolders.map((folder) => folder.name).join('/')
+
+ if (draggedItemPath === currentPath) {
+ console.log('Same location drop detected in list view, ignoring move operation')
+ return
+ }
+
+ const targetDirectory = moveToPath
+ console.log('List view background drop - Target directory:', targetDirectory)
+
+ // Use the new drag & drop function that doesn't interfere with the modal
+ snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
+
+ // Reset drag states
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+ setMoveToPath('')
+
+ console.log('List view file move completed')
+ return
+ }
+ } catch (error) {
+ console.error('Failed to parse dragged item in list view:', error)
+ }
+ }
+ }}
+ />
+ )}
+
{/* Drag drop upload CTA for when column is empty */}
{!(snap.isSearching && itemSearchString.length > 0) &&
column.items.length === 0 &&
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
index 09ad591eb58..d3998ae9591 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
@@ -211,14 +211,22 @@ const FileExplorerRow: ItemRenderer = ({
try {
const draggedItem = JSON.parse(event.dataTransfer.getData('application/json'))
if (draggedItem && draggedItem.type === STORAGE_ROW_TYPES.FILE) {
- // Calculate the target directory path - need the full path to the folder
- // The folder might be nested within other folders, so we need to construct the complete path
+ // Check if this is a drop to the same location (same opened folders)
+ const draggedItemPath = snap.openedFolders
+ .slice(0, draggedItem.columnIndex)
+ .map((folder) => folder.name)
+ .join('/')
const targetDirectory = snap.openedFolders
.slice(0, columnIndex)
.map((folder) => folder.name)
.concat(item.name)
.join('/')
+ if (draggedItemPath === targetDirectory) {
+ console.log('Same location drop detected on folder, ignoring move operation')
+ return
+ }
+
console.log(
'Folder drop - Target directory:',
targetDirectory,
@@ -230,12 +238,8 @@ const FileExplorerRow: ItemRenderer = ({
snap.openedFolders.map((f) => f.name)
)
- // Set the selected items for moving (required by moveFiles)
- snap.setSelectedItemsToMove([draggedItem])
-
- // Trigger the move operation directly without opening the modal
- // moveFiles expects the directory path, not the full file path
- snap.moveFiles(targetDirectory)
+ // Use the new drag & drop function that doesn't interfere with the modal
+ snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
// Reset drag over state
setIsFolderDragOver(false)
diff --git a/apps/studio/state/storage-explorer.tsx b/apps/studio/state/storage-explorer.tsx
index 90ceef7b787..2ab1815e514 100644
--- a/apps/studio/state/storage-explorer.tsx
+++ b/apps/studio/state/storage-explorer.tsx
@@ -1404,6 +1404,58 @@ function createStorageExplorerState({
state.setSelectedItemsToMove([])
},
+ // New function for drag & drop operations that doesn't interfere with the modal
+ moveFilesDragAndDrop: async (items: StorageItemWithColumn[], newPathToFile: string) => {
+ const newPaths = compact(newPathToFile.split('/'))
+ const formattedNewPathToFile = newPaths.join('/')
+ let numberOfFilesMovedFail = 0
+
+ const toastId = toast(`Moving ${items.length} file${items.length > 1 ? 's' : ''}...`, {
+ description: STORAGE_PROGRESS_INFO_TEXT,
+ duration: Infinity,
+ })
+
+ await Promise.all(
+ items.map(async (item) => {
+ const pathToFile = state.openedFolders
+ .slice(0, item.columnIndex)
+ .map((folder) => folder.name)
+ .join('/')
+
+ const fromPath = pathToFile.length > 0 ? `${pathToFile}/${item.name}` : item.name
+ const toPath =
+ newPathToFile.length > 0 ? `${formattedNewPathToFile}/${item.name}` : item.name
+
+ try {
+ await moveStorageObject({
+ projectRef: state.projectRef,
+ bucketId: state.selectedBucket.id,
+ from: fromPath,
+ to: toPath,
+ })
+ } catch (error: any) {
+ numberOfFilesMovedFail += 1
+ toast.error(error.message)
+ }
+ })
+ )
+
+ if (numberOfFilesMovedFail === items.length) {
+ toast.error('Failed to move files')
+ } else {
+ toast(
+ `Successfully moved ${
+ items.length - numberOfFilesMovedFail
+ } files to ${formattedNewPathToFile.length > 0 ? formattedNewPathToFile : 'the root of your bucket'}`
+ )
+ }
+
+ toast.dismiss(toastId)
+
+ // TODO: invalidate the file preview cache when moving files
+ await state.refetchAllOpenedFolders()
+ },
+
deleteFiles: async ({
files,
isDeleteFolder = false,