Files
sbd2-frontend/src/pages/ExtractionsPage.tsx
JSC 6a40311a82
Some checks failed
Frontend CI / lint (push) Failing after 18s
Frontend CI / build (push) Has been skipped
feat: add extraction deletion functionality with confirmation dialog and update extraction list on deletion
2025-08-25 21:40:47 +02:00

249 lines
7.6 KiB
TypeScript

import { AppLayout } from '@/components/AppLayout'
import { AppPagination } from '@/components/AppPagination'
import { CreateExtractionDialog } from '@/components/extractions/CreateExtractionDialog'
import { ExtractionsHeader } from '@/components/extractions/ExtractionsHeader'
import {
ExtractionsEmpty,
ExtractionsError,
ExtractionsLoading,
} from '@/components/extractions/ExtractionsLoadingStates'
import { ExtractionsTable } from '@/components/extractions/ExtractionsTable'
import {
type ExtractionInfo,
type ExtractionSortField,
type ExtractionSortOrder,
type ExtractionStatus,
extractionsService,
} from '@/lib/api/services/extractions'
import { EXTRACTION_EVENTS, extractionEvents } from '@/lib/events'
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
export function ExtractionsPage() {
const [extractions, setExtractions] = useState<ExtractionInfo[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Search, sorting, and filtering state
const [searchQuery, setSearchQuery] = useState('')
const [sortBy, setSortBy] = useState<ExtractionSortField>('created_at')
const [sortOrder, setSortOrder] = useState<ExtractionSortOrder>('desc')
const [statusFilter, setStatusFilter] = useState<ExtractionStatus | 'all'>('all')
// Pagination state
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalCount, setTotalCount] = useState(0)
const [pageSize, setPageSize] = useState(10)
// Create extraction dialog state
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [createLoading, setCreateLoading] = useState(false)
const [url, setUrl] = useState('')
// Debounce search query
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(searchQuery)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearchQuery(searchQuery)
}, 300)
return () => clearTimeout(handler)
}, [searchQuery])
const fetchExtractions = useCallback(async () => {
try {
setLoading(true)
setError(null)
const response = await extractionsService.getAllExtractions({
search: debouncedSearchQuery.trim() || undefined,
sort_by: sortBy,
sort_order: sortOrder,
status_filter: statusFilter !== 'all' ? statusFilter : undefined,
page: currentPage,
limit: pageSize,
})
setExtractions(response.extractions)
setTotalPages(response.total_pages)
setTotalCount(response.total)
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to fetch extractions'
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, currentPage, pageSize])
useEffect(() => {
fetchExtractions()
}, [fetchExtractions])
// Reset to page 1 when filters change
useEffect(() => {
if (currentPage !== 1) {
setCurrentPage(1)
}
}, [debouncedSearchQuery, sortBy, sortOrder, statusFilter, pageSize])
// Listen for extraction events to refresh the list
useEffect(() => {
const handleExtractionStatusUpdate = () => {
fetchExtractions()
}
const handleExtractionCompleted = () => {
fetchExtractions()
}
const handleExtractionFailed = () => {
fetchExtractions()
}
// Subscribe to extraction events
extractionEvents.on(EXTRACTION_EVENTS.EXTRACTION_STATUS_UPDATED, handleExtractionStatusUpdate)
extractionEvents.on(EXTRACTION_EVENTS.EXTRACTION_COMPLETED, handleExtractionCompleted)
extractionEvents.on(EXTRACTION_EVENTS.EXTRACTION_FAILED, handleExtractionFailed)
return () => {
// Cleanup event listeners
extractionEvents.off(EXTRACTION_EVENTS.EXTRACTION_STATUS_UPDATED, handleExtractionStatusUpdate)
extractionEvents.off(EXTRACTION_EVENTS.EXTRACTION_COMPLETED, handleExtractionCompleted)
extractionEvents.off(EXTRACTION_EVENTS.EXTRACTION_FAILED, handleExtractionFailed)
}
}, [fetchExtractions])
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
const handlePageSizeChange = (size: number) => {
setPageSize(size)
setCurrentPage(1) // Reset to first page when changing page size
}
const handleCreateExtraction = async () => {
if (!url.trim()) {
toast.error('Please enter a URL')
return
}
try {
setCreateLoading(true)
const response = await extractionsService.createExtraction(url.trim())
toast.success(response.message)
// Reset form and close dialog
setUrl('')
setShowCreateDialog(false)
// Emit event for new extraction created
extractionEvents.emit(EXTRACTION_EVENTS.EXTRACTION_CREATED, response.extraction)
// Refresh the extractions list
fetchExtractions()
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to create extraction'
toast.error(errorMessage)
} finally {
setCreateLoading(false)
}
}
const handleCancelCreate = () => {
setUrl('')
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 = () => {
if (loading) {
return <ExtractionsLoading />
}
if (error) {
return <ExtractionsError error={error} onRetry={fetchExtractions} />
}
if (extractions.length === 0) {
return <ExtractionsEmpty searchQuery={searchQuery} statusFilter={statusFilter} />
}
return (
<div className="space-y-4">
<ExtractionsTable
extractions={extractions}
onExtractionDeleted={handleExtractionDeleted}
/>
<AppPagination
currentPage={currentPage}
totalPages={totalPages}
totalCount={totalCount}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
itemName="extractions"
/>
</div>
)
}
return (
<AppLayout
breadcrumb={{
items: [{ label: 'Dashboard', href: '/' }, { label: 'Extractions' }],
}}
>
<div className="flex-1 rounded-xl bg-muted/50 p-4">
<ExtractionsHeader
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
sortBy={sortBy}
onSortByChange={setSortBy}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
onRefresh={fetchExtractions}
onCreateClick={() => setShowCreateDialog(true)}
loading={loading}
error={error}
extractionCount={totalCount}
/>
<CreateExtractionDialog
open={showCreateDialog}
onOpenChange={setShowCreateDialog}
loading={createLoading}
url={url}
onUrlChange={setUrl}
onSubmit={handleCreateExtraction}
onCancel={handleCancelCreate}
/>
{renderContent()}
</div>
</AppLayout>
)
}