chore: merge executor worktree (10-10 OS drag overlay)
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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
<OsDragOverlay @files-dropped="onOsFilesDropped" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -15,12 +16,17 @@ import { useRoute } from 'vue-router'
|
|||||||
import AppSidebar from './components/layout/AppSidebar.vue'
|
import AppSidebar from './components/layout/AppSidebar.vue'
|
||||||
import AuthLayout from './layouts/AuthLayout.vue'
|
import AuthLayout from './layouts/AuthLayout.vue'
|
||||||
import ToastContainer from './components/ui/ToastContainer.vue'
|
import ToastContainer from './components/ui/ToastContainer.vue'
|
||||||
|
import OsDragOverlay from './components/layout/OsDragOverlay.vue'
|
||||||
import { useTopicsStore } from './stores/topics.js'
|
import { useTopicsStore } from './stores/topics.js'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const topicsStore = useTopicsStore()
|
const topicsStore = useTopicsStore()
|
||||||
const routeViewRef = ref(null)
|
const routeViewRef = ref(null)
|
||||||
|
|
||||||
|
function onOsFilesDropped(files) {
|
||||||
|
routeViewRef.value?.handleOsDrop?.(files)
|
||||||
|
}
|
||||||
|
|
||||||
function onKeydown(e) {
|
function onKeydown(e) {
|
||||||
const tag = document.activeElement?.tagName
|
const tag = document.activeElement?.tagName
|
||||||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
|
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tag) || document.activeElement?.isContentEditable) return
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="showOverlay"
|
||||||
|
class="fixed inset-0 z-[9998] bg-indigo-900/40 flex items-center justify-center pointer-events-none"
|
||||||
|
data-test="os-drag-overlay"
|
||||||
|
>
|
||||||
|
<div class="bg-white rounded-2xl px-10 py-8 text-center shadow-xl pointer-events-none">
|
||||||
|
<AppIcon name="upload" class="w-10 h-10 text-indigo-400 mx-auto mb-3" />
|
||||||
|
<p class="text-base font-semibold text-gray-800">Drop files to upload</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AppIcon from '../ui/AppIcon.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OsDragOverlay',
|
||||||
|
components: { AppIcon },
|
||||||
|
emits: ['files-dropped'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dragDepth: 0,
|
||||||
|
showOverlay: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDragEnter(e) {
|
||||||
|
if (!e.dataTransfer?.types.includes('Files')) return
|
||||||
|
this.dragDepth++
|
||||||
|
this.showOverlay = true
|
||||||
|
},
|
||||||
|
onDragLeave() {
|
||||||
|
this.dragDepth = Math.max(0, this.dragDepth - 1)
|
||||||
|
if (this.dragDepth === 0) this.showOverlay = false
|
||||||
|
},
|
||||||
|
onDragOver(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
},
|
||||||
|
onDrop(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.dragDepth = 0
|
||||||
|
this.showOverlay = false
|
||||||
|
const files = Array.from(e.dataTransfer.files)
|
||||||
|
if (files.length) this.$emit('files-dropped', files)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('dragenter', this.onDragEnter)
|
||||||
|
window.addEventListener('dragleave', this.onDragLeave)
|
||||||
|
window.addEventListener('dragover', this.onDragOver)
|
||||||
|
window.addEventListener('drop', this.onDrop)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('dragenter', this.onDragEnter)
|
||||||
|
window.removeEventListener('dragleave', this.onDragLeave)
|
||||||
|
window.removeEventListener('dragover', this.onDragOver)
|
||||||
|
window.removeEventListener('drop', this.onDrop)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,158 @@
|
|||||||
import { describe, it } from 'vitest'
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import OsDragOverlay from '../OsDragOverlay.vue'
|
||||||
|
|
||||||
|
function makeDragEvent(type, types = ['Files'], files = []) {
|
||||||
|
const event = new Event(type, { bubbles: true, cancelable: true })
|
||||||
|
Object.defineProperty(event, 'dataTransfer', {
|
||||||
|
value: { types, files, preventDefault: vi.fn() },
|
||||||
|
writable: false,
|
||||||
|
})
|
||||||
|
event.preventDefault = vi.fn()
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
describe('UX-09: OsDragOverlay shows when OS files are dragged over the window', () => {
|
describe('UX-09: OsDragOverlay shows when OS files are dragged over the window', () => {
|
||||||
it.todo('overlay hidden by default (dragDepth=0)')
|
it('overlay hidden by default (dragDepth=0)', () => {
|
||||||
it.todo('dragenter with dataTransfer.types including "Files" shows overlay')
|
const w = mount(OsDragOverlay, {
|
||||||
it.todo('dragenter without "Files" type is ignored (in-app element drag)')
|
attachTo: document.body,
|
||||||
it.todo('dragleave decrements depth; overlay hides when depth reaches 0')
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
it.todo('drop event emits files-dropped with the file list')
|
})
|
||||||
it.todo('drop resets dragDepth to 0 and hides overlay')
|
expect(w.vm.showOverlay).toBe(false)
|
||||||
it.todo('overlay is teleported to body with z-[9998] (below toast z-[9999])')
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dragenter with dataTransfer.types including "Files" shows overlay (dragDepth=1)', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
const event = makeDragEvent('dragenter', ['Files'])
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.showOverlay).toBe(true)
|
||||||
|
expect(w.vm.dragDepth).toBe(1)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dragenter without "Files" type is ignored (in-app element drag)', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
const event = makeDragEvent('dragenter', ['text/plain'])
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.showOverlay).toBe(false)
|
||||||
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dragleave decrements depth; overlay hides when depth reaches 0', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(1)
|
||||||
|
expect(w.vm.showOverlay).toBe(true)
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('dragleave'))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
expect(w.vm.showOverlay).toBe(false)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('nested dragenter+dragleave maintains overlay until depth=0', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(2)
|
||||||
|
expect(w.vm.showOverlay).toBe(true)
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('dragleave'))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(1)
|
||||||
|
expect(w.vm.showOverlay).toBe(true)
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('dragleave'))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
expect(w.vm.showOverlay).toBe(false)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drop event emits files-dropped with the file list', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
|
||||||
|
const file = new File(['x'], 'a.txt', { type: 'text/plain' })
|
||||||
|
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
||||||
|
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||||
|
value: { files: [file] },
|
||||||
|
writable: false,
|
||||||
|
})
|
||||||
|
dropEvent.preventDefault = vi.fn()
|
||||||
|
window.dispatchEvent(dropEvent)
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
|
||||||
|
const emitted = w.emitted('files-dropped')
|
||||||
|
expect(emitted).toBeTruthy()
|
||||||
|
expect(emitted[0][0]).toHaveLength(1)
|
||||||
|
expect(emitted[0][0][0]).toBeInstanceOf(File)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('drop resets dragDepth to 0 and hides overlay', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
global: { stubs: { Teleport: true, AppIcon: true } },
|
||||||
|
})
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(3)
|
||||||
|
|
||||||
|
const dropEvent = new Event('drop', { bubbles: true, cancelable: true })
|
||||||
|
Object.defineProperty(dropEvent, 'dataTransfer', {
|
||||||
|
value: { files: [] },
|
||||||
|
writable: false,
|
||||||
|
})
|
||||||
|
dropEvent.preventDefault = vi.fn()
|
||||||
|
window.dispatchEvent(dropEvent)
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
|
||||||
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
expect(w.vm.showOverlay).toBe(false)
|
||||||
|
|
||||||
|
window.dispatchEvent(new Event('dragleave'))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
expect(w.vm.dragDepth).toBe(0)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overlay is teleported to body with z-[9998] (below toast z-[9999])', async () => {
|
||||||
|
const w = mount(OsDragOverlay, {
|
||||||
|
attachTo: document.body,
|
||||||
|
})
|
||||||
|
window.dispatchEvent(makeDragEvent('dragenter', ['Files']))
|
||||||
|
await w.vm.$nextTick()
|
||||||
|
|
||||||
|
const overlay = document.querySelector('[data-test="os-drag-overlay"]')
|
||||||
|
expect(overlay).toBeTruthy()
|
||||||
|
expect(overlay.classList.contains('z-[9998]')).toBe(true)
|
||||||
|
w.unmount()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -186,5 +186,6 @@ defineExpose({
|
|||||||
triggerUpload: () => browserRef.value?.triggerUpload?.(),
|
triggerUpload: () => browserRef.value?.triggerUpload?.(),
|
||||||
startNewFolder: () => browserRef.value?.startNewFolder?.(),
|
startNewFolder: () => browserRef.value?.startNewFolder?.(),
|
||||||
clearSearch: () => browserRef.value?.clearSearch?.(),
|
clearSearch: () => browserRef.value?.clearSearch?.(),
|
||||||
|
handleOsDrop: (files) => onFilesSelected({ files, autoClassify: true }),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user