Skip to content

Commit 9ca8913

Browse files
committed
feat: delete individual repos via three-dot menu + confirmation dialog (OPE-159)
Each repo card now has a three-dot menu (visible on hover) with 'Delete repository'. Clicking shows confirmation dialog: 'This will permanently remove [name] and all its indexed data. This action cannot be undone.' Flow: hover card -> click ... -> click Delete -> confirm -> DELETE /repos/{id} - Toast notification on success/failure - Repo list refreshes automatically - If deleted repo was selected, clears selection Props chain: DashboardHome (handler) -> RepoListView -> RepoList -> RepoCard Closes OPE-159
1 parent 744b002 commit 9ca8913

3 files changed

Lines changed: 94 additions & 5 deletions

File tree

frontend/src/components/RepoList.tsx

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import { useState, useRef, useMemo } from 'react'
22
import { motion } from 'framer-motion'
3-
import { FolderGit2, Plus, Files, FunctionSquare, Clock } from 'lucide-react'
3+
import { FolderGit2, Plus, Files, FunctionSquare, Clock, MoreVertical, Trash2 } from 'lucide-react'
44
import { cn } from '@/lib/utils'
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuTrigger,
10+
} from './ui/dropdown-menu'
11+
import {
12+
Dialog,
13+
DialogContent,
14+
DialogDescription,
15+
DialogFooter,
16+
DialogHeader,
17+
DialogTitle,
18+
} from './ui/dialog'
19+
import { Button } from './ui/button'
520
import type { Repository } from '../types'
621
import { RepoGridSkeleton } from './ui/Skeleton'
722

823
interface RepoListProps {
924
repos: Repository[]
1025
selectedRepo: string | null
1126
onSelect: (repoId: string) => void
27+
onDelete?: (repoId: string) => void
1228
onAddClick?: () => void
1329
loading?: boolean
1430
}
@@ -60,10 +76,11 @@ const StatusDot = ({ status }: { status: string }) => {
6076
)
6177
}
6278

