Browse Source

ref(InviteModal): Extract some render-related function to their own react files (#62796)

This PR extracts two bits of render logic into their own components.
This is one step towards converting this big modal into a functional
component. Hopefully this is a small enough chunk, mostly copy+paste,
that's easy to review.

The two bits that are extracted are: 
- `get statusMessage() => string` becomes `<InviteStatusMessage />`
- `get inviteButtonLabel() -> string` becomes `<InviteButton />` which
takes no children

The remaining getters and functions in this class are to do with state.
One thing to be careful with is that so many class members are
closed-over inside of the `const hookRenderer` render func... those
callbacks and values will need to continue to be closed over as we
refactor.
Ryan Albrecht 1 year ago
parent
commit
0491b9422b

+ 17 - 105
static/app/components/modals/inviteMembersModal/index.tsx

@@ -12,10 +12,11 @@ import type {
 } from 'sentry/components/deprecatedAsyncComponent';
 import DeprecatedAsyncComponent from 'sentry/components/deprecatedAsyncComponent';
 import HookOrDefault from 'sentry/components/hookOrDefault';
-import LoadingIndicator from 'sentry/components/loadingIndicator';
+import InviteButton from 'sentry/components/modals/inviteMembersModal/inviteButton';
+import InviteStatusMessage from 'sentry/components/modals/inviteMembersModal/inviteStatusMessage';
 import {ORG_ROLES} from 'sentry/constants';
-import {IconAdd, IconCheckmark, IconWarning} from 'sentry/icons';
-import {t, tct, tn} from 'sentry/locale';
+import {IconAdd} from 'sentry/icons';
+import {t} from 'sentry/locale';
 import SentryTypes from 'sentry/sentryTypes';
 import {space} from 'sentry/styles/space';
 import {Organization} from 'sentry/types';
@@ -248,95 +249,10 @@ class InviteMembersModal extends DeprecatedAsyncComponent<
     return this.invites.length > 0 && !this.hasDuplicateEmails;
   }
 
-  get statusMessage() {
-    const {sendingInvites, complete, inviteStatus} = this.state;
-
-    if (sendingInvites) {
-      return (
-        <StatusMessage>
-          <LoadingIndicator mini relative hideMessage size={16} />
-          {this.willInvite
-            ? t('Sending organization invitations\u2026')
-            : t('Sending invite requests\u2026')}
-        </StatusMessage>
-      );
-    }
-
-    if (complete) {
-      const statuses = Object.values(inviteStatus);
-      const sentCount = statuses.filter(i => i.sent).length;
-      const errorCount = statuses.filter(i => i.error).length;
-
-      if (this.willInvite) {
-        const invites = <strong>{tn('%s invite', '%s invites', sentCount)}</strong>;
-        const tctComponents = {
-          invites,
-          failed: errorCount,
-        };
-
-        return (
-          <StatusMessage status="success">
-            <IconCheckmark size="sm" />
-            {errorCount > 0
-              ? tct('Sent [invites], [failed] failed to send.', tctComponents)
-              : tct('Sent [invites]', tctComponents)}
-          </StatusMessage>
-        );
-      }
-      const inviteRequests = (
-        <strong>{tn('%s invite request', '%s invite requests', sentCount)}</strong>
-      );
-      const tctComponents = {
-        inviteRequests,
-        failed: errorCount,
-      };
-      return (
-        <StatusMessage status="success">
-          <IconCheckmark size="sm" />
-          {errorCount > 0
-            ? tct(
-                '[inviteRequests] pending approval, [failed] failed to send.',
-                tctComponents
-              )
-            : tct('[inviteRequests] pending approval', tctComponents)}
-        </StatusMessage>
-      );
-    }
-
-    if (this.hasDuplicateEmails) {
-      return (
-        <StatusMessage status="error">
-          <IconWarning size="sm" />
-          {t('Duplicate emails between invite rows.')}
-        </StatusMessage>
-      );
-    }
-
-    return null;
-  }
-
   get willInvite() {
     return this.props.organization.access?.includes('member:write');
   }
 
