feat(phase-4-09): wire components into views — sidebar, cards, home, folder, shared, settings, admin
- AppSidebar: add 'Shared with me' entry (purple icon, count badge) and Folders section with New folder CTA - DocumentCard: add group class, hover-reveal share button, ShareModal v-if, shared indicator pill - HomeView: add SearchBar + SortControls above document list; fetchFolders on mount - FolderView: new view with FolderBreadcrumb, FolderRow list, inline new-subfolder input, document list - SharedView: new view fetching /api/shares/received with owner_handle display and empty state - DocumentView: add PDF preview logic (in_app=DocumentPreviewModal, new_tab=window.open); load preferences on mount - SettingsView: add Document Preferences card with pdf_open_mode radio buttons, auto-save on change - AdminView: add Audit Log tab alongside Users/Quotas/AI Config tabs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white border border-gray-200 rounded-xl p-4 hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer"
|
||||
class="group bg-white border border-gray-200 rounded-xl p-4 hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer relative"
|
||||
@click="$router.push(`/document/${doc.id}`)"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
@@ -26,20 +26,51 @@
|
||||
/>
|
||||
<span v-if="!doc.topics?.length" class="text-xs text-gray-300 italic">unclassified</span>
|
||||
</div>
|
||||
|
||||
<!-- Shared indicator pill -->
|
||||
<div v-if="doc.share_count > 0" class="mt-2">
|
||||
<span class="bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full">Shared</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share button (hover-reveal) -->
|
||||
<button
|
||||
@click.stop="openShareModal"
|
||||
aria-label="Share document"
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity min-h-[44px] min-w-[44px] flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors shrink-0"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ShareModal -->
|
||||
<ShareModal
|
||||
v-if="showShareModal"
|
||||
:doc="doc"
|
||||
@close="showShareModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useTopicsStore } from '../../stores/topics.js'
|
||||
import TopicBadge from '../topics/TopicBadge.vue'
|
||||
import ShareModal from '../sharing/ShareModal.vue'
|
||||
|
||||
const props = defineProps({
|
||||
doc: Object,
|
||||
})
|
||||
|
||||
const topicsStore = useTopicsStore()
|
||||
const showShareModal = ref(false)
|
||||
|
||||
function openShareModal() {
|
||||
showShareModal.value = true
|
||||
}
|
||||
|
||||
function topicColor(name) {
|
||||
return topicsStore.topics.find(t => t.name === name)?.color ?? '#6366f1'
|
||||
|
||||
@@ -32,6 +32,70 @@
|
||||
All Topics
|
||||
</router-link>
|
||||
|
||||
<!-- Shared with me entry -->
|
||||
<router-link
|
||||
to="/shared"
|
||||
class="nav-link"
|
||||
:class="{ 'nav-link-active': $route.path === '/shared' }"
|
||||
>
|
||||
<div class="w-4 h-4 mr-2 shrink-0 rounded bg-purple-50 flex items-center justify-center">
|
||||
<svg class="w-3 h-3 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="flex-1">Shared with me</span>
|
||||
<span
|
||||
v-if="sharedCount > 0"
|
||||
class="ml-auto bg-purple-100 text-purple-600 text-xs font-semibold rounded-full px-2 min-w-[18px] text-center"
|
||||
>
|
||||
{{ sharedCount }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Folders section -->
|
||||
<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>
|
||||
<button
|
||||
@click="startNewFolder"
|
||||
class="text-xs text-indigo-600 hover:underline"
|
||||
>
|
||||
New folder
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- New folder inline input -->
|
||||
<div v-if="showNewFolderInput" class="px-3 mb-2">
|
||||
<input
|
||||
v-model="newFolderName"
|
||||
type="text"
|
||||
placeholder="Folder name"
|
||||
class="block w-full border border-gray-300 rounded-lg px-2 py-1 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
@keydown.enter="submitNewFolder"
|
||||
@keydown.escape="cancelNewFolder"
|
||||
autofocus
|
||||
/>
|
||||
<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"
|
||||
: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>
|
||||
</div>
|
||||
|
||||
<!-- Topics list -->
|
||||
<div class="mt-3">
|
||||
<p class="px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1">Topics</p>
|
||||
@@ -108,19 +172,66 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } 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 * 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('')
|
||||
|
||||
onMounted(async () => {
|
||||
await foldersStore.fetchFolders(null)
|
||||
try {
|
||||
const data = await api.getSharedWithMe()
|
||||
const items = Array.isArray(data) ? data : (data.items ?? [])
|
||||
sharedCount.value = items.length
|
||||
} catch {
|
||||
sharedCount.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
async function signOut() {
|
||||
await authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function startNewFolder() {
|
||||
newFolderName.value = ''
|
||||
newFolderError.value = ''
|
||||
showNewFolderInput.value = true
|
||||
}
|
||||
|
||||
function cancelNewFolder() {
|
||||
showNewFolderInput.value = false
|
||||
newFolderError.value = ''
|
||||
}
|
||||
|
||||
async function submitNewFolder() {
|
||||
const trimmed = newFolderName.value.trim()
|
||||
if (!trimmed) {
|
||||
newFolderError.value = 'Folder name cannot be empty.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
await foldersStore.createFolder(trimmed, null)
|
||||
showNewFolderInput.value = false
|
||||
newFolderError.value = ''
|
||||
} catch (e) {
|
||||
newFolderError.value = e.message || 'Failed to create folder.'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user