From 961d5b01a1b6c3d8eeacc9a696b2a129d1164304 Mon Sep 17 00:00:00 2001 From: anderdc Date: Tue, 19 May 2026 18:21:20 -0500 Subject: [PATCH] fix(repositories): preserve weight precision, fix paginated next-page reset, drop repo-name tooltip - formatWeight helper rounds weights to up to 5 decimals and trims trailing zeros, replacing the toFixed(2) calls that were turning small shares like 0.0035 into "0.04" across the repositories table, leaderboard cards, repository details, search results, and watchlist views. - setFilter in useDataTableParams now only clears the page slot when the filter value actually changes. react-router rebuilds setSearchParams on every URL update, which propagated through useCallback deps and caused DebouncedSearchInput's effect to re-emit '', wiping the page param immediately after next-page was clicked (most visible at rows=12). - Remove the hover tooltip on the repository-name cell in the repositories table. --- src/components/leaderboard/RepositoryCard.tsx | 6 +-- .../leaderboard/TopRepositoriesTable.tsx | 39 +++++++++---------- .../repositories/RepositoryStats.tsx | 4 +- src/hooks/useDataTableParams.ts | 7 +++- src/pages/WatchlistPage.tsx | 8 ++-- src/pages/search/RepositoryTab.tsx | 4 +- src/utils/format.ts | 15 +++++++ 7 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/components/leaderboard/RepositoryCard.tsx b/src/components/leaderboard/RepositoryCard.tsx index 5592067c..5ea8a742 100644 --- a/src/components/leaderboard/RepositoryCard.tsx +++ b/src/components/leaderboard/RepositoryCard.tsx @@ -4,7 +4,7 @@ import { Avatar, Box, Card, Divider, Tooltip, Typography } from '@mui/material'; import { alpha } from '@mui/material/styles'; import { linkResetSx, useLinkBehavior } from '../common/linkBehavior'; import { WatchlistButton } from '../common'; -import { getRepositoryOwnerAvatarSrc } from '../../utils'; +import { formatWeight, getRepositoryOwnerAvatarSrc } from '../../utils'; import { RankIcon } from './RankIcon'; import { FONTS, @@ -220,7 +220,7 @@ export const RepositoryCard: React.FC = ({ @@ -228,7 +228,7 @@ export const RepositoryCard: React.FC = ({ diff --git a/src/components/leaderboard/TopRepositoriesTable.tsx b/src/components/leaderboard/TopRepositoriesTable.tsx index e19410f9..8754c79d 100644 --- a/src/components/leaderboard/TopRepositoriesTable.tsx +++ b/src/components/leaderboard/TopRepositoriesTable.tsx @@ -55,6 +55,7 @@ import { import { DebouncedSearchInput } from '../common/DebouncedSearchInput'; import { compareByWatchlist, + formatWeight, getRepositoryOwnerAvatarSrc, truncateText, } from '../../utils'; @@ -576,7 +577,7 @@ const TopRepositoriesTable: React.FC = ({ ${statRow('Issue score:', item.discoveryScore.toFixed(2))} ${statRow('Issues:', String(item.discoveryIssues))} ${statRow('Issue contributors:', String(item.discoveryContributors))} - ${statRow('Weight:', item.weight.toFixed(2))} + ${statRow('Weight:', formatWeight(item.weight))} `; @@ -632,7 +633,7 @@ const TopRepositoriesTable: React.FC = ({ fontSize: 11, formatter: (value: number) => { if (value >= 1000) return `${(value / 1000).toFixed(1)}k`; - if (sortColumn === 'weight') return value.toFixed(2); + if (sortColumn === 'weight') return formatWeight(value); if ( sortColumn === 'totalPRs' || sortColumn === 'contributors' || @@ -964,23 +965,21 @@ const TopRepositoriesTable: React.FC = ({ > {(owner[0] || '?').toUpperCase()} - - - {truncateText(repo.repository || '', 40)} - - + + {truncateText(repo.repository || '', 40)} + ); }, @@ -1000,7 +999,7 @@ const TopRepositoriesTable: React.FC = ({ color: 'text.primary', }} > - {repo.weight.toFixed(2)} + {formatWeight(repo.weight)} ), }, diff --git a/src/components/repositories/RepositoryStats.tsx b/src/components/repositories/RepositoryStats.tsx index 27ef174f..b9fe89af 100644 --- a/src/components/repositories/RepositoryStats.tsx +++ b/src/components/repositories/RepositoryStats.tsx @@ -15,7 +15,7 @@ import { useRepositoryConfig, } from '../../api'; import { RANK_COLORS, STATUS_COLORS } from '../../theme'; -import { formatTokenAmount } from '../../utils/format'; +import { formatTokenAmount, formatWeight } from '../../utils/format'; import { isMergedPr } from '../../utils/prStatus'; interface RepositoryStatsProps { @@ -127,7 +127,7 @@ const RepositoryStats: React.FC = ({ fontSize: '13px', }} > - {String(repository.config?.emissionShare ?? '')} + {formatWeight(repository.config?.emissionShare)} diff --git a/src/hooks/useDataTableParams.ts b/src/hooks/useDataTableParams.ts index d432baa7..21567430 100644 --- a/src/hooks/useDataTableParams.ts +++ b/src/hooks/useDataTableParams.ts @@ -288,9 +288,14 @@ export const useDataTableParams = < (prev) => { const next = new URLSearchParams(prev); const serialized = config.serialize(value); + const previous = prev.get(filterParamKey); + const valueChanged = previous !== (serialized ?? null); if (serialized === null) next.delete(filterParamKey); else next.set(filterParamKey, serialized); - if (resetPage) next.delete(paramKeys.page); + // Only reset the page slot when the filter value actually changed, + // so transient re-emissions (e.g. effect deps churning when other + // URL params update) don't clobber the user's current page. + if (resetPage && valueChanged) next.delete(paramKeys.page); return next; }, { replace: true }, diff --git a/src/pages/WatchlistPage.tsx b/src/pages/WatchlistPage.tsx index 290a907a..1266566c 100644 --- a/src/pages/WatchlistPage.tsx +++ b/src/pages/WatchlistPage.tsx @@ -106,7 +106,7 @@ import { } from '../utils/prStatus'; import { filterPrs, type PrStatusFilter } from '../utils/prTable'; import { getIssueStatusMeta } from '../utils/issueStatus'; -import { formatDate, formatTokenAmount } from '../utils/format'; +import { formatDate, formatTokenAmount, formatWeight } from '../utils/format'; import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; import theme, { CHART_COLORS, @@ -1191,7 +1191,7 @@ const repoColumns: DataTableColumn[] = [ cellSx: repoCellSx, renderCell: (repo) => ( - {parseFloat(String(repo.config?.emissionShare ?? 0)).toFixed(2)} + {formatWeight(repo.config?.emissionShare)} ), }, @@ -1752,7 +1752,7 @@ const ReposList: React.FC<{ itemKeys: string[] }> = ({ itemKeys }) => { ${statRow('Issue score:', item.discoveryScore.toFixed(2))} ${statRow('Issues:', String(item.discoveryIssues))} ${statRow('Issue contributors:', String(item.discoveryContributors))} - ${statRow('Weight:', item.weight.toFixed(2))} + ${statRow('Weight:', formatWeight(item.weight))} `; @@ -1789,7 +1789,7 @@ const ReposList: React.FC<{ itemKeys: string[] }> = ({ itemKeys }) => { axisLabel: { color: textColor, fontSize: 11, - formatter: (value: number) => value.toFixed(2), + formatter: (value: number) => formatWeight(value), }, splitLine: { lineStyle: { color: gridColor, type: 'dashed', opacity: 0.5 }, diff --git a/src/pages/search/RepositoryTab.tsx b/src/pages/search/RepositoryTab.tsx index 5d4c5600..040726cc 100644 --- a/src/pages/search/RepositoryTab.tsx +++ b/src/pages/search/RepositoryTab.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Box, Typography } from '@mui/material'; import { alpha, type Theme } from '@mui/material/styles'; import { getRankColors } from '../../components/leaderboard/types'; -import { getGithubAvatarSrc } from '../../utils'; +import { formatWeight, getGithubAvatarSrc } from '../../utils'; import { type DataTableColumn } from '../../components/common/DataTable'; import SearchResultsCard from './SearchResultsCard'; import { @@ -88,7 +88,7 @@ const repositoryColumns: DataTableColumn[] = [ header: 'Weight', width: '14%', align: 'right', - renderCell: (repo: RepoSearchData) => repo.weight.toFixed(2), + renderCell: (repo: RepoSearchData) => formatWeight(repo.weight), cellSx: { fontWeight: 600, }, diff --git a/src/utils/format.ts b/src/utils/format.ts index 4dd8f77c..a8acc125 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -22,6 +22,21 @@ export const formatTokenAmount = ( }); }; +/** + * Format a weight/share value: round to at most `maxDecimals` decimal places + * and trim trailing zeros (e.g. 0.00350 → "0.0035", 1.0 → "1", 1.5 → "1.5"). + */ +export const formatWeight = ( + value: string | number | null | undefined, + maxDecimals: number = 5, +): string => { + if (value === null || value === undefined) return '0'; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (!Number.isFinite(num)) return '0'; + const fixed = num.toFixed(maxDecimals); + return fixed.includes('.') ? fixed.replace(/\.?0+$/, '') : fixed; +}; + /** * Format a USD estimate value for display. *