Browse Source

feat(ui): Swap `<Role>` for `useRole` (#78091)

Scott Cooper 5 months ago
parent
commit
d050bf9436

+ 0 - 162
static/app/components/acl/role.spec.tsx

@@ -1,162 +0,0 @@
-import {OrganizationFixture} from 'sentry-fixture/organization';
-import {UserFixture} from 'sentry-fixture/user';
-
-import {act, render, screen} from 'sentry-test/reactTestingLibrary';
-
-import {Role} from 'sentry/components/acl/role';
-import ConfigStore from 'sentry/stores/configStore';
-import OrganizationStore from 'sentry/stores/organizationStore';
-
-describe('Role', function () {
-  const organization = OrganizationFixture({
-    orgRole: 'admin',
-    orgRoleList: [
-      {
-        id: 'member',
-        name: 'Member',
-        desc: '...',
-        minimumTeamRole: 'contributor',
-        isTeamRolesAllowed: true,
-      },
-      {
-        id: 'admin',
-        name: 'Admin',
-        desc: '...',
-        minimumTeamRole: 'admin',
-        isTeamRolesAllowed: true,
-      },
-      {
-        id: 'manager',
-        name: 'Manager',
-        desc: '...',
-        minimumTeamRole: 'admin',
-        isTeamRolesAllowed: true,
-      },
-      {
-        id: 'owner',
-        name: 'Owner',
-        desc: '...',
-        minimumTeamRole: 'admin',
-        isTeamRolesAllowed: true,
-      },
-    ],
-  });
-
-  describe('as render prop', function () {
-    const childrenMock = jest.fn().mockReturnValue(null);
-    beforeEach(function () {
-      OrganizationStore.init();
-      childrenMock.mockClear();
-    });
-
-    it('has a sufficient role', function () {
-      render(<Role role="admin">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: true,
-      });
-    });
-
-    it('has an insufficient role', function () {
-      render(<Role role="manager">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: false,
-      });
-    });
-
-    it('gives access to a superuser with insufficient role', function () {
-      organization.access = ['org:superuser'];
-      OrganizationStore.onUpdate(organization, {replace: true});
-
-      render(<Role role="owner">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: true,
-      });
-    });
-
-    it('does not give access to a made up role', function () {
-      render(<Role role="abcdefg">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: false,
-      });
-    });
-
-    it('handles no user', function () {
-      const user = {...ConfigStore.get('user')};
-      ConfigStore.set('user', undefined as any);
-      render(<Role role="member">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: false,
-      });
-      act(() => ConfigStore.set('user', user));
-    });
-
-    it('updates if user changes', function () {
-      ConfigStore.set('user', undefined as any);
-      const {rerender} = render(<Role role="member">{childrenMock}</Role>, {
-        organization,
-      });
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: false,
-      });
-      act(() => ConfigStore.set('user', UserFixture()));
-
-      rerender(<Role role="member">{childrenMock}</Role>);
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: true,
-      });
-    });
-
-    it('handles no organization.orgRoleList', function () {
-      render(
-        <Role role="member" organization={{...organization, orgRoleList: []}}>
-          {childrenMock}
-        </Role>,
-        {organization}
-      );
-
-      expect(childrenMock).toHaveBeenCalledWith({
-        hasRole: false,
-      });
-    });
-  });
-
-  describe('as React node', function () {
-    it('has a sufficient role', function () {
-      render(
-        <Role role="member">
-          <div>The Child</div>
-        </Role>,
-        {organization}
-      );
-
-      expect(screen.getByText('The Child')).toBeInTheDocument();
-    });
-
-    it('has an insufficient role', function () {
-      render(
-        <Role role="owner">
-          <div>The Child</div>
-        </Role>,
-        {organization}
-      );
-
-      expect(screen.queryByText('The Child')).not.toBeInTheDocument();
-    });
-  });
-});

+ 0 - 77
static/app/components/acl/role.tsx

