diff --git a/src/app/(dashboard)/(modules)/database/models-new/page.tsx b/src/app/(dashboard)/(modules)/database/models-new/page.tsx index 460d329d..ace8e5c9 100644 --- a/src/app/(dashboard)/(modules)/database/models-new/page.tsx +++ b/src/app/(dashboard)/(modules)/database/models-new/page.tsx @@ -7,9 +7,45 @@ import { ModelsNewPage } from '@/components/database-new/models-page'; export const dynamic = 'force-dynamic'; -export default async function ModelsNew() { +const PAGE_SIZE = 10; + +type ModelsNewProps = { + searchParams: Promise<{ + page?: string; + search?: string; + owner?: string; + }>; +}; + +function parsePage(raw: string | undefined): number { + const n = Number(raw); + if (!Number.isFinite(n) || n < 1) return 1; + return Math.floor(n); +} + +function parseOwners(raw: string | undefined): string[] { + if (!raw) return []; + return raw + .split(',') + .map(s => s.trim()) + .filter(Boolean); +} + +export default async function ModelsNew(props: Readonly) { + const searchParams = await props.searchParams; + + const page = parsePage(searchParams.page); + const search = searchParams.search?.trim() || ''; + const owners = parseOwners(searchParams.owner); + const [schemasData, modulesData, dbTypeRes] = await Promise.all([ - getSchemas({ limit: 1000, enabled: true }), + getSchemas({ + skip: (page - 1) * PAGE_SIZE, + limit: PAGE_SIZE, + enabled: true, + search: search || undefined, + owner: owners.length > 0 ? owners : undefined, + }), getSchemaOwnerModules({ sort: 'name' }), getDatabaseType(), ]); @@ -20,6 +56,11 @@ export default async function ModelsNew() { modules={modulesData.modules} selectedModelId={null} databaseType={dbTypeRes.result} + count={schemasData.count} + page={page} + pageSize={PAGE_SIZE} + initialSearch={search} + initialOwners={owners} /> ); } diff --git a/src/components/database-new/models-list-table.tsx b/src/components/database-new/models-list-table.tsx index 12c2bbf7..1f5c2f25 100644 --- a/src/components/database-new/models-list-table.tsx +++ b/src/components/database-new/models-list-table.tsx @@ -1,7 +1,7 @@ 'use client'; import * as React from 'react'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { DeclaredSchema } from '@/lib/models/database'; import { Table, @@ -14,6 +14,15 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; +import { + Pagination, + PaginationContent, + PaginationEllipsis, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from '@/components/ui/pagination'; import { DropdownMenu, DropdownMenuContent, @@ -34,35 +43,114 @@ import { cn } from '@/lib/utils'; type ModelsListTableProps = { schemas: DeclaredSchema[]; modules: string[]; + count: number; + page: number; + pageSize: number; + initialSearch: string; + initialOwners: string[]; onCreateNew: () => void; onSelect: (modelId: string) => void; onDelete?: (modelId: string) => void; }; +const SEARCH_DEBOUNCE_MS = 300; + export function ModelsListTable({ schemas, modules, + count, + page, + pageSize, + initialSearch, + initialOwners, onCreateNew, onSelect, onDelete, }: ModelsListTableProps) { - const [search, setSearch] = React.useState(''); - - const filteredSchemas = React.useMemo(() => { - if (!search) return schemas; - const searchLower = search.toLowerCase(); - return schemas.filter( - schema => - schema.name.toLowerCase().includes(searchLower) || - schema.ownerModule.toLowerCase().includes(searchLower) - ); - }, [schemas, search]); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [searchInput, setSearchInput] = React.useState(initialSearch); + + // Keep the input in sync if the URL changes from outside (e.g. Back button). + React.useEffect(() => { + setSearchInput(initialSearch); + }, [initialSearch]); + + const setParams = React.useCallback( + (patch: Record) => { + const next = new URLSearchParams(searchParams.toString()); + for (const [key, value] of Object.entries(patch)) { + if (value === null || value === '') { + next.delete(key); + } else { + next.set(key, value); + } + } + const qs = next.toString(); + router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); + }, + [pathname, router, searchParams] + ); + + // Debounce search input -> URL. + React.useEffect(() => { + if (searchInput === initialSearch) return; + const handle = window.setTimeout(() => { + setParams({ search: searchInput || null, page: null }); + }, SEARCH_DEBOUNCE_MS); + return () => window.clearTimeout(handle); + }, [searchInput, initialSearch, setParams]); + + const selectedOwners = React.useMemo( + () => new Set(initialOwners), + [initialOwners] + ); + + const toggleOwner = (owner: string) => { + const next = new Set(selectedOwners); + if (next.has(owner)) { + next.delete(owner); + } else { + next.add(owner); + } + const ordered = modules.filter(m => next.has(m)); + setParams({ + owner: ordered.length > 0 ? ordered.join(',') : null, + page: null, + }); + }; + + const clearOwners = () => { + setParams({ owner: null, page: null }); + }; + + const clearAllFilters = () => { + setSearchInput(''); + setParams({ search: null, owner: null, page: null }); + }; + + const totalPages = Math.max(1, Math.ceil(count / pageSize)); + const hasFilters = initialSearch.length > 0 || selectedOwners.size > 0; + const showPagination = count > pageSize; + + const goToPage = (target: number) => { + const clamped = Math.min(Math.max(target, 1), totalPages); + if (clamped === page) return; + setParams({ page: clamped === 1 ? null : String(clamped) }); + }; const getFieldCount = (schema: DeclaredSchema) => { const fields = schema.compiledFields || schema.fields || {}; return Object.keys(fields).length; }; + const pageNumbers = React.useMemo( + () => buildPageList(page, totalPages), + [page, totalPages] + ); + return (
{/* Header */} @@ -72,7 +160,10 @@ export function ModelsListTable({

Database Models

- {schemas.length} models + + {count} {count === 1 ? 'model' : 'models'} + {hasFilters ? ' (filtered)' : ''} + ) : ( @@ -138,7 +251,7 @@ export function ModelsListTable({ - {filteredSchemas.map(schema => ( + {schemas.map(schema => ( )} + + {/* Pagination */} + {showPagination && ( +
+ + + + { + e.preventDefault(); + goToPage(page - 1); + }} + /> + + {pageNumbers.map((entry, idx) => + entry === 'ellipsis' ? ( + + + + ) : ( + + { + e.preventDefault(); + goToPage(entry); + }} + > + {entry} + + + ) + )} + + = totalPages} + className={cn( + page >= totalPages && 'pointer-events-none opacity-50' + )} + onClick={e => { + e.preventDefault(); + goToPage(page + 1); + }} + /> + + + +
+ )} ); } + +type OwnerChipProps = { + label: string; + isActive: boolean; + onClick: () => void; +}; + +function OwnerChip({ label, isActive, onClick }: OwnerChipProps) { + return ( + + ); +} + +/** + * Build a compact pagination list with leading/trailing ellipses, matching the + * common 1 ... 4 5 6 ... 20 pattern. Always includes first and last page. + */ +function buildPageList( + current: number, + total: number +): Array { + if (total <= 7) { + return Array.from({ length: total }, (_, i) => i + 1); + } + const pages: Array = [1]; + const start = Math.max(2, current - 1); + const end = Math.min(total - 1, current + 1); + if (start > 2) pages.push('ellipsis'); + for (let i = start; i <= end; i++) pages.push(i); + if (end < total - 1) pages.push('ellipsis'); + pages.push(total); + return pages; +} diff --git a/src/components/database-new/models-page.tsx b/src/components/database-new/models-page.tsx index b93856c2..0bee0eff 100644 --- a/src/components/database-new/models-page.tsx +++ b/src/components/database-new/models-page.tsx @@ -57,6 +57,16 @@ type ModelsNewPageProps = { initialTab?: ModelsNewTab; /** From GET /database/database-type; controls Mongo-only schema settings */ databaseType?: string; + /** Total number of schemas matching current filters (server-side). List view only. */ + count?: number; + /** Current 1-based page index for the list view. */ + page?: number; + /** Page size used by the server fetch for the list view. */ + pageSize?: number; + /** Initial search query parsed from the URL for the list view. */ + initialSearch?: string; + /** Initial owner filter parsed from the URL for the list view. */ + initialOwners?: string[]; }; export function ModelsNewPage({ @@ -68,6 +78,11 @@ export function ModelsNewPage({ authResource, initialTab = 'schema', databaseType, + count, + page, + pageSize, + initialSearch, + initialOwners, }: ModelsNewPageProps) { const router = useRouter(); const [activeTab, setActiveTab] = React.useState(() => @@ -119,6 +134,11 @@ export function ModelsNewPage({