b916e485f7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
11 KiB
Markdown
166 lines
11 KiB
Markdown
---
|
|
phase: 04-folders-sharing-quotas-document-ux
|
|
plan: "09"
|
|
subsystem: frontend-ui
|
|
status: complete
|
|
completed_at: "2026-05-25"
|
|
duration_minutes: 35
|
|
tags: [vue, components, folders, sharing, search, sort, audit-log, pdf-preview, tailwind]
|
|
|
|
dependency_graph:
|
|
requires:
|
|
- "04-08" # API client + stores data layer
|
|
provides:
|
|
- "FolderView.vue — folder contents view with breadcrumb + sub-folder rows + document list"
|
|
- "SharedView.vue — shared-with-me virtual folder"
|
|
- "FolderRow.vue — folder row with inline rename and delete confirmation modal trigger"
|
|
- "FolderBreadcrumb.vue — truncated breadcrumb nav (depth > 4 → ellipsis)"
|
|
- "FolderDeleteModal.vue — destructive confirmation modal with document count"
|
|
- "ShareModal.vue — share by handle + revoke access list"
|
|
- "DocumentPreviewModal.vue — in-app PDF preview via proxy iframe"
|
|
- "SearchBar.vue — debounced search input; Escape clears"
|
|
- "SortControls.vue — Name/Date/Size toggle buttons with aria-pressed + direction indicator"
|
|
- "AuditLogTab.vue — paginated audit log table with filters + CSV export"
|
|
- "AppSidebar.vue extended with Shared-with-me entry and Folders section"
|
|
- "DocumentCard.vue extended with share button (hover) and shared indicator pill"
|
|
affects:
|
|
- "frontend/src/views/HomeView.vue"
|
|
- "frontend/src/views/AdminView.vue"
|
|
- "frontend/src/views/SettingsView.vue"
|
|
- "frontend/src/views/DocumentView.vue"
|
|
- "frontend/src/components/layout/AppSidebar.vue"
|
|
- "frontend/src/components/documents/DocumentCard.vue"
|
|
- "frontend/src/api/client.js"
|
|
|
|
tech_stack:
|
|
added: []
|
|
patterns:
|
|
- "Composition API with <script setup> — all components follow existing project style"
|
|
- "iframe with proxy URL for PDF preview (never presigned URL) — T-04-09-01 mitigation"
|
|
- "Optimistic UI for share revoke — re-adds row on API failure"
|
|
- "Debounce pattern for search lives in documents store (300ms, min 2 chars)"
|
|
- "window.location.href for CSV export — authenticated via existing httpOnly cookie"
|
|
- "Document listener pattern for outside-click close in FolderRow dropdown"
|
|
|
|
key_files:
|
|
created:
|
|
- "frontend/src/components/folders/FolderRow.vue"
|
|
- "frontend/src/components/folders/FolderBreadcrumb.vue"
|
|
- "frontend/src/components/folders/FolderDeleteModal.vue"
|
|
- "frontend/src/components/sharing/ShareModal.vue"
|
|
- "frontend/src/components/documents/DocumentPreviewModal.vue"
|
|
- "frontend/src/components/documents/SearchBar.vue"
|
|
- "frontend/src/components/documents/SortControls.vue"
|
|
- "frontend/src/components/admin/AuditLogTab.vue"
|
|
- "frontend/src/views/FolderView.vue"
|
|
- "frontend/src/views/SharedView.vue"
|
|
modified:
|
|
- "frontend/src/components/layout/AppSidebar.vue"
|
|
- "frontend/src/components/documents/DocumentCard.vue"
|
|
- "frontend/src/views/HomeView.vue"
|
|
- "frontend/src/views/DocumentView.vue"
|
|
- "frontend/src/views/SettingsView.vue"
|
|
- "frontend/src/views/AdminView.vue"
|
|
- "frontend/src/api/client.js"
|
|
|
|
decisions:
|
|
- "DocumentPreviewModal uses /api/documents/{id}/content proxy URL — never presigned; satisfies T-04-09-01"
|
|
- "ShareModal exact-handle input only (no autocomplete) — prevents user enumeration (T-04-09-02)"
|
|
- "AuditLogTab only visible inside AdminView which is already admin-role-guarded (T-04-09-03)"
|
|
- "CSV export via window.location.href with filter params only — no auth token in URL (T-04-09-04 accepted)"
|
|
- "Optimistic share revoke (immediate UI removal, re-add on error) — low latency UX with safe rollback"
|
|
- "FolderRow outside-click closes menu via document event listener removed on unmount"
|
|
- "SettingsView auto-saves pdf_open_mode on radio change via watch() — no manual save button needed"
|
|
|
|
metrics:
|
|
duration_minutes: 35
|
|
tasks_completed: 2
|
|
files_created: 10
|
|
files_modified: 7
|
|
---
|
|
|
|
# Phase 4 Plan 09: Vue UI — Folders, Sharing, Search, Sort, Audit Log, PDF Preview Summary
|
|
|
|
All Phase 4 frontend components created and wired into existing views. 10 new files + 7 modified files delivering the complete document management UX: folder navigation with breadcrumb, share-by-handle modal, in-app PDF preview iframe, debounced search, sort controls, and an admin audit log tab with CSV export.
|
|
|
|
## Tasks Completed
|
|
|
|
| Task | Name | Commit | Key Files |
|
|
|------|------|--------|-----------|
|
|
| 1 | Create new components (8 files + api client update) | 3672157 | FolderRow, FolderBreadcrumb, FolderDeleteModal, ShareModal, DocumentPreviewModal, SearchBar, SortControls, AuditLogTab |
|
|
| 2 | Modify existing views and components | a3f5fc2 | AppSidebar, DocumentCard, HomeView, FolderView (new), SharedView (new), DocumentView, SettingsView, AdminView |
|
|
|
|
## What Was Built
|
|
|
|
### New Components
|
|
|
|
**FolderRow.vue** — Flex row with folder icon, name, doc count, three-dot dropdown menu (Rename/Delete). Inline rename replaces name text with focused input (Enter saves, Escape cancels, empty name rejected). Outside click closes dropdown via document event listener removed on unmount.
|
|
|
|
**FolderBreadcrumb.vue** — `<nav aria-label="Folder navigation">` with `<ol>` list structure. Computed `visibleSegments` applies truncation: when depth > 4, shows first segment + ellipsis + last 2 segments. All segments except last clickable (emit `navigate`); last segment is current folder (non-clickable).
|
|
|
|
**FolderDeleteModal.vue** — `role="dialog" aria-modal="true"`. Warning icon, "Delete folder?" heading, body with interpolated `doc_count` (singular/plural). Two buttons: "Keep folder" (neutral) and "Delete folder and documents" (red, `min-h-[44px]`).
|
|
|
|
**ShareModal.vue** — `role="dialog" aria-modal="true"`. Loads current shares on mount via `useDocumentsStore().listShares()`. Handle input + "Share document" button. 404 → "User not found. Check the handle and try again." 409 → "This document is already shared with that user." Recipients list with "Remove access" button; optimistic removal re-adds row on error. Close button `aria-label="Close"`.
|
|
|
|
**DocumentPreviewModal.vue** — `role="dialog" aria-modal="true" aria-label="Document preview"`. `proxyUrl` computed as `/api/documents/${doc.id}/content` (never presigned). Full-viewport overlay `bg-black/60`, header bar with filename + close, `flex-1` iframe. Escape key via document listener removed on unmount. Overlay click checks `event.target === overlayRef.value`.
|
|
|
|
**SearchBar.vue** — `<div role="search">`, `<input type="search" aria-label="Search documents">`, `@keydown.escape` clears modelValue. Width `w-56`.
|
|
|
|
**SortControls.vue** — Three buttons (Name/Date/Size). Active: `text-indigo-600 font-semibold bg-indigo-50`. `aria-pressed="true"` on active. `aria-label` includes direction. Clicking active toggles order; clicking different sort switches with `desc` default.
|
|
|
|
**AuditLogTab.vue** — Filter bar (date range, user input, event_type select, Apply/Export). Table with `<th scope="col">` headers, action-type pill badges (auth=blue, document=gray, folder/share=purple, admin=amber), `font-mono text-xs` timestamps. Previous/Next pagination. `window.location.href` for CSV export (query params only, no auth tokens).
|
|
|
|
### Modified Files
|
|
|
|
**AppSidebar.vue** — "Shared with me" router-link (purple icon `bg-purple-50 text-purple-500`, count badge when `sharedCount > 0`). Folders section below with section label, "New folder" inline input (toggle), top-level folder list from `useFoldersStore`. Count loaded on mount via `getSharedWithMe()`.
|
|
|
|
**DocumentCard.vue** — Added `group` class to outer div. Share button `opacity-0 group-hover:opacity-100 transition-opacity min-h-[44px] min-w-[44px]` with `aria-label="Share document"`. `ShareModal` v-if triggered on button click (stops propagation). Shared indicator pill `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full` when `doc.share_count > 0`.
|
|
|
|
**HomeView.vue** — `SearchBar` (v-model → `docsStore.searchQuery`) + `SortControls` above document list. `handleSortChange` updates store fields and re-fetches. `foldersStore.fetchFolders(null)` added to `onMounted`.
|
|
|
|
**FolderView.vue (new)** — Full folder contents view: `FolderBreadcrumb` + `FolderRow` list + inline new-subfolder input + document list with `SearchBar`/`SortControls`. Watches `route.params.folderId` to re-load on navigation. `FolderDeleteModal` shown when `folderToDelete` is set.
|
|
|
|
**SharedView.vue (new)** — Fetches `/api/shares/received` on mount. Document-card layout with `owner_handle` displayed. Empty state: "No documents shared with you yet. When someone shares a document with you, it will appear here."
|
|
|
|
**DocumentView.vue** — Added `isPdf` computed (checks `mime_type` or filename extension). `openPdf()` shows `DocumentPreviewModal` when `pdfOpenMode === 'in_app'`; otherwise `window.open(getDocumentContentUrl(doc.id), '_blank')`. Loads user preferences on mount via `api.getMyPreferences()`.
|
|
|
|
**SettingsView.vue** — "Document Preferences" section with two radio buttons (`in_app` / `new_tab`). `watch(pdfOpenMode)` auto-saves via `api.updateMyPreferences()` on every change. Save feedback shown for 3 seconds.
|
|
|
|
**AdminView.vue** — Added `{ id: 'audit', label: 'Audit Log' }` to tabs array. `<AuditLogTab v-if="activeTab === 'audit'" />` added to tab content.
|
|
|
|
**api/client.js** — Added `adminListAuditLog({ start, end, user_id, event_type, page, per_page })` — GET `/api/admin/audit-log` with URLSearchParams.
|
|
|
|
## Deviations from Plan
|
|
|
|
None — plan executed exactly as written. All UI-SPEC classes, copywriting, and accessibility contracts applied as specified.
|
|
|
|
## Known Stubs
|
|
|
|
None — all components are fully wired to real stores and API functions. `FolderRow.doc_count` may be 0 if the backend does not return this field; this is a backend data shape issue, not a frontend stub.
|
|
|
|
## Threat Surface Scan
|
|
|
|
No new security-relevant surface introduced beyond what the threat model already covers:
|
|
- T-04-09-01: DocumentPreviewModal verified to use proxy URL only (no presigned)
|
|
- T-04-09-02: ShareModal uses exact-handle input only, no autocomplete API
|
|
- T-04-09-03: AuditLogTab only reachable inside AdminView (admin-guarded route)
|
|
- T-04-09-04: CSV export URL contains only filter params (dates, event types, user IDs) — accepted
|
|
|
|
## Self-Check: PASSED
|
|
|
|
### Files exist:
|
|
- frontend/src/components/folders/FolderRow.vue — FOUND
|
|
- frontend/src/components/folders/FolderBreadcrumb.vue — FOUND
|
|
- frontend/src/components/folders/FolderDeleteModal.vue — FOUND
|
|
- frontend/src/components/sharing/ShareModal.vue — FOUND
|
|
- frontend/src/components/documents/DocumentPreviewModal.vue — FOUND
|
|
- frontend/src/components/documents/SearchBar.vue — FOUND
|
|
- frontend/src/components/documents/SortControls.vue — FOUND
|
|
- frontend/src/components/admin/AuditLogTab.vue — FOUND
|
|
- frontend/src/views/FolderView.vue — FOUND
|
|
- frontend/src/views/SharedView.vue — FOUND
|
|
|
|
### Commits exist:
|
|
- 3672157 — feat(phase-4-09): create new components — FOUND
|
|
- a3f5fc2 — feat(phase-4-09): wire components into views — FOUND
|