Browse Source

ref(ui): Update members page design (#15678)

Megan Heskett 5 years ago
parent
commit
e8e3207b72

+ 1 - 1
src/sentry/conf/server.py

@@ -1247,7 +1247,7 @@ SENTRY_ROLES = (
     },
     {
         "id": "owner",
-        "name": "Organization Owner",
+        "name": "Owner",
         "desc": "Unrestricted access to the organization, its data, and its settings. Can add, modify, and delete projects and members, as well as make billing and plan changes.",
         "is_global": True,
         "scopes": set(

+ 24 - 12
src/sentry/static/sentry/app/views/settings/organizationMembers/inviteRequestRow.tsx

@@ -55,17 +55,17 @@ const InviteRequestRow = ({
         </h5>
         {inviteRequest.inviteStatus === 'requested_to_be_invited' ? (
           inviteRequest.inviterName && (
-            <Tooltip
-              title={t(
-                'An existing member has asked to invite this user to your organization'
-              )}
-            >
-              <Description>
+            <Description>
+              <Tooltip
+                title={t(
+                  'An existing member has asked to invite this user to your organization'
+                )}
+              >
                 {tct('Requested by [inviterName]', {
                   inviterName: inviteRequest.inviterName,
                 })}
-              </Description>
-            </Tooltip>
+              </Tooltip>
+            </Description>
           )
         ) : (
           <Tooltip title={t('This user has asked to join your organization.')}>
@@ -73,13 +73,15 @@ const InviteRequestRow = ({
           </Tooltip>
         )}
       </div>
-      <RoleSelectControl
+
+      <StyledRoleSelectControl
         name="role"
         disableUnallowed
         onChange={r => onUpdate({role: r.value})}
         value={inviteRequest.role}
         roles={allRoles}
       />
+
       <TeamSelectControl
         name="teams"
         placeholder={t('Add to teams...')}
@@ -92,6 +94,7 @@ const InviteRequestRow = ({
         multiple
         clearable
       />
+
       <ButtonGroup>
         <Confirm
           onConfirm={sendInvites}
@@ -168,22 +171,31 @@ const JoinRequestIndicator = styled(Tag)`
 
 const StyledPanelItem = styled(PanelItem)`
   display: grid;
-  grid-template-columns: minmax(200px, auto) minmax(100px, 140px) 220px max-content;
+  grid-template-columns: minmax(150px, auto) minmax(100px, 140px) 220px max-content;
   grid-gap: ${space(2)};
   align-items: center;
 `;
 
 const UserName = styled('div')`
   font-size: ${p => p.theme.fontSizeLarge};
-  word-break: break-all;
+  overflow: hidden;
+  text-overflow: ellipsis;
 `;
 
 const Description = styled('div')`
+  display: block;
   color: ${p => p.theme.gray3};
   font-size: 14px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const StyledRoleSelectControl = styled(RoleSelectControl)`
+  max-width: 140px;
 `;
 
 const TeamSelectControl = styled(SelectControl)`
+  max-width: 220px;
   .Select-value-label {
     max-width: 150px;
     word-break: break-all;
@@ -192,7 +204,7 @@ const TeamSelectControl = styled(SelectControl)`
 
 const ButtonGroup = styled('div')`
   display: inline-grid;
-  grid-template-columns: auto auto;
+  grid-template-columns: repeat(2, max-content);
   grid-gap: ${space(1)};
 `;
 

+ 87 - 79
src/sentry/static/sentry/app/views/settings/organizationMembers/organizationMemberRow.jsx

@@ -1,4 +1,3 @@
-import {Box} from 'grid-emotion';
 import PropTypes from 'prop-types';
 import React from 'react';
 import styled from 'react-emotion';
@@ -12,18 +11,9 @@ import InlineSvg from 'app/components/inlineSvg';
 import Link from 'app/components/links/link';
 import LoadingIndicator from 'app/components/loadingIndicator';
 import SentryTypes from 'app/sentryTypes';
-import Tooltip from 'app/components/tooltip';
+import space from 'app/styles/space';
 import recreateRoute from 'app/utils/recreateRoute';
 
-const UserName = styled(Link)`
-  font-size: 16px;
-`;
-
-const Email = styled('div')`
-  color: ${p => p.theme.gray3};
-  font-size: 14px;
-`;
-
 export default class OrganizationMemberRow extends React.PureComponent {
   static propTypes = {
     routes: PropTypes.array,
@@ -41,10 +31,7 @@ export default class OrganizationMemberRow extends React.PureComponent {
     status: PropTypes.oneOf(['', 'loading', 'success', 'error']),
   };
 
-  constructor(...args) {
-    super(...args);
-    this.state = {busy: false};
-  }
+  state = {busy: false};
 
   handleRemove = e => {
     const {onRemove} = this.props;
@@ -69,13 +56,13 @@ export default class OrganizationMemberRow extends React.PureComponent {
   };
 
   handleSendInvite = e => {
-    const {onSendInvite} = this.props;
+    const {onSendInvite, member} = this.props;
 
     if (typeof onSendInvite !== 'function') {
       return;
     }
 
-    onSendInvite(this.props.member, e);
+    onSendInvite(member, e);
   };
 
   render() {
@@ -106,70 +93,61 @@ export default class OrganizationMemberRow extends React.PureComponent {
     const detailsUrl = recreateRoute(id, {routes, params});
     const isInviteSuccessful = status === 'success';
     const isInviting = status === 'loading';
-    const canResend = !expired && canAddMembers && (pending || needsSso);
+    const showResendButton = pending || needsSso;
 
     return (
-      <PanelItem align="center" p={0} py={2}>
-        <Box pl={2}>
+      <StyledPanelItem data-test-id={email}>
+        <MemberHeading>
           <Avatar size={32} user={user ? user : {id: email, email}} />
-        </Box>
+          <MemberDescription to={detailsUrl}>
+            <h5 style={{margin: '0 0 3px'}}>
+              <UserName>{name}</UserName>
+            </h5>
+            <Email>{email}</Email>
+          </MemberDescription>
+        </MemberHeading>
 
-        <Box pl={1} pr={2} flex="1">
-          <h5 style={{margin: '0 0 3px'}}>
-            <UserName to={detailsUrl}>{name}</UserName>
-          </h5>
-          <Email>{email}</Email>
-        </Box>
-
-        <Box px={2} w={180}>
-          {needsSso || pending ? (
-            <div>
-              <div>
-                {expired ? (
-                  <strong>{t('Expired')}</strong>
-                ) : pending ? (
-                  <strong>{t('Invited')}</strong>
-                ) : (
-                  <strong>{t('Missing SSO Link')}</strong>
-                )}
-              </div>
+        <div data-test-id="member-role">
+          {pending ? (
+            <InvitedRole>
+              <InlineSvg src="icon-mail" size="1.2em" />
+              {expired ? t('Expired Invite') : tct('Invited [roleName]', {roleName})}
+            </InvitedRole>
+          ) : (
+            roleName
+          )}
+        </div>
 
+        <div data-test-id="member-status">
+          {showResendButton ? (
+            <React.Fragment>
               {isInviting && (
-                <div style={{padding: '4px 0 3px'}}>
+                <LoadingContainer>
                   <LoadingIndicator mini />
-                </div>
+                </LoadingContainer>
               )}
               {isInviteSuccessful && <span>Sent!</span>}
-              {!isInviting && !isInviteSuccessful && canResend && (
-                <ResendInviteButton
+              {!isInviting && !isInviteSuccessful && (
+                <Button
+                  disabled={!canAddMembers}
                   priority="primary"
-                  size="xsmall"
+                  size="small"
                   onClick={this.handleSendInvite}
-                  data-test-id="resend-invite"
                 >
-                  {t('Resend invite')}
-                </ResendInviteButton>
+                  {pending ? t('Resend invite') : t('Resend SSO link')}
+                </Button>
               )}
-            </div>
+            </React.Fragment>
           ) : (
-            <div>
-              {!has2fa ? (
-                <Tooltip title={t('Two-factor auth not enabled')}>
-                  <NoTwoFactorIcon />
-                </Tooltip>
-              ) : (
-                <HasTwoFactorIcon />
-              )}
-            </div>
+            <AuthStatus>
+              <AuthIcon has2fa={has2fa} />
+              {has2fa ? t('2FA Enabled') : t('2FA Not Enabled')}
+            </AuthStatus>
           )}
-        </Box>
-
-        <Box px={2} w={140}>
-          {roleName}
-        </Box>
+        </div>
 
         {showRemoveButton || showLeaveButton ? (
-          <Box px={2} w={140}>
+          <div>
             {showRemoveButton && canRemoveMember && (
               <Confirm
                 message={tct('Are you sure you want to remove [name] from [orgName]?', {
@@ -234,27 +212,57 @@ export default class OrganizationMemberRow extends React.PureComponent {
                 {t('Leave')}
               </Button>
             )}
-          </Box>
+          </div>
         ) : null}
-      </PanelItem>
+      </StyledPanelItem>
     );
   }
 }
 
-const NoTwoFactorIcon = styled(props => (
-  <InlineSvg {...props} src="icon-circle-exclamation" />
-))`
-  color: ${p => p.theme.error};
-  font-size: 18px;
+const StyledPanelItem = styled(PanelItem)`
+  display: grid;
+  grid-template-columns: minmax(150px, 2fr) minmax(90px, 1fr) minmax(120px, 1fr) 90px;
+  grid-gap: ${space(2)};
+  align-items: center;
 `;
-const HasTwoFactorIcon = styled(props => (
-  <InlineSvg {...props} src="icon-circle-check" />
-))`
-  color: ${p => p.theme.success};
-  font-size: 18px;
+
+const Section = styled('div')`
+  display: inline-grid;
+  grid-template-columns: max-content auto;
+  grid-gap: ${space(1)};
+  align-items: center;
+`;
+
+const MemberHeading = styled(Section)``;
+const MemberDescription = styled(Link)`
+  overflow: hidden;
+`;
+
+const UserName = styled('div')`
+  display: block;
+  font-size: ${p => p.theme.fontSizeLarge};
+  overflow: hidden;
+  text-overflow: ellipsis;
 `;
 
-const ResendInviteButton = styled(Button)`
-  padding: 0 4px;
-  margin-top: 2px;
+const Email = styled('div')`
+  color: ${p => p.theme.gray4};
+  font-size: ${p => p.theme.fontSizeMedium};
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+const InvitedRole = styled(Section)``;
+const LoadingContainer = styled('div')`
+  margin-top: 0;
+  margin-bottom: ${space(1.5)};
+`;
+
+const AuthStatus = styled(Section)``;
+const AuthIcon = styled(p => (
+  <InlineSvg {...p} src={p.has2fa ? 'icon-circle-check' : 'icon-circle-exclamation'} />
+))`
+  color: ${p => (p.has2fa ? p.theme.success : p.theme.error)};
+  font-size: 18px;
+  margin-bottom: 1px;
 `;

+ 9 - 6
src/sentry/static/sentry/app/views/settings/organizationMembers/organizationMembersList.tsx

@@ -11,6 +11,7 @@ import Pagination from 'app/components/pagination';
 import routeTitleGen from 'app/utils/routeTitle';
 import SentryTypes from 'app/sentryTypes';
 import {redirectToRemainingOrganization} from 'app/actionCreators/organizations';
+import {resendMemberInvite} from 'app/actionCreators/members';
 import withOrganization from 'app/utils/withOrganization';
 
 import OrganizationMemberRow from './organizationMemberRow';
@@ -130,16 +131,17 @@ class OrganizationMembersList extends AsyncView<Props, State> {
     addSuccessMessage(tct('You left [orgName]', {orgName}));
   };
 
-  handleSendInvite = async ({id}: Member) => {
+  handleSendInvite = async ({id, expired}) => {
     this.setState(state => ({
       invited: {...state.invited, [id]: 'loading'},
     }));
 
     try {
-      await this.api.requestPromise(
-        `/organizations/${this.props.params.orgId}/members/${id}/`,
-        {method: 'PUT', data: {reinvite: 1}}
-      );
+      await resendMemberInvite(this.api, {
+        orgId: this.props.params.orgId,
+        memberId: id,
+        regenerate: expired,
+      });
     } catch {
       this.setState(state => ({invited: {...state.invited, [id]: null}}));
       addErrorMessage(t('Error sending invite'));
@@ -160,7 +162,8 @@ class OrganizationMembersList extends AsyncView<Props, State> {
 
     // Find out if current user is the only owner
     const isOnlyOwner = !members.find(
-      ({role, email}) => role === 'owner' && email !== currentUser.email
+      ({role, email, pending}) =>
+        role === 'owner' && email !== currentUser.email && !pending
     );
 
     // Only admins/owners can remove members

+ 0 - 1
src/sentry/static/sentry/app/views/settings/organizationMembers/organizationMembersWrapper.tsx

@@ -160,7 +160,6 @@ class OrganizationMembersWrapper extends AsyncView<Props, State> {
           </TextContainer>
           <Button
             priority="primary"
-            size="small"
             onClick={() => openInviteMembersModal({source: 'members_settings'})}
             disabled={!this.canOpeninviteModal}
             title={

+ 1 - 1
tests/acceptance/test_create_organization_member.py

@@ -33,4 +33,4 @@ class CreateOrganizationMemberTest(AcceptanceTestCase):
 
         # Verify new member on member list.
         self.browser.wait_until_test_id("org-member-list")
-        assert self.browser.find_element_by_link_text(email)
+        assert self.browser.element_exists_by_test_id(email)

+ 1 - 1
tests/acceptance/test_member_list.py

@@ -27,4 +27,4 @@ class ListOrganizationMembersTest(AcceptanceTestCase):
         self.browser.wait_until_not(".loading-indicator")
         self.browser.snapshot(name="list organization members")
         assert self.browser.element_exists_by_aria_label("Invite Members")
-        assert self.browser.element_exists_by_test_id("resend-invite")
+        assert self.browser.element_exists_by_aria_label("Resend invite")

+ 1 - 7
tests/js/sentry-test/fixtures/members.js

@@ -13,13 +13,7 @@ export function Members(params = []) {
       flags: {
         'sso:linked': false,
       },
-      user: {
-        id: '2',
-        has2fa: false,
-        name: 'Sentry 2 Name',
-        email: 'sentry2@test.com',
-        username: 'Sentry 2 Username',
-      },
+      user: null,
     },
     {
       id: '3',

+ 55 - 61
tests/js/spec/views/settings/organizationMembers/organizationMemberRow.spec.jsx

@@ -1,18 +1,15 @@
 import React from 'react';
-import {shallow} from 'sentry-test/enzyme';
+import {shallow, mountWithTheme} from 'sentry-test/enzyme';
 
 import OrganizationMemberRow from 'app/views/settings/organizationMembers/organizationMemberRow';
 
-const findWithText = (wrapper, text) =>
-  wrapper.filterWhere(n => n.prop('children') && n.prop('children').includes(text));
-
 describe('OrganizationMemberRow', function() {
   const member = {
     id: '1',
     email: '',
     name: '',
-    role: '',
-    roleName: '',
+    role: 'member',
+    roleName: 'Member',
     pending: false,
     flags: {
       'sso:linked': false,
@@ -45,6 +42,11 @@ describe('OrganizationMemberRow', function() {
     onLeave: () => {},
   };
 
+  const resendButton = 'StyledButton[aria-label="Resend invite"]';
+  const resendSsoButton = 'StyledButton[aria-label="Resend SSO link"]';
+  const leaveButton = 'StyledButton[aria-label="Leave"]';
+  const removeButton = 'StyledButton[aria-label="Remove"]';
+
   beforeEach(function() {});
 
   it('does not have 2fa warning if user has 2fa', function() {
@@ -60,8 +62,7 @@ describe('OrganizationMemberRow', function() {
         }}
       />
     );
-    expect(wrapper.find('NoTwoFactorIcon')).toHaveLength(0);
-    expect(wrapper.find('HasTwoFactorIcon')).toHaveLength(1);
+    expect(wrapper.find('AuthIcon').prop('has2fa')).toBe(true);
   });
 
   it('has 2fa warning if user does not have 2fa enabled', function() {
@@ -77,8 +78,7 @@ describe('OrganizationMemberRow', function() {
         }}
       />
     );
-    expect(wrapper.find('NoTwoFactorIcon')).toHaveLength(1);
-    expect(wrapper.find('HasTwoFactorIcon')).toHaveLength(0);
+    expect(wrapper.find('AuthIcon').prop('has2fa')).toBe(false);
   });
 
   describe('Pending user', function() {
@@ -91,7 +91,7 @@ describe('OrganizationMemberRow', function() {
     };
 
     it('has "Invited" status, no "Resend Invite"', function() {
-      const wrapper = shallow(
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow
           {...props}
           member={{
@@ -101,50 +101,49 @@ describe('OrganizationMemberRow', function() {
         />
       );
 
-      expect(findWithText(wrapper.find('strong'), 'Invited')).toHaveLength(1);
-
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
+      expect(wrapper.find('[data-test-id="member-role"]').text()).toBe('Invited Member');
+      expect(wrapper.find(resendButton).prop('disabled')).toBe(true);
     });
 
     it('has "Resend Invite" button only if `canAddMembers` is true', function() {
-      const wrapper = shallow(<OrganizationMemberRow {...props} canAddMembers />);
-
-      expect(findWithText(wrapper.find('strong'), 'Invited')).toHaveLength(1);
+      const wrapper = mountWithTheme(<OrganizationMemberRow {...props} canAddMembers />);
 
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(1);
+      expect(wrapper.find('[data-test-id="member-role"]').text()).toBe('Invited Member');
+      expect(wrapper.find(resendButton).prop('disabled')).toBe(false);
     });
 
     it('has the right inviting states', function() {
-      let wrapper = shallow(<OrganizationMemberRow {...props} canAddMembers />);
+      let wrapper = mountWithTheme(<OrganizationMemberRow {...props} canAddMembers />);
 
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(1);
+      expect(wrapper.find(resendButton).exists()).toBe(true);
 
-      wrapper = shallow(
+      wrapper = mountWithTheme(
         <OrganizationMemberRow {...props} canAddMembers status="loading" />
       );
 
       // Should have loader
       expect(wrapper.find('LoadingIndicator')).toHaveLength(1);
       // No Resend Invite button
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
+      expect(wrapper.find(resendButton).exists()).toBe(false);
 
-      wrapper = shallow(
+      wrapper = mountWithTheme(
         <OrganizationMemberRow {...props} canAddMembers status="success" />
       );
 
       // Should have loader
       expect(wrapper.find('LoadingIndicator')).toHaveLength(0);
       // No Resend Invite button
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
-      expect(findWithText(wrapper.find('span'), 'Sent!')).toHaveLength(1);
+      expect(wrapper.find(resendButton).exists()).toBe(false);
+      expect(wrapper.find('[data-test-id="member-status"]').text()).toBe('Sent!');
     });
   });
 
   describe('Expired user', function() {
     it('has "Expired" status', function() {
-      const wrapper = shallow(
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow
           {...defaultProps}
+          canAddMembers
           member={{
             ...member,
             pending: true,
@@ -153,8 +152,8 @@ describe('OrganizationMemberRow', function() {
         />
       );
 
-      expect(findWithText(wrapper.find('strong'), 'Expired')).toHaveLength(1);
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
+      expect(wrapper.find('[data-test-id="member-role"]').text()).toBe('Expired Invite');
+      expect(wrapper.find(resendButton).prop('disabled')).toBe(false);
     });
   });
 
@@ -168,9 +167,10 @@ describe('OrganizationMemberRow', function() {
     };
 
     it('shows "Invited" status if user has not registered and not linked', function() {
-      const wrapper = shallow(
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow
           {...props}
+          canAddMembers
           member={{
             ...member,
             pending: true,
@@ -178,13 +178,12 @@ describe('OrganizationMemberRow', function() {
         />
       );
 
-      expect(findWithText(wrapper.find('strong'), 'Invited')).toHaveLength(1);
-
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
+      expect(wrapper.find('[data-test-id="member-role"]').text()).toBe('Invited Member');
+      expect(wrapper.find(resendButton).prop('disabled')).toBe(false);
     });
 
     it('shows "missing SSO link" message if user is registered and needs link', function() {
-      const wrapper = shallow(
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow
           {...props}
           member={{
@@ -193,13 +192,12 @@ describe('OrganizationMemberRow', function() {
         />
       );
 
-      expect(findWithText(wrapper.find('strong'), 'Invited')).toHaveLength(0);
-      expect(findWithText(wrapper.find('strong'), 'Missing SSO Link')).toHaveLength(1);
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(0);
+      expect(wrapper.find('[data-test-id="member-role"]').text()).toBe('Member');
+      expect(wrapper.find(resendSsoButton).prop('disabled')).toBe(true);
     });
 
-    it('has "Resend Invite" button only if `canAddMembers` is true and no link', function() {
-      const wrapper = shallow(
+    it('has "Resend SSO link" button only if `canAddMembers` is true and no link', function() {
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow
           {...props}
           canAddMembers
@@ -209,7 +207,7 @@ describe('OrganizationMemberRow', function() {
         />
       );
 
-      expect(wrapper.find('ResendInviteButton')).toHaveLength(1);
+      expect(wrapper.find(resendSsoButton).prop('disabled')).toBe(false);
     });
 
     it('has 2fa warning if user is linked does not have 2fa enabled', function() {
@@ -228,8 +226,8 @@ describe('OrganizationMemberRow', function() {
           }}
         />
       );
-      expect(wrapper.find('NoTwoFactorIcon')).toHaveLength(1);
-      expect(wrapper.find('HasTwoFactorIcon')).toHaveLength(0);
+
+      expect(wrapper.find('AuthIcon').prop('has2fa')).toBe(false);
     });
   });
 
@@ -243,22 +241,19 @@ describe('OrganizationMemberRow', function() {
     };
 
     it('has button to leave organization and no button to remove', function() {
-      const wrapper = shallow(<OrganizationMemberRow {...props} memberCanLeave />);
-      expect(findWithText(wrapper.find('Button'), 'Leave')).toHaveLength(1);
-      expect(findWithText(wrapper.find('Button'), 'Remove')).toHaveLength(0);
+      const wrapper = mountWithTheme(<OrganizationMemberRow {...props} memberCanLeave />);
+
+      expect(wrapper.find(leaveButton).exists()).toBe(true);
+      expect(wrapper.find(removeButton).exists()).toBe(false);
     });
 
     it('has disabled button to leave organization and no button to remove when member can not leave', function() {
-      const wrapper = shallow(
+      const wrapper = mountWithTheme(
         <OrganizationMemberRow {...props} memberCanLeave={false} />
       );
-      expect(findWithText(wrapper.find('Button'), 'Leave')).toHaveLength(1);
-      expect(
-        findWithText(wrapper.find('Button'), 'Leave')
-          .first()
-          .prop('disabled')
-      ).toBe(true);
-      expect(findWithText(wrapper.find('Button'), 'Remove')).toHaveLength(0);
+
+      expect(wrapper.find(leaveButton).prop('disabled')).toBe(true);
+      expect(wrapper.find(removeButton).exists()).toBe(false);
     });
   });
 
@@ -268,24 +263,23 @@ describe('OrganizationMemberRow', function() {
     };
 
     it('does not have Leave button', function() {
-      const wrapper = shallow(<OrganizationMemberRow {...props} memberCanLeave />);
+      const wrapper = mountWithTheme(<OrganizationMemberRow {...props} memberCanLeave />);
 
-      expect(findWithText(wrapper.find('Button'), 'Leave')).toHaveLength(0);
+      expect(wrapper.find(leaveButton).exists()).toBe(false);
     });
 
     it('has Remove disabled button when `canRemoveMembers` is false', function() {
-      const wrapper = shallow(<OrganizationMemberRow {...props} />);
+      const wrapper = mountWithTheme(<OrganizationMemberRow {...props} />);
 
-      expect(findWithText(wrapper.find('Button'), 'Remove')).toHaveLength(1);
-      expect(findWithText(wrapper.find('Button'), 'Remove').prop('disabled')).toBe(true);
+      expect(wrapper.find(removeButton).prop('disabled')).toBe(true);
     });
 
     it('has Remove button when `canRemoveMembers` is true', function() {
-      const wrapper = shallow(<OrganizationMemberRow {...props} canRemoveMembers />);
+      const wrapper = mountWithTheme(
+        <OrganizationMemberRow {...props} canRemoveMembers />
+      );
 
-      const removeButton = findWithText(wrapper.find('Button'), 'Remove');
-      expect(removeButton).toHaveLength(1);
-      expect(removeButton.first().prop('disabled')).toBe(false);
+      expect(wrapper.find(removeButton).prop('disabled')).toBe(false);
     });
   });
 });

+ 30 - 5
tests/js/spec/views/settings/organizationMembers/organizationMembersList.spec.jsx

@@ -267,7 +267,7 @@ describe('OrganizationMembers', function() {
     expect(OrganizationsStore.getAll()).toEqual([organization]);
   });
 
-  it('can re-send invite to member', async function() {
+  it('can re-send SSO link to member', async function() {
     const inviteMock = MockApiClient.addMockResponse({
       url: `/organizations/org-id/members/${members[0].id}/`,
       method: 'PUT',
@@ -275,6 +275,7 @@ describe('OrganizationMembers', function() {
         id: '1234',
       },
     });
+
     const wrapper = mountWithTheme(
       <OrganizationMembers
         {...defaultProps}
@@ -287,10 +288,34 @@ describe('OrganizationMembers', function() {
 
     expect(inviteMock).not.toHaveBeenCalled();
 
-    wrapper
-      .find('ResendInviteButton')
-      .first()
-      .simulate('click');
+    wrapper.find('StyledButton[aria-label="Resend SSO link"]').simulate('click');
+
+    await tick();
+    expect(inviteMock).toHaveBeenCalled();
+  });
+
+  it('can re-send invite to member', async function() {
+    const inviteMock = MockApiClient.addMockResponse({
+      url: `/organizations/org-id/members/${members[1].id}/`,
+      method: 'PUT',
+      body: {
+        id: '1234',
+      },
+    });
+
+    const wrapper = mountWithTheme(
+      <OrganizationMembers
+        {...defaultProps}
+        params={{
+          orgId: 'org-id',
+        }}
+      />,
+      TestStubs.routerContext([{organization}])
+    );
+
+    expect(inviteMock).not.toHaveBeenCalled();
+
+    wrapper.find('StyledButton[aria-label="Resend invite"]').simulate('click');
 
     await tick();
     expect(inviteMock).toHaveBeenCalled();

Some files were not shown because too many files changed in this diff