fix: guard folder picker after update install
This commit is contained in:
@@ -8,11 +8,15 @@ vi.mock('../mock-tauri', () => ({
|
||||
mockInvoke: (...args: unknown[]) => mockInvokeFn(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../utils/vault-dialog', () => ({
|
||||
pickFolder: vi.fn(),
|
||||
}))
|
||||
vi.mock('../utils/vault-dialog', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/vault-dialog')>()
|
||||
return {
|
||||
...actual,
|
||||
pickFolder: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { NativeFolderPickerBlockedError, pickFolder } from '../utils/vault-dialog'
|
||||
import { useGettingStartedClone } from './useGettingStartedClone'
|
||||
|
||||
describe('useGettingStartedClone', () => {
|
||||
@@ -70,4 +74,21 @@ describe('useGettingStartedClone', () => {
|
||||
expect(onSuccess).not.toHaveBeenCalled()
|
||||
expect(onError).toHaveBeenCalledWith('Could not download Getting Started vault. Check your connection and try again.')
|
||||
})
|
||||
|
||||
it('surfaces the restart-required message when folder picking is blocked after update install', async () => {
|
||||
vi.mocked(pickFolder).mockRejectedValue(new NativeFolderPickerBlockedError())
|
||||
|
||||
const onSuccess = vi.fn()
|
||||
const onError = vi.fn()
|
||||
const { result } = renderHook(() => useGettingStartedClone({ onError, onSuccess }))
|
||||
|
||||
await act(async () => {
|
||||
await result.current()
|
||||
})
|
||||
|
||||
expect(onSuccess).not.toHaveBeenCalled()
|
||||
expect(onError).toHaveBeenCalledWith(
|
||||
'Tolaria needs a restart before macOS can open another folder picker. Restart to apply the downloaded update and try again.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { formatFolderPickerActionError, pickFolder } from '../utils/vault-dialog'
|
||||
import {
|
||||
buildGettingStartedVaultPath,
|
||||
formatGettingStartedCloneError,
|
||||
@@ -22,7 +22,14 @@ export function useGettingStartedClone({
|
||||
onSuccess,
|
||||
}: UseGettingStartedCloneOptions) {
|
||||
return useCallback(async () => {
|
||||
const parentPath = await pickFolder('Choose a parent folder for the Getting Started vault')
|
||||
let parentPath: string | null
|
||||
try {
|
||||
parentPath = await pickFolder('Choose a parent folder for the Getting Started vault')
|
||||
} catch (err) {
|
||||
onError(formatFolderPickerActionError('Could not choose a parent folder', err))
|
||||
return
|
||||
}
|
||||
|
||||
if (!parentPath) return
|
||||
|
||||
const targetPath = buildGettingStartedVaultPath(parentPath)
|
||||
|
||||
@@ -30,11 +30,15 @@ vi.mock('../mock-tauri', () => ({
|
||||
|
||||
vi.mock('./useVaultSwitcher', () => ({}))
|
||||
|
||||
vi.mock('../utils/vault-dialog', () => ({
|
||||
pickFolder: vi.fn(),
|
||||
}))
|
||||
vi.mock('../utils/vault-dialog', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/vault-dialog')>()
|
||||
return {
|
||||
...actual,
|
||||
pickFolder: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { NativeFolderPickerBlockedError, pickFolder } from '../utils/vault-dialog'
|
||||
import { useOnboarding } from './useOnboarding'
|
||||
|
||||
function mockCommands(overrides: Record<string, MockOverride> = {}) {
|
||||
@@ -314,6 +318,23 @@ describe('useOnboarding', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the restart-required picker message instead of crashing the welcome flow', async () => {
|
||||
mockCommands()
|
||||
vi.mocked(pickFolder).mockRejectedValue(new NativeFolderPickerBlockedError())
|
||||
|
||||
const { result } = await renderOnboarding()
|
||||
|
||||
await expectStatus(result, 'welcome')
|
||||
await act(async () => {
|
||||
await result.current.handleOpenFolder()
|
||||
})
|
||||
|
||||
expect(result.current.error).toBe(
|
||||
'Tolaria needs a restart before macOS can open another folder picker. Restart to apply the downloaded update and try again.',
|
||||
)
|
||||
expect(result.current.state.status).toBe('welcome')
|
||||
})
|
||||
|
||||
it('marks the welcome screen dismissed and keeps the initial vault path', async () => {
|
||||
mockCommands()
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { APP_STORAGE_KEYS, LEGACY_APP_STORAGE_KEYS, getAppStorageItem } from '../constants/appStorage'
|
||||
import { buildGettingStartedVaultPath, formatGettingStartedCloneError } from '../utils/gettingStartedVault'
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { formatFolderPickerActionError, pickFolder } from '../utils/vault-dialog'
|
||||
|
||||
type OnboardingState =
|
||||
| { status: 'loading' }
|
||||
@@ -12,6 +12,21 @@ type OnboardingState =
|
||||
| { status: 'ready'; vaultPath: string }
|
||||
|
||||
type CreatingAction = 'template' | 'empty' | null
|
||||
type SetError = Dispatch<SetStateAction<string | null>>
|
||||
type SetCreatingAction = Dispatch<SetStateAction<CreatingAction>>
|
||||
|
||||
interface ReadyVaultHandlers {
|
||||
setState: Dispatch<SetStateAction<OnboardingState>>
|
||||
setUserReadyVaultPath: Dispatch<SetStateAction<string | null>>
|
||||
}
|
||||
|
||||
interface TemplateVaultCreationOptions {
|
||||
handlers: ReadyVaultHandlers
|
||||
setCreatingAction: SetCreatingAction
|
||||
setError: SetError
|
||||
setLastTemplatePath: Dispatch<SetStateAction<string | null>>
|
||||
onTemplateVaultReady?: (vaultPath: string) => void
|
||||
}
|
||||
|
||||
function tauriCall<T>(command: string, args: Record<string, unknown>): Promise<T> {
|
||||
return isTauri() ? invoke<T>(command, args) : mockInvoke<T>(command, args)
|
||||
@@ -58,6 +73,107 @@ async function clearMissingActiveVault(missingPath: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
function markVaultReady(
|
||||
handlers: ReadyVaultHandlers,
|
||||
vaultPath: string,
|
||||
) {
|
||||
markDismissed()
|
||||
handlers.setState({ status: 'ready', vaultPath })
|
||||
handlers.setUserReadyVaultPath(vaultPath)
|
||||
}
|
||||
|
||||
async function pickFolderWithOnboardingError(
|
||||
title: string,
|
||||
setError: SetError,
|
||||
action: string,
|
||||
): Promise<string | null> {
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
return await pickFolder(title)
|
||||
} catch (err) {
|
||||
setError(formatFolderPickerActionError(action, err))
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function useTemplateVaultCreation(
|
||||
options: TemplateVaultCreationOptions,
|
||||
) {
|
||||
return useCallback(async (targetPath: string) => {
|
||||
options.setCreatingAction('template')
|
||||
options.setError(null)
|
||||
options.setLastTemplatePath(targetPath)
|
||||
|
||||
try {
|
||||
const vaultPath = await tauriCall<string>('create_getting_started_vault', { targetPath })
|
||||
markVaultReady(options.handlers, vaultPath)
|
||||
options.onTemplateVaultReady?.(vaultPath)
|
||||
} catch (err) {
|
||||
options.setError(formatGettingStartedCloneError(err))
|
||||
} finally {
|
||||
options.setCreatingAction(null)
|
||||
}
|
||||
}, [options])
|
||||
}
|
||||
|
||||
function useCreateVaultHandler(
|
||||
createTemplateVault: (targetPath: string) => Promise<void>,
|
||||
setError: SetError,
|
||||
) {
|
||||
return useCallback(async () => {
|
||||
const parentPath = await pickFolderWithOnboardingError(
|
||||
'Choose a parent folder for the Getting Started vault',
|
||||
setError,
|
||||
'Could not choose a parent folder',
|
||||
)
|
||||
if (!parentPath) return
|
||||
|
||||
await createTemplateVault(buildGettingStartedVaultPath(parentPath))
|
||||
}, [createTemplateVault, setError])
|
||||
}
|
||||
|
||||
function useCreateEmptyVaultHandler(
|
||||
handlers: ReadyVaultHandlers,
|
||||
setCreatingAction: SetCreatingAction,
|
||||
setError: SetError,
|
||||
) {
|
||||
return useCallback(async () => {
|
||||
const path = await pickFolderWithOnboardingError(
|
||||
'Choose where to create your vault',
|
||||
setError,
|
||||
'Could not choose where to create your vault',
|
||||
)
|
||||
if (!path) return
|
||||
|
||||
try {
|
||||
setCreatingAction('empty')
|
||||
const vaultPath = await tauriCall<string>('create_empty_vault', { targetPath: path })
|
||||
markVaultReady(handlers, vaultPath)
|
||||
} catch (err) {
|
||||
setError(typeof err === 'string' ? err : `Failed to create vault: ${err}`)
|
||||
} finally {
|
||||
setCreatingAction(null)
|
||||
}
|
||||
}, [handlers, setCreatingAction, setError])
|
||||
}
|
||||
|
||||
function useOpenFolderHandler(
|
||||
handlers: ReadyVaultHandlers,
|
||||
setError: SetError,
|
||||
) {
|
||||
return useCallback(async () => {
|
||||
const path = await pickFolderWithOnboardingError(
|
||||
'Open vault folder',
|
||||
setError,
|
||||
'Failed to open folder',
|
||||
)
|
||||
if (!path) return
|
||||
|
||||
markVaultReady(handlers, path)
|
||||
}, [handlers, setError])
|
||||
}
|
||||
|
||||
export function useOnboarding(
|
||||
initialVaultPath: string,
|
||||
onTemplateVaultReady?: (vaultPath: string) => void,
|
||||
@@ -68,12 +184,12 @@ export function useOnboarding(
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastTemplatePath, setLastTemplatePath] = useState<string | null>(null)
|
||||
const [userReadyVaultPath, setUserReadyVaultPath] = useState<string | null>(null)
|
||||
const readyVaultHandlers = { setState, setUserReadyVaultPath }
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
if (!initialVaultResolved) {
|
||||
setState({ status: 'loading' })
|
||||
return () => { cancelled = true }
|
||||
}
|
||||
|
||||
@@ -108,71 +224,38 @@ export function useOnboarding(
|
||||
return () => { cancelled = true }
|
||||
}, [initialVaultPath, initialVaultResolved])
|
||||
|
||||
const createTemplateVault = useCallback(async (targetPath: string) => {
|
||||
setCreatingAction('template')
|
||||
setError(null)
|
||||
setLastTemplatePath(targetPath)
|
||||
try {
|
||||
const vaultPath = await tauriCall<string>('create_getting_started_vault', { targetPath })
|
||||
markDismissed()
|
||||
setState({ status: 'ready', vaultPath })
|
||||
setUserReadyVaultPath(vaultPath)
|
||||
onTemplateVaultReady?.(vaultPath)
|
||||
} catch (err) {
|
||||
setError(formatGettingStartedCloneError(err))
|
||||
} finally {
|
||||
setCreatingAction(null)
|
||||
}
|
||||
}, [onTemplateVaultReady])
|
||||
const createTemplateVault = useTemplateVaultCreation({
|
||||
handlers: readyVaultHandlers,
|
||||
setCreatingAction,
|
||||
setError,
|
||||
setLastTemplatePath,
|
||||
onTemplateVaultReady,
|
||||
})
|
||||
|
||||
const handleCreateVault = useCallback(async () => {
|
||||
const parentPath = await pickFolder('Choose a parent folder for the Getting Started vault')
|
||||
if (!parentPath) return
|
||||
await createTemplateVault(buildGettingStartedVaultPath(parentPath))
|
||||
}, [createTemplateVault])
|
||||
const handleCreateVault = useCreateVaultHandler(createTemplateVault, setError)
|
||||
|
||||
const retryCreateVault = useCallback(async () => {
|
||||
if (!lastTemplatePath) return
|
||||
await createTemplateVault(lastTemplatePath)
|
||||
}, [createTemplateVault, lastTemplatePath])
|
||||
|
||||
const handleCreateEmptyVault = useCallback(async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const path = await pickFolder('Choose where to create your vault')
|
||||
if (!path) return
|
||||
setCreatingAction('empty')
|
||||
const vaultPath = await tauriCall<string>('create_empty_vault', { targetPath: path })
|
||||
markDismissed()
|
||||
setState({ status: 'ready', vaultPath })
|
||||
setUserReadyVaultPath(vaultPath)
|
||||
} catch (err) {
|
||||
setError(typeof err === 'string' ? err : `Failed to create vault: ${err}`)
|
||||
} finally {
|
||||
setCreatingAction(null)
|
||||
}
|
||||
}, [])
|
||||
const handleCreateEmptyVault = useCreateEmptyVaultHandler(
|
||||
readyVaultHandlers,
|
||||
setCreatingAction,
|
||||
setError,
|
||||
)
|
||||
|
||||
const handleOpenFolder = useCallback(async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const path = await pickFolder('Open vault folder')
|
||||
if (!path) return
|
||||
markDismissed()
|
||||
setState({ status: 'ready', vaultPath: path })
|
||||
setUserReadyVaultPath(path)
|
||||
} catch (err) {
|
||||
setError(`Failed to open folder: ${err}`)
|
||||
}
|
||||
}, [])
|
||||
const handleOpenFolder = useOpenFolderHandler(readyVaultHandlers, setError)
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
markDismissed()
|
||||
setState({ status: 'ready', vaultPath: initialVaultPath })
|
||||
}, [initialVaultPath])
|
||||
|
||||
const resolvedState = initialVaultResolved ? state : { status: 'loading' as const }
|
||||
|
||||
return {
|
||||
state,
|
||||
state: resolvedState,
|
||||
creating: creatingAction !== null,
|
||||
creatingAction,
|
||||
error,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useUpdater } from './useUpdater'
|
||||
import {
|
||||
clearRestartRequiredAfterUpdate,
|
||||
isRestartRequiredAfterUpdate,
|
||||
} from '../lib/appUpdater'
|
||||
|
||||
vi.mock('../mock-tauri', () => ({
|
||||
isTauri: vi.fn(() => false),
|
||||
@@ -108,6 +112,7 @@ describe('useUpdater', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
clearRestartRequiredAfterUpdate()
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
@@ -270,6 +275,7 @@ describe('useUpdater', () => {
|
||||
version: '2026.4.16',
|
||||
displayVersion: '2026.4.16',
|
||||
})
|
||||
expect(isRestartRequiredAfterUpdate()).toBe(true)
|
||||
})
|
||||
|
||||
it('transitions to error when download fails', async () => {
|
||||
|
||||
@@ -28,9 +28,15 @@ vi.mock('../mock-tauri', () => ({
|
||||
mockInvoke: (cmd: string, args?: Record<string, unknown>) => mockInvokeFn(cmd, args),
|
||||
}))
|
||||
|
||||
vi.mock('../utils/vault-dialog', () => ({
|
||||
pickFolder: vi.fn(),
|
||||
}))
|
||||
vi.mock('../utils/vault-dialog', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/vault-dialog')>()
|
||||
return {
|
||||
...actual,
|
||||
pickFolder: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
import { NativeFolderPickerBlockedError } from '../utils/vault-dialog'
|
||||
|
||||
type MockInvokeOverrides = {
|
||||
checkVaultExists?: boolean | ((args: { path?: string }) => boolean)
|
||||
@@ -294,6 +300,21 @@ describe('useVaultSwitcher', () => {
|
||||
expect(onToast).toHaveBeenCalledWith('Vault "MyVault" opened')
|
||||
})
|
||||
|
||||
it('shows a clear toast when folder picking is blocked until restart', async () => {
|
||||
const { pickFolder } = await import('../utils/vault-dialog')
|
||||
vi.mocked(pickFolder).mockRejectedValue(new NativeFolderPickerBlockedError())
|
||||
|
||||
const { result } = await renderLoadedVaultSwitcher()
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleOpenLocalFolder()
|
||||
})
|
||||
|
||||
expect(onToast).toHaveBeenCalledWith(
|
||||
'Tolaria needs a restart before macOS can open another folder picker. Restart to apply the downloaded update and try again.',
|
||||
)
|
||||
})
|
||||
|
||||
it('creates an empty vault and switches to it', async () => {
|
||||
const { pickFolder } = await import('../utils/vault-dialog')
|
||||
vi.mocked(pickFolder).mockResolvedValue('/Users/luca/New Vault')
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { pickFolder } from '../utils/vault-dialog'
|
||||
import { formatFolderPickerActionError, pickFolder } from '../utils/vault-dialog'
|
||||
import { loadVaultList, saveVaultList } from '../utils/vaultListStore'
|
||||
import type { VaultOption } from '../components/StatusBar'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
@@ -650,7 +650,14 @@ function useOpenLocalFolderAction(
|
||||
onToastRef: MutableRefObject<(msg: string) => void>,
|
||||
) {
|
||||
return useCallback(async () => {
|
||||
const path = await pickFolder('Open vault folder')
|
||||
let path: string | null
|
||||
try {
|
||||
path = await pickFolder('Open vault folder')
|
||||
} catch (err) {
|
||||
onToastRef.current(formatFolderPickerActionError('Could not open vault folder', err))
|
||||
return
|
||||
}
|
||||
|
||||
if (!path) return
|
||||
|
||||
const label = labelFromPath({ path })
|
||||
@@ -664,10 +671,16 @@ function useCreateEmptyVaultAction(
|
||||
onToastRef: MutableRefObject<(msg: string) => void>,
|
||||
) {
|
||||
return useCallback(async () => {
|
||||
let targetPath: string | null
|
||||
try {
|
||||
const targetPath = await pickFolder('Choose where to create your vault')
|
||||
if (!targetPath) return
|
||||
targetPath = await pickFolder('Choose where to create your vault')
|
||||
} catch (err) {
|
||||
onToastRef.current(formatFolderPickerActionError('Could not choose where to create your vault', err))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!targetPath) return
|
||||
const vaultPath = await tauriCall<string>('create_empty_vault', { targetPath })
|
||||
const label = labelFromPath({ path: vaultPath })
|
||||
addAndSwitch(vaultPath, label)
|
||||
|
||||
@@ -13,6 +13,23 @@ export type AppUpdateDownloadEvent =
|
||||
| { event: 'Progress'; data: { chunkLength: number } }
|
||||
| { event: 'Finished' }
|
||||
|
||||
export const RESTART_REQUIRED_FOLDER_PICKER_MESSAGE =
|
||||
'Tolaria needs a restart before macOS can open another folder picker. Restart to apply the downloaded update and try again.'
|
||||
|
||||
let restartRequiredAfterUpdate = false
|
||||
|
||||
export function markRestartRequiredAfterUpdate(): void {
|
||||
restartRequiredAfterUpdate = true
|
||||
}
|
||||
|
||||
export function clearRestartRequiredAfterUpdate(): void {
|
||||
restartRequiredAfterUpdate = false
|
||||
}
|
||||
|
||||
export function isRestartRequiredAfterUpdate(): boolean {
|
||||
return restartRequiredAfterUpdate
|
||||
}
|
||||
|
||||
export async function checkForAppUpdate(
|
||||
releaseChannel: string | null | undefined,
|
||||
): Promise<AppUpdateMetadata | null> {
|
||||
@@ -34,4 +51,5 @@ export async function downloadAndInstallAppUpdate(
|
||||
expectedVersion,
|
||||
onEvent: channel,
|
||||
})
|
||||
markRestartRequiredAfterUpdate()
|
||||
}
|
||||
|
||||
@@ -5,8 +5,18 @@ vi.mock('../mock-tauri', () => ({
|
||||
isTauri: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
vi.mock('../lib/appUpdater', () => ({
|
||||
RESTART_REQUIRED_FOLDER_PICKER_MESSAGE:
|
||||
'Tolaria needs a restart before macOS can open another folder picker. Restart to apply the downloaded update and try again.',
|
||||
isRestartRequiredAfterUpdate: vi.fn(() => false),
|
||||
}))
|
||||
|
||||
import { pickFolder } from './vault-dialog'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import {
|
||||
isRestartRequiredAfterUpdate,
|
||||
RESTART_REQUIRED_FOLDER_PICKER_MESSAGE,
|
||||
} from '../lib/appUpdater'
|
||||
|
||||
describe('pickFolder', () => {
|
||||
beforeEach(() => {
|
||||
@@ -37,4 +47,11 @@ describe('pickFolder', () => {
|
||||
await pickFolder()
|
||||
expect(window.prompt).toHaveBeenCalledWith('Enter folder path:')
|
||||
})
|
||||
|
||||
it('blocks the native folder picker when a restart is required after update install', async () => {
|
||||
vi.mocked(isTauri).mockReturnValue(true)
|
||||
vi.mocked(isRestartRequiredAfterUpdate).mockReturnValue(true)
|
||||
|
||||
await expect(pickFolder('Select vault')).rejects.toThrow(RESTART_REQUIRED_FOLDER_PICKER_MESSAGE)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,41 @@
|
||||
*/
|
||||
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import {
|
||||
isRestartRequiredAfterUpdate,
|
||||
RESTART_REQUIRED_FOLDER_PICKER_MESSAGE,
|
||||
} from '../lib/appUpdater'
|
||||
|
||||
export class NativeFolderPickerBlockedError extends Error {
|
||||
constructor(message = RESTART_REQUIRED_FOLDER_PICKER_MESSAGE) {
|
||||
super(message)
|
||||
this.name = 'NativeFolderPickerBlockedError'
|
||||
}
|
||||
}
|
||||
|
||||
export function isNativeFolderPickerBlockedError(
|
||||
error: unknown,
|
||||
): error is NativeFolderPickerBlockedError {
|
||||
return error instanceof NativeFolderPickerBlockedError
|
||||
}
|
||||
|
||||
export function formatFolderPickerActionError(
|
||||
action: string,
|
||||
error: unknown,
|
||||
): string {
|
||||
if (isNativeFolderPickerBlockedError(error)) {
|
||||
return error.message
|
||||
}
|
||||
|
||||
const message =
|
||||
typeof error === 'string'
|
||||
? error
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: ''
|
||||
|
||||
return message ? `${action}: ${message}` : action
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a native folder picker dialog (Tauri) or falls back to prompt (browser).
|
||||
@@ -12,6 +47,10 @@ import { isTauri } from '../mock-tauri'
|
||||
*/
|
||||
export async function pickFolder(title?: string): Promise<string | null> {
|
||||
if (isTauri()) {
|
||||
if (isRestartRequiredAfterUpdate()) {
|
||||
throw new NativeFolderPickerBlockedError()
|
||||
}
|
||||
|
||||
const { open } = await import('@tauri-apps/plugin-dialog')
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
|
||||
Reference in New Issue
Block a user