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:
curo1305
2026-06-17 07:56:00 +02:00
parent 6e3d1f866a
commit a928b54781
3 changed files with 6 additions and 264 deletions
+5
View File
@@ -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()
})
})