feat: add confirm dialog and tree delete UI on library page

Adds a reusable ConfirmDialog component and integrates tree deletion
into the TreeLibraryPage with permission-gated delete buttons and
a destructive confirmation dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-05 23:38:41 -05:00
parent 0dfee5cd36
commit 722e030ba6
2 changed files with 120 additions and 2 deletions

View File

@@ -0,0 +1,65 @@
import { Modal } from './Modal'
import { cn } from '@/lib/utils'
interface ConfirmDialogProps {
isOpen: boolean
onClose: () => void
onConfirm: () => void
title: string
message: string
confirmLabel?: string
confirmVariant?: 'destructive' | 'default'
isLoading?: boolean
}
export function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
message,
confirmLabel = 'Delete',
confirmVariant = 'destructive',
isLoading = false,
}: ConfirmDialogProps) {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="sm"
footer={
<div className="flex justify-end gap-3">
<button
onClick={onClose}
disabled={isLoading}
className={cn(
'rounded-md border border-border px-4 py-2 text-sm font-medium',
'text-card-foreground hover:bg-accent',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
Cancel
</button>
<button
onClick={onConfirm}
disabled={isLoading}
className={cn(
'rounded-md px-4 py-2 text-sm font-medium text-white',
'disabled:opacity-50 disabled:cursor-not-allowed',
confirmVariant === 'destructive'
? 'bg-destructive hover:bg-destructive/90'
: 'bg-primary hover:bg-primary/90'
)}
>
{isLoading ? 'Processing...' : confirmLabel}
</button>
</div>
}
>
<p className="text-sm text-muted-foreground">{message}</p>
</Modal>
)
}
export default ConfirmDialog

View File

@@ -1,17 +1,18 @@
import { useEffect, useState, useCallback } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { Plus, Pencil, Globe, Lock, X } from 'lucide-react'
import { Plus, Pencil, Globe, Lock, X, Trash2 } from 'lucide-react'
import { treesApi, categoriesApi, foldersApi } from '@/api'
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
import { TagBadges } from '@/components/common/TagBadges'
import { FolderSidebar } from '@/components/library/FolderSidebar'
import { FolderEditModal } from '@/components/library/FolderEditModal'
import { AddToFolderMenu } from '@/components/library/AddToFolderMenu'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { cn } from '@/lib/utils'
import { usePermissions } from '@/hooks/usePermissions'
export function TreeLibraryPage() {
const { canCreateTrees, canEditTree } = usePermissions()
const { canCreateTrees, canEditTree, canDeleteTree } = usePermissions()
const navigate = useNavigate()
const [trees, setTrees] = useState<TreeListItem[]>([])
const [categories, setCategories] = useState<CategoryListItem[]>([])
@@ -28,6 +29,11 @@ export function TreeLibraryPage() {
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
const [newFolderParentId, setNewFolderParentId] = useState<string | null>(null)
// Delete confirmation state
const [treeToDelete, setTreeToDelete] = useState<TreeListItem | null>(null)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)
const loadFolders = useCallback(async () => {
try {
const foldersData = await foldersApi.list()
@@ -122,6 +128,22 @@ export function TreeLibraryPage() {
setFolderModalOpen(true)
}
const handleDeleteTree = async () => {
if (!treeToDelete) return
setIsDeleting(true)
try {
await treesApi.delete(treeToDelete.id)
setTrees(trees.filter((t) => t.id !== treeToDelete.id))
} catch (err) {
console.error('Failed to delete tree:', err)
setError('Failed to delete tree')
} finally {
setIsDeleting(false)
setShowDeleteConfirm(false)
setTreeToDelete(null)
}
}
const hasActiveFilters =
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
@@ -322,6 +344,22 @@ export function TreeLibraryPage() {
<Pencil className="h-4 w-4" />
</Link>
)}
{canDeleteTree({ author_id: tree.author_id }) && (
<button
type="button"
onClick={() => {
setTreeToDelete(tree)
setShowDeleteConfirm(true)
}}
className={cn(
'rounded-md border border-input p-1.5 text-muted-foreground',
'hover:bg-destructive/10 hover:text-destructive'
)}
title="Delete tree"
>
<Trash2 className="h-4 w-4" />
</button>
)}
<button
type="button"
onClick={() => handleStartSession(tree.id)}
@@ -353,6 +391,21 @@ export function TreeLibraryPage() {
}}
onSave={loadData}
/>
{/* Delete Confirmation */}
<ConfirmDialog
isOpen={showDeleteConfirm}
onClose={() => {
setShowDeleteConfirm(false)
setTreeToDelete(null)
}}
onConfirm={handleDeleteTree}
title="Delete Tree"
message={`Are you sure you want to delete "${treeToDelete?.name}"? This action can be undone by an administrator.`}
confirmLabel="Delete"
confirmVariant="destructive"
isLoading={isDeleting}
/>
</div>
)
}