63-
const RepoCard = ({ repo, index, onSelect }: {
79+
const RepoCard = ({ repo, index, onSelect, onDeleteClick }: {
6480
repo: Repository
6581
index: number
6682
onSelect: () => void
83+
onDeleteClick?: () => void
6784
}) => {
6885
const cardRef = useRef<HTMLButtonElement>(null)
6986
const [mousePos, setMousePos] = useState({ x: 0, y: 0 })
@@ -100,12 +117,35 @@ const RepoCard = ({ repo, index, onSelect }: {
100117
)}
101118

102119
<div className="relative">
103-
{/* Top row: icon + status */}
120+
{/* Top row: icon + status + menu */}
104121
<div className="flex items-start justify-between mb-3">
105122
<div className="w-10 h-10 rounded-xl bg-primary/10 border border-primary/20 flex items-center justify-center group-hover:bg-primary/15 transition-colors">
106123
<FolderGit2 className="w-5 h-5 text-primary" />
107124
</div>
108-
<StatusDot status={repo.status} />
125+
<div className="flex items-center gap-1">
126+
<StatusDot status={repo.status} />
127+
{onDeleteClick && (
128+
<DropdownMenu>
129+
<DropdownMenuTrigger asChild>
130+
<button
131+
onClick={(e) => e.stopPropagation()}
132+
className="w-6 h-6 flex items-center justify-center rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors opacity-0 group-hover:opacity-100"
133+
>
134+
<MoreVertical className="w-3.5 h-3.5" />
135+
</button>
136+
</DropdownMenuTrigger>
137+
<DropdownMenuContent align="end">
138+
<DropdownMenuItem
139+
onClick={(e) => { e.stopPropagation(); onDeleteClick() }}
140+
className="text-destructive focus:text-destructive"
141+
>
142+
<Trash2 className="w-3.5 h-3.5 mr-2" />
143+
Delete repository
144+
</DropdownMenuItem>
145+
</DropdownMenuContent>
146+
</DropdownMenu>
147+
)}
148+
</div>
109149
</div>
110150

111151
{/* Repo name + slug */}
@@ -158,8 +198,9 @@ const SortTab = ({ label, active, onClick }: {
158198
</button>
159199
)
160200

161-
export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }: RepoListProps) {
201+
export function RepoList({ repos, selectedRepo, onSelect, onDelete, onAddClick, loading }: RepoListProps) {
162202
const [sortMode, setSortMode] = useState<SortMode>('recent')
203+
const [deleteTarget, setDeleteTarget] = useState<Repository | null>(null)
163204

164205
const sortedRepos = useMemo(() => {
165206
const sorted = [...repos]
@@ -222,9 +263,36 @@ export function RepoList({ repos, selectedRepo, onSelect, onAddClick, loading }:
222263
repo={repo}
223264
index={index}
224265
onSelect={() => onSelect(repo.id)}
266+
onDeleteClick={onDelete ? () => setDeleteTarget(repo) : undefined}
225267
/>
226268
))}
227269
</div>
270+
271+
{/* Delete confirmation dialog */}
272+
<Dialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
273+
<DialogContent>
274+
<DialogHeader>
275+
<DialogTitle>Delete repository</DialogTitle>
276+
<DialogDescription>
277+
This will permanently remove <strong>{deleteTarget?.name}</strong> and all its indexed data. This action cannot be undone.
278+
</DialogDescription>
279+
</DialogHeader>
280+
<DialogFooter className="gap-2">
281+
<Button variant="outline" onClick={() => setDeleteTarget(null)}>Cancel</Button>
282+
<Button
283+
variant="destructive"
284+
onClick={() => {
285+
if (deleteTarget && onDelete) {
286+
onDelete(deleteTarget.id)
287+
setDeleteTarget(null)
288+
}
289+
}}
290+
>
291+
Delete
292+
</Button>
293+
</DialogFooter>
294+
</DialogContent>
295+
</Dialog>
228296
</div>
229297
)
230298
}

frontend/src/components/dashboard/DashboardHome.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ export function DashboardHome() {
212212
}
213213

214214
const selectedRepoData = repos.find((r) => r.id === selectedRepo)
215+
const handleDeleteRepo = async (repoId: string) => {
216+
try {
217+
const response = await fetch(`${API_URL}/repos/${repoId}`, {
218+
method: 'DELETE',
219+
headers: { Authorization: `Bearer ${session?.access_token}` },
220+
})
221+
if (!response.ok) throw new Error('Failed to delete')
222+
toast.success('Repository deleted')
223+
refreshRepos()
224+
if (selectedRepo === repoId) setSelectedRepo(null)
225+
} catch (error) {
226+
toast.error('Failed to delete repository', {
227+
description: error instanceof Error ? error.message : 'Please try again',
228+
})
229+
}
230+
}
231+
215232
const isRepoView = selectedRepo && selectedRepoData
216233

217234
return (
@@ -226,6 +243,7 @@ export function DashboardHome() {
226243
selectedRepo={selectedRepo}
227244
maxRepos={maxRepos}
228245
onSelectRepo={(id) => { setSelectedRepo(id); setActiveTab('overview') }}
246+
onDeleteRepo={handleDeleteRepo}
229247
onAddClick={() => setShowAddForm(true)}
230248
onGitHubClick={() => setShowGitHubSelector(true)}
231249
/>

frontend/src/components/dashboard/RepoListView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface RepoListViewProps {
1515
selectedRepo: string | null
1616
maxRepos: number
1717
onSelectRepo: (id: string) => void
18+
onDeleteRepo: (id: string) => void
1819
onAddClick: () => void
1920
onGitHubClick: () => void
2021
}
@@ -26,6 +27,7 @@ export function RepoListView({
2627
selectedRepo,
2728
maxRepos,
2829
onSelectRepo,
30+
onDeleteRepo,
2931
onAddClick,
3032
onGitHubClick,
3133
}: RepoListViewProps) {
@@ -74,6 +76,7 @@ export function RepoListView({
7476
selectedRepo={selectedRepo}
7577
loading={reposLoading}
7678
onSelect={onSelectRepo}
79+
onDelete={onDeleteRepo}
7780
onAddClick={onAddClick}
7881
/>
7982
</motion.div>

0 commit comments

Comments
 (0)