--- phase: 10-ux-interaction reviewed: 2026-06-16T12:00:00Z depth: standard files_reviewed: 34 files_reviewed_list: - frontend/src/App.vue - frontend/src/__tests__/keyboard.test.js - frontend/src/components/documents/DocumentCard.vue - frontend/src/components/documents/SearchBar.vue - frontend/src/components/folders/FolderRow.vue - frontend/src/components/layout/AppSidebar.vue - frontend/src/components/layout/OsDragOverlay.vue - frontend/src/components/layout/__tests__/AppSidebar.empty.test.js - frontend/src/components/layout/__tests__/OsDragOverlay.test.js - frontend/src/components/storage/StorageBrowser.vue - frontend/src/components/storage/__tests__/StorageBrowser.dragmove.test.js - frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js - frontend/src/components/ui/AppIcon.vue - frontend/src/components/ui/BreadcrumbBar.vue - frontend/src/components/ui/EmptyState.vue - frontend/src/components/ui/ToastContainer.vue - frontend/src/components/ui/__tests__/AppIcon.test.js - frontend/src/components/ui/__tests__/BreadcrumbBar.test.js - frontend/src/components/ui/__tests__/EmptyState.test.js - frontend/src/components/ui/__tests__/ToastContainer.test.js - frontend/src/components/ui/__tests__/dropdown.test.js - frontend/src/components/upload/DropZone.vue - frontend/src/stores/__tests__/toast.test.js - frontend/src/stores/toast.js - frontend/src/views/FileManagerView.vue - frontend/src/views/admin/AdminAuditView.vue - frontend/src/views/admin/AdminUsersView.vue - frontend/src/views/admin/__tests__/AdminAuditView.skeleton.test.js - frontend/src/views/admin/__tests__/AdminUsersView.skeleton.test.js - frontend/src/views/SharedView.vue - frontend/src/views/CloudFolderView.vue - frontend/src/views/CloudStorageView.vue - frontend/src/components/sharing/ShareModal.vue - frontend/src/components/folders/FolderDeleteModal.vue findings: critical: 2 warning: 6 info: 4 total: 12 status: issues_found --- # Phase 10: Code Review Report **Reviewed:** 2026-06-16T12:00:00Z **Depth:** standard **Files Reviewed:** 34 **Status:** issues_found ## Summary Phase 10 delivers skeleton loading states, keyboard shortcuts (`/`, `U`, `N`, `Escape`), OS drag-and-drop overlay, drag-to-move documents, Teleport-based dropdown positioning, a toast notification system, and new shared UI components (`EmptyState`, `BreadcrumbBar`, `ToastContainer`, `AppIcon`). The implementation is well-structured: shared formatters are correctly imported in all reviewed views, event listeners are properly cleaned up in `onUnmounted`, and the Teleport+`getBoundingClientRect` pattern is consistent across components. Two blockers were found: a direct Vue prop mutation in `DocumentCard.vue` (raises a runtime warning and can silently fail to update the UI), and a keyboard shortcut bleed-through where pressing Escape while a modal is open fires both the modal's close handler and `clearSearch` simultaneously. Six warnings cover the `closest('.relative')` outside-click detector pattern (present in three files) that prevents pickers from closing correctly, a non-compliant Fisher-Yates shuffle in the admin password generator, an unused import, a custom local date formatter that violates the shared-module rule, an upload count metric that can be unreliable, and a leak of stale DOM references in `StorageBrowser`'s folder picker map. --- ## Critical Issues ### CR-01: Direct Vue prop mutation in DocumentCard raises runtime warning and can fail silently **File:** `frontend/src/components/documents/DocumentCard.vue:86` **Issue:** The `@unshared` event handler directly assigns `doc.is_shared = false` where `doc` is a component prop declared via `defineProps`. Vue 3 wraps props in a `readonly` proxy; writing to `doc.is_shared` triggers `[Vue warn]: Set operation on key "is_shared" failed: target is readonly` in development and silently does nothing in production builds that enforce the readonly constraint, leaving the card's "Shared" pill stale until the next full data fetch. ```html @unshared="doc.is_shared = false" ``` **Fix:** Emit the event upward and let the parent manage the mutation. `DocumentCard` already has an `emit` defined: ```js // DocumentCard.vue — add 'unshared' to defineEmits const emit = defineEmits(['reclassified', 'unshared']) ``` ```html ``` The parent `FileManagerView` already handles `@unshared` correctly by looking the document up in the Pinia store and updating store state there. --- ### CR-02: Escape key fires both modal-close and clearSearch simultaneously **File:** `frontend/src/App.vue:38-40` / `frontend/src/components/sharing/ShareModal.vue:143` / `frontend/src/components/folders/FolderDeleteModal.vue:73` **Issue:** Both `ShareModal` and `FolderDeleteModal` attach a `window` `keydown` listener that closes the modal on Escape. `App.vue` also attaches a `document` `keydown` listener calling `clearSearch` on Escape. The guard at `App.vue:31-32` only returns early when a form **input element** is focused. When a modal is open and no field inside it is active (e.g., immediately after `FolderDeleteModal` opens), the guard does not block, so `clearSearch` fires at the same time as the modal close — erasing any active search query unintentionally. As a secondary issue, the `U` and `N` keyboard shortcuts also fire when `FolderDeleteModal` is visible (no input focused), meaning a user could accidentally trigger an upload picker or new-folder inline input while staring at a destructive confirmation dialog. **Fix:** Check for an open `role="dialog"` element in `App.vue`'s handler before processing any shortcut: ```js // App.vue function onKeydown(e) { const tag = document.activeElement?.tagName if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return // Block all shortcuts when any modal is open if (document.querySelector('[role="dialog"]')) return if (e.key === '/' && !e.ctrlKey && !e.metaKey) { e.preventDefault() routeViewRef.value?.focusSearch?.() } if (e.key === 'Escape') { routeViewRef.value?.clearSearch?.() } if (e.key === 'u' || e.key === 'U') { routeViewRef.value?.triggerUpload?.() } if (e.key === 'n' || e.key === 'N') { routeViewRef.value?.startNewFolder?.() } } ``` The `document.querySelector('[role="dialog"]')` check leverages the existing `role="dialog"` attributes already present on both modal panels. --- ## Warnings ### WR-01: Outside-click detector uses `.closest('.relative')` — picker fails to close when clicking another card **File:** `frontend/src/components/documents/DocumentCard.vue:180-181` (same pattern in `frontend/src/components/folders/FolderRow.vue:179-180` and `frontend/src/components/storage/StorageBrowser.vue:431-432`) **Issue:** The `closeFolderPicker` / `handleOutsideClick` / `onOutsideClick` guards check `e.target.closest('.relative')` to decide whether a click was inside the trigger wrapper. The `DocumentCard` root element itself carries `class="group … relative"` (line 3). This means any click anywhere on any `DocumentCard` in the list matches `.closest('.relative')`, so an open folder-picker in one card will never close when the user clicks a sibling card. In practice, repeated clicks on different move buttons leave multiple visual states stale. `FolderRow`'s `.relative` wrapper (line 34) is narrower (only the three-dot button area), so that bug is less visible but has the same root cause. `StorageBrowser` has one `.relative` per Move button in file rows, which is also narrow but still fragile — any nested `div.relative` elsewhere breaks the assumption. **Fix:** Use a unique `data-*` attribute on the trigger wrapper and check containment against it: ```html