From 413d3f0ff71c86f37c1a8fb78eb2e6f57c5261a8 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:22:26 +0200 Subject: [PATCH 1/4] test(10-06): promote UX-02 skeleton stubs to RED failing tests - Replace 4 it.todo stubs with real assertions for animate-pulse, Loading text, and grid class - Keep UX-13 todo block unchanged (promoted in plan 10-12) - setActivePinia per test; stub BreadcrumbBar/SearchBar/SortControls/DropZone/UploadProgress/TopicBadge/AppIcon/EmptyState --- .../__tests__/StorageBrowser.skeleton.test.js | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js b/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js index 55e84c6..fb6bae2 100644 --- a/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js +++ b/frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js @@ -1,10 +1,59 @@ -import { describe, it } from 'vitest' +import { describe, it, expect, beforeEach } from 'vitest' +import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' +import StorageBrowser from '../StorageBrowser.vue' + +const globalStubs = { + BreadcrumbBar: true, + FolderBreadcrumb: true, + SearchBar: true, + SortControls: true, + DropZone: true, + UploadProgress: true, + TopicBadge: true, + AppIcon: true, + EmptyState: true, +} describe('UX-02: StorageBrowser shows skeleton rows during loading', () => { - it.todo('renders 5+ skeleton rows when loading=true (replaces Loading… text)') - it.todo('skeleton rows use grid-cols-[2rem_1fr_6rem_8rem_6rem] matching real row grid') - it.todo('skeleton rows use animate-pulse') - it.todo('Loading… text is absent when loading=true (skeleton replaces it)') + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('renders 5 skeleton rows when loading=true and lists empty', () => { + const wrapper = mount(StorageBrowser, { + props: { loading: true, folders: [], files: [] }, + global: { stubs: globalStubs }, + }) + const pulseEls = wrapper.findAll('.animate-pulse') + expect(pulseEls.length).toBeGreaterThanOrEqual(5) + }) + + it('Loading… text is absent when loading=true', () => { + const wrapper = mount(StorageBrowser, { + props: { loading: true, folders: [], files: [] }, + global: { stubs: globalStubs }, + }) + expect(wrapper.text()).not.toContain('Loading…') + }) + + it('skeleton rows are NOT rendered when loading=false', () => { + const wrapper = mount(StorageBrowser, { + props: { loading: false, folders: [], files: [] }, + global: { stubs: globalStubs }, + }) + const pulseEls = wrapper.findAll('.animate-pulse') + expect(pulseEls.length).toBe(0) + }) + + it('skeleton row grid matches grid-cols-[2rem_1fr_6rem_8rem_6rem]', () => { + const wrapper = mount(StorageBrowser, { + props: { loading: true, folders: [], files: [] }, + global: { stubs: globalStubs }, + }) + const skeletonGrids = wrapper.findAll('.grid-cols-\\[2rem_1fr_6rem_8rem_6rem\\]') + expect(skeletonGrids.length).toBeGreaterThanOrEqual(1) + }) }) describe('UX-13: StorageBrowser folder picker uses Teleport + getBoundingClientRect', () => { From d040e77548bd9f0387e9b43a5c25fbd12459ff11 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:23:34 +0200 Subject: [PATCH 2/4] =?UTF-8?q?feat(10-06):=20update=20StorageBrowser=20?= =?UTF-8?q?=E2=80=94=20skeleton=20rows,=20EmptyState,=20BreadcrumbBar=20sw?= =?UTF-8?q?ap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace FolderBreadcrumb import with BreadcrumbBar + EmptyState imports - Replace with - Replace Loading… div with 5 animated skeleton rows (animate-pulse, grid-cols-[2rem_1fr_6rem_8rem_6rem]) - Replace inline empty-state divs with three blocks (search/in-folder/root) - UX-02 skeleton tests green (4/4) --- .../src/components/storage/StorageBrowser.vue | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/storage/StorageBrowser.vue b/frontend/src/components/storage/StorageBrowser.vue index 2738fd0..8aa1970 100644 --- a/frontend/src/components/storage/StorageBrowser.vue +++ b/frontend/src/components/storage/StorageBrowser.vue @@ -4,8 +4,9 @@
-
@@ -223,25 +224,46 @@
- -
-

{{ emptyMessage }}

-

{{ emptyHint }}