@@ -1,77 +0,0 @@
-import {useMemo} from 'react';
-
-import type {Organization} from 'sentry/types/organization';
-import type {User} from 'sentry/types/user';
-import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
-import {isRenderFunc} from 'sentry/utils/isRenderFunc';
-import {useUser} from 'sentry/utils/useUser';
-import withOrganization from 'sentry/utils/withOrganization';
-
-type RoleRenderProps = {
-  hasRole: boolean;
-};
-
-type ChildrenRenderFn = (props: RoleRenderProps) => React.ReactElement | null;
-
-function checkUserRole(user: User, organization: Organization, role: RoleProps['role']) {
-  if (!user) {
-    return false;
-  }
-
-  if (isActiveSuperuser()) {
-    return true;
-  }
-
-  if (!Array.isArray(organization.orgRoleList)) {
-    return false;
-  }
-
-  const roleIds = organization.orgRoleList.map(r => r.id);
-
-  if (!roleIds.includes(role) || !roleIds.includes(organization.orgRole ?? '')) {
-    return false;
-  }
-
-  const requiredIndex = roleIds.indexOf(role);
-  const currentIndex = roleIds.indexOf(organization.orgRole ?? '');
-  return currentIndex >= requiredIndex;
-}
-
-interface RoleProps {
-  /**
-   * If children is a function then will be treated as a render prop and
-   * passed RoleRenderProps.
-   *
-   * The other interface is more simple, only show `children` if user has
-   * the minimum required role.
-   */
-  children: React.ReactElement | ChildrenRenderFn;
-  /**
-   * Current Organization
-   */
-  organization: Organization;
-  /**
-   * Minimum required role
-   */
-  role: string;
-}
-
-function Role({role, organization, children}: RoleProps): React.ReactElement | null {
-  const user = useUser();
-
-  const hasRole = useMemo(
-    () => checkUserRole(user, organization, role),
-    // It seems that this returns a stable reference, but
-    [organization, role, user]
-  );
-
-  if (isRenderFunc<ChildrenRenderFn>(children)) {
-    return children({hasRole});
-  }
-
-  return hasRole ? children : null;
-}
-
-const withOrganizationRole = withOrganization(Role);
-
-export {withOrganizationRole as Role};

+ 79 - 0
static/app/components/acl/useRole.spec.tsx

@@ -0,0 +1,79 @@
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {UserFixture} from 'sentry-fixture/user';
+
+import {renderHook} from 'sentry-test/reactTestingLibrary';
+
+import {useRole} from 'sentry/components/acl/useRole';
+import ConfigStore from 'sentry/stores/configStore';
+import OrganizationStore from 'sentry/stores/organizationStore';
+import type {Organization} from 'sentry/types/organization';
+import {OrganizationContext} from 'sentry/views/organizationContext';
+
+function createWrapper(organization: Organization) {
+  return function ({children}: {children: React.ReactNode}) {
+    return (
+      <OrganizationContext.Provider value={organization}>
+        {children}
+      </OrganizationContext.Provider>
+    );
+  };
+}
+
+describe('useRole', () => {
+  const organization = OrganizationFixture({
+    // User is an admin of this test org
+    orgRole: 'admin',
+    // For these tests, attachments will require an admin role
+    attachmentsRole: 'admin',
+    debugFilesRole: 'member',
+  });
+
+  beforeEach(() => {
+    ConfigStore.set('user', UserFixture());
+    // OrganizationStore is still called directly in isActiveSuperuser()
+    OrganizationStore.init();
+    OrganizationStore.onUpdate(organization, {replace: true});
+  });
+
+  it('has a sufficient role', () => {
+    const {result} = renderHook(() => useRole({role: 'attachmentsRole'}), {
+      wrapper: createWrapper(organization),
+    });
+    expect(result.current.hasRole).toBe(true);
+    expect(result.current.roleRequired).toBe('admin');
+  });
+
+  it('has an insufficient role', () => {
+    const org = OrganizationFixture({
+      ...organization,
+      orgRole: 'member',
+    });
+    OrganizationStore.onUpdate(org, {replace: true});
+    const {result} = renderHook(() => useRole({role: 'attachmentsRole'}), {
+      wrapper: createWrapper(org),
+    });
+    expect(result.current.hasRole).toBe(false);
+  });
+
+  it('gives access to a superuser with insufficient role', () => {
+    const org = OrganizationFixture({
+      ...organization,
+      orgRole: 'member',
+      access: ['org:superuser'],
+    });
+    OrganizationStore.onUpdate(org, {replace: true});
+    const {result} = renderHook(() => useRole({role: 'attachmentsRole'}), {
+      wrapper: createWrapper(org),
+    });
+    expect(result.current.hasRole).toBe(true);
+  });
+
+  it('handles no organization.orgRoleList', () => {
+    const org = {...organization, orgRoleList: []};
+    OrganizationStore.onUpdate(org, {replace: true});
+    const {result} = renderHook(() => useRole({role: 'attachmentsRole'}), {
+      wrapper: createWrapper(org),
+    });
+    expect(result.current.hasRole).toBe(false);
+  });
+});

