feat: support vault reordering
This commit is contained in:
32
src/utils/vaultCollections.test.ts
Normal file
32
src/utils/vaultCollections.test.ts
Normal 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])
|
||||
})
|
||||
})
|
||||
54
src/utils/vaultCollections.ts
Normal file
54
src/utils/vaultCollections.ts
Normal 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()]
|
||||
}
|
||||
35
src/utils/vaultOrdering.test.ts
Normal file
35
src/utils/vaultOrdering.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
40
src/utils/vaultOrdering.ts
Normal file
40
src/utils/vaultOrdering.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user