diff --git a/.changeset/chatty-boats-tease.md b/.changeset/chatty-boats-tease.md new file mode 100644 index 0000000000..b8ce56e719 --- /dev/null +++ b/.changeset/chatty-boats-tease.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Fix incorrect pagination counters in data tables inside ``. diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx index ec45520e67..ea1417821b 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx @@ -8,11 +8,16 @@ import { useFetchRoles, useLocalizeCustomRoles } from '../../hooks/useFetchRoles import { handleError } from '../../utils'; import { DataTable, RoleSelect, RowContainer } from './MemberListTable'; +const membershipsParams = { + memberships: { + pageSize: 10, + keepPreviousData: true, + }, +}; + export const ActiveMembersList = () => { const card = useCardState(); - const { organization, memberships } = useCoreOrganization({ - memberships: true, - }); + const { organization, memberships } = useCoreOrganization(membershipsParams); const { options, isLoading: loadingRoles } = useFetchRoles(); @@ -44,6 +49,7 @@ export const ActiveMembersList = () => { onPageChange={n => memberships?.fetchPage?.(n)} itemCount={memberships?.count || 0} pageCount={memberships?.pageCount || 0} + itemsPerPage={membershipsParams.memberships.pageSize} isLoading={memberships?.isLoading || loadingRoles} emptyStateLocalizationKey={localizationKeys('organizationProfile.membersPage.detailsTitle__emptyRow')} headers={[ diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx index f6a96ec4d7..504bbf0431 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InvitedMembersList.tsx @@ -7,11 +7,16 @@ import { useLocalizeCustomRoles } from '../../hooks/useFetchRoles'; import { handleError } from '../../utils'; import { DataTable, RowContainer } from './MemberListTable'; +const invitationsParams = { + invitations: { + pageSize: 10, + keepPreviousData: true, + }, +}; + export const InvitedMembersList = () => { const card = useCardState(); - const { organization, invitations } = useCoreOrganization({ - invitations: true, - }); + const { organization, invitations } = useCoreOrganization(invitationsParams); if (!organization) { return null; @@ -32,6 +37,7 @@ export const InvitedMembersList = () => { onPageChange={invitations?.fetchPage || (() => null)} itemCount={invitations?.count || 0} pageCount={invitations?.pageCount || 0} + itemsPerPage={invitationsParams.invitations.pageSize} isLoading={invitations?.isLoading} emptyStateLocalizationKey={localizationKeys('organizationProfile.membersPage.invitationsTab.table__emptyRow')} headers={[ diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/MemberListTable.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/MemberListTable.tsx index 60187d8493..efdd2d5207 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/MemberListTable.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/MemberListTable.tsx @@ -15,16 +15,9 @@ type MembersListTableProps = { onPageChange: (page: number) => void; itemCount: number; emptyStateLocalizationKey: LocalizationKey; -} & ( - | { - itemsPerPage?: never; - pageCount: number; - } - | { - itemsPerPage: number; - pageCount?: never; - } -); + pageCount: number; + itemsPerPage: number; +}; export const DataTable = (props: MembersListTableProps) => { const { @@ -35,13 +28,12 @@ export const DataTable = (props: MembersListTableProps) => { isLoading, itemCount, itemsPerPage, - pageCount: pageCountProp, + pageCount, emptyStateLocalizationKey, } = props; - const pageCount = rows.length !== 0 ? pageCountProp ?? Math.ceil(itemCount / itemsPerPage) : 1; - const startRowIndex = (page - 1) * rows.length; - const endRowIndex = Math.min(page * rows.length); + const startingRow = itemCount > 0 ? Math.max(0, (page - 1) * itemsPerPage) + 1 : 0; + const endingRow = Math.min(page * itemsPerPage, itemCount); return ( { siblingCount={1} rowInfo={{ allRowsCount: itemCount, - startingRow: rows.length ? startRowIndex + 1 : startRowIndex, - endingRow: endRowIndex, + startingRow, + endingRow, }} /> } diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx index e494b8f4a3..709cbd7043 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/RequestToJoinList.tsx @@ -6,16 +6,16 @@ import { useCardState, UserPreview, withCardStateProvider } from '../../elements import { handleError } from '../../utils'; import { DataTable, RowContainer } from './MemberListTable'; -const ITEMS_PER_PAGE = 10; - const membershipRequestsParams = { - pageSize: ITEMS_PER_PAGE, + membershipRequests: { + pageSize: 10, + keepPreviousData: true, + }, }; + export const RequestToJoinList = () => { const card = useCardState(); - const { organization, membershipRequests } = useCoreOrganization({ - membershipRequests: membershipRequestsParams, - }); + const { organization, membershipRequests } = useCoreOrganization(membershipRequestsParams); if (!organization) { return null; @@ -25,8 +25,9 @@ export const RequestToJoinList = () => { null)} - itemCount={membershipRequests?.count ?? 0} - itemsPerPage={ITEMS_PER_PAGE} + itemCount={membershipRequests?.count || 0} + pageCount={membershipRequests?.pageCount || 0} + itemsPerPage={membershipRequestsParams.membershipRequests.pageSize} isLoading={membershipRequests?.isLoading} emptyStateLocalizationKey={localizationKeys('organizationProfile.membersPage.requestsTab.table__emptyRow')} headers={[ @@ -49,9 +50,7 @@ const RequestRow = withCardStateProvider( (props: { request: OrganizationMembershipRequestResource; onError: ReturnType['setError'] }) => { const { request, onError } = props; const card = useCardState(); - const { membership, membershipRequests } = useCoreOrganization({ - membershipRequests: membershipRequestsParams, - }); + const { membership, membershipRequests } = useCoreOrganization(membershipRequestsParams); const onAccept = () => { if (!membership || !membershipRequests) { diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx index 7309ab1b94..21cf5e1cc2 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationMembers.test.tsx @@ -172,6 +172,76 @@ describe('OrganizationMembers', () => { }); }); + it('display pagination counts for 2 pages', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + organization_memberships: [{ name: 'Org1', id: '1' }], + }); + }); + + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 14, + }), + ); + + fixtures.clerk.organization?.getRoles.mockRejectedValue(null); + + const { queryByText, getByText } = render(, { wrapper }); + + await waitFor(async () => { + expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getInvitations).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getMembershipRequests).not.toHaveBeenCalled(); + + expect(queryByText(/displaying/i)).toBeInTheDocument(); + + expect(queryByText(/1 – 10/i)).toBeInTheDocument(); + expect(queryByText(/of/i)).toBeInTheDocument(); + expect(queryByText(/^14/i)).toBeInTheDocument(); + }); + + await act(async () => await userEvent.click(getByText(/next/i))); + + await waitFor(async () => { + expect(queryByText(/11 – 14/i)).toBeInTheDocument(); + expect(queryByText(/of/i)).toBeInTheDocument(); + expect(queryByText(/^14/i)).toBeInTheDocument(); + }); + }); + + it('display pagination counts for 1 page', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.com'], + organization_memberships: [{ name: 'Org1', id: '1' }], + }); + }); + + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 5, + }), + ); + + fixtures.clerk.organization?.getRoles.mockRejectedValue(null); + + const { queryByText, getByText } = render(, { wrapper }); + + await waitFor(async () => { + expect(queryByText(/displaying/i)).toBeInTheDocument(); + expect(queryByText(/1 – 5/i)).toBeInTheDocument(); + expect(queryByText(/of/i)).toBeInTheDocument(); + expect(queryByText(/^5/i)).toBeInTheDocument(); + expect(getByText(/next/i)).toBeDisabled(); + }); + }); + // TODO: Bring this test back once we can determine the last admin by permissions. it.skip('Last admin cannot change to member', async () => { const membersList: OrganizationMembershipResource[] = [ diff --git a/packages/shared/src/react/hooks/useOrganization.tsx b/packages/shared/src/react/hooks/useOrganization.tsx index a104d240d6..15a6f776bd 100644 --- a/packages/shared/src/react/hooks/useOrganization.tsx +++ b/packages/shared/src/react/hooks/useOrganization.tsx @@ -1,9 +1,11 @@ import type { + ClerkPaginatedResponse, ClerkPaginationParams, GetDomainsParams, GetInvitationsParams, GetMembershipRequestParams, GetMembershipsParams, + GetMembersParams, GetPendingInvitationsParams, OrganizationDomainResource, OrganizationInvitationResource, @@ -11,8 +13,6 @@ import type { OrganizationMembershipResource, OrganizationResource, } from '@clerk/types'; -import type { ClerkPaginatedResponse } from '@clerk/types'; -import type { GetMembersParams } from '@clerk/types'; import { deprecated } from '../../deprecated'; import { useSWR } from '../clerk-swr';