feat: add delete custom role context menu button and modal (#14228)

* feat: delete custom role

* fix: add doc comment
This commit is contained in:
Jaayden Halko
2024-08-09 16:59:44 -04:00
committed by GitHub
parent c648c548d8
commit 0b9ed57c10
4 changed files with 93 additions and 38 deletions
+10 -4
View File
@@ -600,21 +600,27 @@ class ApiMethods {
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
patchOrganizationRole = async (
organizationId: string,
organization: string,
role: TypesGen.Role,
): Promise<TypesGen.Role> => {
const response = await this.axios.patch<TypesGen.Role>(
`/api/v2/organizations/${organizationId}/members/roles`,
`/api/v2/organizations/${organization}/members/roles`,
role,
);
return response.data;
};
deleteOrganizationRole = async (organizationId: string, roleName: string) => {
/**
* @param organization Can be the organization's ID or name
*/
deleteOrganizationRole = async (organization: string, roleName: string) => {
await this.axios.delete(
`/api/v2/organizations/${organizationId}/members/roles/${roleName}`,
`/api/v2/organizations/${organization}/members/roles/${roleName}`,
);
};
+11 -10
View File
@@ -16,36 +16,37 @@ export const roles = () => {
};
};
export const organizationRoles = (organizationId: string) => {
export const organizationRoles = (organization: string) => {
return {
queryKey: ["organization", organizationId, "roles"],
queryFn: () => API.getOrganizationRoles(organizationId),
queryKey: ["organization", organization, "roles"],
queryFn: () => API.getOrganizationRoles(organization),
};
};
export const patchOrganizationRole = (
queryClient: QueryClient,
organizationId: string,
organization: string,
) => {
return {
mutationFn: (request: Role) =>
API.patchOrganizationRole(organizationId, request),
API.patchOrganizationRole(organization, request),
onSuccess: async (updatedRole: Role) =>
await queryClient.invalidateQueries(
getRoleQueryKey(organizationId, updatedRole.name),
getRoleQueryKey(organization, updatedRole.name),
),
};
};
export const deleteRole = (
export const deleteOrganizationRole = (
queryClient: QueryClient,
organizationId: string,
organization: string,
) => {
return {
mutationFn: API.deleteOrganizationRole,
mutationFn: (roleName: string) =>
API.deleteOrganizationRole(organization, roleName),
onSuccess: async (_: void, roleName: string) =>
await queryClient.invalidateQueries(
getRoleQueryKey(organizationId, roleName),
getRoleQueryKey(organization, roleName),
),
};
};
@@ -1,13 +1,15 @@
import AddIcon from "@mui/icons-material/AddOutlined";
import Button from "@mui/material/Button";
import { type FC, useEffect } from "react";
import { type FC, useEffect, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { Link as RouterLink, useParams } from "react-router-dom";
import { getErrorMessage } from "api/errors";
import { organizationPermissions } from "api/queries/organizations";
import { organizationRoles } from "api/queries/roles";
import { displayError } from "components/GlobalSnackbar/utils";
import { deleteOrganizationRole, organizationRoles } from "api/queries/roles";
import type { Role } from "api/typesGenerated";
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { SettingsHeader } from "components/SettingsHeader/SettingsHeader";
import { Stack } from "components/Stack/Stack";
@@ -17,6 +19,7 @@ import { useOrganizationSettings } from "../ManagementSettingsLayout";
import CustomRolesPageView from "./CustomRolesPageView";
export const CustomRolesPage: FC = () => {
const queryClient = useQueryClient();
const { custom_roles: isCustomRolesEnabled } = useFeatureVisibility();
const { organization: organizationName } = useParams() as {
organization: string;
@@ -24,6 +27,10 @@ export const CustomRolesPage: FC = () => {
const { organizations } = useOrganizationSettings();
const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const deleteRoleMutation = useMutation(
deleteOrganizationRole(queryClient, organizationName),
);
const [roleToDelete, setRoleToDelete] = useState<Role>();
const organizationRolesQuery = useQuery(organizationRoles(organizationName));
const filteredRoleData = organizationRolesQuery.data?.filter(
(role) => role.built_in === false,
@@ -69,9 +76,31 @@ export const CustomRolesPage: FC = () => {
<CustomRolesPageView
roles={filteredRoleData}
onDeleteRole={setRoleToDelete}
canAssignOrgRole={permissions.assignOrgRole}
isCustomRolesEnabled={isCustomRolesEnabled}
/>
<DeleteDialog
key={roleToDelete?.name}
isOpen={roleToDelete !== undefined}
confirmLoading={deleteRoleMutation.isLoading}
name={roleToDelete?.name ?? ""}
entity="role"
onCancel={() => setRoleToDelete(undefined)}
onConfirm={async () => {
try {
await deleteRoleMutation.mutateAsync(roleToDelete!.name);
setRoleToDelete(undefined);
await organizationRolesQuery.refetch();
displaySuccess("Custom role deleted successfully!");
} catch (error) {
displayError(
getErrorMessage(error, "Failed to delete custom role"),
);
}
}}
/>
</>
);
};
@@ -1,6 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import AddOutlined from "@mui/icons-material/AddOutlined";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Button from "@mui/material/Button";
import Skeleton from "@mui/material/Skeleton";
import Table from "@mui/material/Table";
@@ -14,22 +13,30 @@ import { Link as RouterLink, useNavigate } from "react-router-dom";
import type { Role } from "api/typesGenerated";
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
import { EmptyState } from "components/EmptyState/EmptyState";
import {
MoreMenu,
MoreMenuContent,
MoreMenuItem,
MoreMenuTrigger,
ThreeDotsButton,
} from "components/MoreMenu/MoreMenu";
import { Paywall } from "components/Paywall/Paywall";
import {
TableLoaderSkeleton,
TableRowSkeleton,
} from "components/TableLoader/TableLoader";
import { useClickableTableRow } from "hooks";
import { docs } from "utils/docs";
export type CustomRolesPageViewProps = {
roles: Role[] | undefined;
onDeleteRole: (role: Role) => void;
canAssignOrgRole: boolean;
isCustomRolesEnabled: boolean;
};
export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
roles,
onDeleteRole,
canAssignOrgRole,
isCustomRolesEnabled,
}) => {
@@ -53,7 +60,7 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
<TableRow>
<TableCell width="50%">Name</TableCell>
<TableCell width="49%">Permissions</TableCell>
<TableCell width="1%"></TableCell>
<TableCell width="1%" />
</TableRow>
</TableHead>
<TableBody>
@@ -91,7 +98,12 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
<Cond>
{roles?.map((role) => (
<RoleRow key={role.name} role={role} />
<RoleRow
key={role.name}
role={role}
canAssignOrgRole={canAssignOrgRole}
onDelete={() => onDeleteRole(role)}
/>
))}
</Cond>
</ChooseOne>
@@ -106,16 +118,15 @@ export const CustomRolesPageView: FC<CustomRolesPageViewProps> = ({
interface RoleRowProps {
role: Role;
onDelete: () => void;
canAssignOrgRole: boolean;
}
const RoleRow: FC<RoleRowProps> = ({ role }) => {
const RoleRow: FC<RoleRowProps> = ({ role, onDelete, canAssignOrgRole }) => {
const navigate = useNavigate();
const rowProps = useClickableTableRow({
onClick: () => navigate(role.name),
});
return (
<TableRow data-testid={`role-${role.name}`} {...rowProps}>
<TableRow data-testid={`role-${role.name}`}>
<TableCell>{role.display_name || role.name}</TableCell>
<TableCell css={styles.secondary}>
@@ -123,9 +134,25 @@ const RoleRow: FC<RoleRowProps> = ({ role }) => {
</TableCell>
<TableCell>
<div css={styles.arrowCell}>
<KeyboardArrowRight css={styles.arrowRight} />
</div>
<MoreMenu>
<MoreMenuTrigger>
<ThreeDotsButton />
</MoreMenuTrigger>
<MoreMenuContent>
<MoreMenuItem
onClick={() => {
navigate(role.name);
}}
>
Edit
</MoreMenuItem>
{canAssignOrgRole && (
<MoreMenuItem danger onClick={onDelete}>
Delete&hellip;
</MoreMenuItem>
)}
</MoreMenuContent>
</MoreMenu>
</TableCell>
</TableRow>
);
@@ -150,14 +177,6 @@ const TableLoader = () => {
};
const styles = {
arrowRight: (theme) => ({
color: theme.palette.text.secondary,
width: 20,
height: 20,
}),
arrowCell: {
display: "flex",
},
secondary: (theme) => ({
color: theme.palette.text.secondary,
}),