From 7cd346bde0dbd05ca26f36ae93328258d72643fe Mon Sep 17 00:00:00 2001 From: lucaronin Date: Thu, 14 May 2026 11:59:20 +0200 Subject: [PATCH] fix: prevent duplicate vault folder pickers --- src/utils/vault-dialog.test.ts | 19 +++++++++++++++++++ src/utils/vault-dialog.ts | 34 +++++++++++++++++++++------------- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/utils/vault-dialog.test.ts b/src/utils/vault-dialog.test.ts index 34fcdc50..5336776f 100644 --- a/src/utils/vault-dialog.test.ts +++ b/src/utils/vault-dialog.test.ts @@ -85,6 +85,25 @@ describe('pickFolder', () => { }) }) + it('ignores overlapping native folder picker requests while one is open', async () => { + vi.mocked(isTauri).mockReturnValue(true) + vi.mocked(isRestartRequiredAfterUpdate).mockReturnValue(false) + + let resolveOpen: ((path: string) => void) | null = null + openMock.mockReturnValueOnce(new Promise((resolve) => { + resolveOpen = resolve + })) + + const firstRequest = pickFolder('Open vault folder') + const secondRequest = pickFolder('Open vault folder') + + await expect(secondRequest).resolves.toBeNull() + expect(openMock).toHaveBeenCalledTimes(1) + + resolveOpen?.('/Users/test/restored-vault') + await expect(firstRequest).resolves.toBe('/Users/test/restored-vault') + }) + it('normalizes native file URLs to filesystem paths', async () => { vi.mocked(isTauri).mockReturnValue(true) vi.mocked(isRestartRequiredAfterUpdate).mockReturnValue(false) diff --git a/src/utils/vault-dialog.ts b/src/utils/vault-dialog.ts index 9b6b8da4..a431191b 100644 --- a/src/utils/vault-dialog.ts +++ b/src/utils/vault-dialog.ts @@ -75,24 +75,32 @@ function normalizePickedFolderPath(selected: string | string[] | null): string | } } +let folderPickerRequestInFlight = false + /** * Opens a native folder picker dialog (Tauri) or falls back to prompt (browser). * Returns the selected folder path, or null if the user cancelled. */ export async function pickFolder(title?: string): Promise { - if (isTauri()) { - if (isRestartRequiredAfterUpdate()) { - throw new NativeFolderPickerBlockedError() - } + if (folderPickerRequestInFlight) return null - const { open } = await import('@tauri-apps/plugin-dialog') - const selected = await open({ - directory: true, - multiple: false, - title: title ?? 'Select folder', - }) - return normalizePickedFolderPath(selected) + folderPickerRequestInFlight = true + try { + if (isTauri()) { + if (isRestartRequiredAfterUpdate()) { + throw new NativeFolderPickerBlockedError() + } + + const { open } = await import('@tauri-apps/plugin-dialog') + const selected = await open({ + directory: true, + multiple: false, + title: title ?? 'Select folder', + }) + return normalizePickedFolderPath(selected) + } + return normalizePickedFolderPath(prompt(title ?? 'Enter folder path:')) + } finally { + folderPickerRequestInFlight = false } - // Browser fallback: prompt for path - return normalizePickedFolderPath(prompt(title ?? 'Enter folder path:')) }