chore(11-06): dead-code cleanup — delete FolderRow.vue, retain admin tab tests
Task 2 — admin test classification:
- AdminAiConfigTab.test.js: RETAINED (tests AdminAiView.vue, a live component)
- AdminQuotasTab.test.js: RETAINED (tests AdminQuotasView.vue, a live component)
- AdminUsersTab.test.js: RETAINED (tests AdminUsersView.vue, a live component)
Task 3 — confirmed absent: HomeView.vue, FolderView.vue, AdminView.vue all absent
Task 4 — dead-code scan:
- FolderRow.vue: DELETED — no import in any live component; StorageBrowser renders
folder rows inline; FolderRow had no active route or active import (CLAUDE.md rule)
Task 5 — stale test removal:
- dropdown.test.js: removed 2 FolderRow tests (tested dead component);
kept 2 DocumentCard Teleport tests (protect live surface)
- No unused named imports found in live components
268/268 tests pass.
This commit is contained in:
@@ -29,5 +29,10 @@
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^6.4.3",
|
||||
"vitest": "^4.1.7"
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild@0.25.12": true,
|
||||
"vue-demi@0.14.10": true,
|
||||
"fsevents@2.3.3": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-3 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer"
|
||||
@click="handleNavigate"
|
||||
>
|
||||
<!-- Folder icon -->
|
||||
<div class="w-9 h-9 rounded-lg bg-gray-100 flex items-center justify-center shrink-0">
|
||||
<AppIcon name="folder" class="w-5 h-5 text-gray-500" />
|
||||
</div>
|
||||
|
||||
<!-- Name and doc count -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Rename mode: inline input -->
|
||||
<div v-if="renaming" @click.stop>
|
||||
<input
|
||||
ref="renameInputRef"
|
||||
v-model="renameValue"
|
||||
type="text"
|
||||
class="border border-gray-300 rounded-lg px-2 py-1 text-sm w-full focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
@keydown.enter="submitRename"
|
||||
@keydown.escape="cancelRename"
|
||||
@click.stop
|
||||
/>
|
||||
<p v-if="renameError" class="text-red-500 text-xs mt-1">{{ renameError }}</p>
|
||||
</div>
|
||||
<!-- Normal display mode -->
|
||||
<template v-else>
|
||||
<p class="font-medium text-gray-900 text-sm truncate">{{ folder.name }}</p>
|
||||
<p class="text-xs text-gray-400 mt-0.5">{{ folder.doc_count ?? 0 }} document{{ (folder.doc_count ?? 0) !== 1 ? 's' : '' }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Three-dot menu -->
|
||||
<div class="relative shrink-0" @click.stop>
|
||||
<button
|
||||
ref="menuTriggerEl"
|
||||
@click="toggleMenu"
|
||||
aria-label="Folder actions"
|
||||
class="p-1.5 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors min-h-[36px] min-w-[36px] flex items-center justify-center"
|
||||
>
|
||||
<AppIcon name="dots" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teleported three-dot menu — avoids overflow:hidden clipping -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="menuOpen"
|
||||
:style="menuStyle"
|
||||
data-test="folder-row-menu"
|
||||
class="fixed z-[9999] bg-white border border-gray-200 rounded-lg shadow-md py-1 min-w-[140px]"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
@click="startRename"
|
||||
class="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
@click="handleDelete"
|
||||
class="w-full text-left px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors"
|
||||
>
|
||||
Delete folder
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import AppIcon from '../ui/AppIcon.vue'
|
||||
|
||||
const props = defineProps({
|
||||
folder: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
onNavigate: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onRename: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
onDelete: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const menuTriggerEl = ref(null)
|
||||
const menuStyle = ref({})
|
||||
const renaming = ref(false)
|
||||
const renameValue = ref('')
|
||||
const renameError = ref('')
|
||||
const renameInputRef = ref(null)
|
||||
|
||||
function handleNavigate() {
|
||||
if (renaming.value) return
|
||||
if (props.onNavigate) props.onNavigate(props.folder.id)
|
||||
}
|
||||
|
||||
function updateMenuPosition() {
|
||||
if (!menuTriggerEl.value) return
|
||||
const rect = menuTriggerEl.value.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const dropH = 100 // approximate height of 2-item menu
|
||||
if (spaceBelow >= dropH || spaceBelow > 60) {
|
||||
menuStyle.value = {
|
||||
top: `${rect.bottom + 4}px`,
|
||||
left: `${Math.max(0, rect.right - 160)}px`,
|
||||
minWidth: '140px',
|
||||
}
|
||||
} else {
|
||||
menuStyle.value = {
|
||||
bottom: `${window.innerHeight - rect.top + 4}px`,
|
||||
left: `${Math.max(0, rect.right - 160)}px`,
|
||||
minWidth: '140px',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
if (menuOpen.value) {
|
||||
menuOpen.value = false
|
||||
} else {
|
||||
updateMenuPosition()
|
||||
menuOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
function startRename() {
|
||||
closeMenu()
|
||||
renameValue.value = props.folder.name
|
||||
renameError.value = ''
|
||||
renaming.value = true
|
||||
nextTick(() => {
|
||||
renameInputRef.value?.focus()
|
||||
renameInputRef.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
function cancelRename() {
|
||||
renaming.value = false
|
||||
renameError.value = ''
|
||||
}
|
||||
|
||||
async function submitRename() {
|
||||
const trimmed = renameValue.value.trim()
|
||||
if (!trimmed) {
|
||||
renameError.value = 'Folder name cannot be empty.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (props.onRename) await props.onRename(props.folder.id, trimmed)
|
||||
renaming.value = false
|
||||
renameError.value = ''
|
||||
} catch (e) {
|
||||
renameError.value = e.message || 'Failed to rename folder.'
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
closeMenu()
|
||||
if (props.onDelete) props.onDelete(props.folder)
|
||||
}
|
||||
|
||||
// Close menu on outside click
|
||||
function handleOutsideClick(e) {
|
||||
if (
|
||||
!e.target.closest('[data-test="folder-row-menu"]') &&
|
||||
!e.target.closest('.relative')
|
||||
) {
|
||||
closeMenu()
|
||||
}
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (menuOpen.value) updateMenuPosition()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
window.addEventListener('scroll', onScroll, true)
|
||||
window.addEventListener('resize', onScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleOutsideClick)
|
||||
window.removeEventListener('scroll', onScroll, true)
|
||||
window.removeEventListener('resize', onScroll)
|
||||
})
|
||||
</script>
|
||||
@@ -1,9 +1,8 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick } from 'vue'
|
||||
import DocumentCard from '../../documents/DocumentCard.vue'
|
||||
import FolderRow from '../../folders/FolderRow.vue'
|
||||
|
||||
// ── Stubs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -24,12 +23,6 @@ const SAMPLE_DOC = {
|
||||
is_shared: false,
|
||||
}
|
||||
|
||||
const SAMPLE_FOLDER = {
|
||||
id: 'folder-1',
|
||||
name: 'Archive',
|
||||
doc_count: 5,
|
||||
}
|
||||
|
||||
describe('UX-13: Teleport + getBoundingClientRect dropdown pattern across components', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
@@ -96,59 +89,4 @@ describe('UX-13: Teleport + getBoundingClientRect dropdown pattern across compon
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('FolderRow three-dot menu uses Teleport to body', async () => {
|
||||
const wrapper = mount(FolderRow, {
|
||||
props: { folder: SAMPLE_FOLDER },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const menuBtn = wrapper.find('[aria-label="Folder actions"]')
|
||||
expect(menuBtn.exists()).toBe(true)
|
||||
await menuBtn.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// The menu should be teleported to body
|
||||
const menu = document.body.querySelector('[data-test="folder-row-menu"]')
|
||||
expect(menu).not.toBeNull()
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('FolderRow three-dot menu repositions on window scroll', async () => {
|
||||
const wrapper = mount(FolderRow, {
|
||||
props: { folder: SAMPLE_FOLDER },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
const menuBtn = wrapper.find('[aria-label="Folder actions"]')
|
||||
await menuBtn.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
const menu = document.body.querySelector('[data-test="folder-row-menu"]')
|
||||
expect(menu).not.toBeNull()
|
||||
const initialTop = menu.style.top || menu.style.bottom
|
||||
|
||||
// Change what getBoundingClientRect returns (simulate scroll)
|
||||
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({
|
||||
top: 200,
|
||||
bottom: 240,
|
||||
left: 50,
|
||||
right: 250,
|
||||
width: 200,
|
||||
height: 40,
|
||||
x: 50,
|
||||
y: 200,
|
||||
})
|
||||
|
||||
// Dispatch scroll event
|
||||
window.dispatchEvent(new Event('scroll', { bubbles: true }))
|
||||
await nextTick()
|
||||
|
||||
const updatedTop = menu.style.top || menu.style.bottom
|
||||
// Position should have updated (top changed from 140+4 to 240+4)
|
||||
expect(updatedTop).not.toBe(initialTop)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user