Просмотр исходного кода

fix(ui): Fix invite request confusion (#27553)

* Moved invite request to sit above member list

* Moved join team requests to Teams page

* onRemoveAccessRequest

* Swapped Deny & Approve buttons and added icons

* Added title line

* Update team member count on team request accept

* Request Access change team state.isPending

* Swapped Filter & Search bar

* Removed tab

* invite requests for members

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* small fix

* added new message

* Changed invite request prompt

* added some tests

* More tests

* Small fixes

* lint

* fixed analytics

* Fixed tooltip & emails

* added redirect

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Zhixing Zhang 3 лет назад
Родитель
Сommit
e7cd63b6c3

+ 1 - 1
src/sentry/models/organizationaccessrequest.py

@@ -37,7 +37,7 @@ class OrganizationAccessRequest(Model):
             "team": self.team,
             "url": absolute_uri(
                 reverse(
-                    "sentry-organization-members-requests",
+                    "sentry-organization-teams",
                     kwargs={"organization_slug": organization.slug},
                 )
             ),

+ 1 - 1
src/sentry/tasks/members.py

@@ -21,7 +21,7 @@ def send_invite_request_notification_email(member_id):
         "email": om.email,
         "organization_name": om.organization.name,
         "pending_requests_link": absolute_uri(
-            reverse("sentry-organization-members-requests", kwargs=link_args)
+            reverse("sentry-organization-members", kwargs=link_args)
         ),
     }
 

+ 1 - 1
src/sentry/web/frontend/debug/debug_organization_invite_request.py

@@ -17,7 +17,7 @@ class DebugOrganizationInviteRequestEmailView(View):
             "inviter_name": user.get_salutation_name,
             "email": "test@gmail.com",
             "pending_requests_link": absolute_uri(
-                reverse("sentry-organization-members-requests", args=[org.slug])
+                reverse("sentry-organization-members", args=[org.slug])
             ),
         }
         return MailPreview(

+ 1 - 1
src/sentry/web/frontend/debug/debug_organization_join_request.py

@@ -14,7 +14,7 @@ class DebugOrganizationJoinRequestEmailView(View):
             "organization_name": org.name,
             "email": "test@gmail.com",
             "pending_requests_link": absolute_uri(
-                reverse("sentry-organization-members-requests", args=[org.slug])
+                reverse("sentry-organization-members", args=[org.slug])
             ),
             "settings_link": absolute_uri(reverse("sentry-organization-settings", args=[org.slug])),
         }

+ 2 - 6
src/sentry/web/frontend/debug/mail.py

@@ -539,9 +539,7 @@ def request_access(request):
             "organization": org,
             "team": team,
             "url": absolute_uri(
-                reverse(
-                    "sentry-organization-members-requests", kwargs={"organization_slug": org.slug}
-                )
+                reverse("sentry-organization-teams", kwargs={"organization_slug": org.slug})
             ),
         },
     ).render(request)
@@ -561,9 +559,7 @@ def request_access_for_another_member(request):
             "organization": org,
             "team": team,
             "url": absolute_uri(
-                reverse(
-                    "sentry-organization-members-requests", kwargs={"organization_slug": org.slug}
-                )
+                reverse("sentry-organization-teams", kwargs={"organization_slug": org.slug})
             ),
             "requester": request.user.get_display_name(),
         },

+ 4 - 4
src/sentry/web/urls.py

