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,