+ 55 - 0
static/app/components/acl/useRole.tsx

@@ -0,0 +1,55 @@
+import {useMemo} from 'react';
+
+import type {Organization} from 'sentry/types/organization';
+import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
+import useOrganization from 'sentry/utils/useOrganization';
+
+function hasOrganizationRole(organization: Organization, roleRequired: string): boolean {
+  if (!Array.isArray(organization.orgRoleList)) {
+    return false;
+  }
+
+  const roleIds = organization.orgRoleList.map(r => r.id);
+
+  const requiredIndex = roleIds.indexOf(roleRequired);
+  const currentIndex = roleIds.indexOf(organization.orgRole ?? '');
+
+  if (requiredIndex === -1 || currentIndex === -1) {
+    return false;
+  }
+
+  // If the user is a lower role than the required role, they do not have access
+  return currentIndex >= requiredIndex;
+}
+
+interface UseRoleOptions {
+  /**
+   * Minimum required role.
+   * The required role ('member', 'admin') are stored in the organization object.
+   * eg: Organization.debugFilesRole = 'member'
+   */
+  role: // Extract keys to enforce that they are available on the Organization type
+  Extract<keyof Organization, 'debugFilesRole' | 'attachmentsRole'>;
+}
+
+interface UseRoleResult {
+  hasRole: boolean;
+  /**
+   * The required role ('member', 'admin') from the organization object.
+   */
+  roleRequired: string;
+}
+
+export function useRole(options: UseRoleOptions): UseRoleResult {
+  const organization = useOrganization();
+
+  return useMemo((): UseRoleResult => {
+    const roleRequired = organization[options.role];
+    if (isActiveSuperuser()) {
+      return {hasRole: true, roleRequired};
+    }
+
+    const hasRole = hasOrganizationRole(organization, roleRequired);
+    return {hasRole, roleRequired};
+  }, [organization, options.role]);
+}

+ 52 - 55
static/app/components/events/eventAttachmentActions.tsx

@@ -1,4 +1,4 @@
-import {Role} from 'sentry/components/acl/role';
+import {useRole} from 'sentry/components/acl/useRole';
 import {Button, LinkButton} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import Confirm from 'sentry/components/confirm';
