diff --git a/.gitignore b/.gitignore index 342c0b258..cb6d28b4e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ ./surfsense_backend/podcasts/ .env node_modules/ -.ruff_cache/ \ No newline at end of file +.ruff_cache/ +.venv +.pnpm-store diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx index 978cdf219..6a7503834 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx @@ -27,6 +27,7 @@ import { import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import type { ColumnVisibility } from "./types"; const fadeInScale: Variants = { @@ -46,13 +47,13 @@ export function DocumentsFilters({ columnVisibility, onToggleColumn, }: { - typeCounts: Record; + typeCounts: Record; selectedIds: Set; onSearch: (v: string) => void; searchValue: string; onBulkDelete: () => Promise; - onToggleType: (type: string, checked: boolean) => void; - activeTypes: string[]; + onToggleType: (type: DocumentTypeEnum, checked: boolean) => void; + activeTypes: DocumentTypeEnum[]; columnVisibility: ColumnVisibility; onToggleColumn: (id: keyof ColumnVisibility, checked: boolean) => void; }) { @@ -61,7 +62,7 @@ export function DocumentsFilters({ const inputRef = useRef(null); const uniqueTypes = useMemo(() => { - return Object.keys(typeCountsRecord).sort(); + return Object.keys(typeCountsRecord).sort() as DocumentTypeEnum[]; }, [typeCountsRecord]); const typeCounts = useMemo(() => { @@ -156,7 +157,7 @@ export function DocumentsFilters({
Filters
- {uniqueTypes.map((value, i) => ( + {uniqueTypes.map((value: DocumentTypeEnum, i) => ( (value: T, delay = 250) { const [debounced, setDebounced] = useState(value); @@ -30,7 +34,7 @@ export default function DocumentsTable() { const [search, setSearch] = useState(""); const debouncedSearch = useDebounced(search, 250); - const [activeTypes, setActiveTypes] = useState([]); + const [activeTypes, setActiveTypes] = useState([]); const [columnVisibility, setColumnVisibility] = useState({ title: true, document_type: true, @@ -42,55 +46,65 @@ export default function DocumentsTable() { const [sortKey, setSortKey] = useState("title"); const [sortDesc, setSortDesc] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); - const [typeCounts, setTypeCounts] = useState>({}); + const { data: typeCounts } = useAtomValue(documentTypeCountsAtom); + const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom); - // Use server-side pagination, search, and filtering + // Build query parameters for fetching documents + const queryParams = useMemo( + () => ({ + search_space_id: searchSpaceId, + page: pageIndex, + page_size: pageSize, + ...(activeTypes.length > 0 && { document_types: activeTypes }), + }), + [searchSpaceId, pageIndex, pageSize, activeTypes] + ); + + // Build search query parameters + const searchQueryParams = useMemo( + () => ({ + search_space_id: searchSpaceId, + page: pageIndex, + page_size: pageSize, + title: debouncedSearch.trim(), + ...(activeTypes.length > 0 && { document_types: activeTypes }), + }), + [searchSpaceId, pageIndex, pageSize, activeTypes, debouncedSearch] + ); + + // Use query for fetching documents const { - documents, - total, - loading, - error, - fetchDocuments, - searchDocuments, - deleteDocument, - getDocumentTypeCounts, - } = useDocuments(searchSpaceId, { - page: pageIndex, - pageSize: pageSize, + data: documentsResponse, + isLoading: isDocumentsLoading, + refetch: refetchDocuments, + error: documentsError, + } = useQuery({ + queryKey: cacheKeys.documents.globalQueryParams(queryParams), + queryFn: () => documentsApiService.getDocuments({ queryParams }), + staleTime: 3 * 60 * 1000, // 3 minutes + enabled: !!searchSpaceId && !debouncedSearch.trim(), }); - // Fetch document type counts on mount and when search space changes - useEffect(() => { - if (searchSpaceId && getDocumentTypeCounts) { - getDocumentTypeCounts().then(setTypeCounts); - } - }, [searchSpaceId, getDocumentTypeCounts]); + // Use query for searching documents + const { + data: searchResponse, + isLoading: isSearchLoading, + refetch: refetchSearch, + error: searchError, + } = useQuery({ + queryKey: cacheKeys.documents.globalQueryParams(searchQueryParams), + queryFn: () => documentsApiService.searchDocuments({ queryParams: searchQueryParams }), + staleTime: 3 * 60 * 1000, // 3 minutes + enabled: !!searchSpaceId && !!debouncedSearch.trim(), + }); - // Refetch when pagination changes or when search/filters change - useEffect(() => { - if (searchSpaceId) { - if (debouncedSearch.trim()) { - // Use search endpoint if there's a search query - searchDocuments?.( - debouncedSearch, - pageIndex, - pageSize, - activeTypes.length > 0 ? activeTypes : undefined - ); - } else { - // Use regular fetch if no search - fetchDocuments?.(pageIndex, pageSize, activeTypes.length > 0 ? activeTypes : undefined); - } - } - }, [ - pageIndex, - pageSize, - debouncedSearch, - activeTypes, - searchSpaceId, - fetchDocuments, - searchDocuments, - ]); + // Extract documents and total based on search state + const documents = debouncedSearch.trim() + ? searchResponse?.items || [] + : documentsResponse?.items || []; + const total = debouncedSearch.trim() ? searchResponse?.total || 0 : documentsResponse?.total || 0; + const loading = debouncedSearch.trim() ? isSearchLoading : isDocumentsLoading; + const error = debouncedSearch.trim() ? searchError : documentsError; // Display server-filtered results directly const displayDocs = documents || []; @@ -98,7 +112,7 @@ export default function DocumentsTable() { const pageStart = pageIndex * pageSize; const pageEnd = Math.min(pageStart + pageSize, displayTotal); - const onToggleType = (type: string, checked: boolean) => { + const onToggleType = (type: DocumentTypeEnum, checked: boolean) => { setActiveTypes((prev) => (checked ? [...prev, type] : prev.filter((t) => t !== type))); setPageIndex(0); }; @@ -109,16 +123,25 @@ export default function DocumentsTable() { const refreshCurrentView = useCallback(async () => { if (debouncedSearch.trim()) { - await searchDocuments?.( - debouncedSearch, - pageIndex, - pageSize, - activeTypes.length > 0 ? activeTypes : undefined - ); + await refetchSearch(); } else { - await fetchDocuments?.(pageIndex, pageSize, activeTypes.length > 0 ? activeTypes : undefined); + await refetchDocuments(); } - }, [debouncedSearch, pageIndex, pageSize, activeTypes, searchDocuments, fetchDocuments]); + }, [debouncedSearch, refetchSearch, refetchDocuments]); + + // Create a delete function for single document deletion + const deleteDocument = useCallback( + async (id: number) => { + try { + await deleteDocumentMutation({ id }); + return true; + } catch (error) { + console.error("Failed to delete document:", error); + return false; + } + }, + [deleteDocumentMutation] + ); const onBulkDelete = async () => { if (selectedIds.size === 0) { @@ -126,7 +149,17 @@ export default function DocumentsTable() { return; } try { - const results = await Promise.all(Array.from(selectedIds).map((id) => deleteDocument?.(id))); + // Delete documents one by one using the mutation + const results = await Promise.all( + Array.from(selectedIds).map(async (id) => { + try { + await deleteDocumentMutation({ id }); + return true; + } catch { + return false; + } + }) + ); const okCount = results.filter((r) => r === true).length; if (okCount === selectedIds.size) toast.success(t("delete_success_count", { count: okCount })); @@ -159,7 +192,7 @@ export default function DocumentsTable() { className="w-full px-6 py-4" > deleteDocument?.(id) ?? Promise.resolve(false)} + deleteDocument={deleteDocument} sortKey={sortKey} sortDesc={sortDesc} onSortChange={(key) => { diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx index 1a9a607fb..a9d8b9649 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx @@ -1,15 +1,15 @@ "use client"; import { type CreateMessage, type Message, useChat } from "@ai-sdk/react"; -import { useAtomValue } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useRef } from "react"; import { createChatMutationAtom, updateChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; import { activeChatAtom } from "@/atoms/chats/chat-query.atoms"; import { activeChatIdAtom } from "@/atoms/chats/ui.atoms"; +import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import ChatInterface from "@/components/chat/ChatInterface"; import { useChatState } from "@/hooks/use-chat"; -import { useDocumentTypes } from "@/hooks/use-document-types"; import type { Document } from "@/hooks/use-documents"; import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; @@ -46,7 +46,19 @@ export default function ResearcherPage() { }); // Fetch all available sources (document types + live search connectors) - const { documentTypes } = useDocumentTypes(Number(search_space_id)); + // Use the documentTypeCountsAtom for fetching document types + const [documentTypeCountsQuery] = useAtom(documentTypeCountsAtom); + const { data: documentTypeCountsData } = documentTypeCountsQuery; + + // Transform the response into the expected format + const documentTypes = useMemo(() => { + if (!documentTypeCountsData) return []; + return Object.entries(documentTypeCountsData).map(([type, count]) => ({ + type, + count, + })); + }, [documentTypeCountsData]); + const { connectors: searchConnectors } = useSearchSourceConnectors( false, Number(search_space_id) diff --git a/surfsense_web/atoms/documents/document-mutation.atoms.ts b/surfsense_web/atoms/documents/document-mutation.atoms.ts new file mode 100644 index 000000000..6eaaa014a --- /dev/null +++ b/surfsense_web/atoms/documents/document-mutation.atoms.ts @@ -0,0 +1,114 @@ +import { atomWithMutation } from "jotai-tanstack-query"; +import { toast } from "sonner"; +import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import type { + CreateDocumentRequest, + DeleteDocumentRequest, + GetDocumentsResponse, + UpdateDocumentRequest, + UploadDocumentRequest, +} from "@/contracts/types/document.types"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { queryClient } from "@/lib/query-client/client"; +import { globalDocumentsQueryParamsAtom } from "./ui.atoms"; + +export const createDocumentMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + const documentsQueryParams = get(globalDocumentsQueryParamsAtom); + + return { + mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + enabled: !!searchSpaceId, + mutationFn: async (request: CreateDocumentRequest) => { + return documentsApiService.createDocument(request); + }, + + onSuccess: () => { + toast.success("Document created successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.typeCounts(searchSpaceId ?? undefined), + }); + }, + }; +}); + +export const uploadDocumentMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + const documentsQueryParams = get(globalDocumentsQueryParamsAtom); + + return { + mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + enabled: !!searchSpaceId, + mutationFn: async (request: UploadDocumentRequest) => { + return documentsApiService.uploadDocument(request); + }, + + onSuccess: () => { + toast.success("Files uploaded for processing"); + }, + }; +}); + +export const updateDocumentMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + const documentsQueryParams = get(globalDocumentsQueryParamsAtom); + + return { + mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + enabled: !!searchSpaceId, + mutationFn: async (request: UpdateDocumentRequest) => { + return documentsApiService.updateDocument(request); + }, + + onSuccess: (_, request: UpdateDocumentRequest) => { + toast.success("Document updated successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.document(String(request.id)), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.typeCounts(searchSpaceId ?? undefined), + }); + }, + }; +}); + +export const deleteDocumentMutationAtom = atomWithMutation((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + const documentsQueryParams = get(globalDocumentsQueryParamsAtom); + + return { + mutationKey: cacheKeys.documents.globalQueryParams(documentsQueryParams), + enabled: !!searchSpaceId, + mutationFn: async (request: DeleteDocumentRequest) => { + return documentsApiService.deleteDocument(request); + }, + + onSuccess: (_, request: DeleteDocumentRequest) => { + toast.success("Document deleted successfully"); + queryClient.setQueryData( + cacheKeys.documents.globalQueryParams(documentsQueryParams), + (oldData: GetDocumentsResponse | undefined) => { + if (!oldData) return oldData; + return { + ...oldData, + items: oldData.items.filter((doc) => doc.id !== request.id), + total: oldData.total - 1, + }; + } + ); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.document(String(request.id)), + }); + queryClient.invalidateQueries({ + queryKey: cacheKeys.documents.typeCounts(searchSpaceId ?? undefined), + }); + }, + }; +}); diff --git a/surfsense_web/atoms/documents/document-query.atoms.ts b/surfsense_web/atoms/documents/document-query.atoms.ts new file mode 100644 index 000000000..acdff9aaf --- /dev/null +++ b/surfsense_web/atoms/documents/document-query.atoms.ts @@ -0,0 +1,38 @@ +import { atomWithQuery } from "jotai-tanstack-query"; +import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import type { SearchDocumentsRequest } from "@/contracts/types/document.types"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { globalDocumentsQueryParamsAtom } from "./ui.atoms"; + +export const documentsAtom = atomWithQuery((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + const queryParams = get(globalDocumentsQueryParamsAtom); + + return { + queryKey: cacheKeys.documents.globalQueryParams(queryParams), + enabled: !!searchSpaceId, + queryFn: async () => { + return documentsApiService.getDocuments({ + queryParams: queryParams, + }); + }, + }; +}); + +export const documentTypeCountsAtom = atomWithQuery((get) => { + const searchSpaceId = get(activeSearchSpaceIdAtom); + + return { + queryKey: cacheKeys.documents.typeCounts(searchSpaceId ?? undefined), + enabled: !!searchSpaceId, + staleTime: 10 * 60 * 1000, // 10 minutes + queryFn: async () => { + return documentsApiService.getDocumentTypeCounts({ + queryParams: { + search_space_id: searchSpaceId ?? undefined, + }, + }); + }, + }; +}); diff --git a/surfsense_web/atoms/documents/ui.atoms.ts b/surfsense_web/atoms/documents/ui.atoms.ts new file mode 100644 index 000000000..33740e9c7 --- /dev/null +++ b/surfsense_web/atoms/documents/ui.atoms.ts @@ -0,0 +1,7 @@ +import { atom } from "jotai"; +import type { GetDocumentsRequest } from "@/contracts/types/document.types"; + +export const globalDocumentsQueryParamsAtom = atom({ + page_size: 10, + page: 0, +}); diff --git a/surfsense_web/components/chat/ChatInputGroup.tsx b/surfsense_web/components/chat/ChatInputGroup.tsx index 7a76c4d56..97473891c 100644 --- a/surfsense_web/components/chat/ChatInputGroup.tsx +++ b/surfsense_web/components/chat/ChatInputGroup.tsx @@ -1,9 +1,11 @@ "use client"; import { ChatInput } from "@llamaindex/chat-ui"; +import { useAtom } from "jotai"; import { Brain, Check, FolderOpen, Minus, Plus, PlusCircle, Zap } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; -import React, { Suspense, useCallback, useState } from "react"; +import React, { Suspense, useCallback, useMemo, useState } from "react"; +import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { DocumentsDataTable } from "@/components/chat/DocumentsDataTable"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -25,7 +27,6 @@ import { } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { useDocumentTypes } from "@/hooks/use-document-types"; import type { Document } from "@/hooks/use-documents"; import { useGlobalLLMConfigs, useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; @@ -118,11 +119,24 @@ const ConnectorSelector = React.memo( const router = useRouter(); const [isOpen, setIsOpen] = useState(false); - // Fetch immediately (not lazy) so the button can show the correct count - const { documentTypes, isLoading, isLoaded, fetchDocumentTypes } = useDocumentTypes( - Number(search_space_id), - false - ); + // Use the documentTypeCountsAtom for fetching document types + const [documentTypeCountsQuery] = useAtom(documentTypeCountsAtom); + const { + data: documentTypeCountsData, + isLoading, + refetch: fetchDocumentTypes, + } = documentTypeCountsQuery; + + // Transform the response into the expected format + const documentTypes = useMemo(() => { + if (!documentTypeCountsData) return []; + return Object.entries(documentTypeCountsData).map(([type, count]) => ({ + type, + count, + })); + }, [documentTypeCountsData]); + + const isLoaded = !!documentTypeCountsData; // Fetch live search connectors immediately (non-indexable) const { diff --git a/surfsense_web/components/chat/DocumentsDataTable.tsx b/surfsense_web/components/chat/DocumentsDataTable.tsx index 331c1b404..77f1a05bd 100644 --- a/surfsense_web/components/chat/DocumentsDataTable.tsx +++ b/surfsense_web/components/chat/DocumentsDataTable.tsx @@ -1,5 +1,6 @@ "use client"; +import { useQuery } from "@tanstack/react-query"; import { type ColumnDef, flexRender, @@ -7,9 +8,11 @@ import { type SortingState, useReactTable, } from "@tanstack/react-table"; +import { useAtomValue } from "jotai"; import { ArrowUpDown, Calendar, FileText, Filter, Plus, Search } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; @@ -31,7 +34,9 @@ import { TableRow, } from "@/components/ui/table"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; -import { type Document, type DocumentType, useDocuments } from "@/hooks/use-documents"; +import type { Document, DocumentTypeEnum } from "@/contracts/types/document.types"; +import { documentsApiService } from "@/lib/apis/documents-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; interface DocumentsDataTableProps { searchSpaceId: number; @@ -104,8 +109,8 @@ const columns: ColumnDef[] = [ cell: ({ row }) => { const type = row.getValue("document_type") as DocumentType; return ( -
- {getConnectorIcon(type)} +
+ {getConnectorIcon(String(type))}
); }, @@ -182,52 +187,55 @@ export function DocumentsDataTable({ const [sorting, setSorting] = useState([]); const [search, setSearch] = useState(""); const debouncedSearch = useDebounced(search, 300); - const [documentTypeFilter, setDocumentTypeFilter] = useState([]); + const [documentTypeFilter, setDocumentTypeFilter] = useState([]); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); - const [typeCounts, setTypeCounts] = useState>({}); + const { data: typeCounts } = useAtomValue(documentTypeCountsAtom); - // Use server-side pagination, search, and filtering - const { documents, total, loading, fetchDocuments, searchDocuments, getDocumentTypeCounts } = - useDocuments(searchSpaceId, { + const fetchQueryParams = useMemo( + () => ({ + search_space_id: searchSpaceId, page: pageIndex, - pageSize: pageSize, - }); + page_size: pageSize, + ...(documentTypeFilter.length > 0 && { document_types: documentTypeFilter }), + }), + [searchSpaceId, pageIndex, pageSize, documentTypeFilter, debouncedSearch] + ); - // Fetch document type counts on mount - useEffect(() => { - if (searchSpaceId && getDocumentTypeCounts) { - getDocumentTypeCounts().then(setTypeCounts); - } - }, [searchSpaceId, getDocumentTypeCounts]); + const searchQueryParams = useMemo(() => { + return { + search_space_id: searchSpaceId, + page: pageIndex, + page_size: pageSize, + ...(documentTypeFilter.length > 0 && { document_types: documentTypeFilter }), + title: debouncedSearch, + }; + }, [debouncedSearch, searchSpaceId, pageIndex, pageSize, documentTypeFilter, debouncedSearch]); - // Refetch when pagination changes or when search/filters change - useEffect(() => { - if (searchSpaceId) { - if (debouncedSearch.trim()) { - searchDocuments?.( - debouncedSearch, - pageIndex, - pageSize, - documentTypeFilter.length > 0 ? documentTypeFilter : undefined - ); - } else { - fetchDocuments?.( - pageIndex, - pageSize, - documentTypeFilter.length > 0 ? documentTypeFilter : undefined - ); - } - } - }, [ - pageIndex, - pageSize, - debouncedSearch, - documentTypeFilter, - searchSpaceId, - fetchDocuments, - searchDocuments, - ]); + // Use query for fetching documents + const { data: documents, isLoading: isDocumentsLoading } = useQuery({ + queryKey: cacheKeys.documents.withQueryParams(fetchQueryParams), + queryFn: () => documentsApiService.getDocuments({ queryParams: fetchQueryParams }), + staleTime: 3 * 60 * 1000, // 3 minutes + enabled: !!searchSpaceId && !debouncedSearch.trim(), + }); + + // Seaching + const { data: searchedDocuments, isLoading: isSearchedDocumentsLoading } = useQuery({ + queryKey: cacheKeys.documents.withQueryParams(searchQueryParams), + queryFn: () => documentsApiService.searchDocuments({ queryParams: searchQueryParams }), + staleTime: 3 * 60 * 1000, // 3 minutes + enabled: !!searchSpaceId && !!debouncedSearch.trim(), + }); + + // Use query data when not searching, otherwise use hook data + const actualDocuments = debouncedSearch.trim() + ? searchedDocuments?.items || [] + : documents?.items || []; + const actualTotal = debouncedSearch.trim() + ? searchedDocuments?.total || 0 + : documents?.total || 0; + const actualLoading = debouncedSearch.trim() ? isSearchedDocumentsLoading : isDocumentsLoading; // Memoize initial row selection to prevent infinite loops const initialRowSelection = useMemo(() => { @@ -272,14 +280,14 @@ export function DocumentsDataTable({ // Update the selected documents map when row selection changes useEffect(() => { - if (!documents || documents.length === 0) return; + if (!actualDocuments || actualDocuments.length === 0) return; setSelectedDocumentsMap((prev) => { const newMap = new Map(prev); let hasChanges = false; // Process only current page documents - for (const doc of documents) { + for (const doc of actualDocuments) { const docId = doc.id; const isSelected = rowSelection[docId.toString()]; const wasInMap = newMap.has(docId); @@ -319,14 +327,14 @@ export function DocumentsDataTable({ }, [selectedDocumentsArray, onSelectionChange]); const table = useReactTable({ - data: documents || [], + data: actualDocuments || [], columns, getRowId: (row) => row.id.toString(), onSortingChange: setSorting, getCoreRowModel: getCoreRowModel(), onRowSelectionChange: setRowSelection, manualPagination: true, - pageCount: Math.ceil(total / pageSize), + pageCount: Math.ceil(actualTotal / pageSize), state: { sorting, rowSelection, pagination: { pageIndex, pageSize } }, }); @@ -344,7 +352,7 @@ export function DocumentsDataTable({ setRowSelection(newSelection); }, [table, rowSelection]); - const handleToggleType = useCallback((type: string, checked: boolean) => { + const handleToggleType = useCallback((type: DocumentTypeEnum, checked: boolean) => { setDocumentTypeFilter((prev) => { if (checked) { return [...prev, type]; @@ -358,7 +366,7 @@ export function DocumentsDataTable({ // Get available document types from type counts (memoized) const availableTypes = useMemo(() => { - const types = Object.keys(typeCounts); + const types = typeCounts ? (Object.keys(typeCounts) as DocumentTypeEnum[]) : []; return types.length > 0 ? types.sort() : []; }, [typeCounts]); @@ -408,7 +416,7 @@ export function DocumentsDataTable({ className="flex grow justify-between gap-2 font-normal text-sm cursor-pointer" > {type.replace(/_/g, " ")} - {typeCounts[type]} + {typeCounts?.[type]}
))} @@ -435,7 +443,7 @@ export function DocumentsDataTable({
- {selectedCount} selected {loading && "· Loading..."} + {selectedCount} selected {actualLoading && "· Loading..."}
@@ -453,7 +461,7 @@ export function DocumentsDataTable({ size="sm" onClick={handleSelectPage} className="text-xs sm:text-sm" - disabled={loading} + disabled={actualLoading} > Select Page @@ -490,7 +498,7 @@ export function DocumentsDataTable({ {/* Table Container */}
- {loading ? ( + {actualLoading ? (
@@ -561,15 +569,15 @@ export function DocumentsDataTable({ {/* Footer Pagination */}
- Showing {pageIndex * pageSize + 1} to {Math.min((pageIndex + 1) * pageSize, total)} of{" "} - {total} documents + Showing {pageIndex * pageSize + 1} to {Math.min((pageIndex + 1) * pageSize, actualTotal)}{" "} + of {actualTotal} documents