---
phase: 04-folders-sharing-quotas-document-ux
plan: 09
type: execute
wave: 7
depends_on:
- "04-08"
files_modified:
- frontend/src/views/FolderView.vue
- frontend/src/views/SharedView.vue
- frontend/src/views/SettingsView.vue
- frontend/src/views/DocumentView.vue
- frontend/src/views/HomeView.vue
- frontend/src/views/AdminView.vue
- 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/components/layout/AppSidebar.vue
- frontend/src/components/documents/DocumentCard.vue
- frontend/src/api/client.js
autonomous: false
requirements:
- FOLD-01
- FOLD-02
- FOLD-03
- FOLD-04
- FOLD-05
- SHARE-01
- SHARE-02
- SHARE-03
- SHARE-04
- SHARE-05
- ADMIN-06
- DOC-01
- DOC-02
must_haves:
truths:
- "Folders section visible in AppSidebar above topics; top-level folders clickable to navigate"
- "Shared with me entry in AppSidebar above folders; shows count badge when non-empty"
- "Document list shows SearchBar and SortControls above; sort persists until changed"
- "DocumentCard shows share button on hover; shared indicator pill when doc.share_count > 0"
- "Share modal: handle input, Share button, current recipient list with Revoke button"
- "FolderView renders sub-folders as FolderRow components + breadcrumb + document list"
- "FolderDeleteModal shows document count and requires explicit confirmation before delete"
- "PDF documents open in DocumentPreviewModal (in_app mode) or new tab (new_tab mode)"
- "SettingsView Document Preferences card shows pdf_open_mode radio; auto-saves on change"
- "Admin AuditLog tab is accessible from AdminView; filters and CSV export work"
artifacts:
- path: "frontend/src/views/FolderView.vue"
provides: "Folder contents: sub-folder rows + breadcrumb + document list + new subfolder inline input"
- path: "frontend/src/views/SharedView.vue"
provides: "Shared with me virtual folder view"
- path: "frontend/src/components/folders/FolderRow.vue"
provides: "Folder row with three-dot menu (rename inline, delete folder) per UI-SPEC"
- path: "frontend/src/components/folders/FolderBreadcrumb.vue"
provides: "Breadcrumb nav with truncation at depth > 4; each segment clickable"
- path: "frontend/src/components/folders/FolderDeleteModal.vue"
provides: "Destructive confirmation modal with document count"
- path: "frontend/src/components/sharing/ShareModal.vue"
provides: "Share by handle + current recipients list + revoke"
- path: "frontend/src/components/documents/DocumentPreviewModal.vue"
provides: "In-app PDF preview via iframe"
- path: "frontend/src/components/documents/SearchBar.vue"
provides: "Debounced search input; clears on Escape"
- path: "frontend/src/components/documents/SortControls.vue"
provides: "Name / Date / Size sort toggle buttons"
- path: "frontend/src/components/admin/AuditLogTab.vue"
provides: "Paginated audit log table with date/user/action filters + CSV export button"
key_links:
- from: "frontend/src/views/FolderView.vue"
to: "frontend/src/stores/folders.js"
via: "useFoldersStore for folder data and navigation"
pattern: "useFoldersStore"
- from: "frontend/src/components/sharing/ShareModal.vue"
to: "frontend/src/stores/documents.js"
via: "shareDocument, revokeShare, listShares actions"
pattern: "useDocumentsStore"
- from: "frontend/src/components/layout/AppSidebar.vue"
to: "frontend/src/stores/folders.js"
via: "top-level folder list + currentFolderId for active state"
pattern: "useFoldersStore"
---
Build all Phase 4 Vue components and wire them into the existing views. This is the final
frontend plan — all stores, API client, and routes are established by plan 04-08.
Purpose: Deliver the complete document management UX visible to users and admins.
Output: 10 new components + 6 modified views following 04-UI-SPEC.md exactly.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md
@.planning/phases/04-folders-sharing-quotas-document-ux/04-CONTEXT.md
@.planning/phases/04-folders-sharing-quotas-document-ux/04-PATTERNS.md
@frontend/src/components/layout/AppSidebar.vue
@frontend/src/components/documents/DocumentCard.vue
@frontend/src/views/HomeView.vue
@frontend/src/views/AdminView.vue
@frontend/src/views/SettingsView.vue
@frontend/src/views/DocumentView.vue
@frontend/src/components/admin/AdminUsersTab.vue
From 04-UI-SPEC.md — exact Tailwind classes for each new component are specified.
Executor must read 04-UI-SPEC.md fully before writing any component template.
Key design tokens from 04-UI-SPEC.md:
- Modal overlay: `fixed inset-0 bg-black/40 flex items-center justify-center z-50`
- Modal panel: `bg-white rounded-2xl shadow-xl p-6 max-w-md w-full mx-4`
- Primary button: `bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg`
- Destructive button: `bg-red-600 hover:bg-red-700 text-white text-sm font-semibold px-4 py-2 rounded-lg min-h-[44px]`
- FolderRow: `flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer`
- Share badge (SHARE-05): `bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full`
- Shared with me icon: `bg-purple-50 text-purple-500` (only purple UI element)
- Section label: `text-xs font-semibold text-gray-400 uppercase tracking-wider mb-1`
- Accessibility: all icon-only buttons have aria-label; modals have role="dialog" aria-modal="true"
Copywriting (from 04-UI-SPEC.md):
- Share modal title: "Share document"
- Handle input placeholder: "Enter username handle"
- Share submit: "Share document"
- Empty share state: "Not shared with anyone yet."
- User not found: "User not found. Check the handle and try again."
- Non-empty folder delete body: "This folder contains {N} document{s}. Deleting it will permanently delete all documents inside. This cannot be undone."
- Folder delete confirm: "Delete folder and documents"
- Folder delete dismiss: "Keep folder"
- Search placeholder: "Search documents..."
- PDF pref section heading: "Document Preferences"
- PDF open modes: "Open documents in-app" / "Open documents in new tab"
- Audit log tab label: "Audit Log"
- Sort labels: "Name" / "Date" / "Size"
Task 1: Create new components (FolderRow, FolderBreadcrumb, FolderDeleteModal, ShareModal, DocumentPreviewModal, SearchBar, SortControls, AuditLogTab)
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/components/admin/AdminUsersTab.vue — read the ENTIRE file; extract the form pattern (reactive form state, loading/error refs, onMounted fetch, table with filters, action buttons, inline confirm pattern); use this as the template for ShareModal and AuditLogTab
frontend/src/components/ui/ConfirmBlock.vue — read fully for the confirm/cancel button pattern
frontend/src/components/documents/DocumentCard.vue — read for the card layout pattern and SVG icon style
frontend/src/views/AdminView.vue — read to understand how tabs are currently implemented; extract the tab switch pattern
Create each component file in the exact directory specified. All use Vue 3 Options API with Composition API (script setup) — follow the same pattern as existing components (read the existing files to confirm which style is used and replicate it exactly).
FolderRow.vue (per UI-SPEC FolderRow section):
- Props: folder {id, name, doc_count}, onNavigate (function), onRename (function), onDelete (function)
- Template: flex container with folder icon (bg-gray-100), folder name + doc count, three-dot menu button
- Three-dot menu: dropdown with "Rename" and "Delete folder" items; closes on outside click (use @click.outside or a boolean ref + document listener)
- Rename mode: toggles to inline input pre-filled with current name; Enter=save, Escape=cancel, empty=rejected with inline error
- No modal for rename — inline text input replaces the name display
FolderBreadcrumb.vue (per UI-SPEC FolderBreadcrumb section):
- Props: segments Array of {id, name}
- Emits: navigate(folderId) — folderId is null for root
- Computed: visibleSegments — if segments.length > 4, return [segments[0], {id: null, name: '...'}, ...segments.slice(-2)]; otherwise return segments as-is
- Template: nav with aria-label="Folder navigation", ol list structure (accessibility), separator chevron SVG between segments
- All segments except the last are clickable (emit navigate); last segment is non-clickable (current folder)
- Wrap with nav aria-label="Folder navigation" and ol per UI-SPEC accessibility contract
FolderDeleteModal.vue (per UI-SPEC FolderDeleteModal section):
- Props: folder {id, name, doc_count}, onConfirm (function), onCancel (function)
- Template: fixed overlay, warning icon (red exclamation), heading "Delete folder?", body text with doc_count interpolated, two buttons: "Keep folder" (neutral) and "Delete folder and documents" (red, min-h-[44px])
- Emits: confirm, cancel
- Accessibility: role="dialog" aria-modal="true" aria-labelledby="modal-title-id"
ShareModal.vue (per UI-SPEC ShareModal section):
- Props: doc {id, filename}
- Emits: close
- Script state: handle ref(''), submitting ref(false), error ref(null), shares ref([])
- onMounted: load current shares via useDocumentsStore().listShares(props.doc.id); set shares.value
- submitShare(): calls store.shareDocument(props.doc.id, handle.value); clears handle on success; updates shares list; shows error for 404/409
- revokeShare(shareId): calls store.revokeShare(shareId); removes from shares array optimistically; on error re-adds
- Template: overlay, panel, title "Share document", handle input + Share document button, separator, recipient list OR empty state, close X button
- Error messages per UI-SPEC copywriting (User not found, already shared)
- Accessibility: role="dialog" aria-modal="true"; close button aria-label="Close"
DocumentPreviewModal.vue (per UI-SPEC DocumentPreviewModal section):
- Props: doc {id, filename}
- Emits: close
- Script: proxyUrl computed = `/api/documents/${props.doc.id}/content`
- Template: fixed overlay (bg-black/60 z-50 flex flex-col), header bar with doc name + close button, flex-1 iframe filling the remaining height
- Close on overlay click (check event.target === overlay element) and Escape key (keydown listener on mounted/unmounted)
- Accessibility: role="dialog" aria-modal="true" aria-label="Document preview"
SearchBar.vue (per UI-SPEC SearchBar section):
- Props: modelValue (String), placeholder defaults to "Search documents..."
- Emits: update:modelValue
- Template: input type="search", role="search" on wrapper div, aria-label="Search documents"
- Clears on Escape key (@keydown.escape)
- Width: w-56
SortControls.vue (per UI-SPEC SortControls section):
- Props: sort ('name'|'date'|'size'), order ('asc'|'desc')
- Emits: change({sort, order})
- Template: three text buttons; active sort has `text-indigo-600 font-semibold` class; direction indicator appended (arrow up/down unicode or ↑/↓ text)
- Clicking the already-active sort toggles order; clicking a different sort switches to it with desc order
- aria-pressed="true" on active button; aria-label includes direction (e.g., "Sort by name, ascending")
AuditLogTab.vue (per UI-SPEC AuditLogTab section):
- Script state: entries ref([]), total ref(0), page ref(1), loading ref(false), filters reactive({start:'', end:'', user_id:'', event_type:''})
- onMounted: fetchLog()
- fetchLog(): calls api.adminListAuditLog({...filters, page: page.value}) from api/client.js (add this function — GET /api/admin/audit-log with query params)
- exportCsv(): sets window.location.href to `/api/admin/audit-log/export?format=csv&${new URLSearchParams(activeFilters)}` — triggers browser download
- Template: filter bar (date inputs, user dropdown placeholder, event_type dropdown, Apply button, Export CSV button), paginated table (Timestamp | User | Action Type | IP Address), Previous/Next pagination, empty state
- Action type pill badge colors per UI-SPEC: auth=bg-blue-50 text-blue-600, document=bg-gray-100 text-gray-600, folder/share=bg-purple-50 text-purple-600, admin=bg-amber-50 text-amber-700
- Table headers with scope="col"; timestamp cell uses font-mono text-xs
Also add adminListAuditLog function to frontend/src/api/client.js (append): GET /api/admin/audit-log with query params start, end, user_id, event_type, page, per_page.
Run: find /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/folders /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/sharing -name "*.vue" 2>/dev/null
Expected: FolderRow.vue, FolderBreadcrumb.vue, FolderDeleteModal.vue appear; ShareModal.vue appears.
Also: find /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/documents /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin -name "*.vue" | sort
Expected: DocumentPreviewModal.vue, SearchBar.vue, SortControls.vue, AuditLogTab.vue appear.
- All 8 component files exist in their respective directories (verify with find)
- FolderBreadcrumb.vue has nav aria-label="Folder navigation" and ol list structure (grep: `aria-label.*Folder navigation` and `ol` in template)
- ShareModal.vue has role="dialog" and aria-modal="true" (grep: `role="dialog"` and `aria-modal="true"`)
- FolderDeleteModal.vue contains "Keep folder" and "Delete folder and documents" button text (grep)
- DocumentPreviewModal.vue contains iframe with :src bound to proxyUrl (grep: `iframe` and `proxyUrl`)
- SearchBar.vue has @keydown.escape handler (grep: `keydown.escape` or `keydown.esc`)
- SortControls.vue has aria-pressed (grep: `aria-pressed`)
- AuditLogTab.vue has window.location.href for CSV export (grep: `window.location.href`)
- frontend/src/api/client.js contains adminListAuditLog function (grep)
All new components created following UI-SPEC; accessibility contracts met; CSV export uses window.location.href.
All frontend views and components for Phase 4:
- AppSidebar.vue extended with "Shared with me" entry (purple icon, count badge) and Folders section with "New folder" CTA; folder links navigate to /folders/:id
- DocumentCard.vue extended with share button (hover, opacity-0 group-hover:opacity-100) and shared indicator pill
- HomeView.vue wired with SearchBar + SortControls above document list; folder-aware fetchDocuments
- FolderView.vue: breadcrumb + sub-folder FolderRow list + document list + inline new-folder input
- SharedView.vue: filtered document list from GET /api/shares/received; empty state
- DocumentView.vue: PDF click triggers DocumentPreviewModal (in_app) or window.open (new_tab) based on pdf_open_mode preference
- SettingsView.vue: Document Preferences card with radio buttons auto-saved via PATCH /api/auth/me/preferences
- AdminView.vue: AuditLog tab added alongside existing tabs
This checkpoint also triggers human verification of all visual and interactive Phase 4 features before the phase is marked complete.
Before this checkpoint is reached, the executor MUST complete the following modifications:
AppSidebar.vue: read the entire file; add "Shared with me" entry (above folders section, inline inbox icon, purple bg-purple-50 text-purple-500 icon container, count badge when sharedCount > 0); add Folders section below (section label "FOLDERS", "New folder" button, top-level folder list from useFoldersStore); import useFoldersStore; add sharedCount computed using getSharedWithMe() or a dedicated ref.
DocumentCard.vue: read the entire file; add `group` class to outer container; add share button (opacity-0 group-hover:opacity-100 transition-opacity, aria-label="Share document", min-h-[44px] min-w-[44px]); add ShareModal component import and v-if=showShareModal; add shared indicator pill below metadata line if doc.share_count > 0.
HomeView.vue: read the entire file; add SearchBar component above document list with v-model=docsStore.searchQuery; add SortControls with sort=docsStore.sortField, order=docsStore.sortOrder, @change handler; add useFoldersStore; update onMounted to also call foldersStore.fetchFolders(null).
FolderView.vue: create new file; uses useFoldersStore and useDocumentsStore; on route param folderId change, calls foldersStore.navigateTo(folderId) and docsStore.fetchDocuments({folderId}); renders FolderBreadcrumb + FolderRow list + document list + inline new-folder input.
SharedView.vue: create new file; on mounted calls api.getSharedWithMe(); renders document list using same DocumentCard layout but with owner handle shown; empty state per UI-SPEC copywriting.
DocumentView.vue: read the entire file; find where PDF documents would be clicked or previewed; add logic — if current_user.pdf_open_mode === 'in_app', show DocumentPreviewModal; if 'new_tab', call window.open(getDocumentContentUrl(doc.id), '_blank'); load preference via api.getMyPreferences() on mount.
SettingsView.vue: read the entire file; add Document Preferences section after existing sections; radio group with v-model=pdfOpenMode; watch pdfOpenMode to auto-call api.updateMyPreferences.
AdminView.vue: read the entire file; add "Audit Log" tab button alongside existing tabs; add AuditLogTab component conditional display.
AFTER completing all view modifications, start docker compose up and navigate to the application:
1. Navigate to the home page — verify Folders section and "Shared with me" appear in sidebar
2. Create a folder via sidebar "New folder" — verify it appears in the folder list
3. Navigate into the folder — verify FolderView loads with breadcrumb showing the folder name
4. Upload a document (use existing upload flow) — verify it appears in the current folder
5. Click the share button on a DocumentCard — verify ShareModal opens with handle input and empty state
6. In ShareModal, type a non-existent handle — verify "User not found" error appears
7. Delete a non-empty folder — verify FolderDeleteModal shows the document count and correct copy
8. Navigate to Settings — verify Document Preferences card with radio buttons appears
9. Navigate to Admin — verify Audit Log tab appears; click Apply filters — verify table loads
10. Click Export CSV in audit log — verify file downloads
11. Open a PDF document (set preference to in_app first) — verify DocumentPreviewModal with iframe appears
Type "approved" after verifying all 11 checkpoints pass. Describe any issues for the executor to fix before approving.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser → /api/documents/{id}/content | All document content access goes through proxy; iframe src is the proxy URL, not a presigned URL |
| Share modal → shares API | Recipient handle is user-supplied text; backend validates existence; no autocomplete/search that could enumerate users |
| Admin audit log export | window.location.href triggers a download authenticated by the existing httpOnly cookie; no token in URL |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-04-09-01 | Information Disclosure | iframe src reveals presigned URL in browser | mitigate | DocumentPreviewModal iframe src = `/api/documents/${docId}/content` (the proxy URL, never presigned); Content-Disposition: inline drives rendering |
| T-04-09-02 | Information Disclosure | Share modal autocomplete reveals user handles | mitigate | D-04: exact handle input only; no autocomplete API; 404 response reveals only that the handle does not exist |
| T-04-09-03 | Broken Access Control | Audit log tab visible to regular users in AdminView | mitigate | AuditLogTab only visible inside AdminView which is already guarded by admin-only route; backend enforces get_current_admin |
| T-04-09-04 | Information Disclosure | window.location.href for CSV export embeds sensitive params in URL | accept | Params are only filter values (dates, event types, user IDs) — not auth tokens; the request is authenticated via httpOnly cookie already set |
| T-04-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this plan |
1. Component existence: find frontend/src/components/folders frontend/src/components/sharing -name "*.vue"
2. Security: grep -rn "presigned" frontend/src/components/documents/DocumentPreviewModal.vue — expect zero matches
3. Accessibility: grep -n "role=\"dialog\"\|aria-modal" frontend/src/components/sharing/ShareModal.vue frontend/src/components/folders/FolderDeleteModal.vue
4. Share IDOR prevention: grep -n "aria-label" frontend/src/components/documents/DocumentCard.vue — share button must have aria-label
5. Human checkpoint: all 11 scenarios verified by the developer in a running instance
- All 10 new component files exist
- AppSidebar has "Shared with me" (purple icon) and Folders section
- DocumentCard has share button (hover-reveal) and shared indicator pill
- DocumentPreviewModal uses proxy URL (/api/documents/{id}/content), not presigned URL
- AuditLogTab is accessible only inside AdminView (protected route)
- Developer approves all 11 manual verification checkpoints