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:
co-authored by
Claude Sonnet 4.6
parent
654622d358
commit
87a32b7ee8
@@ -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