From ebed27a0602b50d1307693d6cca301fcdbfbc2c2 Mon Sep 17 00:00:00 2001 From: AlexisMora Date: Tue, 10 Feb 2026 11:22:16 +0100 Subject: [PATCH 1/3] Fix: hide backup folder selector when selecting path in hide explorer --- src/apps/main/device/service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/apps/main/device/service.ts b/src/apps/main/device/service.ts index 55cb8d2e5..29d6906df 100644 --- a/src/apps/main/device/service.ts +++ b/src/apps/main/device/service.ts @@ -1,5 +1,5 @@ import { aes } from '@internxt/lib'; -import { dialog, IpcMainEvent } from 'electron'; +import { BrowserWindow, dialog, IpcMainEvent } from 'electron'; import fetch from 'electron-fetch'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import os from 'os'; @@ -318,10 +318,23 @@ export async function getPathFromDialog(): Promise<{ path: string; itemName: string; } | null> { + const parentWindow = + BrowserWindow.getFocusedWindow() ?? + BrowserWindow.getAllWindows().find((w) => w.isVisible()) ?? + null; + + if (parentWindow) { + parentWindow.hide(); + } + const result = await dialog.showOpenDialog({ properties: ['openDirectory'], }); + if (parentWindow && !parentWindow.isDestroyed()) { + parentWindow.show(); + } + if (result.canceled) { return null; } From 79ea1f86b6ea9069217eea3d6fd71ff948f85c5c Mon Sep 17 00:00:00 2001 From: AlexisMora Date: Tue, 10 Feb 2026 13:05:08 +0100 Subject: [PATCH 2/3] chore: move getPathFromDialog to proper features backup folder and add tests --- src/apps/main/backups/add-backup.test.ts | 2 +- src/apps/main/backups/add-backup.ts | 2 +- src/apps/main/device/handlers.ts | 2 +- src/apps/main/device/service.ts | 38 +----- src/apps/main/preload.d.ts | 2 +- .../backup/get-path-from-dialog.test.ts | 121 ++++++++++++++++++ .../features/backup/get-path-from-dialog.ts | 34 +++++ 7 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 src/backend/features/backup/get-path-from-dialog.test.ts create mode 100644 src/backend/features/backup/get-path-from-dialog.ts diff --git a/src/apps/main/backups/add-backup.test.ts b/src/apps/main/backups/add-backup.test.ts index 2dbab8d95..a9300526a 100644 --- a/src/apps/main/backups/add-backup.test.ts +++ b/src/apps/main/backups/add-backup.test.ts @@ -1,4 +1,4 @@ -import * as getPathFromDialogModule from '../device/service'; +import * as getPathFromDialogModule from '../../../backend/features/backup/get-path-from-dialog'; import * as createBackupModule from './create-backup'; import * as DeviceModuleModule from './../../../backend/features/device/device.module'; import * as enableExistingBackupModule from './enable-existing-backup'; diff --git a/src/apps/main/backups/add-backup.ts b/src/apps/main/backups/add-backup.ts index bd58531ee..ac7f87cd7 100644 --- a/src/apps/main/backups/add-backup.ts +++ b/src/apps/main/backups/add-backup.ts @@ -1,9 +1,9 @@ -import { getPathFromDialog } from '../device/service'; import configStore from '../config'; import { createBackup } from './create-backup'; import { DeviceModule } from '../../../backend/features/device/device.module'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import { enableExistingBackup } from './enable-existing-backup'; +import { getPathFromDialog } from '../../../backend/features/backup/get-path-from-dialog'; export async function addBackup() { const { error, data } = await DeviceModule.getOrCreateDevice(); diff --git a/src/apps/main/device/handlers.ts b/src/apps/main/device/handlers.ts index 7602c8584..de087976d 100644 --- a/src/apps/main/device/handlers.ts +++ b/src/apps/main/device/handlers.ts @@ -7,10 +7,10 @@ import { disableBackup, downloadBackup, getDevices, - getPathFromDialog, } from './service'; import { DeviceModule } from '../../../backend/features/device/device.module'; import { addBackup } from '../backups/add-backup'; +import { getPathFromDialog } from '../../../backend/features/backup/get-path-from-dialog'; ipcMain.handle('devices.get-all', () => getDevices()); diff --git a/src/apps/main/device/service.ts b/src/apps/main/device/service.ts index 29d6906df..9aa1368d0 100644 --- a/src/apps/main/device/service.ts +++ b/src/apps/main/device/service.ts @@ -21,6 +21,7 @@ import { getBackupFolderUuid } from '../../../infra/drive-server/services/backup import { updateBackupFolderName } from '../../../infra/drive-server/services/backup/services/update-backup-folder-metadata'; import { migrateBackupEntryIfNeeded } from './migrate-backup-entry-if-needed'; import { createBackup } from '../backups/create-backup'; +import { getPathFromDialog } from '../../../backend/features/backup/get-path-from-dialog'; export type Device = { id: number; @@ -314,43 +315,6 @@ export type PathInfo = { isDirectory?: boolean; }; -export async function getPathFromDialog(): Promise<{ - path: string; - itemName: string; -} | null> { - const parentWindow = - BrowserWindow.getFocusedWindow() ?? - BrowserWindow.getAllWindows().find((w) => w.isVisible()) ?? - null; - - if (parentWindow) { - parentWindow.hide(); - } - - const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], - }); - - if (parentWindow && !parentWindow.isDestroyed()) { - parentWindow.show(); - } - - if (result.canceled) { - return null; - } - - const chosenPath = result.filePaths[0]; - - const itemPath = chosenPath + (chosenPath[chosenPath.length - 1] === path.sep ? '' : path.sep); - - const itemName = path.basename(itemPath); - - return { - path: itemPath, - itemName, - }; -} - export async function getMultiplePathsFromDialog(allowFiles = false): Promise { const result = await dialog.showOpenDialog({ properties: ['multiSelections' as const, ...(allowFiles ? (['openFile'] as const) : ['openDirectory' as const])], diff --git a/src/apps/main/preload.d.ts b/src/apps/main/preload.d.ts index b60cf0efa..0f8b3ce56 100644 --- a/src/apps/main/preload.d.ts +++ b/src/apps/main/preload.d.ts @@ -145,7 +145,7 @@ declare interface Window { changeBackupPath: typeof import('../main/device/service').changeBackupPath; - getFolderPath: typeof import('../main/device/service').getPathFromDialog; + getFolderPath: typeof import('../../backend/features/backup/get-path-from-dialog').getPathFromDialog; onRemoteChanges(func: (value: import('../main/realtime').EventPayload) => void): () => void; diff --git a/src/backend/features/backup/get-path-from-dialog.test.ts b/src/backend/features/backup/get-path-from-dialog.test.ts new file mode 100644 index 000000000..182712568 --- /dev/null +++ b/src/backend/features/backup/get-path-from-dialog.test.ts @@ -0,0 +1,121 @@ +import { BrowserWindow, dialog } from 'electron'; +import { call } from 'tests/vitest/utils.helper'; +import { getPathFromDialog } from './get-path-from-dialog'; +import path from 'node:path'; + +const mockWindow = { + hide: vi.fn(), + show: vi.fn(), + isVisible: vi.fn().mockReturnValue(true), + isDestroyed: vi.fn().mockReturnValue(false), +}; + +vi.mock('electron', () => ({ + dialog: { + showOpenDialog: vi.fn(), + }, + BrowserWindow: { + getFocusedWindow: vi.fn(), + getAllWindows: vi.fn(), + }, +})); + +describe('getPathFromDialog', () => { + const mockedDialog = vi.mocked(dialog.showOpenDialog); + const mockedGetFocusedWindow = vi.mocked(BrowserWindow.getFocusedWindow); + const mockedGetAllWindows = vi.mocked(BrowserWindow.getAllWindows); + + beforeEach(() => { + mockedGetFocusedWindow.mockReturnValue(mockWindow as unknown as BrowserWindow); + mockedGetAllWindows.mockReturnValue([mockWindow] as unknown as BrowserWindow[]); + }); + + it('should hide the focused window before opening the dialog', async () => { + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + await getPathFromDialog(); + + expect(mockWindow.hide).toHaveBeenCalled(); + }); + + it('should show the window after the dialog closes', async () => { + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + await getPathFromDialog(); + + expect(mockWindow.show).toHaveBeenCalled(); + }); + + it('should not show the window if it was destroyed', async () => { + mockWindow.isDestroyed.mockReturnValue(true); + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + await getPathFromDialog(); + + expect(mockWindow.show).not.toHaveBeenCalled(); + }); + + it('should use a visible window when no focused window exists', async () => { + mockedGetFocusedWindow.mockReturnValue(null); + + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + await getPathFromDialog(); + + expect(mockWindow.hide).toHaveBeenCalled(); + }); + + it('should return null when the dialog is canceled', async () => { + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + const result = await getPathFromDialog(); + + expect(result).toBe(null); + }); + + it('should return the path with a trailing separator and the item name', async () => { + mockedDialog.mockResolvedValue({ canceled: false, filePaths: ['/home/user/Documents'] }); + + const result = await getPathFromDialog(); + + expect(result).toStrictEqual({ + path: `/home/user/Documents${path.sep}`, + itemName: 'Documents', + }); + }); + + it('should not duplicate the separator if the path already ends with one', async () => { + mockedDialog.mockResolvedValue({ canceled: false, filePaths: [`/home/user/Documents${path.sep}`] }); + + const result = await getPathFromDialog(); + + expect(result).toStrictEqual({ + path: `/home/user/Documents${path.sep}`, + itemName: 'Documents', + }); + }); + + it('should open the dialog with openDirectory property', async () => { + mockedDialog.mockResolvedValue({ canceled: true, filePaths: [] }); + + await getPathFromDialog(); + + call(mockedDialog).toMatchObject({ properties: ['openDirectory'] }); + }); + + it('should skip hide and show and still return the selected path when no BackupFolderSelector window exists', async () => { + mockedGetFocusedWindow.mockReturnValue(null); + mockedGetAllWindows.mockReturnValue([]); + + mockedDialog.mockResolvedValue({ canceled: false, filePaths: ['/home/user/folder'] }); + + const result = await getPathFromDialog(); + + expect(mockWindow.hide).not.toHaveBeenCalled(); + expect(mockWindow.show).not.toHaveBeenCalled(); + expect(result).toStrictEqual({ + path: `/home/user/folder${path.sep}`, + itemName: 'folder', + }); + }); +}); diff --git a/src/backend/features/backup/get-path-from-dialog.ts b/src/backend/features/backup/get-path-from-dialog.ts new file mode 100644 index 000000000..d09d966a6 --- /dev/null +++ b/src/backend/features/backup/get-path-from-dialog.ts @@ -0,0 +1,34 @@ +import { BrowserWindow, dialog } from 'electron'; +import { PathInfo } from '../../../apps/main/device/service'; +import path from 'node:path'; + +export async function getPathFromDialog(): Promise | null> { + const parentWindow = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows().find((w) => w.isVisible()); + + if (parentWindow) { + parentWindow.hide(); + } + + const result = await dialog.showOpenDialog({ + properties: ['openDirectory'], + }); + + if (parentWindow && !parentWindow.isDestroyed()) { + parentWindow.show(); + } + + if (result.canceled) { + return null; + } + + const chosenPath = result.filePaths[0]; + + const itemPath = chosenPath + (chosenPath[chosenPath.length - 1] === path.sep ? '' : path.sep); + + const itemName = path.basename(itemPath); + + return { + path: itemPath, + itemName, + }; +} From 9ea7708614533d67f10240e122326c9ed9fffc12 Mon Sep 17 00:00:00 2001 From: AlexisMora Date: Tue, 10 Feb 2026 13:17:50 +0100 Subject: [PATCH 3/3] fix: Sonarcloud issues --- src/apps/main/device/service.ts | 2 +- src/backend/features/backup/get-path-from-dialog.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/main/device/service.ts b/src/apps/main/device/service.ts index 9aa1368d0..6e7e53a9c 100644 --- a/src/apps/main/device/service.ts +++ b/src/apps/main/device/service.ts @@ -1,5 +1,5 @@ import { aes } from '@internxt/lib'; -import { BrowserWindow, dialog, IpcMainEvent } from 'electron'; +import { dialog, IpcMainEvent } from 'electron'; import fetch from 'electron-fetch'; import { logger } from '@internxt/drive-desktop-core/build/backend'; import os from 'os'; diff --git a/src/backend/features/backup/get-path-from-dialog.ts b/src/backend/features/backup/get-path-from-dialog.ts index d09d966a6..bfa2cdfd3 100644 --- a/src/backend/features/backup/get-path-from-dialog.ts +++ b/src/backend/features/backup/get-path-from-dialog.ts @@ -23,7 +23,7 @@ export async function getPathFromDialog(): Promise const chosenPath = result.filePaths[0]; - const itemPath = chosenPath + (chosenPath[chosenPath.length - 1] === path.sep ? '' : path.sep); + const itemPath = `${chosenPath}${chosenPath.endsWith(path.sep) ? '' : path.sep}`; const itemName = path.basename(itemPath);