feat: support vault reordering

This commit is contained in:
lucaronin
2026-05-14 17:17:54 +02:00
parent 7174061b2a
commit e844cfa0bc
31 changed files with 864 additions and 389 deletions

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import type { VaultOption } from '../components/status-bar/types'
import { buildAllVaults } from './vaultCollections'
const defaultVault: VaultOption = { label: 'Getting Started', path: '/getting-started', managedDefault: true }
const workVault: VaultOption = { label: 'Work', path: '/work' }
describe('vaultCollections', () => {
it('keeps the managed default first until users persist a custom order', () => {
expect(buildAllVaults({
visibleDefaults: [defaultVault],
extraVaults: [workVault],
hiddenDefaults: [],
})).toEqual([defaultVault, workVault])
})
it('uses persisted order when it includes the managed default', () => {
expect(buildAllVaults({
visibleDefaults: [defaultVault],
extraVaults: [workVault, { ...defaultVault, managedDefault: undefined }],
hiddenDefaults: [],
})).toEqual([workVault, defaultVault])
})
it('keeps hidden managed defaults out of persisted order', () => {
expect(buildAllVaults({
visibleDefaults: [],
extraVaults: [workVault, defaultVault],
hiddenDefaults: [defaultVault.path],
})).toEqual([workVault])
})
})

View File

@@ -0,0 +1,54 @@
import type { VaultOption } from '../components/status-bar/types'
function buildPersistedVaultOrder({
extraVaults,
hiddenDefaults,
visibleDefaults,
}: {
extraVaults: VaultOption[]
hiddenDefaults: string[]
visibleDefaults: VaultOption[]
}): VaultOption[] {
const visibleDefaultsByPath = new Map(visibleDefaults.map((vault) => [vault.path, vault]))
const orderedVaults: VaultOption[] = []
const seenPaths = new Set<string>()
for (const vault of extraVaults) {
if (hiddenDefaults.includes(vault.path) || seenPaths.has(vault.path)) continue
const defaultVault = visibleDefaultsByPath.get(vault.path)
orderedVaults.push(defaultVault ? { ...defaultVault, ...vault, managedDefault: defaultVault.managedDefault } : vault)
seenPaths.add(vault.path)
}
for (const vault of visibleDefaults) {
if (!seenPaths.has(vault.path)) orderedVaults.push(vault)
}
return orderedVaults
}
export function buildAllVaults({
hiddenDefaults,
visibleDefaults,
extraVaults,
}: {
hiddenDefaults: string[]
visibleDefaults: VaultOption[]
extraVaults: VaultOption[]
}): VaultOption[] {
const visibleDefaultPaths = new Set(visibleDefaults.map((vault) => vault.path))
const hasPersistedDefaultOrder = extraVaults.some((vault) => visibleDefaultPaths.has(vault.path))
if (hasPersistedDefaultOrder) {
return buildPersistedVaultOrder({ extraVaults, hiddenDefaults, visibleDefaults })
}
const vaultsByPath = new Map<string, VaultOption>()
for (const vault of visibleDefaults) vaultsByPath.set(vault.path, vault)
for (const vault of extraVaults) {
if (hiddenDefaults.includes(vault.path)) continue
const existingVault = vaultsByPath.get(vault.path)
vaultsByPath.set(vault.path, existingVault ? { ...existingVault, ...vault, path: vault.path } : vault)
}
return [...vaultsByPath.values()]
}

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import type { VaultOption } from '../components/status-bar/types'
import { canMoveVaultPath, moveVaultPath, orderVaultsByPath } from './vaultOrdering'
const vaults: VaultOption[] = [
{ label: 'Laputa', path: '/laputa' },
{ label: 'Research', path: '/research' },
{ label: 'Archive', path: '/archive' },
]
describe('vaultOrdering', () => {
it('orders vaults by a complete path list', () => {
expect(orderVaultsByPath(vaults, ['/archive', '/laputa', '/research'])).toEqual([
vaults[2],
vaults[0],
vaults[1],
])
})
it('rejects incomplete or unknown path lists', () => {
expect(orderVaultsByPath(vaults, ['/archive', '/laputa'])).toBeNull()
expect(orderVaultsByPath(vaults, ['/archive', '/laputa', '/missing'])).toBeNull()
})
it('moves vault paths one slot at a time', () => {
expect(moveVaultPath(vaults, '/research', 'up')).toEqual(['/research', '/laputa', '/archive'])
expect(moveVaultPath(vaults, '/research', 'down')).toEqual(['/laputa', '/archive', '/research'])
})
it('reports whether a vault can move in a direction', () => {
expect(canMoveVaultPath(vaults, '/laputa', 'up')).toBe(false)
expect(canMoveVaultPath(vaults, '/laputa', 'down')).toBe(true)
expect(canMoveVaultPath(vaults, '/archive', 'down')).toBe(false)
})
})

View File

@@ -0,0 +1,40 @@
import type { VaultOption } from '../components/status-bar/types'
export type VaultMoveDirection = 'up' | 'down'
function vaultPathList(vaults: VaultOption[]): string[] {
return vaults.map((vault) => vault.path)
}
export function orderVaultsByPath(vaults: VaultOption[], orderedPaths: string[]): VaultOption[] | null {
if (vaults.length !== orderedPaths.length) return null
const vaultsByPath = new Map(vaults.map((vault) => [vault.path, vault]))
const orderedVaults: VaultOption[] = []
for (const path of orderedPaths) {
const vault = vaultsByPath.get(path)
if (!vault) return null
orderedVaults.push(vault)
}
return orderedVaults
}
export function moveVaultPath(vaults: VaultOption[], path: string, direction: VaultMoveDirection): string[] | null {
const orderedPaths = vaultPathList(vaults)
const currentIndex = orderedPaths.indexOf(path)
if (currentIndex === -1) return null
const nextIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
if (nextIndex < 0 || nextIndex >= orderedPaths.length) return null
const nextPaths = [...orderedPaths]
const [movedPath] = nextPaths.splice(currentIndex, 1)
nextPaths.splice(nextIndex, 0, movedPath)
return nextPaths
}
export function canMoveVaultPath(vaults: VaultOption[], path: string, direction: VaultMoveDirection): boolean {
return moveVaultPath(vaults, path, direction) !== null
}