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>
21 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 10-ux-interaction | 11 | execute | 4 |
|
|
true |
|
|
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:
- StorageBrowser folder picker (per-file move-to-folder dropdown)
- DocumentCard folder picker (move-to-folder dropdown on the card)
- 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
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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 **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:
- The existing onDropDocOnFolder already sets
draggingFile.value = nullinside its body, but thedragendevent handler still needs to clear it for the case where the user releases on a non-folder area. - Add an
@dragend="draggingFile = null"handler to file rows. - 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:
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:
@dragend="onFileDragEnd"
with
function onFileDragEnd() {
nextTick(() => { draggingFile.value = null })
}
Dropdown teleport pattern (per SearchableModelSelect.vue):
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:
<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).
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.
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.
Reuse the exact dropdown markup; only the wrapping changes.
<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>
Create `.planning/phases/10-ux-interaction/10-11-SUMMARY.md` when done.