diff --git a/src/components/status-bar/VaultMenu.applyMountedChange.test.ts b/src/components/status-bar/VaultMenu.applyMountedChange.test.ts new file mode 100644 index 00000000..b83aacdb --- /dev/null +++ b/src/components/status-bar/VaultMenu.applyMountedChange.test.ts @@ -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 }) + }) +}) diff --git a/src/components/status-bar/VaultMenu.tsx b/src/components/status-bar/VaultMenu.tsx index e4804fb4..7ec5a14c 100644 --- a/src/components/status-bar/VaultMenu.tsx +++ b/src/components/status-bar/VaultMenu.tsx @@ -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) => 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)) diff --git a/src/components/status-bar/vaultMenuMountedChange.ts b/src/components/status-bar/vaultMenuMountedChange.ts new file mode 100644 index 00000000..a70fe92f --- /dev/null +++ b/src/components/status-bar/vaultMenuMountedChange.ts @@ -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) => 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 }) +}