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'] },