From 9ea51d640159726d1639f0a6128f192766c56095 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:26:06 +0200 Subject: [PATCH] feat(10-06): wire breadcrumb mapping + toast call sites; delete FolderBreadcrumb - FileManagerView: add mappedBreadcrumb computed ({id,label} from foldersStore.breadcrumb) - FileManagerView: import useToastStore; add toast.show() to doMove, doDeleteDoc, onFilesSelected - FileManagerView: remove console.error from doMove/doDeleteDoc (toast communicates errors) - CloudFolderView: add mappedBreadcrumb computed; pass to StorageBrowser :breadcrumb - Delete FolderBreadcrumb.vue + FolderBreadcrumb.test.js (no dead code per CLAUDE.md) - FileManagerView.test.js: replace dead FolderBreadcrumb mock with BreadcrumbBar mock - StorageBrowser.skeleton.test.js: remove unused FolderBreadcrumb stub - Full suite: 164/164 pass, 0 failures --- .../components/folders/FolderBreadcrumb.vue | 68 ----------- .../__tests__/FolderBreadcrumb.test.js | 108 ------------------ .../__tests__/StorageBrowser.skeleton.test.js | 1 - frontend/src/views/CloudFolderView.vue | 6 +- frontend/src/views/FileManagerView.vue | 31 ++++- .../views/__tests__/FileManagerView.test.js | 4 +- 6 files changed, 35 insertions(+), 183 deletions(-) delete mode 100644 frontend/src/components/folders/FolderBreadcrumb.vue delete mode 100644 frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js diff --git a/frontend/src/components/folders/FolderBreadcrumb.vue b/frontend/src/components/folders/FolderBreadcrumb.vue deleted file mode 100644 index 67bd98f..0000000 --- a/frontend/src/components/folders/FolderBreadcrumb.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js b/frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js deleted file mode 100644 index 57b97ae..0000000 --- a/frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { mount } from '@vue/test-utils' -import FolderBreadcrumb from '../FolderBreadcrumb.vue' - -function seg(id, name) { return { id, name } } - -describe('FolderBreadcrumb', () => { - it('always renders a "Home" / "Folders" root button', () => { - const w = mount(FolderBreadcrumb, { props: { segments: [] } }) - expect(w.find('button').exists()).toBe(true) - }) - - it('clicking root button emits navigate(null)', async () => { - const w = mount(FolderBreadcrumb, { props: { segments: [] } }) - await w.find('button').trigger('click') - expect(w.emitted('navigate')).toBeTruthy() - expect(w.emitted('navigate')[0]).toEqual([null]) - }) - - it('renders intermediate segments as clickable buttons', () => { - const w = mount(FolderBreadcrumb, { - props: { segments: [seg('r1', 'Root'), seg('f1', 'Test')] }, - }) - // "Root" is intermediate (not last), "Test" is last (plain text) - const buttons = w.findAll('button') - // first button is "Home/Folders", second is "Root" - expect(buttons.length).toBe(2) - expect(buttons[1].text()).toBe('Root') - }) - - it('clicking intermediate segment emits navigate(id)', async () => { - const w = mount(FolderBreadcrumb, { - props: { segments: [seg('r1', 'Root'), seg('f1', 'Test')] }, - }) - const buttons = w.findAll('button') - await buttons[1].trigger('click') // "Root" button - expect(w.emitted('navigate')).toBeTruthy() - expect(w.emitted('navigate')[0]).toEqual(['r1']) - }) - - it('renders last segment as plain non-interactive text', () => { - const w = mount(FolderBreadcrumb, { - props: { segments: [seg('r1', 'Root'), seg('f1', 'Test')] }, - }) - // Last segment "Test" should be a , not a button - const spans = w.findAll('span') - const lastSpan = spans.find(s => s.text() === 'Test') - expect(lastSpan).toBeTruthy() - }) - - it('last segment is NOT clickable (no navigate event)', async () => { - const w = mount(FolderBreadcrumb, { - props: { segments: [seg('r1', 'Root'), seg('f1', 'Test')] }, - }) - const spans = w.findAll('span') - const lastSpan = spans.find(s => s.text() === 'Test') - if (lastSpan) await lastSpan.trigger('click') - // navigate should NOT have been emitted by clicking the last segment - const navigateEvents = (w.emitted('navigate') || []).filter(e => e[0] === 'f1') - expect(navigateEvents.length).toBe(0) - }) - - it('single segment: just root button + last segment as text', () => { - const w = mount(FolderBreadcrumb, { - props: { segments: [seg('f1', 'OnlyFolder')] }, - }) - // Only the "Home" button and "OnlyFolder" as plain text - const buttons = w.findAll('button') - expect(buttons.length).toBe(1) // just "Home" - expect(w.text()).toContain('OnlyFolder') - }) - - it('collapses >4 segments with ellipsis, preserving first and last two', () => { - const segments = [ - seg('a', 'A'), seg('b', 'B'), seg('c', 'C'), - seg('d', 'D'), seg('e', 'E'), - ] - const w = mount(FolderBreadcrumb, { props: { segments } }) - const text = w.text() - expect(text).toContain('A') // first preserved - expect(text).toContain('…') // ellipsis present - expect(text).toContain('D') // second-to-last preserved - expect(text).toContain('E') // last preserved - expect(text).not.toContain('B') // middle segments collapsed - expect(text).not.toContain('C') - }) - - it('3 segments: all rendered without ellipsis', () => { - const segments = [seg('a', 'A'), seg('b', 'B'), seg('c', 'C')] - const w = mount(FolderBreadcrumb, { props: { segments } }) - const text = w.text() - expect(text).toContain('A') - expect(text).toContain('B') - expect(text).toContain('C') - expect(text).not.toContain('…') - }) - - it('deep 3-level path: clicking middle segment navigates correctly', async () => { - const segments = [seg('root', 'Root'), seg('mid', 'Mid'), seg('cur', 'Current')] - const w = mount(FolderBreadcrumb, { props: { segments } }) - const buttons = w.findAll('button') - // buttons[0] = Home, buttons[1] = Root, buttons[2] = Mid - await buttons[2].trigger('click') - const events = w.emitted('navigate') || [] - const midClicks = events.filter(e => e[0] === 'mid') - expect(midClicks.length).toBe(1) - }) -}) diff --git a/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js b/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js index fb6bae2..95b8066 100644 --- a/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js +++ b/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js @@ -5,7 +5,6 @@ import StorageBrowser from '../StorageBrowser.vue' const globalStubs = { BreadcrumbBar: true, - FolderBreadcrumb: true, SearchBar: true, SortControls: true, DropZone: true, diff --git a/frontend/src/views/CloudFolderView.vue b/frontend/src/views/CloudFolderView.vue index 33b46b6..1d37119 100644 --- a/frontend/src/views/CloudFolderView.vue +++ b/frontend/src/views/CloudFolderView.vue @@ -3,7 +3,7 @@ mode="cloud" :folders="folders" :files="files" - :breadcrumb="breadcrumb" + :breadcrumb="mappedBreadcrumb" :upload-queue="uploadQueue" :loading="loading" :empty-message="error || 'This folder is empty'" @@ -45,6 +45,10 @@ const breadcrumb = computed(() => { })) }) +const mappedBreadcrumb = computed(() => + breadcrumb.value.map(f => ({ id: f.id, label: f.name })) +) + async function load() { loading.value = true error.value = '' diff --git a/frontend/src/views/FileManagerView.vue b/frontend/src/views/FileManagerView.vue index 2afe2b1..ba7a35f 100644 --- a/frontend/src/views/FileManagerView.vue +++ b/frontend/src/views/FileManagerView.vue @@ -3,7 +3,7 @@ mode="local" :folders="foldersStore.folders" :files="docsStore.documents" - :breadcrumb="foldersStore.breadcrumb" + :breadcrumb="mappedBreadcrumb" :upload-queue="uploadQueue" :loading="docsStore.loading || foldersStore.loading" :search-query="docsStore.searchQuery" @@ -49,6 +49,7 @@ import { useRoute, useRouter } from 'vue-router' import { useFoldersStore } from '../stores/folders.js' import { useDocumentsStore } from '../stores/documents.js' import { useTopicsStore } from '../stores/topics.js' +import { useToastStore } from '../stores/toast.js' import StorageBrowser from '../components/storage/StorageBrowser.vue' import FolderDeleteModal from '../components/folders/FolderDeleteModal.vue' import ShareModal from '../components/sharing/ShareModal.vue' @@ -63,6 +64,9 @@ const browserRef = ref(null) // ── Navigation ──────────────────────────────────────────────────────────────── const currentFolderId = computed(() => route.params.folderId ?? null) +const mappedBreadcrumb = computed(() => + (foldersStore.breadcrumb || []).map(f => ({ id: f.id, label: f.name })) +) async function loadFolder(folderId) { if (folderId === null) { @@ -100,6 +104,7 @@ const uploadQueue = ref([]) async function onFilesSelected({ files, autoClassify }) { const folderId = currentFolderId.value + const toast = useToastStore() const promises = files.map(file => { const item = reactive({ name: file.name, done: false, error: null, quotaError: null, topics: null }) uploadQueue.value.unshift(item) @@ -112,6 +117,14 @@ async function onFilesSelected({ files, autoClassify }) { }) await Promise.allSettled(promises) await topicsStore.fetchTopics() + const succeeded = uploadQueue.value.slice(0, files.length).filter(i => i.done).length + if (succeeded === files.length) { + toast.show(`${succeeded} file(s) uploaded`, 'success') + } else if (succeeded > 0) { + toast.show(`${succeeded} of ${files.length} file(s) uploaded`, 'warning') + } else { + toast.show('Upload failed', 'error') + } } // ── Folder CRUD ─────────────────────────────────────────────────────────────── @@ -143,11 +156,23 @@ async function confirmDeleteFolder() { const shareDoc = ref(null) async function doMove(docId, folderId) { - try { await docsStore.moveToFolder(docId, folderId) } catch (e) { console.error(e.message) } + const toast = useToastStore() + try { + await docsStore.moveToFolder(docId, folderId) + toast.show('Document moved', 'success') + } catch (e) { + toast.show('Move failed: ' + (e.message || 'unknown error'), 'error') + } } async function doDeleteDoc(docId) { - try { await docsStore.remove(docId) } catch (e) { console.error(e.message) } + const toast = useToastStore() + try { + await docsStore.remove(docId) + toast.show('Document deleted', 'success') + } catch (e) { + toast.show('Delete failed: ' + (e.message || 'unknown error'), 'error') + } } // ── Topic color lookup ──────────────────────────────────────────────────────── diff --git a/frontend/src/views/__tests__/FileManagerView.test.js b/frontend/src/views/__tests__/FileManagerView.test.js index 487a9f2..991a39e 100644 --- a/frontend/src/views/__tests__/FileManagerView.test.js +++ b/frontend/src/views/__tests__/FileManagerView.test.js @@ -29,8 +29,8 @@ vi.mock('../../api/client.js', () => ({ })) // Stub heavy child components so we only test FileManagerView logic -vi.mock('../../components/folders/FolderBreadcrumb.vue', () => ({ - default: { template: '', props: ['segments'], emits: ['navigate'] }, +vi.mock('../../components/ui/BreadcrumbBar.vue', () => ({ + default: { template: '', props: ['segments', 'rootLabel', 'showRoot'], emits: ['navigate'] }, })) vi.mock('../../components/upload/DropZone.vue', () => ({ default: { template: '
', emits: ['files-selected'] },