-  get inviteButtonLabel() {
-    if (this.invites.length > 0) {
-      const numberInvites = this.invites.length;
-
-      // Note we use `t()` here because `tn()` expects the same # of string formatters
-      const inviteText =
-        numberInvites === 1 ? t('Send invite') : t('Send invites (%s)', numberInvites);
-      const requestText =
-        numberInvites === 1
-          ? t('Send invite request')
-          : t('Send invite requests (%s)', numberInvites);
-
-      return this.willInvite ? inviteText : requestText;
-    }
-
-    return this.willInvite ? t('Send invite') : t('Send invite request');
-  }
-
   render() {
     const {Footer, closeModal, organization} = this.props;
     const {pendingInvites, sendingInvites, complete, inviteStatus, member} = this.state;
@@ -398,7 +314,15 @@ class InviteMembersModal extends DeprecatedAsyncComponent<
 
         <Footer>
           <FooterContent>
-            <div>{this.statusMessage}</div>
+            <div>
+              <InviteStatusMessage
+                complete={this.state.complete}
+                hasDuplicateEmails={this.hasDuplicateEmails}
+                inviteStatus={this.state.inviteStatus}
+                sendingInvites={this.state.sendingInvites}
+                willInvite={this.willInvite}
+              />
+            </div>
 
             <ButtonBar gap={1}>
               {complete ? (
@@ -431,15 +355,15 @@ class InviteMembersModal extends DeprecatedAsyncComponent<
                   >
                     {t('Cancel')}
                   </Button>
-                  <Button
+                  <InviteButton
+                    invites={this.invites}
+                    willInvite={this.willInvite}
                     size="sm"
                     data-test-id="send-invites"
                     priority="primary"
                     disabled={!canSend || !this.isValidInvites || disableInputs}
                     onClick={sendInvites}
-                  >
-                    {this.inviteButtonLabel}
-                  </Button>
+                  />
                 </Fragment>
               )}
             </ButtonBar>
@@ -511,18 +435,6 @@ const FooterContent = styled('div')`
   flex: 1;
 `;
 
-export const StatusMessage = styled('div')<{status?: 'success' | 'error'}>`
-  display: flex;
-  gap: ${space(1)};
-  align-items: center;
-  font-size: ${p => p.theme.fontSizeMedium};
-  color: ${p => (p.status === 'error' ? p.theme.errorText : p.theme.textColor)};
-
-  > :first-child {
-    ${p => p.status === 'success' && `color: ${p.theme.successText}`};
-  }
-`;
-
 export const modalCss = css`
   width: 100%;
   max-width: 900px;

+ 33 - 0
static/app/components/modals/inviteMembersModal/inviteButton.tsx

@@ -0,0 +1,33 @@
+import {Button, ButtonProps} from 'sentry/components/button';
+import {t} from 'sentry/locale';
+
+import {NormalizedInvite} from './types';
+
+interface Props extends Omit<ButtonProps, 'children'> {
+  invites: NormalizedInvite[];
+  willInvite: boolean;
+}
+
+export default function InviteButton({invites, willInvite, ...buttonProps}: Props) {
+  const label = buttonLabel(invites, willInvite);
+
+  return <Button {...buttonProps}>{label}</Button>;
+}
+
+function buttonLabel(invites: NormalizedInvite[], willInvite: boolean) {
+  if (invites.length > 0) {
+    const numberInvites = invites.length;
+
+    // Note we use `t()` here because `tn()` expects the same # of string formatters
+    const inviteText =
+      numberInvites === 1 ? t('Send invite') : t('Send invites (%s)', numberInvites);
+    const requestText =
+      numberInvites === 1
+        ? t('Send invite request')
+        : t('Send invite requests (%s)', numberInvites);
+
+    return willInvite ? inviteText : requestText;
+  }
+
+  return willInvite ? t('Send invite') : t('Send invite request');
+}

+ 99 - 0
static/app/components/modals/inviteMembersModal/inviteStatusMessage.tsx

@@ -0,0 +1,99 @@
+import styled from '@emotion/styled';
+
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {IconCheckmark, IconWarning} from 'sentry/icons';
+import {t, tct, tn} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+
+import {InviteStatus} from './types';
+
+interface Props {
+  complete: boolean;
+  hasDuplicateEmails: boolean;
+  inviteStatus: InviteStatus;
+  sendingInvites: boolean;
+  willInvite: boolean;
+}
+
+export default function InviteStatusMessage({
+  complete,
+  hasDuplicateEmails,
+  inviteStatus,
+  sendingInvites,
+  willInvite,
+}: Props) {
+  if (sendingInvites) {
+    return (
+      <StatusMessage>
+        <LoadingIndicator mini relative hideMessage size={16} />
+        {willInvite
+          ? t('Sending organization invitations\u2026')
+          : t('Sending invite requests\u2026')}
+      </StatusMessage>
+    );
+  }
+
+  if (complete) {
+    const statuses = Object.values(inviteStatus);
+    const sentCount = statuses.filter(i => i.sent).length;
+    const errorCount = statuses.filter(i => i.error).length;
+
+    if (willInvite) {
+      const invites = <strong>{tn('%s invite', '%s invites', sentCount)}</strong>;
+      const tctComponents = {
+        invites,
+        failed: errorCount,
+      };
+
+      return (
+        <StatusMessage status="success">
+          <IconCheckmark size="sm" />
+          {errorCount > 0
+            ? tct('Sent [invites], [failed] failed to send.', tctComponents)
+            : tct('Sent [invites]', tctComponents)}
+        </StatusMessage>
+      );
+    }
+    const inviteRequests = (
+      <strong>{tn('%s invite request', '%s invite requests', sentCount)}</strong>
+    );
+    const tctComponents = {
+      inviteRequests,
+      failed: errorCount,
+    };
+    return (
+      <StatusMessage status="success">
+        <IconCheckmark size="sm" />
+        {errorCount > 0
+          ? tct(
+              '[inviteRequests] pending approval, [failed] failed to send.',
+              tctComponents
+            )
+          : tct('[inviteRequests] pending approval', tctComponents)}
+      </StatusMessage>
+    );
+  }
+
+  if (hasDuplicateEmails) {
+    return (
+      <StatusMessage status="error">
+        <IconWarning size="sm" />
+        {t('Duplicate emails between invite rows.')}
+      </StatusMessage>
+    );
+  }
+
+  return null;
+}
+
+export const StatusMessage = styled('div')<{status?: 'success' | 'error'}>`
+  display: flex;
+  gap: ${space(1)};
+  align-items: center;
+  font-size: ${p => p.theme.fontSizeMedium};
+  color: ${p => (p.status === 'error' ? p.theme.errorText : p.theme.textColor)};
+
+  > :first-child {
+    ${p => p.status === 'success' && `color: ${p.theme.successText}`};
+  }
+`;

+ 1 - 1
static/app/components/modals/inviteMissingMembersModal/index.tsx

@@ -10,8 +10,8 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {
   InviteModalHook,
   InviteModalRenderFunc,
-  StatusMessage,
 } from 'sentry/components/modals/inviteMembersModal';
+import {StatusMessage} from 'sentry/components/modals/inviteMembersModal/inviteStatusMessage';
 import {InviteStatus} from 'sentry/components/modals/inviteMembersModal/types';
 import {MissingMemberInvite} from 'sentry/components/modals/inviteMissingMembersModal/types';
 import PanelItem from 'sentry/components/panels/panelItem';