From a544b29e6d875af810223e63d2418d8caab34c36 Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 15 Mar 2026 08:41:50 +0530 Subject: [PATCH] repositories: migrate table to tanstack --- bun.lock | 5 + package.json | 1 + .../repositories/RepositoryTable.tsx | 188 ++++++++++++++---- 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/bun.lock b/bun.lock index 26d661a..547c669 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.22", "@types/canvas-confetti": "^1.9.0", "@types/react": "^19.2.14", @@ -576,8 +577,12 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="], + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.22", "", { "dependencies": { "@tanstack/virtual-core": "3.13.22" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-EaOrBBJLi3M0bTMQRjGkxLXRw7Gizwntoy5E2Q2UnSbML7Mo2a1P/Hfkw5tw9FLzK62bj34Jl6VNbQfRV6eJcA=="], + "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.22", "", {}, "sha512-isuUGKsc5TAPDoHSbWTbl1SCil54zOS2MiWz/9GCWHPUQOvNTQx8qJEWC7UWR0lShhbK0Lmkcf0SZYxvch7G3g=="], "@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="], diff --git a/package.json b/package.json index 761f5a3..cfa0f84 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.2.1", + "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.22", "@types/canvas-confetti": "^1.9.0", "@types/react": "^19.2.14", diff --git a/src/components/repositories/RepositoryTable.tsx b/src/components/repositories/RepositoryTable.tsx index 23c0eb8..cfe8705 100644 --- a/src/components/repositories/RepositoryTable.tsx +++ b/src/components/repositories/RepositoryTable.tsx @@ -1,11 +1,19 @@ -import { useMemo, useRef } from "react"; -import Fuse from "fuse.js"; +import { useMemo, useRef, useState } from "react"; +import { + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + useReactTable, + type ColumnDef, + type ColumnFiltersState, + type SortingState, +} from "@tanstack/react-table"; import { useVirtualizer } from "@tanstack/react-virtual"; import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star, Lock, Ban, Check, ChevronDown, Trash2, X } from "lucide-react"; import { SiGithub, SiGitea } from "react-icons/si"; import type { Repository } from "@/lib/db/schema"; import { Button } from "@/components/ui/button"; -import { formatDate, formatLastSyncTime, getStatusColor } from "@/lib/utils"; +import { formatLastSyncTime } from "@/lib/utils"; import type { FilterParams } from "@/types/filter"; import { Skeleton } from "@/components/ui/skeleton"; import { useGiteaConfig } from "@/hooks/useGiteaConfig"; @@ -66,6 +74,7 @@ export default function RepositoryTable({ }: RepositoryTableProps) { const tableParentRef = useRef(null); const { giteaConfig } = useGiteaConfig(); + const [sorting, setSorting] = useState([]); const handleUpdateDestination = async (repoId: string, newDestination: string | null) => { // Call API to update repository destination @@ -120,40 +129,90 @@ export default function RepositoryTable({ return `${baseUrl}/${repoPath}`; }; - const hasAnyFilter = Object.values(filter).some( + const hasAnyFilter = [filter.searchTerm, filter.status, filter.organization, filter.owner].some( (val) => val?.toString().trim() !== "" ); - const filteredRepositories = useMemo(() => { - let result = repositories; - + const columnFilters = useMemo(() => { + const next: ColumnFiltersState = []; if (filter.status) { - result = result.filter((repo) => repo.status === filter.status); + next.push({ id: "status", value: filter.status }); } - if (filter.owner) { - result = result.filter((repo) => repo.owner === filter.owner); + next.push({ id: "owner", value: filter.owner }); } - if (filter.organization) { - result = result.filter( - (repo) => repo.organization === filter.organization - ); + next.push({ id: "organization", value: filter.organization }); } + return next; + }, [filter.status, filter.owner, filter.organization]); - if (filter.searchTerm) { - const fuse = new Fuse(result, { - keys: ["name", "fullName", "owner", "organization"], - threshold: 0.3, - }); - result = fuse.search(filter.searchTerm).map((res) => res.item); - } + const columns = useMemo[]>( + () => [ + { + id: "fullName", + accessorFn: (row) => row.fullName, + }, + { + id: "owner", + accessorFn: (row) => row.owner, + filterFn: "equalsString", + }, + { + id: "organization", + accessorFn: (row) => row.organization ?? "", + filterFn: "equalsString", + }, + { + id: "status", + accessorFn: (row) => row.status, + filterFn: "equalsString", + }, + { + id: "lastMirrored", + accessorFn: (row) => + row.lastMirrored ? new Date(row.lastMirrored).getTime() : 0, + enableGlobalFilter: false, + enableColumnFilter: false, + }, + ], + [] + ); - return result; - }, [repositories, filter]); + const table = useReactTable({ + data: repositories, + columns, + state: { + globalFilter: filter.searchTerm ?? "", + columnFilters, + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + }); + + const visibleRepositories = table + .getRowModel() + .rows.map((row) => row.original); + + const getSortDirection = (columnId: string) => + table.getColumn(columnId)?.getIsSorted() ?? false; + + const getSortLabel = (columnId: string) => { + const direction = getSortDirection(columnId); + if (direction === "asc") return " ↑"; + if (direction === "desc") return " ↓"; + return ""; + }; + + const toggleSort = (columnId: string) => { + table.getColumn(columnId)?.toggleSorting(); + }; const rowVirtualizer = useVirtualizer({ - count: filteredRepositories.length, + count: visibleRepositories.length, getScrollElement: () => tableParentRef.current, estimateSize: () => 65, overscan: 5, @@ -162,7 +221,11 @@ export default function RepositoryTable({ // Selection handlers const handleSelectAll = (checked: boolean) => { if (checked) { - const allIds = new Set(filteredRepositories.map(repo => repo.id).filter((id): id is string => !!id)); + const allIds = new Set( + visibleRepositories + .map((repo) => repo.id) + .filter((id): id is string => !!id) + ); onSelectionChange(allIds); } else { onSelectionChange(new Set()); @@ -179,8 +242,9 @@ export default function RepositoryTable({ onSelectionChange(newSelection); }; - const isAllSelected = filteredRepositories.length > 0 && - filteredRepositories.every(repo => repo.id && selectedRepoIds.has(repo.id)); + const isAllSelected = + visibleRepositories.length > 0 && + visibleRepositories.every((repo) => repo.id && selectedRepoIds.has(repo.id)); const isPartiallySelected = selectedRepoIds.size > 0 && !isAllSelected; // Mobile card layout for repository @@ -510,7 +574,7 @@ export default function RepositoryTable({ {hasAnyFilter && (
- Showing {filteredRepositories.length} of {repositories.length} repositories + Showing {visibleRepositories.length} of {repositories.length} repositories
@@ -572,16 +636,55 @@ export default function RepositoryTable({ />
- Repository -
-
Owner
-
- Organization +
- Last Mirrored + +
+
+ +
+
+ +
+
+
-
Status
Actions
@@ -601,13 +704,14 @@ export default function RepositoryTable({ position: "relative", }} > - {rowVirtualizer.getVirtualItems().map((virtualRow, index) => { - const repo = filteredRepositories[virtualRow.index]; + {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const repo = visibleRepositories[virtualRow.index]; + if (!repo) return null; const isLoading = loadingRepoIds.has(repo.id ?? ""); return (
{hasAnyFilter - ? `Showing ${filteredRepositories.length} of ${repositories.length} repositories` + ? `Showing ${visibleRepositories.length} of ${repositories.length} repositories` : `${repositories.length} ${repositories.length === 1 ? 'repository' : 'repositories'} total`}