diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
index 5d1ed5f3066..0c250c4a90b 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerColumn.tsx
@@ -32,12 +32,12 @@ const DragOverOverlay = ({ isOpen, onDragLeave, onDrop, folderIsEmpty }: any) =>
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
- className="h-full w-full absolute top-0"
+ className="h-full w-full absolute top-0 pointer-events-none"
>
{!folderIsEmpty && (
@@ -80,6 +80,8 @@ const FileExplorerColumn = ({
onColumnLoadMore = noop,
}: FileExplorerColumnProps) => {
const [isDraggedOver, setIsDraggedOver] = useState(false)
+ const [isInternalDragOver, setIsInternalDragOver] = useState(false)
+ const [moveToPath, setMoveToPath] = useState
('')
const fileExplorerColumnRef = useRef(null)
const snap = useStorageExplorerStateSnapshot()
@@ -94,6 +96,65 @@ const FileExplorerColumn = ({
}
}, [column])
+ // Reset drag states when column changes
+ useEffect(() => {
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+ setMoveToPath('')
+ }, [column.id])
+
+ // Listen for folder drag over events from child rows
+ useEffect(() => {
+ const handleFolderDragOver = (event: CustomEvent) => {
+ const { targetPath, folderName } = event.detail
+ console.log('Folder drag over detected:', folderName, 'Target path:', targetPath)
+ setMoveToPath(targetPath)
+ }
+
+ const columnElement = fileExplorerColumnRef.current
+ if (columnElement) {
+ columnElement.addEventListener('folderDragOver', handleFolderDragOver as EventListener)
+
+ return () => {
+ columnElement.removeEventListener('folderDragOver', handleFolderDragOver as EventListener)
+ }
+ }
+ }, [])
+
+ // Helper function to get the path for this column
+ const getColumnPath = () => {
+ // Use the index prop directly instead of trying to find it dynamically
+ const columnIndex = index
+ console.log(
+ 'getColumnPath called - Column index:',
+ columnIndex,
+ 'Opened folders:',
+ snap.openedFolders.map((f) => f.name)
+ )
+ console.log(
+ 'All columns:',
+ snap.columns.map((col, idx) => `${idx}: ${col.name} (${col.id})`)
+ )
+ console.log('Current column:', column.name, 'ID:', column.id, 'Index prop:', index)
+
+ if (columnIndex >= 0) {
+ // Column index 0 = root level (empty path)
+ // Column index 1 = first folder level (e.g., "foldher")
+ // Column index 2 = second folder level (e.g., "foldher/nested-folder")
+ const path =
+ columnIndex === 0
+ ? ''
+ : snap.openedFolders
+ .slice(0, columnIndex)
+ .map((folder) => folder.name)
+ .join('/')
+ console.log('Calculated column path:', path, '(column index:', columnIndex, ')')
+ return path
+ }
+ console.log('No column index found, returning empty path')
+ return ''
+ }
+
const haveSelectedItems = selectedItems.length > 0
const columnItemsId = column.items.map((item) => item.id)
const columnFiles = column.items.filter((item) => item.type === STORAGE_ROW_TYPES.FILE)
@@ -117,23 +178,159 @@ const FileExplorerColumn = ({
}
const onDragOver = (event: any) => {
+ console.log('onDragOver called for column:', column.name, 'Event type:', event.type)
+
if (event) {
- event.stopPropagation()
- event.preventDefault()
- if (event.type === 'dragover' && !isDraggedOver) {
- setIsDraggedOver(true)
+ // Reset states if this is a new drag operation
+ if (event.type === 'dragenter' && !isDraggedOver && !isInternalDragOver) {
+ console.log('New drag operation started, resetting states')
+ }
+ // Check if the drag target is over a folder/item or just the column background
+ const target = event.target as HTMLElement
+ const isOverItem = target.closest('[data-item-type]') || target.closest('.storage-row')
+
+ console.log('Target element:', target)
+ console.log('Is over item:', isOverItem)
+
+ // Check if this is an internal file drag (from another column) or external file drag
+ let isInternalFileDrag = false
+
+ // Check if the dataTransfer contains our custom JSON type
+ if (event.dataTransfer.types.includes('application/json')) {
+ isInternalFileDrag = true
+ console.log('Internal file drag detected - JSON type found')
+ } else {
+ isInternalFileDrag = false
+ console.log('External drag - no JSON type found')
+ }
+
+ if (isOverItem) {
+ // Let the item handle the drag over event
+ // Hide the drag overlay since we're over an item
+ if (isDraggedOver) {
+ setIsDraggedOver(false)
+ }
+ if (isInternalDragOver) {
+ setIsInternalDragOver(false)
+ }
+ // Don't clear moveToPath here - let the folder row set it
+ console.log('Over item, returning early')
+ return
+ }
+
+ // We're over column background (not over a specific item)
+ if (isInternalFileDrag) {
+ // Internal file drag over column background - show blue ring to indicate valid drop zone
+ event.preventDefault()
+ if (event.type === 'dragover') {
+ if (!isInternalDragOver) {
+ setIsInternalDragOver(true)
+ }
+ // Always update the path when dragging over column background
+ const columnPath = getColumnPath()
+ setMoveToPath(columnPath)
+ console.log(
+ 'Dragging over column background:',
+ column.name,
+ 'Column index:',
+ snap.columns.findIndex((col) => col.id === column.id),
+ 'Target path:',
+ columnPath,
+ 'Setting moveToPath to:',
+ columnPath
+ )
+ }
+ } 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)
+ }
+ }
}
}
}
const onDrop = (event: any) => {
+ console.log('onDrop called for column:', column.name)
onDragOver(event)
if (!canUpdateStorage) {
toast('You need additional permissions to upload files to this project')
- } else {
- onFilesUpload(event, index)
+ return
}
+
+ // Check if this is a file drop from another column
+ try {
+ const draggedItem = JSON.parse(event.dataTransfer.getData('application/json'))
+ if (draggedItem && draggedItem.type === STORAGE_ROW_TYPES.FILE) {
+ // Check if the drop target is actually a folder item (not the column background)
+ // If it's a folder drop, let the folder handle it
+ const target = event.target as HTMLElement
+ const isFolderDrop =
+ target.closest('[data-item-type="folder"]') ||
+ target.closest('[data-item-type="file"]') ||
+ target.closest('.storage-row')
+
+ if (isFolderDrop) {
+ // This is a folder/file drop, let the item handle it
+ return
+ }
+
+ // This is a column background drop
+ // Use the pre-calculated path from drag over
+ const targetDirectory = moveToPath
+
+ console.log(
+ 'Column background drop - Target directory:',
+ targetDirectory,
+ 'Column:',
+ column.name,
+ 'Column index:',
+ snap.columns.findIndex((col) => col.id === column.id),
+ 'Opened folders:',
+ snap.openedFolders.map((f) => f.name),
+ 'MoveToPath:',
+ moveToPath,
+ 'Current moveToPath state:',
+ moveToPath
+ )
+ console.log('Drop event target:', event.target)
+ console.log(
+ 'Is over item check:',
+ 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)
+
+ // Reset drag states after successful drop
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+
+ console.log('File move completed, drag states reset')
+ return
+ }
+ } catch (error) {
+ // Not a JSON drag, likely external file upload - continue with normal upload logic
+ }
+
+ // Normal file upload logic
+ onFilesUpload(event, index)
}
const SelectAllCheckbox = () => (
@@ -152,11 +349,20 @@ const FileExplorerColumn = ({
className={cn(
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'
+ 'hide-scrollbar relative flex flex-shrink-0 flex-col overflow-auto',
+ (isDraggedOver || isInternalDragOver) && 'ring-2 ring-blue-500 ring-opacity-50'
)}
onContextMenu={displayMenu}
onDragOver={onDragOver}
onDrop={onDrop}
+ onDragLeave={(event) => {
+ // Only reset if we're actually leaving the column (not just moving to a child element)
+ if (!event.currentTarget.contains(event.relatedTarget as Node)) {
+ console.log('Leaving column, resetting drag states')
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+ }
+ }}
onClick={(event) => {
const eventTarget = get(event.target, ['className'], '')
if (typeof eventTarget === 'string' && eventTarget.includes('react-contexify')) return
@@ -260,8 +466,14 @@ const FileExplorerColumn = ({
setIsDraggedOver(false)}
- onDrop={() => setIsDraggedOver(false)}
+ onDragLeave={() => {
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+ }}
+ onDrop={() => {
+ setIsDraggedOver(false)
+ setIsInternalDragOver(false)
+ }}
/>
{/* List interface footer */}
diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
index 8979041793e..09ad591eb58 100644
--- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
+++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx
@@ -15,6 +15,7 @@ import {
Trash2,
} from 'lucide-react'
import { useContextMenu } from 'react-contexify'
+import { useState } from 'react'
import SVG from 'react-inlinesvg'
import { useParams } from 'common'
@@ -112,7 +113,9 @@ const FileExplorerRow: ItemRenderer = ({
selectedItems = [],
}) => {
const { ref: projectRef, bucketId } = useParams()
+ const [isFolderDragOver, setIsFolderDragOver] = useState(false)
+ const snap = useStorageExplorerStateSnapshot()
const {
selectedBucket,
selectedFilePreview,
@@ -129,7 +132,7 @@ const FileExplorerRow: ItemRenderer = ({
openFolder,
downloadFolder,
selectRangeItems,
- } = useStorageExplorerStateSnapshot()
+ } = snap
const { show } = useContextMenu()
const { onCopyUrl } = useCopyUrl()
@@ -164,6 +167,85 @@ const FileExplorerRow: ItemRenderer = ({
setSelectedFilePreview(undefined)
}
+ // Drag and drop handlers
+ const onDragStart = (event: React.DragEvent) => {
+ if (item.type === STORAGE_ROW_TYPES.FILE && canUpdateFiles) {
+ event.dataTransfer.setData('application/json', JSON.stringify(itemWithColumnIndex))
+ event.dataTransfer.effectAllowed = 'move'
+ }
+ }
+
+ const onDragOver = (event: React.DragEvent) => {
+ // Always prevent default for folders to allow drops
+ if (item.type === STORAGE_ROW_TYPES.FOLDER && canUpdateFiles) {
+ event.preventDefault()
+ event.stopPropagation()
+ event.dataTransfer.dropEffect = 'move'
+ setIsFolderDragOver(true)
+
+ // Calculate and dispatch the target path for this folder
+ const targetDirectory = snap.openedFolders
+ .slice(0, columnIndex)
+ .map((folder) => folder.name)
+ .concat(item.name)
+ .join('/')
+
+ // Dispatch custom event to communicate with parent column
+ const customEvent = new CustomEvent('folderDragOver', {
+ detail: { targetPath: targetDirectory, folderName: item.name },
+ })
+ event.currentTarget.dispatchEvent(customEvent)
+ }
+ }
+
+ const onDragLeave = (event: React.DragEvent) => {
+ if (item.type === STORAGE_ROW_TYPES.FOLDER && canUpdateFiles) {
+ setIsFolderDragOver(false)
+ }
+ }
+
+ const onDrop = (event: React.DragEvent) => {
+ if (item.type === STORAGE_ROW_TYPES.FOLDER && canUpdateFiles) {
+ event.preventDefault()
+ console.log('Folder drop event triggered for:', item.name)
+ 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
+ const targetDirectory = snap.openedFolders
+ .slice(0, columnIndex)
+ .map((folder) => folder.name)
+ .concat(item.name)
+ .join('/')
+
+ console.log(
+ 'Folder drop - Target directory:',
+ targetDirectory,
+ 'Folder:',
+ item.name,
+ 'Column index:',
+ columnIndex,
+ 'Opened folders:',
+ 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)
+
+ // Reset drag over state
+ setIsFolderDragOver(false)
+ }
+ } catch (error) {
+ console.error('Failed to parse dragged item:', error)
+ }
+ }
+ }
+
const rowOptions =
item.type === STORAGE_ROW_TYPES.FOLDER
? [
@@ -305,12 +387,24 @@ const FileExplorerRow: ItemRenderer = ({
return (
{
event.stopPropagation()
item.type === STORAGE_ROW_TYPES.FILE
? displayMenu(event, STORAGE_ROW_TYPES.FILE)
: displayMenu(event, STORAGE_ROW_TYPES.FOLDER)
}}
+ onDragStart={onDragStart}
+ onDragOver={onDragOver}
+ onDragLeave={onDragLeave}
+ onDrop={onDrop}
+ onDragEnter={(event) => {
+ if (item.type === STORAGE_ROW_TYPES.FOLDER && canUpdateFiles) {
+ setIsFolderDragOver(true)
+ }
+ }}
>
= ({
'hover:bg-panel-footer-light [[data-theme*=dark]_&]:hover:bg-panel-footer-dark',
`${isOpened ? 'bg-surface-200' : ''}`,
`${isPreviewed ? 'bg-green-500 hover:bg-green-500' : ''}`,
+ `${isFolderDragOver ? 'bg-blue-200 ring-2 ring-blue-500 shadow-lg' : ''}`,
`${item.status !== STORAGE_ROW_STATUS.LOADING ? 'cursor-pointer' : ''}`
)}
onClick={(event) => {
@@ -329,6 +424,17 @@ const FileExplorerRow: ItemRenderer
= ({
: onSelectFile(columnIndex, item)
}
}}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ if (item.status !== STORAGE_ROW_STATUS.LOADING && !isOpened && !isPreviewed) {
+ item.type === STORAGE_ROW_TYPES.FOLDER || item.type === STORAGE_ROW_TYPES.BUCKET
+ ? openFolder(columnIndex, item)
+ : onSelectFile(columnIndex, item)
+ }
+ }
+ }}
+ tabIndex={item.status !== STORAGE_ROW_STATUS.LOADING ? 0 : -1}
>
= ({
view === STORAGE_VIEWS.LIST ? 'w-[40%] min-w-[250px]' : 'w-[90%]'
)}
>
-
event.stopPropagation()}>
+
event.stopPropagation()}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }}
+ >
{!isSelected && (
= ({
// Stops click event from this div, to resolve an issue with menu item's click event triggering unexpected row select
event.stopPropagation()
}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ event.stopPropagation()
+ }
+ }}
>
{item.status === STORAGE_ROW_STATUS.LOADING ? (