WIP
This commit is contained in:
@@ -4,11 +4,13 @@ import type {
|
||||
DocumentDetailResponse,
|
||||
DocumentItem,
|
||||
UploadDocumentResponse,
|
||||
DocumentCategoriesResponse,
|
||||
} from '../types'
|
||||
|
||||
export const documentsApi = {
|
||||
list: async (params?: {
|
||||
status?: string
|
||||
category?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<DocumentListResponse> => {
|
||||
@@ -16,18 +18,29 @@ export const documentsApi = {
|
||||
return data
|
||||
},
|
||||
|
||||
getCategories: async (): Promise<DocumentCategoriesResponse> => {
|
||||
const { data } = await apiClient.get('/api/v1/admin/documents/categories')
|
||||
return data
|
||||
},
|
||||
|
||||
getDetail: async (documentId: string): Promise<DocumentDetailResponse> => {
|
||||
const { data } = await apiClient.get(`/api/v1/admin/documents/${documentId}`)
|
||||
return data
|
||||
},
|
||||
|
||||
upload: async (file: File, groupKey?: string): Promise<UploadDocumentResponse> => {
|
||||
upload: async (
|
||||
file: File,
|
||||
options?: { groupKey?: string; category?: string }
|
||||
): Promise<UploadDocumentResponse> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const params: Record<string, string> = {}
|
||||
if (groupKey) {
|
||||
params.group_key = groupKey
|
||||
if (options?.groupKey) {
|
||||
params.group_key = options.groupKey
|
||||
}
|
||||
if (options?.category) {
|
||||
params.category = options.category
|
||||
}
|
||||
|
||||
const { data } = await apiClient.post('/api/v1/admin/documents', formData, {
|
||||
@@ -95,4 +108,15 @@ export const documentsApi = {
|
||||
)
|
||||
return data
|
||||
},
|
||||
|
||||
updateCategory: async (
|
||||
documentId: string,
|
||||
category: string
|
||||
): Promise<{ status: string; document_id: string; category: string; message: string }> => {
|
||||
const { data } = await apiClient.patch(
|
||||
`/api/v1/admin/documents/${documentId}/category`,
|
||||
{ category }
|
||||
)
|
||||
return data
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface DocumentItem {
|
||||
auto_label_error: string | null
|
||||
upload_source: string
|
||||
group_key: string | null
|
||||
category: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
annotation_count?: number
|
||||
@@ -61,6 +62,7 @@ export interface DocumentDetailResponse {
|
||||
upload_source: string
|
||||
batch_id: string | null
|
||||
group_key: string | null
|
||||
category: string
|
||||
csv_field_values: Record<string, string> | null
|
||||
can_annotate: boolean
|
||||
annotation_lock_until: string | null
|
||||
@@ -101,8 +103,21 @@ export interface TrainingTask {
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ModelVersionItem {
|
||||
version_id: string
|
||||
version: string
|
||||
name: string
|
||||
status: string
|
||||
is_active: boolean
|
||||
metrics_mAP: number | null
|
||||
document_count: number
|
||||
trained_at: string | null
|
||||
activated_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface TrainingModelsResponse {
|
||||
models: TrainingTask[]
|
||||
models: ModelVersionItem[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
@@ -118,11 +133,17 @@ export interface UploadDocumentResponse {
|
||||
file_size: number
|
||||
page_count: number
|
||||
status: string
|
||||
category: string
|
||||
group_key: string | null
|
||||
auto_label_started: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DocumentCategoriesResponse {
|
||||
categories: string[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface CreateAnnotationRequest {
|
||||
page_number: number
|
||||
class_id: number
|
||||
@@ -228,6 +249,8 @@ export interface DatasetDetailResponse {
|
||||
name: string
|
||||
description: string | null
|
||||
status: string
|
||||
training_status: string | null
|
||||
active_training_task_id: string | null
|
||||
train_ratio: number
|
||||
val_ratio: number
|
||||
seed: number
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Search, ChevronDown, MoreHorizontal, FileText } from 'lucide-react'
|
||||
import { Badge } from './Badge'
|
||||
import { Button } from './Button'
|
||||
import { UploadModal } from './UploadModal'
|
||||
import { useDocuments } from '../hooks/useDocuments'
|
||||
import { useDocuments, useCategories } from '../hooks/useDocuments'
|
||||
import type { DocumentItem } from '../api/types'
|
||||
|
||||
interface DashboardProps {
|
||||
@@ -34,11 +34,15 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false)
|
||||
const [selectedDocs, setSelectedDocs] = useState<Set<string>>(new Set())
|
||||
const [statusFilter, setStatusFilter] = useState<string>('')
|
||||
const [categoryFilter, setCategoryFilter] = useState<string>('')
|
||||
const [limit] = useState(20)
|
||||
const [offset] = useState(0)
|
||||
|
||||
const { categories } = useCategories()
|
||||
|
||||
const { documents, total, isLoading, error, refetch } = useDocuments({
|
||||
status: statusFilter || undefined,
|
||||
category: categoryFilter || undefined,
|
||||
limit,
|
||||
offset,
|
||||
})
|
||||
@@ -102,6 +106,24 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="relative">
|
||||
<select
|
||||
value={categoryFilter}
|
||||
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||
className="h-10 pl-3 pr-8 rounded-md border border-warm-border bg-white text-sm text-warm-text-secondary focus:outline-none appearance-none cursor-pointer hover:bg-warm-hover"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none text-warm-text-muted"
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={statusFilter}
|
||||
@@ -144,6 +166,9 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase tracking-wider">
|
||||
Annotations
|
||||
</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase tracking-wider">
|
||||
Category
|
||||
</th>
|
||||
<th className="py-3 px-4 text-xs font-semibold text-warm-text-muted uppercase tracking-wider">
|
||||
Group
|
||||
</th>
|
||||
@@ -156,13 +181,13 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-8 text-center text-warm-text-muted">
|
||||
<td colSpan={9} className="py-8 text-center text-warm-text-muted">
|
||||
Loading documents...
|
||||
</td>
|
||||
</tr>
|
||||
) : documents.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={8} className="py-8 text-center text-warm-text-muted">
|
||||
<td colSpan={9} className="py-8 text-center text-warm-text-muted">
|
||||
No documents found. Upload your first document to get started.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -216,6 +241,9 @@ export const Dashboard: React.FC<DashboardProps> = ({ onNavigate }) => {
|
||||
<td className="py-4 px-4 text-sm text-warm-text-secondary">
|
||||
{doc.annotation_count || 0} annotations
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-warm-text-secondary capitalize">
|
||||
{doc.category || 'invoice'}
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-warm-text-muted">
|
||||
{doc.group_key || '-'}
|
||||
</td>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { ArrowLeft, Loader2, Play, AlertCircle, Check } from 'lucide-react'
|
||||
import { ArrowLeft, Loader2, Play, AlertCircle, Check, Award } from 'lucide-react'
|
||||
import { Button } from './Button'
|
||||
import { useDatasetDetail } from '../hooks/useDatasets'
|
||||
|
||||
@@ -14,6 +14,23 @@ const SPLIT_STYLES: Record<string, string> = {
|
||||
test: 'bg-warm-state-success/10 text-warm-state-success',
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
building: { bg: 'bg-warm-state-info/10', text: 'text-warm-state-info', label: 'Building' },
|
||||
ready: { bg: 'bg-warm-state-success/10', text: 'text-warm-state-success', label: 'Ready' },
|
||||
trained: { bg: 'bg-purple-100', text: 'text-purple-700', label: 'Trained' },
|
||||
failed: { bg: 'bg-warm-state-error/10', text: 'text-warm-state-error', label: 'Failed' },
|
||||
archived: { bg: 'bg-warm-border', text: 'text-warm-text-muted', label: 'Archived' },
|
||||
}
|
||||
|
||||
const TRAINING_STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
pending: { bg: 'bg-warm-state-warning/10', text: 'text-warm-state-warning', label: 'Pending' },
|
||||
scheduled: { bg: 'bg-warm-state-warning/10', text: 'text-warm-state-warning', label: 'Scheduled' },
|
||||
running: { bg: 'bg-warm-state-info/10', text: 'text-warm-state-info', label: 'Training' },
|
||||
completed: { bg: 'bg-warm-state-success/10', text: 'text-warm-state-success', label: 'Completed' },
|
||||
failed: { bg: 'bg-warm-state-error/10', text: 'text-warm-state-error', label: 'Failed' },
|
||||
cancelled: { bg: 'bg-warm-border', text: 'text-warm-text-muted', label: 'Cancelled' },
|
||||
}
|
||||
|
||||
export const DatasetDetail: React.FC<DatasetDetailProps> = ({ datasetId, onBack }) => {
|
||||
const { dataset, isLoading, error } = useDatasetDetail(datasetId)
|
||||
|
||||
@@ -36,11 +53,25 @@ export const DatasetDetail: React.FC<DatasetDetailProps> = ({ datasetId, onBack
|
||||
)
|
||||
}
|
||||
|
||||
const statusIcon = dataset.status === 'ready'
|
||||
const statusConfig = STATUS_STYLES[dataset.status] || STATUS_STYLES.ready
|
||||
const trainingStatusConfig = dataset.training_status
|
||||
? TRAINING_STATUS_STYLES[dataset.training_status]
|
||||
: null
|
||||
|
||||
// Determine if training button should be shown and enabled
|
||||
const isTrainingInProgress = dataset.training_status === 'running' || dataset.training_status === 'pending'
|
||||
const canStartTraining = dataset.status === 'ready' && !isTrainingInProgress
|
||||
|
||||
// Determine status icon
|
||||
const statusIcon = dataset.status === 'trained'
|
||||
? <Award size={14} className="text-purple-700" />
|
||||
: dataset.status === 'ready'
|
||||
? <Check size={14} className="text-warm-state-success" />
|
||||
: dataset.status === 'failed'
|
||||
? <AlertCircle size={14} className="text-warm-state-error" />
|
||||
: <Loader2 size={14} className="animate-spin text-warm-state-info" />
|
||||
: dataset.status === 'building'
|
||||
? <Loader2 size={14} className="animate-spin text-warm-state-info" />
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="p-8 max-w-7xl mx-auto">
|
||||
@@ -51,15 +82,38 @@ export const DatasetDetail: React.FC<DatasetDetailProps> = ({ datasetId, onBack
|
||||
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-warm-text-primary flex items-center gap-2">
|
||||
{dataset.name} {statusIcon}
|
||||
</h2>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h2 className="text-2xl font-bold text-warm-text-primary flex items-center gap-2">
|
||||
{dataset.name} {statusIcon}
|
||||
</h2>
|
||||
{/* Status Badge */}
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${statusConfig.bg} ${statusConfig.text}`}>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
{/* Training Status Badge */}
|
||||
{trainingStatusConfig && (
|
||||
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium ${trainingStatusConfig.bg} ${trainingStatusConfig.text}`}>
|
||||
{isTrainingInProgress && <Loader2 size={12} className="mr-1 animate-spin" />}
|
||||
{trainingStatusConfig.label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{dataset.description && (
|
||||
<p className="text-sm text-warm-text-muted mt-1">{dataset.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{dataset.status === 'ready' && (
|
||||
<Button><Play size={14} className="mr-1" />Start Training</Button>
|
||||
{/* Training Button */}
|
||||
{(dataset.status === 'ready' || dataset.status === 'trained') && (
|
||||
<Button
|
||||
disabled={isTrainingInProgress}
|
||||
className={isTrainingInProgress ? 'opacity-50 cursor-not-allowed' : ''}
|
||||
>
|
||||
{isTrainingInProgress ? (
|
||||
<><Loader2 size={14} className="mr-1 animate-spin" />Training...</>
|
||||
) : (
|
||||
<><Play size={14} className="mr-1" />Start Training</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -72,12 +72,13 @@ const TrainDialog: React.FC<TrainDialogProps> = ({ dataset, onClose, onSubmit, i
|
||||
const [augmentationConfig, setAugmentationConfig] = useState<Partial<AugmentationConfigType>>({})
|
||||
const [augmentationMultiplier, setAugmentationMultiplier] = useState(2)
|
||||
|
||||
// Fetch available trained models
|
||||
// Fetch available trained models (active or inactive, not archived)
|
||||
const { data: modelsData } = useQuery({
|
||||
queryKey: ['training', 'models', 'completed'],
|
||||
queryFn: () => trainingApi.getModels({ status: 'completed' }),
|
||||
queryKey: ['training', 'models', 'available'],
|
||||
queryFn: () => trainingApi.getModels(),
|
||||
})
|
||||
const completedModels = modelsData?.models ?? []
|
||||
// Filter out archived models - only show active/inactive models for base model selection
|
||||
const availableModels = (modelsData?.models ?? []).filter(m => m.status !== 'archived')
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit({
|
||||
@@ -128,9 +129,9 @@ const TrainDialog: React.FC<TrainDialogProps> = ({ dataset, onClose, onSubmit, i
|
||||
className="w-full h-10 px-3 rounded-md border border-warm-divider bg-white text-warm-text-primary focus:outline-none focus:ring-1 focus:ring-warm-state-info"
|
||||
>
|
||||
<option value="pretrained">yolo11n.pt (Pretrained)</option>
|
||||
{completedModels.map(m => (
|
||||
<option key={m.task_id} value={m.task_id}>
|
||||
{m.name} ({m.metrics_mAP ? `${(m.metrics_mAP * 100).toFixed(1)}% mAP` : 'No metrics'})
|
||||
{availableModels.map(m => (
|
||||
<option key={m.version_id} value={m.version_id}>
|
||||
{m.name} v{m.version} ({m.metrics_mAP ? `${(m.metrics_mAP * 100).toFixed(1)}% mAP` : 'No metrics'})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -293,8 +294,12 @@ const DatasetList: React.FC<{
|
||||
</button>
|
||||
)}
|
||||
<button title="Delete" onClick={() => deleteDataset(ds.dataset_id)}
|
||||
disabled={isDeleting}
|
||||
className="p-1.5 rounded hover:bg-warm-selected text-warm-text-muted hover:text-warm-state-error transition-colors">
|
||||
disabled={isDeleting || ds.status === 'pending' || ds.status === 'building'}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
ds.status === 'pending' || ds.status === 'building'
|
||||
? 'text-warm-text-muted/40 cursor-not-allowed'
|
||||
: 'hover:bg-warm-selected text-warm-text-muted hover:text-warm-state-error'
|
||||
}`}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { X, UploadCloud, File, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
import { X, UploadCloud, File, CheckCircle, AlertCircle, ChevronDown } from 'lucide-react'
|
||||
import { Button } from './Button'
|
||||
import { useDocuments } from '../hooks/useDocuments'
|
||||
import { useDocuments, useCategories } from '../hooks/useDocuments'
|
||||
|
||||
interface UploadModalProps {
|
||||
isOpen: boolean
|
||||
@@ -12,11 +12,13 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose }) =>
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||
const [groupKey, setGroupKey] = useState('')
|
||||
const [category, setCategory] = useState('invoice')
|
||||
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'success' | 'error'>('idle')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const { uploadDocument, isUploading } = useDocuments({})
|
||||
const { categories } = useCategories()
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
@@ -63,7 +65,7 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose }) =>
|
||||
for (const file of selectedFiles) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
uploadDocument(
|
||||
{ file, groupKey: groupKey || undefined },
|
||||
{ file, groupKey: groupKey || undefined, category: category || 'invoice' },
|
||||
{
|
||||
onSuccess: () => resolve(),
|
||||
onError: (error: Error) => reject(error),
|
||||
@@ -77,6 +79,7 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose }) =>
|
||||
onClose()
|
||||
setSelectedFiles([])
|
||||
setGroupKey('')
|
||||
setCategory('invoice')
|
||||
setUploadStatus('idle')
|
||||
}, 1500)
|
||||
} catch (error) {
|
||||
@@ -91,6 +94,7 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose }) =>
|
||||
}
|
||||
setSelectedFiles([])
|
||||
setGroupKey('')
|
||||
setCategory('invoice')
|
||||
setUploadStatus('idle')
|
||||
setErrorMessage('')
|
||||
onClose()
|
||||
@@ -179,6 +183,42 @@ export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose }) =>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category Select */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-warm-text-secondary mb-2">
|
||||
Category
|
||||
</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
className="w-full h-10 pl-3 pr-8 rounded-md border border-warm-border bg-white text-sm text-warm-text-secondary focus:outline-none focus:ring-1 focus:ring-warm-state-info appearance-none cursor-pointer"
|
||||
disabled={uploadStatus === 'uploading'}
|
||||
>
|
||||
<option value="invoice">Invoice</option>
|
||||
<option value="letter">Letter</option>
|
||||
<option value="receipt">Receipt</option>
|
||||
<option value="contract">Contract</option>
|
||||
{categories
|
||||
.filter((cat) => !['invoice', 'letter', 'receipt', 'contract'].includes(cat))
|
||||
.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat.charAt(0).toUpperCase() + cat.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none text-warm-text-muted"
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-warm-text-muted mt-1">
|
||||
Select document type for training different models
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Group Key Input */}
|
||||
{selectedFiles.length > 0 && (
|
||||
<div className="mb-6">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { useDocuments } from './useDocuments'
|
||||
export { useDocuments, useCategories } from './useDocuments'
|
||||
export { useDocumentDetail } from './useDocumentDetail'
|
||||
export { useAnnotations } from './useAnnotations'
|
||||
export { useTraining, useTrainingDocuments } from './useTraining'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { documentsApi } from '../api/endpoints'
|
||||
import type { DocumentListResponse, UploadDocumentResponse } from '../api/types'
|
||||
import type { DocumentListResponse, DocumentCategoriesResponse } from '../api/types'
|
||||
|
||||
interface UseDocumentsParams {
|
||||
status?: string
|
||||
category?: string
|
||||
limit?: number
|
||||
offset?: number
|
||||
}
|
||||
@@ -18,10 +19,11 @@ export const useDocuments = (params: UseDocumentsParams = {}) => {
|
||||
})
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: ({ file, groupKey }: { file: File; groupKey?: string }) =>
|
||||
documentsApi.upload(file, groupKey),
|
||||
mutationFn: ({ file, groupKey, category }: { file: File; groupKey?: string; category?: string }) =>
|
||||
documentsApi.upload(file, { groupKey, category }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['categories'] })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -63,6 +65,15 @@ export const useDocuments = (params: UseDocumentsParams = {}) => {
|
||||
},
|
||||
})
|
||||
|
||||
const updateCategoryMutation = useMutation({
|
||||
mutationFn: ({ documentId, category }: { documentId: string; category: string }) =>
|
||||
documentsApi.updateCategory(documentId, category),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['categories'] })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
documents: data?.documents || [],
|
||||
total: data?.total || 0,
|
||||
@@ -86,5 +97,24 @@ export const useDocuments = (params: UseDocumentsParams = {}) => {
|
||||
updateGroupKey: updateGroupKeyMutation.mutate,
|
||||
updateGroupKeyAsync: updateGroupKeyMutation.mutateAsync,
|
||||
isUpdatingGroupKey: updateGroupKeyMutation.isPending,
|
||||
updateCategory: updateCategoryMutation.mutate,
|
||||
updateCategoryAsync: updateCategoryMutation.mutateAsync,
|
||||
isUpdatingCategory: updateCategoryMutation.isPending,
|
||||
}
|
||||
}
|
||||
|
||||
export const useCategories = () => {
|
||||
const { data, isLoading, error, refetch } = useQuery<DocumentCategoriesResponse>({
|
||||
queryKey: ['categories'],
|
||||
queryFn: () => documentsApi.getCategories(),
|
||||
staleTime: 60000,
|
||||
})
|
||||
|
||||
return {
|
||||
categories: data?.categories || [],
|
||||
total: data?.total || 0,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user