Merge remote-tracking branch 'refs/remotes/pr/740'

This commit is contained in:
lucaronin
2026-05-26 10:24:52 +02:00
3 changed files with 157 additions and 31 deletions

View File

@@ -0,0 +1,107 @@
import { describe, expect, it, vi } from 'vitest'
import { applyMountedChange } from './vaultMenuMountedChange'
import type { VaultOption } from './types'
function vault(path: string, mounted = true): VaultOption {
return { label: path, path, alias: path.replace('/', ''), mounted, available: true }
}
interface ChangeOverrides {
defaultPath?: string
vaultPath?: string
path?: string
mounted?: boolean
includedVaults?: VaultOption[]
}
function callApply(overrides: ChangeOverrides) {
const onSetDefaultWorkspace = vi.fn()
const onSwitchVault = vi.fn()
const onUpdateWorkspaceIdentity = vi.fn()
applyMountedChange({
defaultPath: overrides.defaultPath ?? '/a',
vaultPath: overrides.vaultPath ?? '/a',
includedVaults: overrides.includedVaults ?? [vault('/a'), vault('/b')],
mounted: overrides.mounted ?? false,
path: overrides.path ?? '/a',
callbacks: { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity },
})
return { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity }
}
describe('applyMountedChange', () => {
it('reroutes both the default workspace and the active vault when the unmounted vault is both', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/a',
vaultPath: '/a',
path: '/a',
})
expect(onSetDefaultWorkspace).toHaveBeenCalledWith('/b')
expect(onSwitchVault).toHaveBeenCalledWith('/b')
expect(onUpdateWorkspaceIdentity).toHaveBeenCalledWith('/a', { mounted: false })
})
it('reroutes the active vault when only the active vault is unmounted (default workspace differs)', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/b',
vaultPath: '/a',
path: '/a',
})
expect(onSetDefaultWorkspace).not.toHaveBeenCalled()
expect(onSwitchVault).toHaveBeenCalledWith('/b')
expect(onUpdateWorkspaceIdentity).toHaveBeenCalledWith('/a', { mounted: false })
})
it('reroutes the default workspace when only the default is unmounted (active vault differs)', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/a',
vaultPath: '/b',
path: '/a',
})
expect(onSetDefaultWorkspace).toHaveBeenCalledWith('/b')
expect(onSwitchVault).not.toHaveBeenCalled()
expect(onUpdateWorkspaceIdentity).toHaveBeenCalledWith('/a', { mounted: false })
})
it('does not reroute when the unmounted vault is neither default nor active', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/a',
vaultPath: '/a',
path: '/b',
includedVaults: [vault('/a'), vault('/b'), vault('/c')],
})
expect(onSetDefaultWorkspace).not.toHaveBeenCalled()
expect(onSwitchVault).not.toHaveBeenCalled()
expect(onUpdateWorkspaceIdentity).toHaveBeenCalledWith('/b', { mounted: false })
})
it('bails out without changing anything when no alternative mounted vault exists', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/a',
vaultPath: '/a',
path: '/a',
includedVaults: [vault('/a')],
})
expect(onSetDefaultWorkspace).not.toHaveBeenCalled()
expect(onSwitchVault).not.toHaveBeenCalled()
expect(onUpdateWorkspaceIdentity).not.toHaveBeenCalled()
})
it('only marks the vault unmounted (no reroute) when remounting', () => {
const { onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity } = callApply({
defaultPath: '/a',
vaultPath: '/a',
path: '/b',
mounted: true,
})
expect(onSetDefaultWorkspace).not.toHaveBeenCalled()
expect(onSwitchVault).not.toHaveBeenCalled()
expect(onUpdateWorkspaceIdentity).toHaveBeenCalledWith('/b', { mounted: true })
})
})

View File

