improve dropzones

This commit is contained in:
Francesco Sansalvadore committed 2025-08-29 17:39:58 +02:00
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) => ({
+22 -6
View File
@@ -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