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:
65
frontend/src/components/common/ConfirmDialog.tsx
Normal file
65
frontend/src/components/common/ConfirmDialog.tsx
Normal 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
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react'
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
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 { treesApi, categoriesApi, foldersApi } from '@/api'
|
||||||
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
|
import type { TreeListItem, CategoryListItem, FolderListItem } from '@/types'
|
||||||
import { TagBadges } from '@/components/common/TagBadges'
|
import { TagBadges } from '@/components/common/TagBadges'
|
||||||
import { FolderSidebar } from '@/components/library/FolderSidebar'
|
import { FolderSidebar } from '@/components/library/FolderSidebar'
|
||||||
import { FolderEditModal } from '@/components/library/FolderEditModal'
|
import { FolderEditModal } from '@/components/library/FolderEditModal'
|
||||||
import { AddToFolderMenu } from '@/components/library/AddToFolderMenu'
|
import { AddToFolderMenu } from '@/components/library/AddToFolderMenu'
|
||||||
|
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
|
|
||||||
export function TreeLibraryPage() {
|
export function TreeLibraryPage() {
|
||||||
const { canCreateTrees, canEditTree } = usePermissions()
|
const { canCreateTrees, canEditTree, canDeleteTree } = usePermissions()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [trees, setTrees] = useState<TreeListItem[]>([])
|
const [trees, setTrees] = useState<TreeListItem[]>([])
|
||||||
const [categories, setCategories] = useState<CategoryListItem[]>([])
|
const [categories, setCategories] = useState<CategoryListItem[]>([])
|
||||||
@@ -28,6 +29,11 @@ export function TreeLibraryPage() {
|
|||||||
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
|
const [editingFolder, setEditingFolder] = useState<FolderListItem | null>(null)
|
||||||
const [newFolderParentId, setNewFolderParentId] = useState<string | 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 () => {
|
const loadFolders = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const foldersData = await foldersApi.list()
|
const foldersData = await foldersApi.list()
|
||||||
@@ -122,6 +128,22 @@ export function TreeLibraryPage() {
|
|||||||
setFolderModalOpen(true)
|
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 =
|
const hasActiveFilters =
|
||||||
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
|
selectedCategoryId || selectedTags.length > 0 || searchQuery || selectedFolderId
|
||||||
|
|
||||||
@@ -322,6 +344,22 @@ export function TreeLibraryPage() {
|
|||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Link>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleStartSession(tree.id)}
|
onClick={() => handleStartSession(tree.id)}
|
||||||
@@ -353,6 +391,21 @@ export function TreeLibraryPage() {
|
|||||||
}}
|
}}
|
||||||
onSave={loadData}
|
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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user