fix(ui): address Greptile P2 — double setPage, stale page reset, filtered member count

- MemberTable: skip redundant setPage(p) when page size changes (only call setPage(1))
- TeamMemberTab: reset page synchronously in event handlers instead of useEffect,
  eliminating the one-render stale page; removes useEffect import
- MemberTable: accept optional totalMembers prop; show "X of Y Members" when a
  search/filter is active so users can see total team size without clearing the filter
- Add test: "1 of 2 Members" label appears when search narrows results

Co-Authored-By: Claude Sonnet 4 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bytechoreographer 2026-04-28 11:32:51 +08:00
parent 4b3a170d38
commit a3d496ad58
3 changed files with 39 additions and 9 deletions

View file

@ -25,6 +25,8 @@ export interface MemberTableProps {
/** Controlled current page (optional). When provided, pair with onPageChange. */
currentPage?: number;
onPageChange?: (page: number) => void;
/** Total unfiltered member count. When provided and differs from members.length, shows "X of Y Members". */
totalMembers?: number;
}
export default function MemberTable({
@ -43,6 +45,7 @@ export default function MemberTable({
defaultPageSize = 50,
currentPage: controlledPage,
onPageChange,
totalMembers,
}: MemberTableProps) {
const [internalPage, setInternalPage] = useState(1);
const [pageSize, setPageSize] = useState(defaultPageSize);
@ -54,6 +57,9 @@ export default function MemberTable({
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const safePage = Math.min(page, totalPages);
const pagedMembers = withPagination ? members.slice((safePage - 1) * pageSize, safePage * pageSize) : members;
const countDisplay = totalMembers !== undefined && total !== totalMembers
? `${total} of ${totalMembers} Members`
: `${total} Member${total !== 1 ? "s" : ""}`;
const baseColumns: ColumnsType<Member> = [
{
title: "User Email",
@ -128,7 +134,7 @@ export default function MemberTable({
<Space direction="vertical" style={{ width: "100%" }}>
<div className="flex items-center justify-between w-full">
<span className="text-sm text-gray-700">
{total} Member{total !== 1 ? "s" : ""}
{countDisplay}
</span>
{withPagination && (
<Pagination
@ -139,7 +145,14 @@ export default function MemberTable({
showSizeChanger
pageSizeOptions={["10", "25", "50", "100"]}
showQuickJumper
onChange={(p, ps) => { setPage(p); if (ps !== pageSize) { setPageSize(ps); setPage(1); } }}
onChange={(p, ps) => {
if (ps !== pageSize) {
setPageSize(ps);
setPage(1);
} else {
setPage(p);
}
}}
/>
)}
</div>

View file

@ -453,6 +453,25 @@ describe("TeamMembersComponent", () => {
expect(screen.getAllByText("user2@test.com").length).toBeGreaterThanOrEqual(1);
});
it("should show filtered count when search is active", async () => {
const user = userEvent.setup();
renderWithProviders(
<TeamMembersComponent
teamData={createMockTeamData()}
canEditTeam={false}
handleMemberDelete={mockHandleMemberDelete}
setSelectedEditMember={mockSetSelectedEditMember}
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
/>,
);
const searchInput = screen.getByPlaceholderText(/search by email or user id/i);
await user.type(searchInput, "user1");
expect(screen.getByText("1 of 2 Members")).toBeInTheDocument();
});
it("should hide action buttons when canEditTeam is false", () => {
renderWithProviders(
<TeamMembersComponent

View file

@ -9,7 +9,7 @@ import { Input, Select, Space, Tooltip, Typography } from "antd";
import type { ColumnsType } from "antd/es/table";
import MemberTable from "@/components/common_components/MemberTable";
import { TeamData, TeamMembership } from "./TeamInfo";
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
interface TeamMemberTabProps {
teamData: TeamData;
@ -33,9 +33,6 @@ export default function TeamMemberTab({
const [roleFilter, setRoleFilter] = useState<string | null>(null);
const [memberTablePage, setMemberTablePage] = useState(1);
// Reset to page 1 when filter/search changes — without remounting MemberTable
useEffect(() => { setMemberTablePage(1); }, [searchText, roleFilter]);
// O(1) lookup instead of O(n) find() per member per column
const membershipsMap = useMemo(
() =>
@ -226,8 +223,8 @@ export default function TeamMemberTab({
placeholder="Search by email or user ID"
allowClear
style={{ width: 280 }}
onChange={(e) => setSearchText(e.target.value)}
onSearch={(v) => setSearchText(v)}
onChange={(e) => { setSearchText(e.target.value); setMemberTablePage(1); }}
onSearch={(v) => { setSearchText(v); setMemberTablePage(1); }}
/>
<Select
placeholder="Filter by role"
@ -237,11 +234,12 @@ export default function TeamMemberTab({
{ value: "admin", label: "Admin" },
{ value: "non-admin", label: "Non-admin" },
]}
onChange={(v) => setRoleFilter(v ?? null)}
onChange={(v) => { setRoleFilter(v ?? null); setMemberTablePage(1); }}
/>
</Space>
<MemberTable
members={filteredMembers}
totalMembers={teamData.team_info.members_with_roles.length}
canEdit={canEditTeam}
withPagination
currentPage={memberTablePage}