From 37f49bc6eae050628b66eaf5e374d7e232da752c Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 16 Jun 2026 10:13:37 +0200 Subject: [PATCH] docs(10): add code review report Co-Authored-By: Claude Sonnet 4.6 --- .../phases/10-ux-interaction/10-REVIEW.md | 573 +++++++----------- 1 file changed, 215 insertions(+), 358 deletions(-) diff --git a/.planning/phases/10-ux-interaction/10-REVIEW.md b/.planning/phases/10-ux-interaction/10-REVIEW.md index b351941..cde8c97 100644 --- a/.planning/phases/10-ux-interaction/10-REVIEW.md +++ b/.planning/phases/10-ux-interaction/10-REVIEW.md @@ -1,15 +1,11 @@ --- phase: 10-ux-interaction -reviewed: 2026-06-16T00:00:00Z +reviewed: 2026-06-16T12:00:00Z depth: standard -files_reviewed: 45 +files_reviewed: 34 files_reviewed_list: - frontend/src/App.vue - frontend/src/__tests__/keyboard.test.js - - frontend/src/components/auth/TotpEnrollment.vue - - frontend/src/components/cloud/CloudCredentialModal.vue - - frontend/src/components/cloud/CloudFolderTreeItem.vue - - frontend/src/components/cloud/CloudProviderTreeItem.vue - frontend/src/components/documents/DocumentCard.vue - frontend/src/components/documents/SearchBar.vue - frontend/src/components/folders/FolderRow.vue @@ -17,15 +13,12 @@ files_reviewed_list: - 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/settings/SettingsAccountTab.vue - - frontend/src/components/settings/SettingsCloudTab.vue - 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/SearchableModelSelect.vue - frontend/src/components/ui/ToastContainer.vue - frontend/src/components/ui/__tests__/AppIcon.test.js - frontend/src/components/ui/__tests__/BreadcrumbBar.test.js @@ -35,314 +28,139 @@ files_reviewed_list: - frontend/src/components/upload/DropZone.vue - frontend/src/stores/__tests__/toast.test.js - frontend/src/stores/toast.js - - frontend/src/views/AccountView.vue - - frontend/src/views/CloudFolderView.vue - - frontend/src/views/CloudStorageView.vue - - frontend/src/views/DocumentView.vue - frontend/src/views/FileManagerView.vue - - frontend/src/views/SettingsView.vue - - frontend/src/views/SharedView.vue - - frontend/src/views/__tests__/FileManagerView.test.js - - frontend/src/views/admin/AdminAiView.vue - frontend/src/views/admin/AdminAuditView.vue - - frontend/src/views/admin/AdminOverviewView.vue - - frontend/src/views/admin/AdminQuotasView.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: 4 - warning: 9 + critical: 2 + warning: 6 info: 4 - total: 17 + total: 12 status: issues_found --- # Phase 10: Code Review Report -**Reviewed:** 2026-06-16T00:00:00Z +**Reviewed:** 2026-06-16T12:00:00Z **Depth:** standard -**Files Reviewed:** 45 +**Files Reviewed:** 34 **Status:** issues_found ## Summary -This phase delivered 10+ UX interaction plans covering keyboard shortcuts, skeleton loading, drag-and-drop, OS-level file drag overlay, toast notifications, Teleport-based dropdowns, and breadcrumb/empty-state polish. The overall architecture is sound and the CLAUDE.md shared-module rules are consistently followed. However, several correctness bugs, one security gap, and multiple quality issues were found. +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. -The most critical problems are: (1) a duplicate, diverged implementation of the entire account settings screen split between `AccountView.vue` and `SettingsAccountTab.vue`; (2) `closeFolderPicker` in `DocumentCard.vue` closing on any click inside any `.relative` element across the entire page — not only its own trigger; (3) global `window.addEventListener` calls in `SearchableModelSelect.vue` are registered outside `onMounted`, so they run during SSR or module evaluation in test environments and are never cleaned up when multiple instances mount and unmount; and (4) the `generateRandomPassword` Fisher-Yates shuffle in `AdminUsersView.vue` reuses only 4 bytes for shuffling a 16-character array, which produces a weak shuffle distribution. +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: Duplicate account settings implementation — diverged behaviour +### CR-01: Direct Vue prop mutation in DocumentCard raises runtime warning and can fail silently -**File:** `frontend/src/views/AccountView.vue:1` and `frontend/src/components/settings/SettingsAccountTab.vue:1` +**File:** `frontend/src/components/documents/DocumentCard.vue:86` -**Issue:** Both files implement the identical Account settings UI (account info, TOTP, change password, sign-out all). They have already diverged: `SettingsAccountTab` calls `useToastStore` and shows toasts when sessions are revoked after TOTP disable or password change (lines 154–156, 239–241); `AccountView` does neither. The CLAUDE.md rule "things that look the same are the same in code" and "no dead code — files with no active route and no active import are deleted immediately" is violated. Whichever is currently routed, the other leaks functionality and will continue to diverge. +**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. -**Fix:** Determine which is the canonical implementation (should be `SettingsAccountTab` because it is the smart component under `SettingsView`). If `AccountView.vue` has an active route pointing directly to it, that route must be removed and the component deleted. If `AccountView.vue` is already orphaned, delete it immediately. Do not keep both alive. +```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: `closeFolderPicker` in `DocumentCard.vue` closes on any `.relative` element — not just its own trigger +### CR-02: Escape key fires both modal-close and clearSearch simultaneously -**File:** `frontend/src/components/documents/DocumentCard.vue:179-184` +**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: -**Issue:** ```js -function closeFolderPicker(e) { - if ( - !e.target.closest('[data-test="folder-picker"]') && - !e.target.closest('.relative') // ← matches ANY .relative on the page - ) { - showFolderPicker.value = false +// 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 guard `e.target.closest('.relative')` matches any ancestor `div.relative` on the page, not only the move-button container inside this card. This means clicking any other component that wraps its content in a `div class="relative"` — including `SearchableModelSelect`, `StorageBrowser`'s own picker trigger, or any other card — will prevent this picker from closing. In practice, clicking a sibling `DocumentCard`'s move button will open both pickers simultaneously and neither will close the other. The same pattern also exists in `FolderRow.vue:178-183` with identical semantics. -**Fix:** Use a `ref` on the trigger element and check it directly: -```js -// Store a ref to the wrapper div: -//
-const moveWrapperRef = ref(null) - -function closeFolderPicker(e) { - if ( - !e.target.closest('[data-test="folder-picker"]') && - !(moveWrapperRef.value && moveWrapperRef.value.contains(e.target)) - ) { - showFolderPicker.value = false - } -} -``` -Apply the same fix to `FolderRow.vue`'s `handleOutsideClick`. - ---- - -### CR-03: Global `window.addEventListener` called at module/setup scope in `SearchableModelSelect.vue` - -**File:** `frontend/src/components/ui/SearchableModelSelect.vue:268-273` - -**Issue:** -```js -window.addEventListener('scroll', onScroll, true) -window.addEventListener('resize', onScroll) -onUnmounted(() => { - window.removeEventListener('scroll', onScroll, true) - window.removeEventListener('resize', onScroll) -}) -``` -The `addEventListener` calls are at the top level of `