chore: archive v0.2 phase directories to milestones/v0.2-phases/
Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e008bf7dae
commit
123ae5b29b
@@ -0,0 +1,380 @@
|
||||
---
|
||||
phase: 10-ux-interaction
|
||||
plan: 11
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: [10-06, 10-09, 10-10, 10-05]
|
||||
files_modified:
|
||||
- frontend/src/components/storage/StorageBrowser.vue
|
||||
- frontend/src/components/documents/DocumentCard.vue
|
||||
- frontend/src/components/folders/FolderRow.vue
|
||||
- frontend/src/components/storage/__tests__/StorageBrowser.dragmove.test.js
|
||||
- frontend/src/components/ui/__tests__/dropdown.test.js
|
||||
autonomous: true
|
||||
requirements: [UX-11, UX-13]
|
||||
must_haves:
|
||||
truths:
|
||||
- "Drag-to-move document onto a folder applies the ring-2 ring-inset ring-amber-300 highlight while dragging (already wired in StorageBrowser - this plan verifies + adds click guard)"
|
||||
- "Dropping a document on a folder emits file-move which fires a success toast via the FileManagerView.doMove chain (toast already added in 10-06)"
|
||||
- "File-row click is suppressed when draggingFile is non-null (Pitfall 2 - prevents click-after-drag navigation)"
|
||||
- "StorageBrowser folder picker dropdown is teleported to body with getBoundingClientRect positioning"
|
||||
- "DocumentCard folder picker dropdown is teleported to body with getBoundingClientRect positioning"
|
||||
- "FolderRow three-dot menu is teleported to body with getBoundingClientRect positioning"
|
||||
- "All three teleported dropdowns reposition on window scroll"
|
||||
artifacts:
|
||||
- path: "frontend/src/components/storage/StorageBrowser.vue"
|
||||
provides: "Updated with click-guard for drag + Teleport-based folder picker"
|
||||
- path: "frontend/src/components/documents/DocumentCard.vue"
|
||||
provides: "Folder picker teleported"
|
||||
- path: "frontend/src/components/folders/FolderRow.vue"
|
||||
provides: "Three-dot menu teleported"
|
||||
key_links:
|
||||
- from: "StorageBrowser.vue file row @click"
|
||||
to: "draggingFile guard"
|
||||
via: "v-if/v-else or inline check"
|
||||
pattern: "draggingFile.*null"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Complete the drag-to-move flow (UX-11) and fix the three dropdown clipping risks (UX-13). The drag-to-move highlight + drop handlers are already in StorageBrowser.vue; the missing pieces are the click-after-drag guard (Pitfall 2) and verification of the toast emit (toast was wired in 10-06).
|
||||
|
||||
The dropdown fix uses the SearchableModelSelect.vue pattern: `<Teleport to="body">` + `getBoundingClientRect()` to compute fixed position, with a `window.addEventListener('scroll', updatePosition, true)` listener to reposition on scroll. Apply to:
|
||||
1. StorageBrowser folder picker (per-file move-to-folder dropdown)
|
||||
2. DocumentCard folder picker (move-to-folder dropdown on the card)
|
||||
3. FolderRow three-dot menu (rename/delete actions)
|
||||
|
||||
Output:
|
||||
- StorageBrowser file-row click guard + folder picker teleport
|
||||
- DocumentCard folder picker teleport
|
||||
- FolderRow three-dot menu teleport
|
||||
|
||||
**D-18 scope note (locked decision from CONTEXT.md):** D-18 specifies "dedicated drag handle element in DocumentCard.vue to prevent dragend-then-click navigation." DocumentCard.vue does NOT have `draggable="true"` set in Phase 10 — it is not a drag source in this phase. The dragend-then-click guard (D-18's protection goal) is implemented on StorageBrowser file rows, which ARE the drag sources. D-18 is fully satisfied for the elements that are actually draggable. DocumentCard drag capability, if added in a future phase, would need its own drag handle at that time. Document this conclusion in the SUMMARY.md.
|
||||
- Two stub test files (StorageBrowser.dragmove + dropdown) promoted to real tests
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@CLAUDE.md
|
||||
@.planning/phases/10-ux-interaction/10-CONTEXT.md
|
||||
@.planning/phases/10-ux-interaction/10-RESEARCH.md
|
||||
@.planning/phases/10-ux-interaction/10-PATTERNS.md
|
||||
@frontend/src/components/storage/StorageBrowser.vue
|
||||
@frontend/src/components/documents/DocumentCard.vue
|
||||
@frontend/src/components/folders/FolderRow.vue
|
||||
@frontend/src/components/ui/SearchableModelSelect.vue
|
||||
@frontend/src/views/FileManagerView.vue
|
||||
|
||||
<interfaces>
|
||||
**Drag-to-move click guard (Pitfall 2):**
|
||||
|
||||
File rows in StorageBrowser.vue have `@click="$emit('file-open', file)"` (current line ~107). After a drag (even one shorter than 4px) the browser fires `dragend` followed by `click` - causing the document to open accidentally after drag.
|
||||
|
||||
Fix:
|
||||
1. The existing onDropDocOnFolder already sets `draggingFile.value = null` inside its body, but the `dragend` event handler still needs to clear it for the case where the user releases on a non-folder area.
|
||||
2. Add an `@dragend="draggingFile = null"` handler to file rows.
|
||||
3. Guard the click: change `@click="$emit('file-open', file)"` to `@click="draggingFile ? null : $emit('file-open', file)"`.
|
||||
|
||||
Per RESEARCH.md, the existing onDropDocOnFolder already resets draggingFile -> null synchronously after emitting file-move; that means click WILL still fire with draggingFile=null. To reliably block click-after-drag, defer the reset using nextTick:
|
||||
|
||||
```js
|
||||
function onDropDocOnFolder(folderId) {
|
||||
if (!draggingFile.value) return
|
||||
const fileId = draggingFile.value.id
|
||||
const f = draggingFile.value
|
||||
dragOverFolderId.value = null
|
||||
emit('file-move', { fileId, folderId })
|
||||
nextTick(() => { draggingFile.value = null })
|
||||
}
|
||||
```
|
||||
|
||||
And on file row dragend:
|
||||
```html
|
||||
@dragend="onFileDragEnd"
|
||||
```
|
||||
with
|
||||
```js
|
||||
function onFileDragEnd() {
|
||||
nextTick(() => { draggingFile.value = null })
|
||||
}
|
||||
```
|
||||
|
||||
**Dropdown teleport pattern (per SearchableModelSelect.vue):**
|
||||
|
||||
```js
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const triggerEl = ref(null)
|
||||
const dropdownStyle = ref({})
|
||||
const isOpen = ref(false)
|
||||
|
||||
function updatePosition() {
|
||||
if (!triggerEl.value) return
|
||||
const rect = triggerEl.value.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const dropH = 240
|
||||
if (spaceBelow >= dropH || spaceBelow > 120) {
|
||||
dropdownStyle.value = {
|
||||
top: `${rect.bottom + 4}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: '192px',
|
||||
}
|
||||
} else {
|
||||
dropdownStyle.value = {
|
||||
bottom: `${window.innerHeight - rect.top + 4}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: '192px',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openDropdown() {
|
||||
updatePosition()
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
function onScroll() { if (isOpen.value) updatePosition() }
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll, true)
|
||||
window.addEventListener('resize', onScroll)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', onScroll, true)
|
||||
window.removeEventListener('resize', onScroll)
|
||||
})
|
||||
```
|
||||
|
||||
Template:
|
||||
```html
|
||||
<button ref="triggerEl" @click="openDropdown">...</button>
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" :style="dropdownStyle" class="fixed z-[9999] bg-white border border-gray-200 rounded-xl shadow-lg ...">
|
||||
...
|
||||
</div>
|
||||
</Teleport>
|
||||
```
|
||||
|
||||
Three targets:
|
||||
- StorageBrowser folder picker (lines ~196-213 of current file): the per-file `<button @click.stop="folderPickerFileId = ...">` + `<div v-if="folderPickerFileId === file.id">` dropdown.
|
||||
- DocumentCard folder picker (lines ~63-91 per RESEARCH.md): `<button @click.stop="toggleFolderPicker">` + `<div v-if="showFolderPicker">` dropdown.
|
||||
- FolderRow three-dot menu (lines ~37-66): `<button @click="toggleMenu">` + `<div v-if="menuOpen">` dropdown.
|
||||
|
||||
For all three: keep the existing close-on-outside-click logic (already implemented via `onOutsideClick` in StorageBrowser; verify each component has equivalent).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Promote StorageBrowser.dragmove + dropdown stubs to real tests</name>
|
||||
<files>frontend/src/components/storage/__tests__/StorageBrowser.dragmove.test.js, frontend/src/components/ui/__tests__/dropdown.test.js</files>
|
||||
<read_first>
|
||||
- Both stub files (Wave 0 outputs)
|
||||
- frontend/src/components/storage/StorageBrowser.vue (current state with drag handlers)
|
||||
- frontend/src/components/documents/DocumentCard.vue
|
||||
- frontend/src/components/folders/FolderRow.vue
|
||||
</read_first>
|
||||
<behavior>
|
||||
StorageBrowser.dragmove.test.js (6 tests):
|
||||
UX-11 group (4 tests):
|
||||
1. `dragOverFolderId applied to a folder row results in ring-2 ring-inset ring-amber-300 class` - mount StorageBrowser with folders=[{id:'f1', name:'A'}] and stub draggingFile via vm.$forceUpdate after setting wrapper.vm.draggingFile, find the folder row, assert classList includes 'ring-2', 'ring-inset', 'ring-amber-300', 'bg-amber-50'.
|
||||
2. `drop on folder emits file-move with { fileId, folderId }` - simulate dragstart on a file row (sets draggingFile), dispatch drop on a folder row, assert emitted('file-move')[0] === [{ fileId: '...', folderId: 'f1' }].
|
||||
3. `file-row click is suppressed while draggingFile is non-null` - set draggingFile manually, click a file row, assert NO file-open emitted.
|
||||
4. `dragend resets draggingFile after nextTick` - dispatch dragend, await nextTick, assert wrapper.vm.draggingFile === null.
|
||||
|
||||
UX-11 toast group (2 tests, integration with FileManagerView):
|
||||
5. `successful moveToFolder triggers useToastStore.show("Document moved", "success")` - already wired in 10-06; this test verifies the wiring still exists. Mount FileManagerView with a mocked docsStore.moveToFolder that resolves, call doMove via vm, assert toastStore.show called with 'Document moved' and 'success'.
|
||||
6. `failed moveToFolder triggers useToastStore.show("Move failed: ...", "error")` - same setup but moveToFolder rejects.
|
||||
|
||||
dropdown.test.js (4 tests):
|
||||
1. `DocumentCard folder picker uses Teleport to body` - mount DocumentCard, open the picker, assert `document.body.querySelector('[data-test="folder-picker"]')` is non-null (or use Teleport stub to verify).
|
||||
2. `DocumentCard folder picker position matches trigger getBoundingClientRect` - mock getBoundingClientRect to return a known rect, open picker, assert the picker style contains top/left matching the rect.
|
||||
3. `FolderRow three-dot menu uses Teleport to body` - same approach with FolderRow.
|
||||
4. `Window scroll while open recalculates position` - open the picker, mock getBoundingClientRect to return a different rect, dispatch window scroll event, assert the picker style updated.
|
||||
|
||||
Use happy-dom default. Stub child components where needed.
|
||||
</behavior>
|
||||
<action>
|
||||
Modify both test files. Replace .todo entries with the 10 tests above (6 dragmove + 4 dropdown). Tests fail until Tasks 2-4 implement.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd frontend && npm run test -- --run dragmove; cd frontend && npm run test -- --run dropdown</automated>
|
||||
Expected: 10 tests RED (some may currently pass if drag-to-move was wired in 10-06 - the dropdown tests must all fail).
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- StorageBrowser.dragmove.test.js contains 6 real `it(...)` blocks
|
||||
- dropdown.test.js contains 4 real `it(...)` blocks
|
||||
- No `.todo` entries remain in either file
|
||||
- Dropdown tests assert Teleport behavior (either via Teleport stub or by querying document.body)
|
||||
</acceptance_criteria>
|
||||
<done>10 tests in place describing UX-11 + UX-13 contracts.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add click-after-drag guard to StorageBrowser + teleport folder picker</name>
|
||||
<files>frontend/src/components/storage/StorageBrowser.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/storage/StorageBrowser.vue (current state - drag-to-move already wired)
|
||||
- frontend/src/components/ui/SearchableModelSelect.vue (Teleport pattern reference)
|
||||
- frontend/src/components/storage/__tests__/StorageBrowser.dragmove.test.js (failing tests)
|
||||
- .planning/phases/10-ux-interaction/10-PATTERNS.md (StorageBrowser drag + dropdown sections)
|
||||
</read_first>
|
||||
<action>
|
||||
Step A - Click-after-drag guard:
|
||||
1. Update the file-row `@click` (around line 107 of current file): change `@click="$emit('file-open', file)"` to `@click="draggingFile ? null : $emit('file-open', file)"`.
|
||||
2. Add `@dragend` handler on file rows that defers the reset:
|
||||
Add a new function `function onFileDragEnd() { nextTick(() => { draggingFile.value = null }) }` and bind `@dragend="onFileDragEnd"` on the file row.
|
||||
3. Modify `onDropDocOnFolder` so it also uses nextTick for the draggingFile reset:
|
||||
```js
|
||||
async function onDropDocOnFolder(folderId) {
|
||||
if (!draggingFile.value) return
|
||||
const fileId = draggingFile.value.id
|
||||
dragOverFolderId.value = null
|
||||
emit('file-move', { fileId, folderId })
|
||||
await nextTick()
|
||||
draggingFile.value = null
|
||||
}
|
||||
```
|
||||
`nextTick` is already imported at the top of the file.
|
||||
|
||||
Step B - Teleport the folder picker dropdown:
|
||||
The current picker (around lines 196-213) uses `<div class="relative"><button>...</button><div v-if="folderPickerFileId === file.id" class="absolute right-0 top-full mt-1 ...">`. Replace with:
|
||||
|
||||
1. Add per-file refs is impractical (folders are iterated). Use a single shared trigger ref + a single Teleport.
|
||||
2. Add new refs: `const pickerTriggerEl = ref(null)`, `const pickerStyle = ref({})`.
|
||||
3. Add a function `function openFolderPicker(fileId, ev) { folderPickerFileId.value = fileId; const trigger = ev.currentTarget; const rect = trigger.getBoundingClientRect(); updatePickerPosition(rect) }`.
|
||||
4. Add `function updatePickerPosition(rect) { const spaceBelow = window.innerHeight - rect.bottom; const dropH = 240; if (spaceBelow >= dropH || spaceBelow > 120) { pickerStyle.value = { top: `${rect.bottom + 4}px`, left: `${rect.left}px`, width: '192px' } } else { pickerStyle.value = { bottom: `${window.innerHeight - rect.top + 4}px`, left: `${rect.left}px`, width: '192px' } } }`.
|
||||
5. Update the move-button to use `@click.stop="openFolderPicker(file.id, $event)"` (replacing the inline state setter).
|
||||
6. Store the trigger button in a Map keyed by fileId (`const pickerTriggerMap = new Map()`) so reposition-on-scroll knows which trigger to recompute. On `openFolderPicker`, set `pickerTriggerMap.set(fileId, ev.currentTarget)`.
|
||||
7. Move the dropdown `<div>` OUT of the inline `<div class="relative">` and into a single `<Teleport to="body">` block at the end of the template:
|
||||
```html
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="folderPickerFileId"
|
||||
:style="pickerStyle"
|
||||
data-test="folder-picker"
|
||||
class="fixed z-[9999] bg-white border border-gray-200 rounded-xl shadow-lg overflow-y-auto py-1"
|
||||
style="max-height: 240px;"
|
||||
>
|
||||
<!-- existing dropdown content from the original block -->
|
||||
</div>
|
||||
</Teleport>
|
||||
```
|
||||
Reuse the existing list-of-folders markup verbatim - only the wrapping changes.
|
||||
8. Add a window scroll listener to keep the picker positioned:
|
||||
```js
|
||||
function onWindowScroll() {
|
||||
if (!folderPickerFileId.value) return
|
||||
const trig = pickerTriggerMap.get(folderPickerFileId.value)
|
||||
if (trig) updatePickerPosition(trig.getBoundingClientRect())
|
||||
}
|
||||
onMounted(() => { window.addEventListener('scroll', onWindowScroll, true); window.addEventListener('resize', onWindowScroll) })
|
||||
onUnmounted(() => { window.removeEventListener('scroll', onWindowScroll, true); window.removeEventListener('resize', onWindowScroll) })
|
||||
```
|
||||
9. Keep the existing outside-click handler (`onOutsideClick`) but update it to also close the picker when clicking outside both the trigger AND the teleported dropdown. Simplest: in onOutsideClick, check `if (!e.target.closest('[data-test="folder-picker"]') && !e.target.closest('.relative')) folderPickerFileId.value = null`. Or rely on the existing `.relative` check since the trigger button is still wrapped in `.relative` even if the dropdown is teleported.
|
||||
|
||||
Preserve all other StorageBrowser functionality untouched.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd frontend && npm run test -- --run dragmove; cd frontend && npm run test -- --run StorageBrowser</automated>
|
||||
Expected: 4 UX-11 dragmove tests pass; existing StorageBrowser suite unaffected.
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -E "draggingFile\\s*\\?\\s*null\\s*:\\s*\\$emit\\('file-open'" frontend/src/components/storage/StorageBrowser.vue` returns 1 (click guard in place)
|
||||
- `grep -E "function onFileDragEnd" frontend/src/components/storage/StorageBrowser.vue` returns 1
|
||||
- `grep -E "@dragend=\"onFileDragEnd\"" frontend/src/components/storage/StorageBrowser.vue` returns 1
|
||||
- `grep -E "await nextTick\\(\\)" frontend/src/components/storage/StorageBrowser.vue` returns >= 1 (in onDropDocOnFolder)
|
||||
- `grep -E "<Teleport to=\"body\">" frontend/src/components/storage/StorageBrowser.vue` returns >= 1
|
||||
- `grep -E "data-test=\"folder-picker\"" frontend/src/components/storage/StorageBrowser.vue` returns 1
|
||||
- `grep -E "getBoundingClientRect" frontend/src/components/storage/StorageBrowser.vue` returns >= 1
|
||||
- `grep -E "addEventListener\\('scroll'" frontend/src/components/storage/StorageBrowser.vue` returns 1
|
||||
- 4 UX-11 dragmove tests pass
|
||||
- Existing StorageBrowser test suite passes
|
||||
</acceptance_criteria>
|
||||
<done>Click guard + teleport in place; UX-11 dragmove tests green.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Teleport DocumentCard folder picker</name>
|
||||
<files>frontend/src/components/documents/DocumentCard.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/documents/DocumentCard.vue (current state - has folder picker absolute-positioned)
|
||||
- frontend/src/components/ui/SearchableModelSelect.vue (Teleport reference)
|
||||
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"DocumentCard.vue + FolderRow.vue"
|
||||
</read_first>
|
||||
<action>
|
||||
Apply the same Teleport pattern to the DocumentCard folder picker:
|
||||
1. Add refs: `pickerTriggerEl`, `pickerStyle`. Add `updatePosition()` helper using getBoundingClientRect.
|
||||
2. Change `toggleFolderPicker()` to capture the trigger element and recompute position before setting `showFolderPicker = true`.
|
||||
3. Wrap the picker dropdown in `<Teleport to="body">` with `class="fixed z-[9999]"` and `:style="pickerStyle"`. Add `data-test="folder-picker"`.
|
||||
4. Add window scroll + resize listeners that call updatePosition when open.
|
||||
5. Add window scroll + resize listener cleanup in onUnmounted.
|
||||
6. Ensure outside-click closes the picker (existing logic should still work if it checks for the trigger button's wrapper class).
|
||||
|
||||
Reuse the exact dropdown markup; only the wrapping changes.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd frontend && npm run test -- --run dropdown</automated>
|
||||
Expected: 2 DocumentCard-related tests pass (Teleport + position).
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -E "<Teleport to=\"body\">" frontend/src/components/documents/DocumentCard.vue` returns 1
|
||||
- `grep -E "getBoundingClientRect" frontend/src/components/documents/DocumentCard.vue` returns >= 1
|
||||
- `grep -E "data-test=\"folder-picker\"" frontend/src/components/documents/DocumentCard.vue` returns 1
|
||||
- `grep -E "addEventListener\\('scroll'" frontend/src/components/documents/DocumentCard.vue` returns 1
|
||||
- 2 DocumentCard-related dropdown tests pass
|
||||
</acceptance_criteria>
|
||||
<done>DocumentCard folder picker teleported.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Teleport FolderRow three-dot menu</name>
|
||||
<files>frontend/src/components/folders/FolderRow.vue</files>
|
||||
<read_first>
|
||||
- frontend/src/components/folders/FolderRow.vue (current state - has three-dot menu absolute-positioned, lines ~37-66)
|
||||
- frontend/src/components/ui/SearchableModelSelect.vue (Teleport reference)
|
||||
- .planning/phases/10-ux-interaction/10-PATTERNS.md §"DocumentCard.vue + FolderRow.vue"
|
||||
</read_first>
|
||||
<action>
|
||||
Apply the same Teleport pattern to FolderRow's three-dot menu:
|
||||
1. Add refs: `menuTriggerEl`, `menuStyle`. Add `updatePosition()` helper using getBoundingClientRect.
|
||||
2. Change `toggleMenu()` to capture the trigger element and recompute position before flipping `menuOpen`.
|
||||
3. Wrap the menu `<div>` in `<Teleport to="body">` with `class="fixed z-[9999]"` and `:style="menuStyle"`. Add `data-test="folder-row-menu"`.
|
||||
4. Add window scroll + resize listeners.
|
||||
5. Outside-click logic preserved or updated.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd frontend && npm run test -- --run dropdown; cd frontend && npm run test -- --run</automated>
|
||||
Expected: all 4 dropdown tests pass + full suite green.
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -E "<Teleport to=\"body\">" frontend/src/components/folders/FolderRow.vue` returns 1
|
||||
- `grep -E "getBoundingClientRect" frontend/src/components/folders/FolderRow.vue` returns >= 1
|
||||
- `grep -E "data-test=\"folder-row-menu\"" frontend/src/components/folders/FolderRow.vue` returns 1
|
||||
- `grep -E "addEventListener\\('scroll'" frontend/src/components/folders/FolderRow.vue` returns 1
|
||||
- All 4 dropdown tests pass
|
||||
- Full test suite passes
|
||||
</acceptance_criteria>
|
||||
<done>FolderRow three-dot menu teleported; UX-13 fully closed.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `cd frontend && npm run test -- --run dragmove` exits 0
|
||||
- `cd frontend && npm run test -- --run dropdown` exits 0
|
||||
- `cd frontend && npm run test -- --run` (full suite) exits 0
|
||||
- StorageBrowser, DocumentCard, FolderRow each contain `<Teleport to="body">`
|
||||
- StorageBrowser file-row click guard present (grep on draggingFile + file-open)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
Drag a document onto a folder row -> folder row gets the amber ring -> release -> document moves and a "Document moved" toast appears -> clicking the same file row does NOT navigate (the drag suppressed the click). Opening the move-to-folder dropdown on any file (in StorageBrowser or DocumentCard) shows a properly positioned dropdown that does not clip at the viewport edge and follows the trigger when the page scrolls. Same for FolderRow three-dot menu.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/10-ux-interaction/10-11-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user