feat: add extraction deletion functionality with confirmation dialog and update extraction list on deletion
This commit is contained in:
@@ -1,7 +1,15 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
import { TableCell, TableRow } from '@/components/ui/table'
|
import { TableCell, TableRow } from '@/components/ui/table'
|
||||||
import type { ExtractionInfo } from '@/lib/api/services/extractions'
|
import { extractionsService, type ExtractionInfo } from '@/lib/api/services/extractions'
|
||||||
import { formatDateDistanceToNow } from '@/utils/format-date'
|
import { formatDateDistanceToNow } from '@/utils/format-date'
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -10,14 +18,44 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Trash2,
|
||||||
User
|
User
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
interface ExtractionsRowProps {
|
interface ExtractionsRowProps {
|
||||||
extraction: ExtractionInfo
|
extraction: ExtractionInfo
|
||||||
|
onExtractionDeleted?: (extractionId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExtractionsRow({ extraction }: ExtractionsRowProps) {
|
export function ExtractionsRow({ extraction, onExtractionDeleted }: ExtractionsRowProps) {
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleDeleteExtraction = async () => {
|
||||||
|
if (!extraction.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setDeleteLoading(true)
|
||||||
|
const response = await extractionsService.deleteExtraction(extraction.id)
|
||||||
|
toast.success(response.message)
|
||||||
|
|
||||||
|
// Close dialog
|
||||||
|
setShowDeleteDialog(false)
|
||||||
|
|
||||||
|
// Notify parent component
|
||||||
|
if (onExtractionDeleted) {
|
||||||
|
onExtractionDeleted(extraction.id)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Failed to delete extraction'
|
||||||
|
toast.error(errorMessage)
|
||||||
|
} finally {
|
||||||
|
setDeleteLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
const getStatusBadge = (status: ExtractionInfo['status']) => {
|
const getStatusBadge = (status: ExtractionInfo['status']) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
@@ -75,6 +113,7 @@ export function ExtractionsRow({ extraction }: ExtractionsRowProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<TableRow className="hover:bg-muted/50">
|
<TableRow className="hover:bg-muted/50">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -128,8 +167,64 @@ export function ExtractionsRow({ extraction }: ExtractionsRowProps) {
|
|||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowDeleteDialog(true)}
|
||||||
|
title="Delete extraction"
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
|
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete Extraction</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this extraction? This action cannot be undone.
|
||||||
|
{extraction.title && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<strong>Title:</strong> {extraction.title}
|
||||||
|
<br />
|
||||||
|
<strong>URL:</strong> {extraction.url}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowDeleteDialog(false)}
|
||||||
|
disabled={deleteLoading}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDeleteExtraction}
|
||||||
|
disabled={deleteLoading}
|
||||||
|
>
|
||||||
|
{deleteLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Deleting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,10 @@ import type { ExtractionInfo } from '@/lib/api/services/extractions'
|
|||||||
|
|
||||||
interface ExtractionsTableProps {
|
interface ExtractionsTableProps {
|
||||||
extractions: ExtractionInfo[]
|
extractions: ExtractionInfo[]
|
||||||
|
onExtractionDeleted?: (extractionId: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExtractionsTable({ extractions }: ExtractionsTableProps) {
|
export function ExtractionsTable({ extractions, onExtractionDeleted }: ExtractionsTableProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -31,6 +32,7 @@ export function ExtractionsTable({ extractions }: ExtractionsTableProps) {
|
|||||||
<ExtractionsRow
|
<ExtractionsRow
|
||||||
key={extraction.id}
|
key={extraction.id}
|
||||||
extraction={extraction}
|
extraction={extraction}
|
||||||
|
onExtractionDeleted={onExtractionDeleted}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export interface GetExtractionsParams {
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteExtractionResponse {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
export class ExtractionsService {
|
export class ExtractionsService {
|
||||||
/**
|
/**
|
||||||
* Create a new extraction job
|
* Create a new extraction job
|
||||||
@@ -135,6 +139,16 @@ export class ExtractionsService {
|
|||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an extraction
|
||||||
|
*/
|
||||||
|
async deleteExtraction(extractionId: number): Promise<DeleteExtractionResponse> {
|
||||||
|
const response = await apiClient.delete<DeleteExtractionResponse>(
|
||||||
|
`/api/v1/extractions/${extractionId}`
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const extractionsService = new ExtractionsService()
|
export const extractionsService = new ExtractionsService()
|
||||||
|
|||||||
@@ -159,6 +159,23 @@ export function ExtractionsPage() {
|
|||||||
setShowCreateDialog(false)
|
setShowCreateDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExtractionDeleted = (extractionId: number) => {
|
||||||
|
// Remove the deleted extraction from the current list
|
||||||
|
setExtractions(prev => prev.filter(extraction => extraction.id !== extractionId))
|
||||||
|
|
||||||
|
// Update total count
|
||||||
|
setTotalCount(prev => prev - 1)
|
||||||
|
|
||||||
|
// If current page is now empty and not the first page, go to previous page
|
||||||
|
const remainingOnCurrentPage = extractions.length - 1
|
||||||
|
if (remainingOnCurrentPage === 0 && currentPage > 1) {
|
||||||
|
setCurrentPage(currentPage - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the full list to ensure accuracy
|
||||||
|
fetchExtractions()
|
||||||
|
}
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ExtractionsLoading />
|
return <ExtractionsLoading />
|
||||||
@@ -174,7 +191,10 @@ export function ExtractionsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<ExtractionsTable extractions={extractions} />
|
<ExtractionsTable
|
||||||
|
extractions={extractions}
|
||||||
|
onExtractionDeleted={handleExtractionDeleted}
|
||||||
|
/>
|
||||||
<AppPagination
|
<AppPagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
|
|||||||
Reference in New Issue
Block a user