-
+ - -
- No items match "{{ searchQuery }}". -
+ + - -
Loading…
+ + +
@@ -249,7 +271,8 @@ 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'] }, From b076ec9cda3611ef5c42a3e870dec44361e31503 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 15 Jun 2026 20:27:15 +0200 Subject: [PATCH 4/4] =?UTF-8?q?docs(10-06):=20create=20plan=20summary=20?= =?UTF-8?q?=E2=80=94=20skeleton/EmptyState/BreadcrumbBar/toast=20wired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Documents 3 tasks (TDD RED→GREEN→Task3) - Records 164/164 tests passing, FolderBreadcrumb deleted - Captures dead-code hygiene deviation and decisions --- .../phases/10-ux-interaction/10-06-SUMMARY.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 .planning/phases/10-ux-interaction/10-06-SUMMARY.md diff --git a/.planning/phases/10-ux-interaction/10-06-SUMMARY.md b/.planning/phases/10-ux-interaction/10-06-SUMMARY.md new file mode 100644 index 0000000..d0bf707 --- /dev/null +++ b/.planning/phases/10-ux-interaction/10-06-SUMMARY.md @@ -0,0 +1,152 @@ +--- +phase: 10-ux-interaction +plan: "06" +subsystem: frontend/storage +tags: [wave-1, skeleton, empty-state, breadcrumb, toast, ux, vitest, tdd] +dependency_graph: + requires: [10-02, 10-03, 10-04, 10-05] + provides: [StorageBrowser-skeleton-UX-02, StorageBrowser-EmptyState-UX-01, BreadcrumbBar-wired-UX-12, toast-call-sites-UX-10] + affects: [FileManagerView, CloudFolderView, StorageBrowser] +tech_stack: + added: [] + patterns: [TDD-red-green, skeleton-grid, EmptyState-discriminator, breadcrumb-label-mapping, toast-call-site] +key_files: + created: [] + modified: + - frontend/src/components/storage/StorageBrowser.vue + - frontend/src/views/FileManagerView.vue + - frontend/src/views/CloudFolderView.vue + - frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js + - frontend/src/views/__tests__/FileManagerView.test.js + deleted: + - frontend/src/components/folders/FolderBreadcrumb.vue + - frontend/src/components/folders/__tests__/FolderBreadcrumb.test.js +decisions: + - "StorageBrowser uses breadcrumb.length > 0 as in-folder discriminator for EmptyState icon (folder vs document)" + - "BreadcrumbBar receives :root-label based on mode prop ('Cloud' for cloud, 'Home' for local)" + - "FolderBreadcrumb.vue deleted in same commit as BreadcrumbBar swap (no dead code per CLAUDE.md)" + - "FileManagerView.test.js FolderBreadcrumb mock replaced with BreadcrumbBar mock (dead-code hygiene)" + - "Toast wiring: useToastStore() called at call-site inside doMove/doDeleteDoc/onFilesSelected (no top-level const)" +metrics: + duration_minutes: 10 + completed_date: "2026-06-15T20:26:00Z" + tasks_completed: 3 + tasks_total: 3 + files_created: 0 + files_modified: 5 + files_deleted: 2 +--- + +# Phase 10 Plan 06: StorageBrowser Wire-up Summary + +**One-liner:** StorageBrowser replaced Loading text with 5 animated skeleton rows, inline empty divs with three EmptyState variants, and FolderBreadcrumb with BreadcrumbBar; FileManagerView and CloudFolderView mapped breadcrumb segments to `{id, label}` and wired toast call sites for move/delete/upload. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Promote UX-02 skeleton stubs to RED failing tests | 413d3f0 | `StorageBrowser.skeleton.test.js` | +| 2 | Update StorageBrowser — skeleton, EmptyState, BreadcrumbBar swap | d040e77 | `StorageBrowser.vue` | +| 3 | Update FileManagerView + CloudFolderView; delete FolderBreadcrumb | 9ea51d6 | `FileManagerView.vue`, `CloudFolderView.vue`, `FolderBreadcrumb.vue` (deleted), `FolderBreadcrumb.test.js` (deleted), `FileManagerView.test.js`, `StorageBrowser.skeleton.test.js` | + +## What Was Built + +### Task 1: RED tests for UX-02 skeleton + +Promoted 4 `it.todo` stubs in `StorageBrowser.skeleton.test.js` to real assertions: +- `renders 5 skeleton rows when loading=true and lists empty` — asserts `wrapper.findAll('.animate-pulse').length >= 5` +- `Loading… text is absent when loading=true` — asserts `wrapper.text()` does not contain `'Loading…'` +- `skeleton rows are NOT rendered when loading=false` — asserts zero `.animate-pulse` elements +- `skeleton row grid matches grid-cols-[2rem_1fr_6rem_8rem_6rem]` — asserts at least one matching grid container + +Tests 1 and 2 were RED before Task 2. All 4 turn GREEN after Task 2. + +### Task 2: StorageBrowser.vue updated (GREEN) + +**Imports:** `FolderBreadcrumb` replaced by `BreadcrumbBar` + `EmptyState` added. + +**Template — BreadcrumbBar:** Replaced `` with: +```vue + +``` + +**Template — Skeleton rows (lines 226-238):** Replaced `
Loading…
` with `