mirror of
https://github.com/supabase/supabase.git
synced 2026-10-13 21:29:08 +03:00
improve dropzones
This commit is contained in:
1 parent
9ea88cfc40
commit
ad765e2edd
3 files changed
+93
-36
No files matched your search
@@ -92,16 +92,11 @@ const FileExplorerColumn = ({
|
||||
const columnIndex = 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('/')
|
||||
// The column index represents the folder level we're currently viewing
|
||||
const path = snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -110,13 +105,22 @@ const FileExplorerColumn = ({
|
||||
}
|
||||
|
||||
// Drop target for column background
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
const [dropProps, drop] = useDrop({
|
||||
accept: 'storage-item',
|
||||
canDrop: (draggedItem: any) => {
|
||||
canDrop: (draggedItem: any, monitor: any) => {
|
||||
// Don't allow dropping on the same column
|
||||
if (draggedItem.sourceColumnIndex === index) return false
|
||||
|
||||
// Don't allow dropping a folder into its own subfolder (circular reference)
|
||||
// Only allow drops when we're actually hovering over the column background
|
||||
// This prevents interference with folder item drops
|
||||
const isOverColumnBackground = monitor.isOver({ shallow: true })
|
||||
|
||||
if (!isOverColumnBackground) {
|
||||
// Not over column background, blocking drop
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't allow dropping a folder into itself or any of its subdirectories (circular reference)
|
||||
if (draggedItem.type === STORAGE_ROW_TYPES.FOLDER) {
|
||||
const draggedItemPath = snap.openedFolders
|
||||
.slice(0, draggedItem.sourceColumnIndex)
|
||||
@@ -124,12 +128,35 @@ const FileExplorerColumn = ({
|
||||
.join('/')
|
||||
const targetPath = getColumnPath()
|
||||
|
||||
return !targetPath.startsWith(draggedItemPath + '/')
|
||||
// Build the full path of the dragged folder
|
||||
const draggedItemFullPath =
|
||||
draggedItemPath.length > 0 ? `${draggedItemPath}/${draggedItem.name}` : draggedItem.name
|
||||
|
||||
// Check if target path is the same as dragged item path (dropping on itself)
|
||||
if (targetPath === draggedItemFullPath) {
|
||||
// Cannot drop folder on itself
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional check: Block column background drops for folders when the target would be invalid
|
||||
// This prevents the column from intercepting drops that should go to folder items
|
||||
// Allow valid moves (like moving a folder to root) while preventing invalid ones
|
||||
const draggedItemCurrentPath = snap.openedFolders
|
||||
.slice(0, draggedItem.sourceColumnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.join('/')
|
||||
|
||||
// Block if the target path would be the same as the dragged item's current location
|
||||
// (This prevents self-drops, but allows moves from subdirectories to parent directories)
|
||||
if (targetPath === draggedItemCurrentPath) {
|
||||
// Blocking folder drop to same location
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
drop: (draggedItem: any) => {
|
||||
drop: (draggedItem: any, monitor: any) => {
|
||||
if (canUpdateStorage) {
|
||||
const targetDirectory = getColumnPath()
|
||||
|
||||
@@ -213,7 +240,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',
|
||||
(isOver || isDraggedOver) && 'bg-selection/10'
|
||||
(dropProps.isOver || isDraggedOver) && 'bg-selection/10'
|
||||
)}
|
||||
onContextMenu={displayMenu}
|
||||
onDrop={handleExternalFileDrop}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Trash2,
|
||||
} from 'lucide-react'
|
||||
import { useContextMenu } from 'react-contexify'
|
||||
import { useState, useRef } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { useDrag, useDrop } from 'react-dnd'
|
||||
import SVG from 'react-inlinesvg'
|
||||
|
||||
@@ -114,7 +114,6 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
selectedItems = [],
|
||||
}) => {
|
||||
const { ref: projectRef, bucketId } = useParams()
|
||||
const [isFolderDragOver, setIsFolderDragOver] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const snap = useStorageExplorerStateSnapshot()
|
||||
@@ -178,7 +177,7 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
canDrop: (draggedItem: any) => {
|
||||
// Only allow drops on folders
|
||||
if (item.type !== STORAGE_ROW_TYPES.FOLDER) {
|
||||
// Not a folder, canDrop returning false
|
||||
// Not a folder
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -198,32 +197,49 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
targetItemPath.length > 0 ? `${targetItemPath}/${item.name}` : item.name
|
||||
|
||||
if (draggedItemFullPath === targetItemFullPath) {
|
||||
// Dropping on itself (same path), canDrop returning false
|
||||
// Can't drop on itself (same path)
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't allow dropping a folder into its own subfolder (circular reference)
|
||||
// Don't allow dropping a folder into itself or any of its subdirectories (circular reference)
|
||||
if (draggedItem.type === STORAGE_ROW_TYPES.FOLDER) {
|
||||
// For folder drops, the target is the folder itself
|
||||
// When dropping on a folder item, we want to move INTO that folder
|
||||
const targetPath = snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
.concat(item.name)
|
||||
.join('/')
|
||||
|
||||
const wouldCreateCircularReference = targetPath.startsWith(draggedItemFullPath + '/')
|
||||
// Check if target path is the same as dragged item path (dropping on itself)
|
||||
if (targetPath === draggedItemFullPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (wouldCreateCircularReference) {
|
||||
// Would create circular reference, canDrop returning false
|
||||
// Check if target path is a subdirectory of the dragged item (would create circular reference)
|
||||
const droppedOnOwnSubdir = targetPath.startsWith(draggedItemFullPath + '/')
|
||||
|
||||
if (droppedOnOwnSubdir) {
|
||||
// Can't drop in own subdirectory
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if target path is the IMMEDIATE parent of the dragged item
|
||||
// This prevents dropping a folder into its direct parent, but allows moving to ancestor directories
|
||||
const draggedItemImmediateParent = draggedItemFullPath.split('/').slice(0, -1).join('/')
|
||||
const isDroppingOnImmediateParent = targetPath === draggedItemImmediateParent
|
||||
|
||||
if (isDroppingOnImmediateParent) {
|
||||
// Cannot drop folder into its immediate parent directory (same directory)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// canDrop returning true
|
||||
return true
|
||||
},
|
||||
drop: (draggedItem: any) => {
|
||||
drop: (draggedItem: any, monitor: any) => {
|
||||
if (item.type === STORAGE_ROW_TYPES.FOLDER && canUpdateFiles) {
|
||||
// Calculate target directory path
|
||||
// Calculate target directory path - for folder drops, target is the folder itself
|
||||
const targetDirectory = snap.openedFolders
|
||||
.slice(0, columnIndex)
|
||||
.map((folder) => folder.name)
|
||||
@@ -244,9 +260,7 @@ const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
|
||||
// Use the drag & drop function that doesn't interfere with the modal
|
||||
snap.moveFilesDragAndDrop([draggedItem], targetDirectory)
|
||||
} else {
|
||||
// 'Drop conditions not met:', {
|
||||
// isFolder: item.type === STORAGE_ROW_TYPES.FOLDER,
|
||||
// canUpdateFiles,
|
||||
// Drop conditions not met
|
||||
}
|
||||
},
|
||||
collect: (monitor) => ({
|
||||
|
||||
@@ -51,6 +51,7 @@ import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { lookupMime } from 'lib/mime'
|
||||
import { Button, SONNER_DEFAULT_DURATION, SonnerProgress, Progress } from 'ui'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
// Custom progress toast component for folder moves
|
||||
const FolderMoveProgressToast = ({
|
||||
@@ -63,17 +64,22 @@ const FolderMoveProgressToast = ({
|
||||
folderName: string
|
||||
}) => {
|
||||
const progress = total > 0 ? Math.round((current / total) * 100) : 0
|
||||
const isCalculating = total === 0
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 w-full">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex w-full justify-between">
|
||||
<p className="text-foreground text-sm">Moving folder "{folderName}"...</p>
|
||||
<p className="text-foreground-light text-sm font-mono">{`${progress}%`}</p>
|
||||
<p className="text-foreground text-sm">Moving folder "{folderName}"</p>
|
||||
{isCalculating ? (
|
||||
<Loader2 className="animate-spin text-foreground-muted" size={16} />
|
||||
) : (
|
||||
<p className="text-foreground-light text-sm font-mono">{`${progress}%`}</p>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={progress} className="w-full" />
|
||||
<small className="text-foreground-light text-xs">
|
||||
Moved {current} / {total} items...
|
||||
{!isCalculating && <Progress value={progress} className="w-full" />}
|
||||
<small className="text-foreground-light text-xs flex items-center gap-0.5">
|
||||
{isCalculating ? 'Calculating items to move...' : `Moved ${current} / ${total} items...`}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1661,13 +1667,23 @@ function createStorageExplorerState({
|
||||
const allItemsToMove = await getAllFilesRecursively(sourceFullPath)
|
||||
const filesToMove = allItemsToMove.filter((item) => !item.isFolder)
|
||||
|
||||
// Update progress tracking with total count
|
||||
// Update progress tracking with total count and show calculation complete
|
||||
const initialProgressData = state.folderMoveProgress.get(sourceFullPath)
|
||||
if (initialProgressData) {
|
||||
state.setFolderMoveProgress(sourceFullPath, {
|
||||
...initialProgressData,
|
||||
total: filesToMove.length,
|
||||
})
|
||||
|
||||
// Update toast to show calculation complete and move starting
|
||||
toast.loading(
|
||||
<FolderMoveProgressToast
|
||||
current={0}
|
||||
total={filesToMove.length}
|
||||
folderName={folderName}
|
||||
/>,
|
||||
{ id: initialProgressData.toastId }
|
||||
)
|
||||
}
|
||||
|
||||
// Move all files from source to target
|
||||
|
||||
Reference in new issue
Block a user