diff --git a/.changeset/lucky-rockets-yawn.md b/.changeset/lucky-rockets-yawn.md new file mode 100644 index 0000000000..5edfba135b --- /dev/null +++ b/.changeset/lucky-rockets-yawn.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Disable role selection for the last admin in OrganizationProfile diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx index 98c1fbcaac..49c49f6bbc 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/ActiveMembersList.tsx @@ -15,9 +15,13 @@ export const ActiveMembersList = () => { organization, membershipList, membership: currentUserMembership, + memberships: adminMembers, ...rest } = useCoreOrganization({ membershipList: { offset: (page - 1) * ITEMS_PER_PAGE, limit: ITEMS_PER_PAGE }, + memberships: { + role: ['admin'], + }, }); const isAdmin = currentUserMembership?.role === 'admin'; @@ -32,14 +36,16 @@ export const ActiveMembersList = () => { return null; } - //TODO: calculate if user is the only admin - const canChangeOwnAdminRole = isAdmin && organization?.membersCount > 1; - const handleRoleChange = (membership: OrganizationMembershipResource) => (newRole: MembershipRole) => { if (!isAdmin) { return; } - return card.runAsync(membership.update({ role: newRole })).catch(err => handleError(err, [], card.setError)); + return card + .runAsync(async () => { + await membership.update({ role: newRole }); + await (adminMembers as any).unstable__mutate?.(); + }) + .catch(err => handleError(err, [], card.setError)); }; const handleRemove = (membership: OrganizationMembershipResource) => () => { @@ -47,7 +53,11 @@ export const ActiveMembersList = () => { return; } return card - .runAsync(membership.destroy()) + .runAsync(async () => { + const destroyedMembership = membership.destroy(); + await (adminMembers as any).unstable__mutate?.(); + return destroyedMembership; + }) .then(mutateSwrState) .catch(err => handleError(err, [], card.setError)); }; @@ -70,8 +80,9 @@ export const ActiveMembersList = () => { ))} /> @@ -81,15 +92,17 @@ export const ActiveMembersList = () => { const MemberRow = (props: { membership: OrganizationMembershipResource; onRemove: () => unknown; + adminCount: number; onRoleChange?: (role: MembershipRole) => unknown; }) => { - const { membership, onRemove, onRoleChange } = props; + const { membership, onRemove, onRoleChange, adminCount } = props; const card = useCardState(); const { membership: currentUserMembership } = useCoreOrganization(); const user = useCoreUser(); const isAdmin = currentUserMembership?.role === 'admin'; const isCurrentUser = user.id === membership.publicUserData.userId; + const isLastAdmin = adminCount <= 1 && membership.role === 'admin'; return ( @@ -112,7 +125,7 @@ const MemberRow = (props: { {isAdmin ? ( 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 1030acc862..b4ee975f7a 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 @@ -111,6 +111,15 @@ describe('OrganizationMembers', () => { lastName: 'Last2', createdAt: new Date('2022-01-01'), }), + createFakeMember({ + id: '3', + orgId: '1', + role: 'admin', + identifier: 'test_user3', + firstName: 'First3', + lastName: 'Last3', + createdAt: new Date('2022-01-01'), + }), ]; const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); @@ -120,17 +129,74 @@ describe('OrganizationMembers', () => { }); }); - fixtures.clerk.organization?.getMemberships.mockReturnValue(Promise.resolve(membersList)); - const { queryByText } = render(, { wrapper }); - expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled(); - expect(fixtures.clerk.organization?.getPendingInvitations).not.toHaveBeenCalled(); - expect(fixtures.clerk.organization?.getMembershipRequests).not.toHaveBeenCalled(); - expect(queryByText('test_user1')).toBeDefined(); - expect(queryByText('First1 Last1')).toBeDefined(); - expect(queryByText('Admin')).toBeDefined(); - expect(queryByText('test_user2')).toBeDefined(); - expect(queryByText('First2 Last2')).toBeDefined(); - expect(queryByText('Member')).toBeDefined(); + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 2, + }), + ); + + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce(Promise.resolve(membersList)); + + const { queryByText, queryAllByRole } = render(, { wrapper }); + + await waitFor(() => { + expect(fixtures.clerk.organization?.getMemberships).toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getPendingInvitations).not.toHaveBeenCalled(); + expect(fixtures.clerk.organization?.getMembershipRequests).not.toHaveBeenCalled(); + expect(queryByText('test_user1')).toBeInTheDocument(); + expect(queryByText('First1 Last1')).toBeInTheDocument(); + const buttons = queryAllByRole('button', { name: 'Admin' }); + buttons.forEach(button => expect(button).not.toBeDisabled()); + expect(queryByText('test_user2')).toBeInTheDocument(); + expect(queryByText('First2 Last2')).toBeInTheDocument(); + expect(queryByText('Member')).toBeInTheDocument(); + }); + }); + + it('Last admin cannot change to member', async () => { + const membersList: OrganizationMembershipResource[] = [ + createFakeMember({ + id: '1', + orgId: '1', + role: 'admin', + identifier: 'test_user1', + firstName: 'First1', + lastName: 'Last1', + createdAt: new Date('2022-01-01'), + }), + createFakeMember({ + id: '2', + orgId: '1', + role: 'basic_member', + identifier: 'test_user2', + firstName: 'First2', + lastName: 'Last2', + createdAt: new Date('2022-01-01'), + }), + ]; + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', id: '1', role: 'admin' }], + }); + }); + + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [], + total_count: 0, + }), + ); + + fixtures.clerk.organization?.getMemberships.mockReturnValueOnce(Promise.resolve(membersList)); + + const { queryByRole } = render(, { wrapper }); + + await waitFor(() => { + expect(queryByRole('button', { name: 'Admin' })).toBeDisabled(); + }); }); it('displays counter in requests tab', async () => {