From 6382b8a053d0f1f5f4fdcd64f95fd4e5b369abd1 Mon Sep 17 00:00:00 2001 From: seoo2001 <96180608+seoo2001@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:50:38 -0700 Subject: [PATCH] fix(file-explorer): report the saved filename in the download toast (#12959) * fix(file-explorer): report the saved filename in the download toast The success toast named the remote node, so renaming a file in the native save dialog left the label disagreeing with its own Open action, which opens the real destination. Folder downloads had the same gap whenever sanitizeLocalDownloadFilename rewrote the remote basename. * fix(file-explorer): respect local download path semantics --------- Co-authored-by: Jinwoo-H --- .../right-sidebar/FileExplorer.test.tsx | 40 +++++++++++++++++-- .../right-sidebar/FileExplorerRow.tsx | 17 +++++++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/right-sidebar/FileExplorer.test.tsx b/src/renderer/src/components/right-sidebar/FileExplorer.test.tsx index 1688b0721..37fce9ae6 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorer.test.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorer.test.tsx @@ -293,6 +293,14 @@ function makeToolbar(overrides: Partial[0 }) } +function setDownloadPlatform(platform: NodeJS.Platform): void { + ;( + window as unknown as { + api: { platform: { get: () => { platform: NodeJS.Platform } } } + } + ).api.platform = { get: () => ({ platform }) } +} + beforeEach(() => { toastErrorMock.mockReset() toastSuccessMock.mockReset() @@ -677,7 +685,10 @@ describe('FileExplorerRow collapse folder action', () => { it('calls the preload download API and shows success only when not canceled', async () => { const downloadFile = vi .fn() - .mockResolvedValueOnce({ canceled: false, destinationPath: '/downloads/index.ts' }) + .mockResolvedValueOnce({ + canceled: false, + destinationPath: '/downloads/renamed\\entry.ts' + }) .mockResolvedValueOnce({ canceled: true }) const openPath = vi.fn().mockResolvedValue(undefined) ;( @@ -690,6 +701,7 @@ describe('FileExplorerRow collapse folder action', () => { } } ).window = { api: { fs: { downloadFile }, shell: { openPath } } } + setDownloadPlatform('linux') await downloadRemoteFile(fileNode, 'ssh-1') await downloadRemoteFile(fileNode, 'ssh-1') @@ -699,7 +711,7 @@ describe('FileExplorerRow collapse folder action', () => { connectionId: 'ssh-1' }) expect(toastSuccessMock).toHaveBeenCalledTimes(1) - expect(toastSuccessMock).toHaveBeenCalledWith("Downloaded 'index.ts'", { + expect(toastSuccessMock).toHaveBeenCalledWith("Downloaded 'renamed\\entry.ts'", { action: { label: 'Open', onClick: expect.any(Function) @@ -709,10 +721,30 @@ describe('FileExplorerRow collapse folder action', () => { | { onClick: () => void } | undefined action?.onClick() - expect(openPath).toHaveBeenCalledWith('/downloads/index.ts') + expect(openPath).toHaveBeenCalledWith('/downloads/renamed\\entry.ts') expect(toastErrorMock).not.toHaveBeenCalled() }) + it('reports the saved folder name from a Windows destination path', async () => { + const downloadFolder = vi.fn().mockResolvedValue({ + canceled: false, + destinationPath: 'C:\\Users\\dev\\Downloads\\src-copy' + }) + ;( + globalThis as unknown as { + window: { api: { fs: { downloadFolder: typeof downloadFolder } } } + } + ).window = { api: { fs: { downloadFolder } } } + setDownloadPlatform('win32') + + await downloadRemoteFile(directoryNode, 'ssh-1') + + expect(toastSuccessMock).toHaveBeenCalledWith( + "Downloaded folder 'src-copy'", + expect.objectContaining({ action: expect.anything() }) + ) + }) + it('calls the preload folder download API for SSH directory rows', async () => { const downloadFolder = vi.fn().mockResolvedValue({ canceled: false, @@ -729,6 +761,7 @@ describe('FileExplorerRow collapse folder action', () => { } } ).window = { api: { fs: { downloadFolder }, shell: { openPath } } } + setDownloadPlatform('linux') await downloadRemoteFile(directoryNode, 'ssh-1') @@ -765,6 +798,7 @@ describe('FileExplorerRow collapse folder action', () => { } } ).window = { api: { shell: { openPath } } } + setDownloadPlatform('linux') await downloadRemoteFile(fileNode, runtimeContext) diff --git a/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx b/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx index 1b621454a..6167f08d5 100644 --- a/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx +++ b/src/renderer/src/components/right-sidebar/FileExplorerRow.tsx @@ -342,6 +342,14 @@ export function shouldShowCopyFileAction( ) } +function getLocalDownloadName(destinationPath: string, platform: NodeJS.Platform): string { + const lastSeparatorIndex = + platform === 'win32' + ? Math.max(destinationPath.lastIndexOf('/'), destinationPath.lastIndexOf('\\')) + : destinationPath.lastIndexOf('/') + return destinationPath.slice(lastSeparatorIndex + 1) +} + export async function downloadRemoteFile( node: TreeNode, connectionIdOrRuntimeContext: string | RuntimeFileOperationArgs @@ -363,17 +371,22 @@ export async function downloadRemoteFile( if (result.canceled) { return } + // Why: POSIX permits backslashes in saved names; only Windows treats them as separators. + const savedName = getLocalDownloadName( + result.destinationPath, + window.api.platform.get().platform + ) toast.success( node.isDirectory ? translate( 'auto.components.right.sidebar.FileExplorerRow.a4029c996b', "Downloaded folder '{{value0}}'", - { value0: node.name } + { value0: savedName } ) : translate( 'auto.components.right.sidebar.FileExplorerRow.bce4d4e44f', "Downloaded '{{value0}}'", - { value0: node.name } + { value0: savedName } ), { action: {