Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 34 additions & 41 deletions src/components/miners/MinerOpenDiscoveryIssuesByRepo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useTheme,
} from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useMinerIssues } from '../../api';
import type { MinerIssue } from '../../api/models/Dashboard';
import {
Expand All @@ -29,6 +29,7 @@ import FilterButton from '../FilterButton';
import { ClearSearchAdornment } from '../common/ClearSearchAdornment';
import TablePagination from '../common/TablePagination';
import { tooltipSlotProps } from '../../theme';
import { useDataTableParams } from '../../hooks/useDataTableParams';

type IssueStatusFilter = 'all' | 'open' | 'solved' | 'closed';
type IssueSortField = 'number' | 'repository' | 'date';
Expand Down Expand Up @@ -101,7 +102,6 @@ const MinerOpenDiscoveryIssuesByRepo: React.FC<
> = ({ githubId }) => {
const theme = useTheme();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
// Pin the `since` cutoff per mount so React Query's cache key stays stable
// while the component is open. The 35-day scoring window slides on remount.
const since = useMemo(() => getScoringWindowStartIso(), []);
Expand All @@ -110,48 +110,35 @@ const MinerOpenDiscoveryIssuesByRepo: React.FC<
const [sortField, setSortField] = useState<IssueSortField>('date');
const [sortDir, setSortDir] = useState<SortDir>('desc');

const issueStatusParam = searchParams.get('issueStatus');
const statusFilter: IssueStatusFilter = isIssueStatusFilter(issueStatusParam)
? issueStatusParam
: 'all';

useEffect(() => {
setSearchQuery('');
setSortField('date');
setSortDir('desc');
}, [githubId]);

const page = parseInt(searchParams.get('issuePage') || '0', 10);
const setPage = useCallback(
(updater: number | ((prev: number) => number)) => {
const next = typeof updater === 'function' ? updater(page) : updater;
setSearchParams(
(prev) => {
const p = new URLSearchParams(prev);
if (next === 0) p.delete('issuePage');
else p.set('issuePage', String(next));
return p;
},
{ replace: true },
);
},
[page, setSearchParams],
const filtersConfig = useMemo(
() => ({
status: {
paramKey: 'issueStatus',
parse: (raw: string | null): IssueStatusFilter =>
isIssueStatusFilter(raw) ? raw : 'all',
serialize: (value: IssueStatusFilter): string | null =>
value === 'all' ? null : value,
},
}),
[],
);

// Status filter and page are URL-backed via the shared hook (`issueStatus`
// and `issuePage`). Sort stays local on miner tables.
const { page, setPage, filters, setFilter } = useDataTableParams<
IssueSortField,
{ status: IssueStatusFilter }
>({
sortKeys: [],
defaultSortKey: 'date',
paramKeys: { page: 'issuePage' },
filters: filtersConfig,
});

const statusFilter = filters.status;
const setStatusFilter = useCallback(
(next: IssueStatusFilter) => {
setSearchParams(
(prev) => {
const p = new URLSearchParams(prev);
if (next === 'all') p.delete('issueStatus');
else p.set('issueStatus', next);
p.delete('issuePage');
return p;
},
{ replace: true },
);
},
[setSearchParams],
(next: IssueStatusFilter) => setFilter('status', next),
[setFilter],
);

const handleSort = useCallback(
Expand All @@ -167,6 +154,12 @@ const MinerOpenDiscoveryIssuesByRepo: React.FC<
[sortField, setPage],
);

useEffect(() => {
setSearchQuery('');
setSortField('date');
setSortDir('desc');
}, [githubId]);

const filteredIssues = useMemo(
() => filterIssues(issues ?? [], { statusFilter, searchQuery }),
[issues, statusFilter, searchQuery],
Expand Down
56 changes: 34 additions & 22 deletions src/components/miners/MinerPRsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
ExpandMore as ExpandMoreIcon,
Search as SearchIcon,
} from '@mui/icons-material';
import { useSearchParams } from 'react-router-dom';
import { useMinerPRs, type CommitLog } from '../../api';
import {
filterPrs,
Expand All @@ -46,6 +45,7 @@ import {
serializePRKey,
useWatchlist,
} from '../../hooks/useWatchlist';
import { useDataTableParams } from '../../hooks/useDataTableParams';
import MinerTableRowsSelect from './MinerTableRowsSelect';
import TablePagination, {
getMinerExplorerPaging,
Expand Down Expand Up @@ -119,7 +119,6 @@ interface MinerPRsTableProps {

const MinerPRsTable: React.FC<MinerPRsTableProps> = ({ githubId }) => {
const theme = useTheme();
const [searchParams, setSearchParams] = useSearchParams();
const { data: prs, isLoading } = useMinerPRs(githubId);
const { isWatched } = useWatchlist('prs');
const [selectedAuthor, setSelectedAuthor] = useState<string | null>(null);
Expand All @@ -129,10 +128,39 @@ const MinerPRsTable: React.FC<MinerPRsTableProps> = ({ githubId }) => {
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(
() => new Set(),
);
const prStatusParam = searchParams.get('prStatus');
const statusFilter: PrStatusFilter = isPrStatusFilter(prStatusParam)
? prStatusParam
: 'all';

const filtersConfig = useMemo(
() => ({
status: {
paramKey: 'prStatus',
parse: (raw: string | null): PrStatusFilter =>
isPrStatusFilter(raw) ? raw : 'all',
serialize: (value: PrStatusFilter): string | null =>
value === 'all' ? null : value,
},
}),
[],
);

// Status filter is URL-backed (`prStatus`); pagination is owned by
// `useMinerExplorerPagination` below. Wiring `paramKeys.page` to
// `MINER_EXPLORER_PAGE_PARAM` lets the hook clear the same page slot
// when the filter changes.
const { filters, setFilter } = useDataTableParams<
PrSortField,
{ status: PrStatusFilter }
>({
sortKeys: [],
defaultSortKey: 'date',
paramKeys: { page: MINER_EXPLORER_PAGE_PARAM },
filters: filtersConfig,
});

const statusFilter = filters.status;
const setStatusFilter = useCallback(
(next: PrStatusFilter) => setFilter('status', next),
[setFilter],
);

useEffect(() => {
setSelectedAuthor(null);
Expand All @@ -142,22 +170,6 @@ const MinerPRsTable: React.FC<MinerPRsTableProps> = ({ githubId }) => {
setExpandedKeys(new Set());
}, [githubId]);

const setStatusFilter = useCallback(
(next: PrStatusFilter) => {
setSearchParams(
(prev) => {
const p = new URLSearchParams(prev);
if (next === 'all') p.delete('prStatus');
else p.set('prStatus', next);
p.delete(MINER_EXPLORER_PAGE_PARAM);
return p;
},
{ replace: true },
);
},
[setSearchParams],
);

const filteredPRs = useMemo(
() =>
filterPrs(prs ?? [], {
Expand Down
77 changes: 52 additions & 25 deletions src/components/repositories/RepositoryIssuesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
DataTable,
type DataTableColumn,
} from '../../components/common/DataTable';
import { formatTokenAmount, getLowerText, type SortOrder } from '../../utils';
import { formatTokenAmount, getLowerText } from '../../utils';
import { formatDate } from '../../utils/format';
import { ScrollAwareTooltip } from '../../components/common/ScrollAwareTooltip';
import {
Expand All @@ -33,6 +33,7 @@ import {
import { STATUS_COLORS, TEXT_OPACITY, scrollbarSx } from '../../theme';
import FilterButton from '../FilterButton';
import TablePagination from '../../components/common/TablePagination';
import { useDataTableParams } from '../../hooks/useDataTableParams';
import { TableSearchFilter } from './TableSearchFilter';

interface RepositoryIssuesTableProps {
Expand All @@ -47,6 +48,15 @@ type SortKey =
| 'created'
| 'closed';

const ISSUE_SORT_KEYS: readonly SortKey[] = [
'number',
'title',
'status',
'linkedPr',
'created',
'closed',
];

type RepoIssuesFilter = 'all' | 'open' | 'closed';

const isRepoIssuesFilter = (v: unknown): v is RepoIssuesFilter =>
Expand Down Expand Up @@ -83,15 +93,48 @@ const RepositoryIssuesTable: React.FC<RepositoryIssuesTableProps> = ({
'all',
isRepoIssuesFilter,
);
const [sortKey, setSortKey] = useState<SortKey>('number');
const [sortDirection, setSortDirection] = useState<SortOrder>('desc');
const [searchQuery, setSearchQuery] = useState('');

const {
sortField: sortKey,
sortOrder: sortDirection,
setSort: handleSort,
page,
setPage,
} = useDataTableParams<SortKey>({
sortKeys: ISSUE_SORT_KEYS,
defaultSortKey: 'number',
// String columns (title, status) feel natural ascending; others desc.
defaultOrderOverrides: { title: 'asc', status: 'asc' },
paramKeys: { sort: 'issueSort', order: 'issueDir', page: 'issuePage' },
});

// Search resets when navigating between repositories so the input does
// not leak across detail pages.
useEffect(() => {
setSearchQuery('');
}, [repositoryFullName]);

const [page, setPage] = useState(0);
// Filter and search are local; the hook handles page reset for URL-backed
// sort changes. Reset page here when the local status or search filter
// changes, gated on actual value change to avoid clobbering deep links.
const handleFilterChange = useCallback(
(next: RepoIssuesFilter) => {
if (next === filter) return;
setFilter(next);
setPage(0);
},
[filter, setFilter, setPage],
);

const handleSearchChange = useCallback(
(next: string) => {
if (next === searchQuery) return;
setSearchQuery(next);
setPage(0);
},
[searchQuery, setPage],
);

const counts = useMemo(() => {
if (!issues) return { total: 0, open: 0, closed: 0 };
Expand Down Expand Up @@ -161,11 +204,6 @@ const RepositoryIssuesTable: React.FC<RepositoryIssuesTableProps> = ({
return decorated.map((item) => item.issue);
}, [searchFilteredIssues, sortKey, sortDirection]);

// Reset to the first page whenever the result set changes underneath us.
useEffect(() => {
setPage(0);
}, [filter, sortKey, sortDirection, searchQuery]);

const totalPages = Math.ceil(sortedIssues.length / ISSUE_PAGE_SIZE);
const pagedIssues = useMemo(
() =>
Expand All @@ -176,18 +214,6 @@ const RepositoryIssuesTable: React.FC<RepositoryIssuesTableProps> = ({
[sortedIssues, page],
);

const handleSort = useCallback(
(key: SortKey) => {
if (sortKey === key) {
setSortDirection((prev) => (prev === 'asc' ? 'desc' : 'asc'));
return;
}
setSortKey(key);
setSortDirection(key === 'title' || key === 'status' ? 'asc' : 'desc');
},
[sortKey],
);

const handleRowClick = useCallback((issue: RepositoryIssue) => {
// Row navigates to GitHub in a new tab; using onRowClick (not getRowHref)
// keeps nested <a> cells valid HTML.
Expand Down Expand Up @@ -343,34 +369,35 @@ const RepositoryIssuesTable: React.FC<RepositoryIssuesTableProps> = ({
alignItems="center"
flexWrap="wrap"
useFlexGap
sx={{ rowGap: 1 }}
>
<FilterButton
label="All"
isActive={filter === 'all'}
onClick={() => setFilter('all')}
onClick={() => handleFilterChange('all')}
count={counts.total}
color={STATUS_COLORS.open}
activeTextColor="text.primary"
/>
<FilterButton
label="Open"
isActive={filter === 'open'}
onClick={() => setFilter('open')}
onClick={() => handleFilterChange('open')}
count={counts.open}
color={STATUS_COLORS.open}
activeTextColor="text.primary"
/>
<FilterButton
label="Closed"
isActive={filter === 'closed'}
onClick={() => setFilter('closed')}
onClick={() => handleFilterChange('closed')}
count={counts.closed}
color={STATUS_COLORS.merged}
activeTextColor="text.primary"
/>
<TableSearchFilter
value={searchQuery}
onChange={setSearchQuery}
onChange={handleSearchChange}
popoverTitle="Search issues"
placeholder="Search (#, title, author)…"
/>
Expand Down
Loading
Loading