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:
@@ -106,7 +106,7 @@ const foldersStore = useFoldersStore()
|
||||
const showShareModal = ref(false)
|
||||
const showFolderPicker = ref(false)
|
||||
|
||||
const allFolders = computed(() => foldersStore.folders)
|
||||
const allFolders = computed(() => foldersStore.rootFolders)
|
||||
|
||||
function openShareModal() {
|
||||
showShareModal.value = true
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Row -->
|
||||
<div
|
||||
class="flex items-center group"
|
||||
:style="{ paddingLeft: `${depth * 12}px` }"
|
||||
>
|
||||
<!-- Expand/collapse arrow — hidden when folder is a known leaf (has_children === false) -->
|
||||
<button
|
||||
v-if="folder.has_children !== false && (!childrenLoaded || (children && children.length > 0))"
|
||||
@click.prevent.stop="toggleExpand"
|
||||
class="w-5 h-5 flex items-center justify-center text-gray-400 hover:text-gray-600 shrink-0 transition-colors"
|
||||
:aria-label="expanded ? 'Collapse' : 'Expand'"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 transition-transform duration-150"
|
||||
:class="{ 'rotate-90': expanded }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Spacer for leaf nodes -->
|
||||
<span v-else class="w-5 h-5 shrink-0"></span>
|
||||
|
||||
<!-- Folder name (router-link) -->
|
||||
<router-link
|
||||
:to="`/folders/${folder.id}`"
|
||||
class="flex items-center gap-2 flex-1 min-w-0 px-2 py-1.5 rounded-lg text-sm font-medium transition-colors"
|
||||
:class="isActive
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 shrink-0"
|
||||
:class="isActive ? 'text-indigo-500' : 'text-gray-400'"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ folder.name }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Children (recursively rendered) -->
|
||||
<div v-if="expanded && children && children.length > 0">
|
||||
<FolderTreeItem
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:folder="child"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useFoldersStore } from '../../stores/folders.js'
|
||||
import * as api from '../../api/client.js'
|
||||
|
||||
const props = defineProps({
|
||||
folder: { type: Object, required: true },
|
||||
depth: { type: Number, default: 0 },
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const foldersStore = useFoldersStore()
|
||||
|
||||
const expanded = ref(false)
|
||||
const children = ref(null) // null = not yet loaded
|
||||
const childrenLoaded = ref(false)
|
||||
|
||||
const isActive = computed(() =>
|
||||
route.params.folderId != null &&
|
||||
(route.params.folderId === String(props.folder.id) || route.params.folderId === props.folder.id)
|
||||
)
|
||||
|
||||
async function loadChildren() {
|
||||
try {
|
||||
const data = await api.listFolders(props.folder.id)
|
||||
children.value = data.items ?? data
|
||||
} catch {
|
||||
children.value = []
|
||||
}
|
||||
childrenLoaded.value = true
|
||||
}
|
||||
|
||||
async function toggleExpand() {
|
||||
if (!childrenLoaded.value) {
|
||||
await loadChildren()
|
||||
}
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
|
||||
// Re-fetch children when any folder mutation occurs (create/rename/delete)
|
||||
watch(() => foldersStore.treeVersion, () => {
|
||||
if (expanded.value && childrenLoaded.value) {
|
||||
loadChildren()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,108 @@
|
||||
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 <span>, 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)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import FolderTreeItem from '../FolderTreeItem.vue'
|
||||
|
||||
const mockListFolders = vi.fn()
|
||||
|
||||
vi.mock('../../../api/client.js', () => ({
|
||||
listFolders: (...a) => mockListFolders(...a),
|
||||
}))
|
||||
|
||||
// Minimal router so router-link renders correctly
|
||||
function makeRouter(currentPath = '/') {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div/>' } },
|
||||
{ path: '/folders/:folderId', component: { template: '<div/>' } },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
function makeFolder(overrides = {}) {
|
||||
return {
|
||||
id: overrides.id ?? 'f1',
|
||||
name: overrides.name ?? 'Test',
|
||||
parent_id: overrides.parent_id ?? null,
|
||||
has_children: overrides.has_children ?? false,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
async function mountItem(folderOverrides = {}, routerPath = '/') {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter(routerPath)
|
||||
await router.push(routerPath)
|
||||
await router.isReady()
|
||||
return mount(FolderTreeItem, {
|
||||
props: { folder: makeFolder(folderOverrides), depth: 1 },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
}
|
||||
|
||||
describe('FolderTreeItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockListFolders.mockResolvedValue({ items: [] })
|
||||
})
|
||||
|
||||
// ── Expand arrow visibility ───────────────────────────────────────────────
|
||||
|
||||
it('hides expand arrow when has_children is false', async () => {
|
||||
const w = await mountItem({ has_children: false })
|
||||
// The expand button should not be rendered
|
||||
const expandBtn = w.find('button')
|
||||
expect(expandBtn.exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows expand arrow when has_children is true', async () => {
|
||||
const w = await mountItem({ has_children: true })
|
||||
const expandBtn = w.find('button')
|
||||
expect(expandBtn.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('shows expand arrow when has_children is undefined (unknown)', async () => {
|
||||
// Newly created folders from optimistic push lack has_children
|
||||
const folder = { id: 'f1', name: 'New', parent_id: null, created_at: '2026-01-01T00:00:00Z' }
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter('/')
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
const w = mount(FolderTreeItem, {
|
||||
props: { folder, depth: 1 },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
// has_children undefined → arrow shown (conservative: assume could have children)
|
||||
const expandBtn = w.find('button')
|
||||
expect(expandBtn.exists()).toBe(true)
|
||||
})
|
||||
|
||||
// ── Router-link href ──────────────────────────────────────────────────────
|
||||
|
||||
it('router-link points to /folders/<id>', async () => {
|
||||
const w = await mountItem({ id: 'abc-123' })
|
||||
const link = w.find('a')
|
||||
expect(link.attributes('href')).toBe('/folders/abc-123')
|
||||
})
|
||||
|
||||
// ── Active state ─────────────────────────────────────────────────────────
|
||||
|
||||
it('router-link has active class when route matches folder id', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter()
|
||||
await router.push('/folders/f1')
|
||||
await router.isReady()
|
||||
|
||||
const w = mount(FolderTreeItem, {
|
||||
props: { folder: makeFolder({ id: 'f1' }), depth: 1 },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
|
||||
const link = w.find('a')
|
||||
expect(link.classes()).toContain('bg-indigo-50')
|
||||
})
|
||||
|
||||
it('router-link does NOT have active class when route does not match', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter()
|
||||
await router.push('/folders/other-folder')
|
||||
await router.isReady()
|
||||
|
||||
const w = mount(FolderTreeItem, {
|
||||
props: { folder: makeFolder({ id: 'f1' }), depth: 1 },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
|
||||
const link = w.find('a')
|
||||
expect(link.classes()).not.toContain('bg-indigo-50')
|
||||
})
|
||||
|
||||
// ── Expand / collapse children ────────────────────────────────────────────
|
||||
|
||||
it('clicking expand arrow loads children from API', async () => {
|
||||
const children = [makeFolder({ id: 'child-1', name: 'Child', parent_id: 'f1' })]
|
||||
mockListFolders.mockResolvedValue({ items: children })
|
||||
|
||||
const w = await mountItem({ id: 'f1', has_children: true })
|
||||
const btn = w.find('button')
|
||||
await btn.trigger('click')
|
||||
await w.vm.$nextTick()
|
||||
|
||||
expect(mockListFolders).toHaveBeenCalledWith('f1')
|
||||
})
|
||||
|
||||
it('children are rendered after expand', async () => {
|
||||
const children = [makeFolder({ id: 'child-1', name: 'ChildFolder', parent_id: 'f1' })]
|
||||
mockListFolders.mockResolvedValue({ items: children })
|
||||
|
||||
const w = await mountItem({ id: 'f1', has_children: true })
|
||||
await w.find('button').trigger('click')
|
||||
await w.vm.$nextTick()
|
||||
|
||||
expect(w.text()).toContain('ChildFolder')
|
||||
})
|
||||
|
||||
it('expand arrow disappears after loading empty children', async () => {
|
||||
mockListFolders.mockResolvedValue({ items: [] })
|
||||
|
||||
const w = await mountItem({ id: 'f1', has_children: true })
|
||||
await w.find('button').trigger('click')
|
||||
await w.vm.$nextTick()
|
||||
|
||||
// After loading empty children, button should be gone
|
||||
expect(w.find('button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
// ── Depth indentation ────────────────────────────────────────────────────
|
||||
|
||||
it('applies correct left padding based on depth', async () => {
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter('/')
|
||||
await router.push('/')
|
||||
await router.isReady()
|
||||
|
||||
const w = mount(FolderTreeItem, {
|
||||
props: { folder: makeFolder(), depth: 3 },
|
||||
global: { plugins: [router] },
|
||||
})
|
||||
|
||||
const row = w.find('.flex.items-center')
|
||||
expect(row.attributes('style')).toContain('padding-left: 36px')
|
||||
})
|
||||
})
|
||||
@@ -2,24 +2,12 @@
|
||||
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col h-full shrink-0">
|
||||
<!-- Logo -->
|
||||
<div class="px-6 py-5 border-b border-gray-100">
|
||||
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocScanner</h1>
|
||||
<p class="text-xs text-gray-400 mt-0.5">AI Document Classifier</p>
|
||||
<h1 class="text-lg font-bold text-indigo-600 tracking-tight">DocuVault</h1>
|
||||
<p class="text-xs text-gray-400 mt-0.5">Document Manager</p>
|
||||
</div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 px-3 py-4 overflow-y-auto">
|
||||
<router-link
|
||||
to="/"
|
||||
class="nav-link"
|
||||
:class="{ 'nav-link-active': $route.path === '/' }"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Home
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
to="/topics"
|
||||
class="nav-link"
|
||||
@@ -32,7 +20,7 @@
|
||||
All Topics
|
||||
</router-link>
|
||||
|
||||
<!-- Shared with me entry -->
|
||||
<!-- Shared with me -->
|
||||
<router-link
|
||||
to="/shared"
|
||||
class="nav-link"
|
||||
@@ -53,20 +41,32 @@
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Folders section -->
|
||||
<!-- Folders root + collapsible tree -->
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between px-3 mb-1">
|
||||
<p class="text-xs font-semibold text-gray-400 uppercase tracking-wider">Folders</p>
|
||||
<!-- "Folders" is the root entry — clicking navigates to the root folder view -->
|
||||
<div class="flex items-center justify-between">
|
||||
<router-link
|
||||
to="/"
|
||||
class="nav-link flex-1"
|
||||
:class="{ 'nav-link-active': $route.path === '/' || $route.path.startsWith('/folders/') }"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2 shrink-0 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||
</svg>
|
||||
Folders
|
||||
</router-link>
|
||||
<button
|
||||
@click="startNewFolder"
|
||||
class="text-xs text-indigo-600 hover:underline"
|
||||
class="text-xs text-indigo-600 hover:underline shrink-0 mr-1"
|
||||
title="New root folder"
|
||||
>
|
||||
New folder
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New folder inline input -->
|
||||
<div v-if="showNewFolderInput" class="px-3 mb-2">
|
||||
<!-- Inline new root folder input -->
|
||||
<div v-if="showNewFolderInput" class="px-3 mb-2 mt-1">
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
type="text"
|
||||
@@ -79,21 +79,16 @@
|
||||
<p v-if="newFolderError" class="text-red-500 text-xs mt-1">{{ newFolderError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Folder list -->
|
||||
<div v-if="foldersStore.loading && foldersStore.folders.length === 0" class="px-3 py-1 text-xs text-gray-400">Loading…</div>
|
||||
<router-link
|
||||
v-for="folder in foldersStore.folders"
|
||||
<!-- Sub-folders tree (indented under Folders) -->
|
||||
<div v-if="loadingRoots" class="pl-7 py-1 text-xs text-gray-400">Loading…</div>
|
||||
<div v-else-if="foldersStore.rootFolders.length === 0 && !showNewFolderInput"
|
||||
class="pl-7 py-1 text-xs text-gray-400">No folders yet</div>
|
||||
<FolderTreeItem
|
||||
v-for="folder in foldersStore.rootFolders"
|
||||
:key="folder.id"
|
||||
:to="`/folders/${folder.id}`"
|
||||
class="nav-link text-sm"
|
||||
:class="{ 'nav-link-active': $route.params.folderId === folder.id || $route.params.folderId === String(folder.id) }"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2 shrink-0 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||
</svg>
|
||||
<span class="truncate flex-1">{{ folder.name }}</span>
|
||||
</router-link>
|
||||
:folder="folder"
|
||||
:depth="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Topics list -->
|
||||
@@ -118,7 +113,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Quota bar (between topics nav and settings footer, UI-SPEC Phase 3) -->
|
||||
<!-- Quota bar -->
|
||||
<QuotaBar />
|
||||
|
||||
<!-- Settings + Admin link -->
|
||||
@@ -172,27 +167,34 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useTopicsStore } from '../../stores/topics.js'
|
||||
import { useAuthStore } from '../../stores/auth.js'
|
||||
import { useFoldersStore } from '../../stores/folders.js'
|
||||
import QuotaBar from './QuotaBar.vue'
|
||||
import FolderTreeItem from '../folders/FolderTreeItem.vue'
|
||||
import * as api from '../../api/client.js'
|
||||
|
||||
const topicsStore = useTopicsStore()
|
||||
const authStore = useAuthStore()
|
||||
const foldersStore = useFoldersStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const sharedCount = ref(0)
|
||||
const showNewFolderInput = ref(false)
|
||||
const newFolderName = ref('')
|
||||
const newFolderError = ref('')
|
||||
const loadingRoots = ref(true)
|
||||
|
||||
watch(() => foldersStore.treeVersion, () => foldersStore.fetchRootFolders())
|
||||
|
||||
onMounted(async () => {
|
||||
await foldersStore.fetchFolders(null)
|
||||
try {
|
||||
await foldersStore.fetchRootFolders()
|
||||
} finally {
|
||||
loadingRoots.value = false
|
||||
}
|
||||
try {
|
||||
const data = await api.getSharedWithMe()
|
||||
const items = Array.isArray(data) ? data : (data.items ?? [])
|
||||
|
||||
Reference in New Issue
Block a user