feat(phase-4): complete UX redesign — FileManagerView, FolderTreeItem, test suite, and all Phase 4 fixes

Adds the unified file manager view (Windows Explorer-style), collapsible
folder tree sidebar item, full vitest test suite (55 tests, 4 files), and
commits all Phase 4 backend/frontend fixes that were staged but uncommitted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-28 17:10:52 +02:00
parent 654622d358
commit 87a32b7ee8
25 changed files with 2534 additions and 163 deletions
@@ -0,0 +1,220 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useFoldersStore } from '../folders.js'
// ── API mock ──────────────────────────────────────────────────────────────────
const mockListFolders = vi.fn()
const mockCreateFolder = vi.fn()
const mockGetFolder = vi.fn()
const mockRenameFolder = vi.fn()
const mockDeleteFolder = vi.fn()
vi.mock('../../api/client.js', () => ({
listFolders: (...a) => mockListFolders(...a),
createFolder: (...a) => mockCreateFolder(...a),
getFolder: (...a) => mockGetFolder(...a),
renameFolder: (...a) => mockRenameFolder(...a),
deleteFolder: (...a) => mockDeleteFolder(...a),
moveDocument: vi.fn(),
}))
// ── Helpers ───────────────────────────────────────────────────────────────────
function makeFolder(overrides = {}) {
return {
id: overrides.id ?? 'folder-1',
name: overrides.name ?? 'Test',
parent_id: overrides.parent_id ?? null,
has_children: overrides.has_children ?? false,
created_at: '2026-01-01T00:00:00Z',
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
describe('foldersStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
// ── fetchFolders ─────────────────────────────────────────────────────────
it('fetchFolders(null) sets folders and rootFolders', async () => {
const roots = [makeFolder({ id: 'r1', name: 'Root1' })]
mockListFolders.mockResolvedValue({ items: roots })
const store = useFoldersStore()
await store.fetchFolders(null)
expect(store.folders).toEqual(roots)
expect(store.rootFolders).toEqual(roots)
})
it('fetchFolders(parentId) sets folders but NOT rootFolders', async () => {
const children = [makeFolder({ id: 'c1', name: 'Child', parent_id: 'parent-1' })]
mockListFolders.mockResolvedValue({ items: children })
const store = useFoldersStore()
store.rootFolders = [makeFolder({ id: 'r1' })] // pre-populate rootFolders
await store.fetchFolders('parent-1')
expect(store.folders).toEqual(children)
expect(store.rootFolders).toEqual([makeFolder({ id: 'r1' })]) // unchanged
})
it('fetchRootFolders sets rootFolders without touching folders', async () => {
const roots = [makeFolder({ id: 'r1' })]
mockListFolders.mockResolvedValue({ items: roots })
const store = useFoldersStore()
store.folders = [makeFolder({ id: 'sub1', parent_id: 'r1' })] // simulate being inside a folder
await store.fetchRootFolders()
expect(store.rootFolders).toEqual(roots)
expect(store.folders[0].id).toBe('sub1') // unchanged
})
// ── createFolder ─────────────────────────────────────────────────────────
it('createFolder(name, null) adds to both folders and rootFolders', async () => {
const newFolder = makeFolder({ id: 'new-1', name: 'NewRoot' })
mockCreateFolder.mockResolvedValue(newFolder)
const store = useFoldersStore()
await store.createFolder('NewRoot', null)
expect(store.folders.some(f => f.id === 'new-1')).toBe(true)
expect(store.rootFolders.some(f => f.id === 'new-1')).toBe(true)
})
it('createFolder(name, parentId) adds to folders but NOT rootFolders', async () => {
const newFolder = makeFolder({ id: 'sub-1', name: 'SubFolder', parent_id: 'parent-1' })
mockCreateFolder.mockResolvedValue(newFolder)
const store = useFoldersStore()
await store.createFolder('SubFolder', 'parent-1')
expect(store.folders.some(f => f.id === 'sub-1')).toBe(true)
expect(store.rootFolders.some(f => f.id === 'sub-1')).toBe(false)
})
it('createFolder bumps treeVersion', async () => {
mockCreateFolder.mockResolvedValue(makeFolder())
const store = useFoldersStore()
const before = store.treeVersion
await store.createFolder('Test', null)
expect(store.treeVersion).toBe(before + 1)
})
it('createFolder throws on API error and propagates', async () => {
mockCreateFolder.mockRejectedValue(new Error('409 Conflict'))
const store = useFoldersStore()
await expect(store.createFolder('Dup', null)).rejects.toThrow('409 Conflict')
})
// ── renameFolder ─────────────────────────────────────────────────────────
it('renameFolder updates folder in both folders and rootFolders', async () => {
const original = makeFolder({ id: 'f1', name: 'OldName' })
const updated = { ...original, name: 'NewName' }
mockRenameFolder.mockResolvedValue(updated)
const store = useFoldersStore()
store.folders = [original]
store.rootFolders = [original]
await store.renameFolder('f1', 'NewName')
expect(store.folders[0].name).toBe('NewName')
expect(store.rootFolders[0].name).toBe('NewName')
})
it('renameFolder bumps treeVersion', async () => {
const folder = makeFolder({ id: 'f1' })
mockRenameFolder.mockResolvedValue({ ...folder, name: 'New' })
const store = useFoldersStore()
store.folders = [folder]
const before = store.treeVersion
await store.renameFolder('f1', 'New')
expect(store.treeVersion).toBe(before + 1)
})
// ── deleteFolder ─────────────────────────────────────────────────────────
it('deleteFolder removes from both folders and rootFolders', async () => {
mockDeleteFolder.mockResolvedValue(null)
const store = useFoldersStore()
const folder = makeFolder({ id: 'del-1' })
store.folders = [folder]
store.rootFolders = [folder]
await store.deleteFolder('del-1')
expect(store.folders.some(f => f.id === 'del-1')).toBe(false)
expect(store.rootFolders.some(f => f.id === 'del-1')).toBe(false)
})
it('deleteFolder bumps treeVersion', async () => {
mockDeleteFolder.mockResolvedValue(null)
const store = useFoldersStore()
store.folders = [makeFolder({ id: 'del-1' })]
const before = store.treeVersion
await store.deleteFolder('del-1')
expect(store.treeVersion).toBe(before + 1)
})
// ── navigateTo ───────────────────────────────────────────────────────────
it('navigateTo(null) clears folders and breadcrumb', async () => {
const store = useFoldersStore()
store.folders = [makeFolder()]
store.breadcrumb = [{ id: 'x', name: 'X' }]
await store.navigateTo(null)
expect(store.folders).toEqual([])
expect(store.breadcrumb).toEqual([])
})
it('navigateTo(id) sets breadcrumb from API', async () => {
const crumbs = [{ id: 'r1', name: 'Root' }, { id: 'f1', name: 'Test' }]
mockGetFolder.mockResolvedValue({ id: 'f1', name: 'Test', breadcrumb: crumbs })
const store = useFoldersStore()
await store.navigateTo('f1')
expect(store.breadcrumb).toEqual(crumbs)
})
it('navigateTo(id) sets breadcrumb to [] on API error', async () => {
mockGetFolder.mockRejectedValue(new Error('404'))
const store = useFoldersStore()
await store.navigateTo('bad-id')
expect(store.breadcrumb).toEqual([])
})
it('navigateTo sets currentFolderId', async () => {
mockGetFolder.mockResolvedValue({ id: 'f1', name: 'Test', breadcrumb: [] })
const store = useFoldersStore()
await store.navigateTo('f1')
expect(store.currentFolderId).toBe('f1')
})
})