Skip to content

Commit 0b9ed57

Browse files
authored
feat: add delete custom role context menu button and modal (#14228)
* feat: delete custom role * fix: add doc comment
1 parent c648c54 commit 0b9ed57

File tree

4 files changed

+93
-38
lines changed

4 files changed

+93
-38
lines changed

site/src/api/api.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -600,21 +600,27 @@ class ApiMethods {
600600
return response.data;
601601
};
602602

603+
/**
604+
* @param organization Can be the organization's ID or name
605+
*/
603606
patchOrganizationRole = async (
604-
organizationId: string,
607+
organization: string,
605608
role: TypesGen.Role,
606609
): Promise<TypesGen.Role> => {
607610
const response = await this.axios.patch<TypesGen.Role>(
608-
`/api/v2/organizations/${organizationId}/members/roles`,
611+
`/api/v2/organizations/${organization}/members/roles`,
609612
role,
610613
);
611614

612615
return response.data;
613616
};
614617

615-
deleteOrganizationRole = async (organizationId: string, roleName: string) => {
618+
/**
619+
* @param organization Can be the organization's ID or name
620+
*/
621+
deleteOrganizationRole = async (organization: string, roleName: string) => {
616622
await this.axios.delete(
617-
`/api/v2/organizations/${organizationId}/members/roles/${roleName}`,
623+
`/api/v2/organizations/${organization}/members/roles/${roleName}`,
618624
);
619625
};
620626

site/src/api/queries/roles.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,37 @@ export const roles = () => {
1616
};
1717
};
1818

19-
export const organizationRoles = (organizationId: string) => {
19+
export const organizationRoles = (organization: string) => {
2020
return {
21-
queryKey: ["organization", organizationId, "roles"],
22-
queryFn: () => API.getOrganizationRoles(organizationId),
21+
queryKey: ["organization", organization, "roles"],
22+
queryFn: () => API.getOrganizationRoles(organization),
2323
};
2424
};
2525

2626
export const patchOrganizationRole = (
2727
queryClient: QueryClient,
28-
organizationId: string,
28+
organization: string,
2929
) => {
3030
return {
3131
mutationFn: (request: Role) =>
32-
API.patchOrganizationRole(organizationId, request),
32+
API.patchOrganizationRole(organization, request),
3333
onSuccess: async (updatedRole: Role) =>
3434
await queryClient.invalidateQueries(
35-
getRoleQueryKey(organizationId, updatedRole.name),
35+
getRoleQueryKey(organization, updatedRole.name),
3636
),
3737
};
3838
};
3939

40-
export const deleteRole = (
40+
export const deleteOrganizationRole = (
4141
queryClient: QueryClient,
42-
organizationId: string,
42+
organization: string,
4343
) => {
4444
return {
45-
mutationFn: API.deleteOrganizationRole,
45+
mutationFn: (roleName: string) =>
46+
API.deleteOrganizationRole(organization, roleName),
4647
onSuccess: async (_: void, roleName: string) =>
4748
await queryClient.invalidateQueries(
48-
getRoleQueryKey(organizationId, roleName),
49+
getRoleQueryKey(organization, roleName),
4950
),
5051
};
5152
};

site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPage.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import AddIcon from "@mui/icons-material/AddOutlined";
22
import Button from "@mui/material/Button";
3-
import { type FC, useEffect } from "react";
3+
import { type FC, useEffect, useState } from "react";
44
import { Helmet } from "react-helmet-async";
5-
import { useQuery } from "react-query";
5+
import { useMutation, useQuery, useQueryClient } from "react-query";
66
import { Link as RouterLink, useParams } from "react-router-dom";
77
import { getErrorMessage } from "api/errors";
88
import { organizationPermissions } from "api/queries/organizations";
9-
import { organizationRoles } from "api/queries/roles";
10-
import { displayError } from "components/GlobalSnackbar/utils";
9+
import { deleteOrganizationRole, organizationRoles } from "api/queries/roles";
10+
import type { Role } from "api/typesGenerated";
11+
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
12+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
1113
import { Loader } from "components/Loader/Loader";
1214
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
1315
import { Stack } from "components/Stack/Stack";
@@ -17,13 +19,18 @@ import { useOrganizationSettings } from "../ManagementSettingsLayout";
1719
import CustomRolesPageView from "./CustomRolesPageView";
1820

1921
export const CustomRolesPage: FC = () => {
22+
const queryClient = useQueryClient();
2023
const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility();
2124
const { organization: organizationName } = useParams() as {
2225
organization: string;
2326
};
2427
const { organizations } = useOrganizationSettings();
2528
const organization = organizations?.find((o) => o.name === organizationName);
2629
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
30+
const deleteRoleMutation = useMutation(
31+
deleteOrganizationRole(queryClient, organizationName),
32+
);
33+
const [roleToDelete, setRoleToDelete] = useState<Role>();
2734
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
2835
const filteredRoleData = organizationRolesQuery.data?.filter(
2936
(role) => role.built_in === false,
@@ -69,9 +76,31 @@ export const CustomRolesPage: FC = () => {
6976

7077
<CustomRolesPageView
7178
roles={filteredRoleData}
79+
onDeleteRole={setRoleToDelete}
7280
canAssignOrgRole={permissions.assignOrgRole}
7381
isCustomRolesEnabled={isCustomRolesEnabled}
7482
/>
83+
84+
<DeleteDialog
85+
key={roleToDelete?.name}
86+
isOpen={roleToDelete !== undefined}
87+
confirmLoading={deleteRoleMutation.isLoading}
88+
name={roleToDelete?.name ?? ""}
89+
entity="role"
90+
onCancel={() => setRoleToDelete(undefined)}
91+
onConfirm={async () => {
92+
try {
93+
await deleteRoleMutation.mutateAsync(roleToDelete!.name);
94+
setRoleToDelete(undefined);
95+
await organizationRolesQuery.refetch();
96+
displaySuccess("Custom role deleted successfully!");
97+
} catch (error) {
98+
displayError(
99+
getErrorMessage(error, "Failed to delete custom role"),
100+
);
101+
}
102+
}}
103+
/>
75104
</>
76105
);
77106
};

site/src/pages/ManagementSettingsPage/CustomRolesPage/CustomRolesPageView.tsx

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import AddOutlined from "@mui/icons-material/AddOutlined";
3-
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
43
import Button from "@mui/material/Button";
54
import Skeleton from "@mui/material/Skeleton";
65
import Table from "@mui/material/Table";
@@ -14,22 +13,30 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
1413
import type { Role } from "api/typesGenerated";
1514
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
1615
import { EmptyState } from "components/EmptyState/EmptyState";
16+
import {
17+
MoreMenu,
18+
MoreMenuContent,
19+
MoreMenuItem,
20+
MoreMenuTrigger,
21+
ThreeDotsButton,
22+
} from "components/MoreMenu/MoreMenu";
1723
import { Paywall } from "components/Paywall/Paywall";
1824
import {
1925
TableLoaderSkeleton,
2026
TableRowSkeleton,
2127
} from "components/TableLoader/TableLoader";
22-
import { useClickableTableRow } from "hooks";
2328
import { docs } from "utils/docs";
2429

2530
export type CustomRolesPageViewProps = {
2631
roles: Role[] | undefined;
32+
onDeleteRole: (role: Role) => void;
2733
canAssignOrgRole: boolean;
2834
isCustomRolesEnabled: boolean;
2935
};
3036

3137
export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
3238
roles,
39+
onDeleteRole,
3340
canAssignOrgRole,
3441
isCustomRolesEnabled,
3542
}) => {
@@ -53,7 +60,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
5360
<TableRow>
5461
<TableCell width="50%">Name</TableCell>
5562
<TableCell width="49%">Permissions</TableCell>
56-
<TableCell width="1%"></TableCell>
63+
<TableCell width="1%" />
5764
</TableRow>
5865
</TableHead>
5966
<TableBody>
@@ -91,7 +98,12 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
9198

9299
<Cond>
93100
{roles?.map((role) => (
94-
<RoleRow key={role.name} role={role} />
101+
<RoleRow
102+
key={role.name}
103+
role={role}
104+
canAssignOrgRole={canAssignOrgRole}
105+
onDelete={() => onDeleteRole(role)}
106+
/>
95107
))}
96108
</Cond>
97109
</ChooseOne>
@@ -106,26 +118,41 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
106118

107119
interface RoleRowProps {
108120
role: Role;
121+
onDelete: () => void;
122+
canAssignOrgRole: boolean;
109123
}
110124

111-
const RoleRow: FC<RoleRowProps> = ({ role }) => {
125+
const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
112126
const navigate = useNavigate();
113-
const rowProps = useClickableTableRow({
114-
onClick: () => navigate(role.name),
115-
});
116127

117128
return (
118-
<TableRow data-testid={`role-${role.name}`} {...rowProps}>
129+
<TableRow data-testid={`role-${role.name}`}>
119130
<TableCell>{role.display_name || role.name}</TableCell>
120131

121132
<TableCell css={styles.secondary}>
122133
{role.organization_permissions.length}
123134
</TableCell>
124135

125136
<TableCell>
126-
<div css={styles.arrowCell}>
127-
<KeyboardArrowRight css={styles.arrowRight} />
128-
</div>
137+
<MoreMenu>
138+
<MoreMenuTrigger>
139+
<ThreeDotsButton />
140+
</MoreMenuTrigger>
141+
<MoreMenuContent>
142+
<MoreMenuItem
143+
onClick={() => {
144+
navigate(role.name);
145+
}}
146+
>
147+
Edit
148+
</MoreMenuItem>
149+
{canAssignOrgRole && (
150+
<MoreMenuItem danger onClick={onDelete}>
151+
Delete&hellip;
152+
</MoreMenuItem>
153+
)}
154+
</MoreMenuContent>
155+
</MoreMenu>
129156
</TableCell>
130157
</TableRow>
131158
);
@@ -150,14 +177,6 @@ const TableLoader = () => {
150177
};
151178

152179
const styles = {
153-
arrowRight: (theme) => ({
154-
color: theme.palette.text.secondary,
155-
width: 20,
156-
height: 20,
157-
}),
158-
arrowCell: {
159-
display: "flex",
160-
},
161180
secondary: (theme) => ({
162181
color: theme.palette.text.secondary,
163182
}),

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy