diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx index 4a69a7533..a0a2e4d92 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx @@ -26,8 +26,11 @@ export default function DocumentsTable() { const params = useParams(); const searchSpaceId = Number(params.search_space_id); - const { documents, loading, error, refreshDocuments, deleteDocument } = - useDocuments(searchSpaceId); + const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(10); + + const { documents, loading, error, refreshDocuments, deleteDocument, hasMore } = + useDocuments(searchSpaceId, { pageIndex, pageSize }); const [data, setData] = useState([]); const [search, setSearch] = useState(""); @@ -39,8 +42,7 @@ export default function DocumentsTable() { content: true, created_at: true, }); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); + // pageIndex/pageSize state moved above to feed the hook const [sortKey, setSortKey] = useState("title"); const [sortDesc, setSortDesc] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -61,10 +63,11 @@ export default function DocumentsTable() { return result; }, [data, debouncedSearch, activeTypes]); - const total = filtered.length; - const pageStart = pageIndex * pageSize; - const pageEnd = Math.min(pageStart + pageSize, total); - const pageDocs = filtered.slice(pageStart, pageEnd); + // Server-side pagination: we filter only the current page's data client-side + const pageDocs = filtered; + const total = pageIndex * pageSize + pageDocs.length + (hasMore ? 1 : 0) - 1; + const pageStart = pageIndex * pageSize; + const pageEnd = pageStart + pageDocs.length; const onToggleType = (type: string, checked: boolean) => { setActiveTypes((prev) => (checked ? [...prev, type] : prev.filter((t) => t !== type))); @@ -151,16 +154,32 @@ export default function DocumentsTable() { pageIndex={pageIndex} pageSize={pageSize} total={total} - onPageSizeChange={(s) => { - setPageSize(s); - setPageIndex(0); - }} - onFirst={() => setPageIndex(0)} - onPrev={() => setPageIndex((i) => Math.max(0, i - 1))} - onNext={() => setPageIndex((i) => (pageEnd < total ? i + 1 : i))} - onLast={() => setPageIndex(Math.max(0, Math.ceil(total / pageSize) - 1))} - canPrev={pageIndex > 0} - canNext={pageEnd < total} + onPageSizeChange={async (s) => { + setPageIndex(0); + setPageSize(s); + await refreshDocuments?.({ pageIndex: 0, pageSize: s }); + }} + onFirst={async () => { + setPageIndex(0); + await refreshDocuments?.({ pageIndex: 0, pageSize }); + }} + onPrev={async () => { + const next = Math.max(0, pageIndex - 1); + if (next !== pageIndex) { + setPageIndex(next); + await refreshDocuments?.({ pageIndex: next, pageSize }); + } + }} + onNext={async () => { + if (hasMore) { + const next = pageIndex + 1; + setPageIndex(next); + await refreshDocuments?.({ pageIndex: next, pageSize }); + } + }} + onLast={() => {}} + canPrev={pageIndex > 0} + canNext={hasMore} id={id} /> diff --git a/surfsense_web/hooks/use-documents.ts b/surfsense_web/hooks/use-documents.ts index 4c7536689..bca27c25b 100644 --- a/surfsense_web/hooks/use-documents.ts +++ b/surfsense_web/hooks/use-documents.ts @@ -29,55 +29,70 @@ export type DocumentType = | "GOOGLE_GMAIL_CONNECTOR" | "AIRTABLE_CONNECTOR"; -export function useDocuments(searchSpaceId: number, lazy: boolean = false) { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(!lazy); // Don't show loading initially for lazy mode - const [error, setError] = useState(null); - const [isLoaded, setIsLoaded] = useState(false); // Memoization flag +export function useDocuments( + searchSpaceId: number, + optionsOrLazy?: boolean | { pageIndex?: number; pageSize?: number; lazy?: boolean } +) { + const lazy = typeof optionsOrLazy === "boolean" ? optionsOrLazy : optionsOrLazy?.lazy ?? false; + const pageIndex = typeof optionsOrLazy === "object" && optionsOrLazy?.pageIndex !== undefined ? optionsOrLazy.pageIndex : 0; + const pageSize = typeof optionsOrLazy === "object" && optionsOrLazy?.pageSize !== undefined ? optionsOrLazy.pageSize : 50; - const fetchDocuments = useCallback(async () => { - if (isLoaded && lazy) return; // Avoid redundant calls in lazy mode + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(!lazy); + const [error, setError] = useState(null); + const [isLoaded, setIsLoaded] = useState(false); + const [hasMore, setHasMore] = useState(false); - try { - setLoading(true); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents?search_space_id=${searchSpaceId}`, - { - headers: { - Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, - }, - method: "GET", - } - ); + const fetchDocuments = useCallback(async (override?: { pageIndex?: number; pageSize?: number }) => { + if (isLoaded && lazy && !override) return; - if (!response.ok) { - toast.error("Failed to fetch documents"); - throw new Error("Failed to fetch documents"); - } + const effectivePageIndex = override?.pageIndex ?? pageIndex; + const effectivePageSize = override?.pageSize ?? pageSize; + const skip = effectivePageIndex * effectivePageSize; + const limit = effectivePageSize; - const data = await response.json(); - setDocuments(data); - setError(null); - setIsLoaded(true); - } catch (err: any) { - setError(err.message || "Failed to fetch documents"); - console.error("Error fetching documents:", err); - } finally { - setLoading(false); - } - }, [searchSpaceId, isLoaded, lazy]); + try { + setLoading(true); + const url = new URL(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents`); + if (searchSpaceId) url.searchParams.set("search_space_id", String(searchSpaceId)); + url.searchParams.set("skip", String(skip)); + url.searchParams.set("limit", String(limit)); - useEffect(() => { - if (!lazy && searchSpaceId) { - fetchDocuments(); - } - }, [searchSpaceId, lazy, fetchDocuments]); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + method: "GET", + }); - // Function to refresh the documents list - const refreshDocuments = useCallback(async () => { - setIsLoaded(false); // Reset memoization flag to allow refetch - await fetchDocuments(); - }, [fetchDocuments]); + if (!response.ok) { + toast.error("Failed to fetch documents"); + throw new Error("Failed to fetch documents"); + } + + const data = await response.json(); + setDocuments(data); + setHasMore(Array.isArray(data) && data.length === effectivePageSize); + setError(null); + setIsLoaded(true); + } catch (err: any) { + setError(err.message || "Failed to fetch documents"); + console.error("Error fetching documents:", err); + } finally { + setLoading(false); + } + }, [searchSpaceId, isLoaded, lazy, pageIndex, pageSize]); + + useEffect(() => { + if (!lazy && searchSpaceId !== undefined && searchSpaceId !== null) { + fetchDocuments(); + } + }, [searchSpaceId, lazy, fetchDocuments, pageIndex, pageSize]); + + const refreshDocuments = useCallback(async (override?: { pageIndex?: number; pageSize?: number }) => { + setIsLoaded(false); + await fetchDocuments(override); + }, [fetchDocuments]); // Function to delete a document const deleteDocument = useCallback( @@ -118,6 +133,9 @@ export function useDocuments(searchSpaceId: number, lazy: boolean = false) { isLoaded, fetchDocuments, // Manual fetch function for lazy mode refreshDocuments, - deleteDocument, + deleteDocument, + hasMore, + pageIndex, + pageSize, }; }