Skip to content

Commit ced9757

Browse files
authored
fix(members): Allow searched members to leave orgs (#88086)
The search bar on the members list will filter the list down, and the check for other owners only has access to that filtered list, meaning it doesn't find another owner and thus disables the `Leave` button incorrectly. Instead, we'll fetch the owners separately (excluding invites), and if any of those results aren't the current user, we should allow them to leave. **Before** <img width="861" alt="image" src="https://github.com/user-attachments/assets/edf17aac-4251-4e01-8e19-fa2015862cd1" /> **After** <img width="852" alt="Screenshot 2025-03-27 at 11 08 00 AM" src="https://github.com/user-attachments/assets/055a5384-39b0-4113-bc05-dc0fb04c78fa" />
1 parent 6782da9 commit ced9757

File tree

2 files changed

+41
-7
lines changed

2 files changed

+41
-7
lines changed

static/app/views/settings/organizationMembers/organizationMembersList.spec.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,5 +665,29 @@ describe('OrganizationMembersList', function () {
665665

666666
(isDemoModeActive as jest.Mock).mockReset();
667667
});
668+
669+
it('allows you to leave as a member after searching', async function () {
670+
const searchQuery = MockApiClient.addMockResponse({
671+
url: '/organizations/org-slug/members/',
672+
method: 'GET',
673+
body: [currentUser],
674+
match: [MockApiClient.matchQuery({query: currentUser.name})],
675+
});
676+
const ownerQuery = MockApiClient.addMockResponse({
677+
url: '/organizations/org-slug/members/',
678+
method: 'GET',
679+
body: [members[2]],
680+
match: [MockApiClient.matchQuery({query: 'role:owner isInvited:false'})],
681+
});
682+
const searchRouter = RouterFixture({location: {query: {query: currentUser.name}}});
683+
render(<OrganizationMembersList />, {organization, router: searchRouter});
684+
renderGlobalModal({router});
685+
686+
expect(await screen.findByText('Members')).toBeInTheDocument();
687+
expect(searchQuery).toHaveBeenCalled();
688+
expect(ownerQuery).toHaveBeenCalled();
689+
const leaveButton = screen.getByRole('button', {name: 'Leave'});
690+
expect(leaveButton).toBeEnabled();
691+
});
668692
});
669693
});

static/app/views/settings/organizationMembers/organizationMembersList.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,11 @@ function OrganizationMembersList() {
8484
);
8585
const {data: currentMember} = useApiQuery<Member>(
8686
[`/organizations/${organization.slug}/members/me/`],
87-
{staleTime: 0}
87+
{staleTime: 30000}
8888
);
8989
const {
9090
data: members = [],
91-
isLoading: isLoadingMembers,
91+
isPending: isPendingMembers,
9292
refetch: refetchMembers,
9393
getResponseHeader,
9494
} = useApiQuery<Member[]>(
@@ -101,6 +101,19 @@ function OrganizationMembersList() {
101101
}),
102102
{staleTime: 0}
103103
);
104+
const {data: activeOwnerMembers = [], isPending: isPendingOwners} = useApiQuery<
105+
Member[]
106+
>(
107+
getMembersQueryKey({
108+
orgSlug: organization.slug,
109+
// Ignore search queries since this isn't displayed, it's okay not to paginate.
110+
// This is only used to determine if the current user is the only owner.
111+
// We also filter out active invites, so only active users are included.
112+
query: {query: 'role:owner isInvited:false'},
113+
}),
114+
{staleTime: 30000}
115+
);
116+
104117
const [invited, setInvited] = useState<{
105118
[memberId: string]: 'loading' | 'success' | null;
106119
}>({});
@@ -266,10 +279,7 @@ function OrganizationMembersList() {
266279
const currentUser = ConfigStore.get('user');
267280

268281
// Find out if current user is the only owner
269-
const isOnlyOwner = !members.find(
270-
({role, email, pending}) =>
271-
role === 'owner' && email !== currentUser.email && !pending
272-
);
282+
const isOnlyOwner = !activeOwnerMembers.some(({email}) => email !== currentUser.email);
273283

274284
// Only admins/owners can remove members
275285
const requireLink = !!authProvider && authProvider.require_link;
@@ -359,7 +369,7 @@ function OrganizationMembersList() {
359369
<Panel data-test-id="org-member-list">
360370
<MemberListHeader members={membersToShow} organization={organization} />
361371
<PanelBody>
362-
{isLoadingMembers ? (
372+
{isPendingMembers || isPendingOwners ? (
363373
<LoadingIndicator />
364374
) : (
365375
<Fragment>

0 commit comments

Comments
 (0)