docs(10-10): complete OS drag overlay plan — OsDragOverlay.vue + App.vue wiring + 8 tests
UX-09 fully implemented: depth-counter drag detection, Teleport overlay, FileManagerView.handleOsDrop wired through App.vue routeViewRef. 198/198 tests passing, 0 regressions.
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
phase: 10-ux-interaction
|
||||||
|
plan: "10"
|
||||||
|
subsystem: frontend/ux
|
||||||
|
tags: [os-drag, overlay, depth-counter, teleport, vitest, tdd, ux-09, wave-3]
|
||||||
|
dependency_graph:
|
||||||
|
requires: [10-04, 10-05, 10-06, 10-09]
|
||||||
|
provides: [UX-09-os-drag-overlay, OsDragOverlay-component, App.vue-osDrop-wiring, FileManagerView-handleOsDrop]
|
||||||
|
affects: [App.vue, FileManagerView.vue, OsDragOverlay.vue, OsDragOverlay.test.js]
|
||||||
|
tech_stack:
|
||||||
|
added: []
|
||||||
|
patterns: [depth-counter-drag-detection, teleport-to-body, window-event-listeners, options-api-component, tdd-red-green]
|
||||||
|
key_files:
|
||||||
|
created:
|
||||||
|
- frontend/src/components/layout/OsDragOverlay.vue
|
||||||
|
modified:
|
||||||
|
- frontend/src/components/layout/__tests__/OsDragOverlay.test.js
|
||||||
|
- frontend/src/App.vue
|
||||||
|
- frontend/src/views/FileManagerView.vue
|
||||||
|
decisions:
|
||||||
|
- "Depth-counter pattern (dragDepth++) used instead of boolean flag to prevent flicker when cursor crosses child element boundaries"
|
||||||
|
- "dataTransfer.types.includes('Files') guard ensures in-app drags (file rows, folder rows) do not trigger the overlay"
|
||||||
|
- "OsDragOverlay placed after ToastContainer in App.vue template, maintaining z-[9998] < z-[9999] z-order invariant"
|
||||||
|
- "handleOsDrop delegates to existing onFilesSelected rather than duplicating upload logic — quota, progress, toast wiring all reused"
|
||||||
|
- "node_modules symlinked/installed in worktree frontend to make vitest available without polluting main checkout"
|
||||||
|
metrics:
|
||||||
|
duration_minutes: 6
|
||||||
|
completed_date: "2026-06-15T18:46:33Z"
|
||||||
|
tasks_completed: 3
|
||||||
|
tasks_total: 3
|
||||||
|
files_created: 1
|
||||||
|
files_modified: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 10 Plan 10: OS File Drag Overlay Summary
|
||||||
|
|
||||||
|
**One-liner:** Full-screen OS drag overlay (OsDragOverlay.vue) wired end-to-end via depth-counter pattern: window dragenter/drop events in App.vue route through routeViewRef to FileManagerView.handleOsDrop which calls the existing upload flow.
|
||||||
|
|
||||||
|
## Tasks Completed
|
||||||
|
|
||||||
|
| Task | Name | Commit | Files |
|
||||||
|
|------|------|--------|-------|
|
||||||
|
| 1 | Promote OsDragOverlay stubs to 8 RED failing tests | 71f55b8 | `layout/__tests__/OsDragOverlay.test.js` |
|
||||||
|
| 2 | Implement OsDragOverlay.vue (GREEN) | 20eceb8 | `layout/OsDragOverlay.vue` |
|
||||||
|
| 3 | Mount in App.vue; expose handleOsDrop in FileManagerView | 69bf40a | `App.vue`, `FileManagerView.vue` |
|
||||||
|
|
||||||
|
## What Was Built
|
||||||
|
|
||||||
|
### Task 1: RED Tests
|
||||||
|
|
||||||
|
Replaced 7 `it.todo` stubs in `OsDragOverlay.test.js` with 8 real tests. The plan specified 8 tests while the stub file had 7 entries; the extra test covers "nested dragenter+dragleave" (depth-2 scenario, critical for the depth-counter correctness). Tests use synthetic DragEvent / Event dispatched on `window` with `Object.defineProperty` to attach fake `dataTransfer`. All 8 were RED before Task 2.
|
||||||
|
|
||||||
|
### Task 2: OsDragOverlay.vue
|
||||||
|
|
||||||
|
Options API component implementing UX-09:
|
||||||
|
|
||||||
|
- `data`: `dragDepth: 0`, `showOverlay: false`
|
||||||
|
- `onDragEnter`: ignores events where `e.dataTransfer?.types.includes('Files')` is false (in-app drags); increments `dragDepth` and sets `showOverlay = true`
|
||||||
|
- `onDragLeave`: decrements via `Math.max(0, dragDepth - 1)`; hides when depth reaches 0
|
||||||
|
- `onDragOver`: calls `e.preventDefault()` (required for drop to fire)
|
||||||
|
- `onDrop`: resets depth=0, hides overlay, emits `files-dropped` with `Array.from(e.dataTransfer.files)`
|
||||||
|
- `mounted()`/`beforeUnmount()`: register/remove 4 window listeners
|
||||||
|
- Template: `<Teleport to="body">` + `<Transition name="fade">` + overlay div with `z-[9998]` and `data-test="os-drag-overlay"`
|
||||||
|
- Scoped CSS: `.fade-enter-active/.fade-leave-active` with `transition: opacity 0.15s ease`
|
||||||
|
|
||||||
|
### Task 3: Wiring
|
||||||
|
|
||||||
|
**App.vue:**
|
||||||
|
- Added `import OsDragOverlay from './components/layout/OsDragOverlay.vue'`
|
||||||
|
- Added `<OsDragOverlay @files-dropped="onOsFilesDropped" />` immediately after `<ToastContainer />` (line 10 vs line 9)
|
||||||
|
- Added `function onOsFilesDropped(files) { routeViewRef.value?.handleOsDrop?.(files) }` — double optional chain: no-ops on non-file-manager routes where `handleOsDrop` is not exposed
|
||||||
|
|
||||||
|
**FileManagerView.vue:**
|
||||||
|
- Extended `defineExpose` block with `handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true })`
|
||||||
|
- No duplicate upload logic — existing `onFilesSelected` function handles per-file UploadProgress items, quota error detection (e.status 413), and the summary toast
|
||||||
|
|
||||||
|
## Verification Results
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|-------|--------|
|
||||||
|
| `vitest run OsDragOverlay` — 8 tests | PASS (all GREEN) |
|
||||||
|
| `vitest run keyboard` — 9 tests | PASS (no regression) |
|
||||||
|
| `vitest run FileManagerView` — 20 tests | PASS (no regression) |
|
||||||
|
| Full suite — 198 tests, 13 todo | PASS (0 failures) |
|
||||||
|
| OsDragOverlay placed AFTER ToastContainer | PASS (line 10 vs line 9) |
|
||||||
|
| OsDragOverlay z-[9998] < ToastContainer z-[9999] | PASS |
|
||||||
|
| FileManagerView.handleOsDrop reuses onFilesSelected | PASS |
|
||||||
|
| `routeViewRef.value?.handleOsDrop?.(files)` in App.vue | PASS |
|
||||||
|
| No duplicate upload logic | PASS |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 3 - Blocking] node_modules not available in git worktree**
|
||||||
|
- **Found during:** Task 1
|
||||||
|
- **Issue:** The worktree has no `node_modules` directory; `npm run test` (using the global vitest) and the main repo's vitest binary both fail to resolve imports when the test root points at the worktree
|
||||||
|
- **Fix:** Ran `npm install` in the worktree's `frontend/` directory to create a local `node_modules` matching the `package.json`. Tests then run correctly with `worktree/frontend/node_modules/.bin/vitest`
|
||||||
|
- **Files modified:** none (runtime setup only)
|
||||||
|
- **Commit:** none (npm install not committed — `node_modules` is gitignored)
|
||||||
|
|
||||||
|
## Known Stubs
|
||||||
|
|
||||||
|
None. All files created and modified in this plan have complete implementations with no placeholder values, TODO markers, or hardcoded empty data flowing to the UI.
|
||||||
|
|
||||||
|
## Threat Flags
|
||||||
|
|
||||||
|
None. OsDragOverlay handles OS file drop at the browser level and delegates to the existing `onFilesSelected` → `docsStore.upload` path. No new network endpoints, auth paths, or schema changes are introduced. The existing upload path has quota enforcement, JWT auth headers, and error handling already in place.
|
||||||
|
|
||||||
|
## TDD Gate Compliance
|
||||||
|
|
||||||
|
- RED gate: commit `71f55b8` — 8 failing tests (`test(10-10): promote OsDragOverlay stubs...`)
|
||||||
|
- GREEN gate: commit `20eceb8` — implementation passes 8 tests (`feat(10-10): implement OsDragOverlay.vue...`)
|
||||||
|
- REFACTOR gate: not required (no cleanup needed after GREEN)
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
- `frontend/src/components/layout/OsDragOverlay.vue` — exists, contains `name: 'OsDragOverlay'`, `emits: ['files-dropped']`, `dragDepth: 0`, `dataTransfer?.types.includes('Files')`, 4 window listeners, `<Teleport to="body">`, `z-[9998]`, no `<script setup>`
|
||||||
|
- `frontend/src/App.vue` — contains `import OsDragOverlay`, `<OsDragOverlay @files-dropped="onOsFilesDropped" />` after `<ToastContainer />`, `onOsFilesDropped` handler, `routeViewRef.value?.handleOsDrop?.(files)`
|
||||||
|
- `frontend/src/views/FileManagerView.vue` — `handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true })` in defineExpose
|
||||||
|
- Commit 71f55b8 — confirmed in git log
|
||||||
|
- Commit 20eceb8 — confirmed in git log
|
||||||
|
- Commit 69bf40a — confirmed in git log
|
||||||
|
- No unexpected file deletions in any commit
|
||||||
Reference in New Issue
Block a user