Skip to content

Commit 3b89659

Browse files
authored
fix(member-invite): use hook to get default org roles (#85994)
We determine the role options for the member invite modal using the inviter's member details. However, if a superuser is using the invite modal, then the inviter is not a member and the organization member details endpoint will 404. In this case, we use a default list of org roles to populate the role dropdown in the modal. We should use a hook to get the default list of org roles so that we can include a Billing role, and only show the Admin role when the org is not on a business or enterprise plan.
1 parent a97fa64 commit 3b89659

File tree

5 files changed

+86
-2
lines changed

5 files changed

+86
-2
lines changed

static/app/components/modals/inviteMembersModal/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import useInviteModal from 'sentry/components/modals/inviteMembersModal/useInvit
1717
import {InviteModalHook} from 'sentry/components/modals/memberInviteModalCustomization';
1818
import {ORG_ROLES} from 'sentry/constants';
1919
import {t} from 'sentry/locale';
20+
import HookStore from 'sentry/stores/hookStore';
2021
import {space} from 'sentry/styles/space';
2122
import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
2223
import useOrganization from 'sentry/utils/useOrganization';
@@ -69,6 +70,10 @@ function InviteMembersModal({
6970
);
7071
}
7172

73+
const defaultOrgRoles =
74+
HookStore.get('member-invite-modal:organization-roles')[0]?.(organization) ??
75+
ORG_ROLES;
76+
7277
return (
7378
<ErrorBoundary>
7479
<InviteModalHook
@@ -109,7 +114,7 @@ function InviteMembersModal({
109114
<InviteMessage />
110115
{headerInfo}
111116
<StyledInviteRow
112-
roleOptions={memberResult.data?.orgRoleList ?? ORG_ROLES}
117+
roleOptions={memberResult.data?.orgRoleList ?? defaultOrgRoles}
113118
roleDisabledUnallowed={willInvite}
114119
/>
115120
</Body>

static/app/types/hooks.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import type {
2222
RouteComponentProps,
2323
RouteContextInterface,
2424
} from './legacyReactRouter';
25-
import type {Member, Organization} from './organization';
25+
import type {Member, Organization, OrgRole} from './organization';
2626
import type {Project} from './project';
2727
import type {User} from './user';
2828

@@ -232,6 +232,7 @@ export type CustomizationHooks = {
232232
'integrations:feature-gates': IntegrationsFeatureGatesHook;
233233
'member-invite-button:customization': InviteButtonCustomizationHook;
234234
'member-invite-modal:customization': InviteModalCustomizationHook;
235+
'member-invite-modal:organization-roles': (organization: Organization) => OrgRole[];
235236
'sidebar:navigation-item': SidebarNavigationItemHook;
236237
};
237238

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {OrganizationFixture} from 'sentry-fixture/organization';
2+
3+
import {getOrgRoles} from 'getsentry/hooks/organizationRoles';
4+
5+
describe('OrganizationRoles', function () {
6+
it('includes admin if org does not have team-roles', function () {
7+
const organization = OrganizationFixture({features: []});
8+
const result = getOrgRoles(organization);
9+
expect(result).toHaveLength(5);
10+
expect(result[2]?.id).toBe('admin');
11+
});
12+
13+
it('does not include admin if org has team-roles', function () {
14+
const organization = OrganizationFixture({features: ['team-roles']});
15+
const result = getOrgRoles(organization);
16+
expect(result).toHaveLength(4);
17+
});
18+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type {Organization, OrgRole} from 'sentry/types/organization';
2+
3+
export const ORG_ROLES: OrgRole[] = [
4+
{
5+
id: 'billing',
6+
name: 'Billing',
7+
isAllowed: true,
8+
desc: 'Can manage payment and compliance details.',
9+
minimumTeamRole: 'contributor',
10+
isTeamRolesAllowed: false,
11+
},
12+
{
13+
id: 'member',
14+
name: 'Member',
15+
isAllowed: true,
16+
desc: 'Members can view and act on events, as well as view most other data within the organization.',
17+
minimumTeamRole: 'contributor',
18+
isTeamRolesAllowed: true,
19+
},
20+
{
21+
id: 'manager',
22+
name: 'Manager',
23+
isAllowed: true,
24+
desc: 'Gains admin access on all teams as well as the ability to add and remove members.',
25+
minimumTeamRole: 'admin',
26+
isTeamRolesAllowed: true,
27+
},
28+
{
29+
id: 'owner',
30+
name: 'Owner',
31+
isAllowed: true,
32+
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.',
33+
minimumTeamRole: 'admin',
34+
isTeamRolesAllowed: true,
35+
},
36+
];
37+
38+
export function getOrgRoles(organization: Organization): OrgRole[] {
39+
if (organization.features.includes('team-roles')) {
40+
return ORG_ROLES;
41+
}
42+
const adminRole: OrgRole = {
43+
id: 'admin',
44+
name: 'Admin',
45+
isAllowed: true,
46+
desc: "Admin privileges on any teams of which they're a member. They can create new teams and projects, as well as remove teams and projects on which they already hold membership (or all teams, if open membership is enabled). Additionally, they can manage memberships of teams that they are members of. They cannot invite members to the organization.",
47+
minimumTeamRole: 'admin',
48+
isTeamRolesAllowed: true,
49+
};
50+
const rolesWithAdmin = [...ORG_ROLES];
51+
// insert admin role to keep roles ordered from least to most permissions
52+
rolesWithAdmin.splice(2, 0, adminRole);
53+
return rolesWithAdmin;
54+
}

static/gsApp/registerHooks.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import hookIntegrationFeatures from 'getsentry/hooks/integrationFeatures';
5757
import legacyOrganizationRedirectRoutes from 'getsentry/hooks/legacyOrganizationRedirectRoutes';
5858
import MemberListHeader from 'getsentry/hooks/memberListHeader';
5959
import OrganizationMembershipSettingsForm from 'getsentry/hooks/organizationMembershipSettingsForm';
60+
import {getOrgRoles} from 'getsentry/hooks/organizationRoles';
6061
import OrgStatsBanner from 'getsentry/hooks/orgStatsBanner';
6162
import hookRootRoutes from 'getsentry/hooks/rootRoutes';
6263
import hookSettingsRoutes from 'getsentry/hooks/settingsRoutes';
@@ -174,6 +175,11 @@ const GETSENTRY_HOOKS: Partial<Hooks> = {
174175
*/
175176
'onboarding-wizard:skip-help': () => OnboardingWizardHelp,
176177

178+
/**
179+
* Get list of organization roles
180+
*/
181+
'member-invite-modal:organization-roles': getOrgRoles,
182+
177183
/**
178184
* Ensure we enable/disable Pendo when guides change
179185
*/

0 commit comments

Comments
 (0)