@@ -416,14 +416,14 @@ urlpatterns += [
                     name="sentry-organization-settings",
                 ),
                 url(
-                    r"^(?P<organization_slug>[\w_-]+)/members/$",
+                    r"^(?P<organization_slug>[\w_-]+)/teams/$",
                     react_page_view,
-                    name="sentry-organization-members",
+                    name="sentry-organization-teams",
                 ),
                 url(
-                    r"^(?P<organization_slug>[\w_-]+)/members/requests/$",
+                    r"^(?P<organization_slug>[\w_-]+)/members/$",
                     react_page_view,
-                    name="sentry-organization-members-requests",
+                    name="sentry-organization-members",
                 ),
                 url(
                     r"^(?P<organization_slug>[\w_-]+)/members/(?P<member_id>\d+)/$",

+ 35 - 14
static/app/components/modals/inviteMembersModal/index.tsx

@@ -255,20 +255,41 @@ class InviteMembersModal extends AsyncComponent<Props, State> {
       const sentCount = statuses.filter(i => i.sent).length;
       const errorCount = statuses.filter(i => i.error).length;
 
-      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>
-      );
+      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>
+        );
+      } else {
+        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) {

+ 1 - 9
static/app/routes.tsx

@@ -497,6 +497,7 @@ function routes() {
         component={errorHandler(LazyLoad)}
       />
 
+      <Redirect from="members/requests" to="members/" />
       <Route path="members/" name="Members">
         <Route
           componentPromise={() =>
@@ -510,15 +511,6 @@ function routes() {
             }
             component={errorHandler(LazyLoad)}
           />
-
-          <Route
-            path="requests/"
-            name="Requests"
-            componentPromise={() =>
-              import('app/views/settings/organizationMembers/organizationRequestsView')
-            }
-            component={errorHandler(LazyLoad)}
-          />
         </Route>
 
         <Route

+ 9 - 0
static/app/utils/growthAnalyticsEvents.tsx

@@ -34,6 +34,11 @@ type SampleEventParam = {
   platform?: PlatformKey;
 };
 
+type InviteRequestParam = {
+  member_id: number;
+  invite_status: string;
+};
+
 // define the event key to payload mappings
 export type GrowthEventParameters = {
   'growth.show_mobile_prompt_banner': ShowParams;
@@ -54,6 +59,8 @@ export type GrowthEventParameters = {
   'growth.onboarding_take_to_error': {};
   'growth.onboarding_view_full_docs': {};
   'growth.onboarding_view_sample_event': SampleEventParam;
+  'invite_request.approved': InviteRequestParam;
+  'invite_request.denied': InviteRequestParam;
 };
 
 type GrowthAnalyticsKey = keyof GrowthEventParameters;
@@ -82,4 +89,6 @@ export const growthEventMap: Record<GrowthAnalyticsKey, string> = {
   'growth.onboarding_take_to_error': 'Growth: Onboarding Take to Error',
   'growth.onboarding_view_full_docs': 'Growth: Onboarding View Full Docs',
   'growth.onboarding_view_sample_event': 'Growth: Onboarding View Sample Event',
+  'invite_request.approved': 'Invite Request Approved',
+  'invite_request.denied': 'Invite Request Denied',
 };

+ 55 - 35
static/app/views/settings/organizationMembers/inviteRequestRow.tsx

@@ -11,6 +11,7 @@ import {PanelItem} from 'app/components/panels';
 import RoleSelectControl from 'app/components/roleSelectControl';
 import Tag from 'app/components/tag';
 import Tooltip from 'app/components/tooltip';
+import {IconCheckmark, IconClose} from 'app/icons';
 import {t, tct} from 'app/locale';
 import space from 'app/styles/space';
 import {Member, MemberRole, Organization, Team} from 'app/types';
@@ -47,6 +48,8 @@ const InviteRequestRow = ({
 }: Props) => {
   const role = allRoles.find(r => r.id === inviteRequest.role);
   const roleDisallowed = !(role && role.allowed);
+  const {access} = organization;
+  const canApprove = access.includes('member:admin');
 
   // eslint-disable-next-line react/prop-types
   const hookRenderer: InviteModalRenderFunc = ({sendInvites, canSend, headerInfo}) => (
@@ -78,33 +81,54 @@ const InviteRequestRow = ({
         )}
       </div>
 
-      <StyledRoleSelectControl
-        name="role"
-        disableUnallowed
-        onChange={r => onUpdate({role: r.value})}
-        value={inviteRequest.role}
-        roles={allRoles}
-      />
-
-      <TeamSelectControl
-        name="teams"
-        placeholder={t('Add to teams\u2026')}
-        onChange={(teams: OnChangeArgs) =>
-          onUpdate({teams: (teams || []).map(team => team.value)})
-        }
-        value={inviteRequest.teams}
-        options={allTeams.map(({slug}) => ({
-          value: slug,
-          label: `#${slug}`,
-        }))}
-        clearable
-      />
+      {canApprove ? (
+        <StyledRoleSelectControl
+          name="role"
+          disableUnallowed
+          onChange={r => onUpdate({role: r.value})}
+          value={inviteRequest.role}
+          roles={allRoles}
+        />
+      ) : (
+        <div>{inviteRequest.roleName}</div>
+      )}
+      {canApprove ? (
+        <TeamSelectControl
+          name="teams"
+          placeholder={t('Add to teams\u2026')}
+          onChange={(teams: OnChangeArgs) =>
+            onUpdate({teams: (teams || []).map(team => team.value)})
+          }
+          value={inviteRequest.teams}
+          options={allTeams.map(({slug}) => ({
+            value: slug,
+            label: `#${slug}`,
+          }))}
+          clearable
+        />
+      ) : (
+        <div>{inviteRequest.teams.join(', ')}</div>
+      )}
 
       <ButtonGroup>
+        <Button
+          size="small"
+          busy={inviteRequestBusy[inviteRequest.id]}
+          onClick={() => onDeny(inviteRequest)}
+          icon={<IconClose />}
+          disabled={!canApprove}
+          title={
+            canApprove
+              ? undefined
+              : t('This request needs to be reviewed by a privileged user')
+          }
+        >
+          {t('Deny')}
+        </Button>
         <Confirm
           onConfirm={sendInvites}
           disableConfirmButton={!canSend}
-          disabled={roleDisallowed}
+          disabled={!canApprove || roleDisallowed}
           message={
             <React.Fragment>
               {tct('Are you sure you want to invite [email] to your organization?', {
@@ -119,24 +143,20 @@ const InviteRequestRow = ({
             size="small"
             busy={inviteRequestBusy[inviteRequest.id]}
             title={
-              roleDisallowed
-                ? t(
-                    `You do not have permission to approve a user of this role.
-                     Select a different role to approve this user.`
-                  )
-                : undefined
+              canApprove
+                ? roleDisallowed
+                  ? t(
+                      `You do not have permission to approve a user of this role.
+                      Select a different role to approve this user.`
+                    )
+                  : undefined
+                : t('This request needs to be reviewed by a privileged user')
             }
+            icon={<IconCheckmark />}
           >
             {t('Approve')}
           </Button>
         </Confirm>
-        <Button
-          size="small"
-          busy={inviteRequestBusy[inviteRequest.id]}
-          onClick={() => onDeny(inviteRequest)}
-        >
-          {t('Deny')}
-        </Button>
       </ButtonGroup>
     </StyledPanelItem>
   );

Некоторые файлы не были показаны из-за большого количества измененных файлов