organizationMembersWrapper.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import {cloneElement, Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import {openInviteMembersModal} from 'sentry/actionCreators/modal';
  4. import FeatureDisabled from 'sentry/components/acl/featureDisabled';
  5. import {Button} from 'sentry/components/button';
  6. import HookOrDefault from 'sentry/components/hookOrDefault';
  7. import {Hovercard} from 'sentry/components/hovercard';
  8. import {IconMail} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import type {Member, Organization} from 'sentry/types/organization';
  11. import routeTitleGen from 'sentry/utils/routeTitle';
  12. import withOrganization from 'sentry/utils/withOrganization';
  13. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  14. import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
  15. type Props = {
  16. organization: Organization;
  17. children?: any;
  18. } & RouteComponentProps<{}, {}>;
  19. type State = DeprecatedAsyncView['state'] & {
  20. inviteRequests: Member[];
  21. };
  22. const InviteMembersButtonHook = HookOrDefault({
  23. hookName: 'member-invite-button:customization',
  24. defaultComponent: ({children, organization, onTriggerModal}) =>
  25. children({
  26. disabled: !organization.features.includes('invite-members'),
  27. onTriggerModal,
  28. }),
  29. });
  30. class OrganizationMembersWrapper extends DeprecatedAsyncView<Props, State> {
  31. getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
  32. const {organization} = this.props;
  33. return [
  34. ['inviteRequests', `/organizations/${organization.slug}/invite-requests/`],
  35. ['requestList', `/organizations/${organization.slug}/access-requests/`],
  36. ];
  37. }
  38. getTitle() {
  39. const {organization} = this.props;
  40. return routeTitleGen(t('Members'), organization.slug, false);
  41. }
  42. get onRequestsTab() {
  43. return location.pathname.includes('/requests/');
  44. }
  45. get hasWriteAccess() {
  46. const {organization} = this.props;
  47. if (!organization || !organization.access) {
  48. return false;
  49. }
  50. return organization.access.includes('member:write');
  51. }
  52. get showInviteRequests() {
  53. return this.hasWriteAccess;
  54. }
  55. get showNavTabs() {
  56. const {requestList} = this.state;
  57. // show the requests tab if there are pending team requests,
  58. // or if the user has access to approve or deny invite requests
  59. return (requestList && requestList.length > 0) || this.showInviteRequests;
  60. }
  61. get requestCount() {
  62. const {requestList, inviteRequests} = this.state;
  63. let count = requestList.length;
  64. // if the user can't see the invite requests panel,
  65. // exclude those requests from the total count
  66. if (this.showInviteRequests) {
  67. count += inviteRequests.length;
  68. }
  69. return count ? count.toString() : null;
  70. }
  71. removeAccessRequest = (id: string) =>
  72. this.setState(state => ({
  73. requestList: state.requestList.filter(request => request.id !== id),
  74. }));
  75. removeInviteRequest = (id: string) =>
  76. this.setState(state => ({
  77. inviteRequests: state.inviteRequests.filter(request => request.id !== id),
  78. }));
  79. updateInviteRequest = (id: string, data: Partial<Member>) =>
  80. this.setState(state => {
  81. const inviteRequests = [...state.inviteRequests];
  82. const inviteIndex = inviteRequests.findIndex(request => request.id === id);
  83. inviteRequests[inviteIndex] = {...inviteRequests[inviteIndex], ...data};
  84. return {inviteRequests};
  85. });
  86. renderBody() {
  87. const {children, organization} = this.props;
  88. const {requestList, inviteRequests} = this.state;
  89. const action = (
  90. <InviteMembersButtonHook
  91. organization={organization}
  92. onTriggerModal={() =>
  93. openInviteMembersModal({
  94. onClose: () => {
  95. this.fetchData();
  96. },
  97. source: 'members_settings',
  98. })
  99. }
  100. >
  101. {renderInviteMembersButton}
  102. </InviteMembersButtonHook>
  103. );
  104. return (
  105. <Fragment>
  106. <SettingsPageHeader title="Members" action={action} />
  107. {children &&
  108. cloneElement(children, {
  109. requestList,
  110. inviteRequests,
  111. onRemoveInviteRequest: this.removeInviteRequest,
  112. onUpdateInviteRequest: this.updateInviteRequest,
  113. onRemoveAccessRequest: this.removeAccessRequest,
  114. showInviteRequests: this.showInviteRequests,
  115. })}
  116. </Fragment>
  117. );
  118. }
  119. }
  120. function renderInviteMembersButton({
  121. disabled,
  122. onTriggerModal,
  123. }: {
  124. onTriggerModal: () => void;
  125. disabled?: boolean;
  126. }) {
  127. const action = (
  128. <Button
  129. priority="primary"
  130. size="sm"
  131. onClick={onTriggerModal}
  132. data-test-id="email-invite"
  133. icon={<IconMail />}
  134. disabled={disabled}
  135. >
  136. {t('Invite Members')}
  137. </Button>
  138. );
  139. return disabled ? (
  140. <Hovercard
  141. body={
  142. <FeatureDisabled
  143. featureName={t('Invite Members')}
  144. features="organizations:invite-members"
  145. hideHelpToggle
  146. />
  147. }
  148. >
  149. {action}
  150. </Hovercard>
  151. ) : (
  152. action
  153. );
  154. }
  155. export default withOrganization(OrganizationMembersWrapper);