diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts b/src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts index c215fd5e453..8d66ad1bfd7 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerRowDrag.test.ts @@ -64,4 +64,24 @@ describe('getWorkspaceFileDragPaths', () => { }) ).toEqual(paths) }) + + it('drops selected descendants so moving a folder does not also move its children', () => { + const paths = ['/repo/src', '/repo/src/components/Button.tsx', '/repo/src-extra/index.ts'] + expect( + getWorkspaceFileDragPaths({ + getData: (type) => + type === WORKSPACE_FILE_PATHS_MIME ? encodeWorkspaceFilePaths(paths) : '' + }) + ).toEqual(['/repo/src', '/repo/src-extra/index.ts']) + }) + + it('drops selected descendants with Windows separators and case', () => { + const paths = ['C:\\Repo\\src', 'c:\\repo\\src\\components\\Button.tsx'] + expect( + getWorkspaceFileDragPaths({ + getData: (type) => + type === WORKSPACE_FILE_PATHS_MIME ? encodeWorkspaceFilePaths(paths) : '' + }) + ).toEqual(['C:\\Repo\\src']) + }) }) diff --git a/src/renderer/src/lib/workspace-file-drag.ts b/src/renderer/src/lib/workspace-file-drag.ts index e69ccf4f69e..48b61df4ce1 100644 --- a/src/renderer/src/lib/workspace-file-drag.ts +++ b/src/renderer/src/lib/workspace-file-drag.ts @@ -1,3 +1,8 @@ +import { + isPathInsideOrEqual, + normalizeRuntimePathForComparison +} from '../../../shared/cross-platform-path' + export const WORKSPACE_FILE_PATH_MIME = 'text/x-orca-file-path' export const WORKSPACE_FILE_PATHS_MIME = 'text/x-orca-file-paths' @@ -20,10 +25,40 @@ export function decodeWorkspaceFilePaths(data: string): string[] { return [data] } +function getTopLevelWorkspaceFilePaths(paths: readonly string[]): string[] { + const uniquePaths: string[] = [] + for (const path of paths) { + if ( + path && + !uniquePaths.some( + (existing) => + normalizeRuntimePathForComparison(existing) === normalizeRuntimePathForComparison(path) + ) + ) { + uniquePaths.push(path) + } + } + + // Why: moving a selected folder already moves its descendants; issuing + // extra moves for selected children races against paths that no longer exist. + return uniquePaths.filter( + (path) => + !uniquePaths.some( + (candidateRoot) => + candidateRoot !== path && + normalizeRuntimePathForComparison(candidateRoot) !== + normalizeRuntimePathForComparison(path) && + isPathInsideOrEqual(candidateRoot, path) + ) + ) +} + export function getWorkspaceFileDragPaths(dataTransfer: Pick): string[] { const multiPathData = dataTransfer.getData(WORKSPACE_FILE_PATHS_MIME) if (multiPathData) { - return decodeWorkspaceFilePaths(multiPathData) + return getTopLevelWorkspaceFilePaths(decodeWorkspaceFilePaths(multiPathData)) } - return decodeWorkspaceFilePaths(dataTransfer.getData(WORKSPACE_FILE_PATH_MIME)) + return getTopLevelWorkspaceFilePaths( + decodeWorkspaceFilePaths(dataTransfer.getData(WORKSPACE_FILE_PATH_MIME)) + ) }