@@ -19,6 +19,7 @@ import type { VaultOption } from './types'
import { useDismissibleLayer } from './useDismissibleLayer'
import { workspaceAliasFromOption, workspaceIdentityFromVault } from '../../utils/workspaces'
import { reorderVaultPath, vaultPathList } from '../../utils/vaultOrdering'
import { applyMountedChange } from './vaultMenuMountedChange'
interface VaultMenuProps {
vaults: VaultOption[]
@@ -90,6 +91,7 @@ interface VaultMenuInteractionOptions {
onSwitchVault: (path: string) => void
onUpdateWorkspaceIdentity?: (path: string, patch: Partial<VaultOption>) => void
setOpen: (open: boolean) => void
vaultPath: string
}
interface MountToggleRequest {
@@ -104,10 +106,6 @@ interface VaultPathSelection extends VaultMenuInteractionOptions {
path: string
}
interface VaultMountChangeRequest extends VaultMenuInteractionOptions {
mounted: boolean
path: string
}
function getVaultTriggerClassName(open: boolean, compact: boolean) {
if (compact) {
@@ -293,10 +291,6 @@ function useIncludedVaults(vaults: VaultOption[], defaultPath: string): VaultOpt
return useMemo(() => vaults.filter((vault) => isIncludedVault(vault, defaultPath)), [defaultPath, vaults])
}
function nextIncludedVaultPath(includedVaults: VaultOption[], currentPath: string): string | null {
return includedVaults.find((vault) => vault.path !== currentPath)?.path ?? null
}
function shouldDisableMountToggle({
canSetDefaultWorkspace,
defaultPath,
@@ -321,22 +315,6 @@ function selectVaultPath({
setOpen(false)
}
function applyMountedChange({
defaultPath,
includedVaults,
mounted,
onSetDefaultWorkspace,
onUpdateWorkspaceIdentity,
path,
}: VaultMountChangeRequest): void {
if (!mounted && path === defaultPath) {
const nextDefaultPath = nextIncludedVaultPath(includedVaults, path)
if (!nextDefaultPath) return
onSetDefaultWorkspace?.(nextDefaultPath)
}
onUpdateWorkspaceIdentity?.(path, { mounted })
}
function useVaultMenuInteractions({
defaultPath,
includedVaults,
@@ -345,6 +323,7 @@ function useVaultMenuInteractions({
onSwitchVault,
onUpdateWorkspaceIdentity,
setOpen,
vaultPath,
}: VaultMenuInteractionOptions) {
const disableMountToggleForPath = useCallback((path: string) => (
shouldDisableMountToggle({
@@ -366,22 +345,24 @@ function useVaultMenuInteractions({
onUpdateWorkspaceIdentity,
path,
setOpen,
vaultPath,
})
}, [defaultPath, includedVaults, multiWorkspaceEnabled, onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity, setOpen])
}, [defaultPath, includedVaults, multiWorkspaceEnabled, onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity, setOpen, vaultPath])
const handleMountedChange = useCallback((path: string, mounted: boolean) => {
applyMountedChange({
defaultPath,
vaultPath,
includedVaults,
mounted,
multiWorkspaceEnabled,
onSetDefaultWorkspace,
onSwitchVault,
onUpdateWorkspaceIdentity,
path,
setOpen,
callbacks: {
onSetDefaultWorkspace,
onSwitchVault,
onUpdateWorkspaceIdentity,
},
})
}, [defaultPath, includedVaults, multiWorkspaceEnabled, onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity, setOpen])
}, [defaultPath, includedVaults, onSetDefaultWorkspace, onSwitchVault, onUpdateWorkspaceIdentity, vaultPath])
return { disableMountToggleForPath, handleMountedChange, handleSelectVault }
}
@@ -747,6 +728,7 @@ export function VaultMenu(props: VaultMenuProps) {
onSwitchVault,
onUpdateWorkspaceIdentity,
setOpen,
vaultPath,
})
useDismissibleLayer(open, menuRef, () => setOpen(false))

View File

@@ -0,0 +1,37 @@
import type { VaultOption } from './types'
export interface VaultMountChangeCallbacks {
onSetDefaultWorkspace?: (path: string) => void
onSwitchVault: (path: string) => void
onUpdateWorkspaceIdentity?: (path: string, patch: Partial<VaultOption>) => void
}
export interface VaultMountChangeRequest {
defaultPath: string
vaultPath: string
includedVaults: VaultOption[]
mounted: boolean
path: string
callbacks: VaultMountChangeCallbacks
}
function nextIncludedVaultPath(includedVaults: VaultOption[], currentPath: string): string | null {
return includedVaults.find((vault) => vault.path !== currentPath)?.path ?? null
}
export function applyMountedChange({
defaultPath,
vaultPath,
includedVaults,
mounted,
path,
callbacks,
}: VaultMountChangeRequest): void {
if (!mounted && (path === defaultPath || path === vaultPath)) {
const nextPath = nextIncludedVaultPath(includedVaults, path)
if (!nextPath) return
if (path === defaultPath) callbacks.onSetDefaultWorkspace?.(nextPath)
if (path === vaultPath) callbacks.onSwitchVault(nextPath)
}
callbacks.onUpdateWorkspaceIdentity?.(path, { mounted })
}