@@ -26,65 +26,62 @@ function EventAttachmentActions({
   onDelete,
 }: Props) {
   const organization = useOrganization();
+  const {hasRole: hasAttachmentRole} = useRole({role: 'attachmentsRole'});
   const url = `/api/0/projects/${organization.slug}/${projectSlug}/events/${attachment.event_id}/attachments/${attachment.id}/`;
   const hasPreview = hasInlineAttachmentRenderer(attachment);
 
   return (
-    <Role role={organization.attachmentsRole}>
-      {({hasRole: hasAttachmentRole}) => (
-        <ButtonBar gap={1}>
-          {withPreviewButton && (
-            <Button
-              size="xs"
-              disabled={!hasAttachmentRole || !hasPreview}
-              priority={previewIsOpen ? 'primary' : 'default'}
-              icon={<IconShow />}
-              onClick={onPreviewClick}
-              title={
-                !hasAttachmentRole
-                  ? t('Insufficient permissions to preview attachments')
-                  : !hasPreview
-                    ? t('This attachment cannot be previewed')
-                    : undefined
-              }
-            >
-              {t('Preview')}
-            </Button>
-          )}
-          <LinkButton
-            size="xs"
-            icon={<IconDownload />}
-            href={hasAttachmentRole ? `${url}?download=1` : ''}
-            disabled={!hasAttachmentRole}
-            title={
-              hasAttachmentRole
-                ? t('Download')
-                : t('Insufficient permissions to download attachments')
-            }
-            aria-label={t('Download')}
-          />
-          <Confirm
-            confirmText={t('Delete')}
-            message={t('Are you sure you wish to delete this file?')}
-            priority="danger"
-            onConfirm={onDelete}
-            disabled={!hasAttachmentRole}
-          >
-            <Button
-              size="xs"
-              icon={<IconDelete />}
-              aria-label={t('Delete')}
-              disabled={!hasAttachmentRole}
-              title={
-                hasAttachmentRole
-                  ? t('Delete')
-                  : t('Insufficient permissions to delete attachments')
-              }
-            />
-          </Confirm>
-        </ButtonBar>
+    <ButtonBar gap={1}>
+      {withPreviewButton && (
+        <Button
+          size="xs"
+          disabled={!hasAttachmentRole || !hasPreview}
+          priority={previewIsOpen ? 'primary' : 'default'}
+          icon={<IconShow />}
+          onClick={onPreviewClick}
+          title={
+            !hasAttachmentRole
+              ? t('Insufficient permissions to preview attachments')
+              : !hasPreview
+                ? t('This attachment cannot be previewed')
+                : undefined
+          }
+        >
+          {t('Preview')}
+        </Button>
       )}
-    </Role>
+      <LinkButton
+        size="xs"
+        icon={<IconDownload />}
+        href={hasAttachmentRole ? `${url}?download=1` : ''}
+        disabled={!hasAttachmentRole}
+        title={
+          hasAttachmentRole
+            ? t('Download')
+            : t('Insufficient permissions to download attachments')
+        }
+        aria-label={t('Download')}
+      />
+      <Confirm
+        confirmText={t('Delete')}
+        message={t('Are you sure you wish to delete this file?')}
+        priority="danger"
+        onConfirm={onDelete}
+        disabled={!hasAttachmentRole}
+      >
+        <Button
+          size="xs"
+          icon={<IconDelete />}
+          aria-label={t('Delete')}
+          disabled={!hasAttachmentRole}
+          title={
+            hasAttachmentRole
+              ? t('Delete')
+              : t('Insufficient permissions to delete attachments')
+          }
+        />
+      </Confirm>
+    </ButtonBar>
   );
 }
 

+ 6 - 11
static/app/components/events/eventTagsAndScreenshot/screenshot/index.tsx

@@ -2,7 +2,7 @@ import type {ReactEventHandler} from 'react';
 import {Fragment, useState} from 'react';
 import styled from '@emotion/styled';
 
-import {Role} from 'sentry/components/acl/role';
+import {useRole} from 'sentry/components/acl/useRole';
 import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import {openConfirmModal} from 'sentry/components/confirm';
@@ -52,6 +52,7 @@ function Screenshot({
 }: Props) {
   const orgSlug = organization.slug;
   const [loadingImage, setLoadingImage] = useState(true);
+  const {hasRole} = useRole({role: 'attachmentsRole'});
 
   function handleDelete(screenshotAttachmentId: string) {
     trackAnalytics('issue_details.issue_tab.screenshot_dropdown_deleted', {
@@ -164,17 +165,11 @@ function Screenshot({
     );
   }
 
-  return (
-    <Role organization={organization} role={organization.attachmentsRole}>
-      {({hasRole}) => {
-        if (!hasRole) {
-          return null;
-        }
+  if (!hasRole) {
+    return null;
+  }
 
-        return <StyledPanel>{renderContent(screenshot)}</StyledPanel>;
-      }}
-    </Role>
-  );
+  return <StyledPanel>{renderContent(screenshot)}</StyledPanel>;
 }
 
 export default Screenshot;

+ 72 - 75
static/app/components/events/interfaces/debugMeta/debugImageDetails/candidate/actions.tsx

@@ -2,7 +2,7 @@ import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
 import Access from 'sentry/components/acl/access';
-import {Role} from 'sentry/components/acl/role';
+import {useRole} from 'sentry/components/acl/useRole';
 import MenuItemActionLink from 'sentry/components/actions/menuItemActionLink';
 import {Button, LinkButton} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
@@ -45,6 +45,7 @@ function Actions({
 }: Props) {
   const {download, location: debugFileId} = candidate;
   const {status} = download;
+  const {hasRole} = useRole({role: 'debugFilesRole'});
 
   if (!debugFileId || !isInternalSource) {
     return null;
@@ -54,82 +55,78 @@ function Actions({
   const downloadUrl = `${baseUrl}/projects/${organization.slug}/${projSlug}/files/dsyms/?id=${debugFileId}`;
 
   const actions = (
-    <Role role={organization.debugFilesRole} organization={organization}>
-      {({hasRole}) => (
-        <Access access={['project:write']}>
-          {({hasAccess}) => (
-            <Fragment>
-              <StyledDropdownLink
-                caret={false}
-                customTitle={
-                  <Button
-                    size="xs"
-                    aria-label={t('Actions')}
-                    disabled={deleted}
-                    icon={<IconEllipsis />}
-                  />
-                }
-                anchorRight
+    <Access access={['project:write']}>
+      {({hasAccess}) => (
+        <Fragment>
+          <StyledDropdownLink
+            caret={false}
+            customTitle={
+              <Button
+                size="xs"
+                aria-label={t('Actions')}
+                disabled={deleted}
+                icon={<IconEllipsis />}
+              />
+            }
+            anchorRight
+          >
+            <Tooltip disabled={hasRole} title={noPermissionToDownloadDebugFilesInfo}>
+              <MenuItemActionLink
+                shouldConfirm={false}
+                icon={<IconDownload size="xs" />}
+                href={downloadUrl}
+                onClick={event => {
+                  if (deleted) {
+                    event.preventDefault();
+                  }
+                }}
+                disabled={!hasRole || deleted}
               >
-                <Tooltip disabled={hasRole} title={noPermissionToDownloadDebugFilesInfo}>
-                  <MenuItemActionLink
-                    shouldConfirm={false}
-                    icon={<IconDownload size="xs" />}
-                    href={downloadUrl}
-                    onClick={event => {
-                      if (deleted) {
-                        event.preventDefault();
-                      }
-                    }}
-                    disabled={!hasRole || deleted}
-                  >
-                    {t('Download')}
-                  </MenuItemActionLink>
-                </Tooltip>
-                <Tooltip disabled={hasAccess} title={noPermissionToDeleteDebugFilesInfo}>
-                  <MenuItemActionLink
-                    onAction={() => onDelete(debugFileId)}
-                    message={debugFileDeleteConfirmationInfo}
-                    disabled={!hasAccess || deleted}
-                    shouldConfirm
-                  >
-                    {t('Delete')}
-                  </MenuItemActionLink>
-                </Tooltip>
-              </StyledDropdownLink>
-              <StyledButtonBar gap={1}>
-                <Tooltip disabled={hasRole} title={noPermissionToDownloadDebugFilesInfo}>
-                  <LinkButton
-                    size="xs"
-                    icon={<IconDownload />}
-                    href={downloadUrl}
-                    disabled={!hasRole}
-                  >
-                    {t('Download')}
-                  </LinkButton>
-                </Tooltip>
-                <Tooltip disabled={hasAccess} title={noPermissionToDeleteDebugFilesInfo}>
-                  <Confirm
-                    confirmText={t('Delete')}
-                    message={debugFileDeleteConfirmationInfo}
-                    onConfirm={() => onDelete(debugFileId)}
-                    disabled={!hasAccess}
-                  >
-                    <Button
-                      priority="danger"
-                      icon={<IconDelete />}
-                      size="xs"
-                      disabled={!hasAccess}
-                      aria-label={t('Delete')}
-                    />
-                  </Confirm>
-                </Tooltip>
-              </StyledButtonBar>
-            </Fragment>
-          )}
-        </Access>
+                {t('Download')}
+              </MenuItemActionLink>
+            </Tooltip>
+            <Tooltip disabled={hasAccess} title={noPermissionToDeleteDebugFilesInfo}>
+              <MenuItemActionLink
+                onAction={() => onDelete(debugFileId)}
+                message={debugFileDeleteConfirmationInfo}
+                disabled={!hasAccess || deleted}
+                shouldConfirm
+              >
+                {t('Delete')}
+              </MenuItemActionLink>
+            </Tooltip>
+          </StyledDropdownLink>
+          <StyledButtonBar gap={1}>
+            <Tooltip disabled={hasRole} title={noPermissionToDownloadDebugFilesInfo}>
+              <LinkButton
+                size="xs"
+                icon={<IconDownload />}
+                href={downloadUrl}
+                disabled={!hasRole}
+              >
+                {t('Download')}
+              </LinkButton>
+            </Tooltip>
+            <Tooltip disabled={hasAccess} title={noPermissionToDeleteDebugFilesInfo}>
+              <Confirm
+                confirmText={t('Delete')}
+                message={debugFileDeleteConfirmationInfo}
+                onConfirm={() => onDelete(debugFileId)}
+                disabled={!hasAccess}
+              >
+                <Button
+                  priority="danger"
+                  icon={<IconDelete />}
+                  size="xs"
+                  disabled={!hasAccess}
+                  aria-label={t('Delete')}
+                />
+              </Confirm>
+            </Tooltip>
+          </StyledButtonBar>
+        </Fragment>
       )}
-    </Role>
+    </Access>
   );
 
   if (!deleted) {

+ 8 - 13
static/app/components/feedback/feedbackItem/messageSection.tsx

@@ -1,7 +1,7 @@
 import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
-import {Role} from 'sentry/components/acl/role';
+import {useRole} from 'sentry/components/acl/useRole';
 import {Flex} from 'sentry/components/container/flex';
 import FeedbackItemUsername from 'sentry/components/feedback/feedbackItem/feedbackItemUsername';
 import FeedbackTimestampsTooltip from 'sentry/components/feedback/feedbackItem/feedbackTimestampsTooltip';
@@ -21,6 +21,7 @@ interface Props {
 
 export default function MessageSection({eventData, feedbackItem}: Props) {
   const organization = useOrganization();
+  const {hasRole} = useRole({role: 'attachmentsRole'});
   const project = feedbackItem.project;
   return (
     <Fragment>
@@ -40,18 +41,12 @@ export default function MessageSection({eventData, feedbackItem}: Props) {
       <Blockquote>
         <pre>{feedbackItem.metadata.message}</pre>
 
-        {eventData && project ? (
-          <Role organization={organization} role={organization.attachmentsRole}>
-            {({hasRole}) =>
-              hasRole ? (
-                <ScreenshotSection
-                  event={eventData}
-                  organization={organization}
-                  projectSlug={project.slug}
-                />
-              ) : null
-            }
-          </Role>
+        {eventData && project && hasRole ? (
+          <ScreenshotSection
+            event={eventData}
+            organization={organization}
+            projectSlug={project.slug}
+          />
         ) : null}
       </Blockquote>
       <Flex justify="flex-end">

+ 8 - 1
static/app/types/organization.tsx

@@ -71,6 +71,10 @@ export interface Organization extends OrganizationSummary {
   isDynamicallySampled: boolean;
   onboardingTasks: OnboardingTaskStatus[];
   openMembership: boolean;
+  /**
+   * A list of roles that are available to the organization.
+   * eg: billing, admin, member, manager, owner
+   */
   orgRoleList: OrgRole[];
   pendingAccessRequests: number;
   quota: {
@@ -132,7 +136,10 @@ export interface BaseRole {
 export interface OrgRole extends BaseRole {
   minimumTeamRole: string;
   isGlobal?: boolean;
-  is_global?: boolean; // Deprecated: use isGlobal
+  /**
+   * @deprecated use isGlobal
+   */
+  is_global?: boolean;
 }
 export interface TeamRole extends BaseRole {
   isMinimumRoleFor: string;

+ 2 - 2
static/app/views/settings/organizationMembers/organizationMemberDetail.spec.tsx

@@ -150,7 +150,7 @@ describe('OrganizationMemberDetail', function () {
 
       // Should have 4 roles
       const radios = screen.getAllByRole('radio');
-      expect(radios).toHaveLength(4);
+      expect(radios).toHaveLength(5);
 
       // Click last radio
       await userEvent.click(radios.at(-1) as Element);
@@ -781,7 +781,7 @@ describe('OrganizationMemberDetail', function () {
 
       // Change member to owner
       const orgRoleRadio = screen.getAllByRole('radio');
-      expect(orgRoleRadio).toHaveLength(4);
+      expect(orgRoleRadio).toHaveLength(5);
       await userEvent.click(orgRoleRadio.at(-1) as Element);
       expect(orgRoleRadio.at(-1)).toBeChecked();
 

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