feat: add warning dialog when removing member from organization (#14695)

resolves #14705 


<img width="684" alt="Screenshot 2024-09-27 at 4 34 02 PM"
src="https://github.com/user-attachments/assets/5c3b6c3e-2afc-4405-8bed-d9ea80607411">
This commit is contained in:
Jaayden Halko
2024-09-27 17:20:01 -04:00
committed by GitHub
parent b80550957f
commit e6cd3005d3
4 changed files with 100 additions and 53 deletions
@@ -87,7 +87,9 @@ export const CustomRolesPage: FC = () => {
onCancel={() => setRoleToDelete(undefined)} onCancel={() => setRoleToDelete(undefined)}
onConfirm={async () => { onConfirm={async () => {
try { try {
await deleteRoleMutation.mutateAsync(roleToDelete!.name); if (roleToDelete) {
await deleteRoleMutation.mutateAsync(roleToDelete.name);
}
setRoleToDelete(undefined); setRoleToDelete(undefined);
await organizationRolesQuery.refetch(); await organizationRolesQuery.refetch();
displaySuccess("Custom role deleted successfully!"); displaySuccess("Custom role deleted successfully!");
@@ -50,6 +50,9 @@ const removeMember = async () => {
const removeButton = screen.getByText(/Remove/); const removeButton = screen.getByText(/Remove/);
await user.click(removeButton); await user.click(removeButton);
const dialog = await within(document.body).findByRole("dialog");
await user.click(within(dialog).getByRole("button", { name: "Remove" }));
}; };
const updateUserRole = async (role: SlimRole) => { const updateUserRole = async (role: SlimRole) => {
@@ -80,7 +83,7 @@ describe("OrganizationMembersPage", () => {
it("shows a success message", async () => { it("shows a success message", async () => {
await renderPage(); await renderPage();
await removeMember(); await removeMember();
await screen.findByText("Member removed successfully."); await screen.findByText("User removed from organization successfully!");
}); });
}); });
}); });
@@ -1,7 +1,6 @@
import { import type { Interpolation, Theme } from "@emotion/react";
groupsByUserId, import { getErrorMessage } from "api/errors";
groupsByUserIdInOrganization, import { groupsByUserIdInOrganization } from "api/queries/groups";
} from "api/queries/groups";
import { import {
addOrganizationMember, addOrganizationMember,
organizationMembers, organizationMembers,
@@ -11,9 +10,12 @@ import {
} from "api/queries/organizations"; } from "api/queries/organizations";
import { organizationRoles } from "api/queries/roles"; import { organizationRoles } from "api/queries/roles";
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader"; import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import { useAuthenticated } from "contexts/auth/RequireAuth"; import { useAuthenticated } from "contexts/auth/RequireAuth";
import type { FC } from "react"; import { type FC, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query"; import { useMutation, useQuery, useQueryClient } from "react-query";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useOrganizationSettings } from "./ManagementSettingsLayout"; import { useOrganizationSettings } from "./ManagementSettingsLayout";
@@ -52,45 +54,97 @@ const OrganizationMembersPage: FC = () => {
const organization = organizations?.find((o) => o.name === organizationName); const organization = organizations?.find((o) => o.name === organizationName);
const permissionsQuery = useQuery(organizationPermissions(organization?.id)); const permissionsQuery = useQuery(organizationPermissions(organization?.id));
const [memberToDelete, setMemberToDelete] =
useState<OrganizationMemberWithUserData>();
const permissions = permissionsQuery.data; const permissions = permissionsQuery.data;
if (!permissions) { if (!permissions) {
return <Loader />; return <Loader />;
} }
return ( return (
<OrganizationMembersPageView <>
allAvailableRoles={organizationRolesQuery.data} <OrganizationMembersPageView
canEditMembers={permissions.editMembers} allAvailableRoles={organizationRolesQuery.data}
error={ canEditMembers={permissions.editMembers}
membersQuery.error ?? error={
addMemberMutation.error ?? membersQuery.error ??
removeMemberMutation.error ?? addMemberMutation.error ??
updateMemberRolesMutation.error removeMemberMutation.error ??
} updateMemberRolesMutation.error
isAddingMember={addMemberMutation.isLoading} }
isUpdatingMemberRoles={updateMemberRolesMutation.isLoading} isAddingMember={addMemberMutation.isLoading}
me={me} isUpdatingMemberRoles={updateMemberRolesMutation.isLoading}
members={members} me={me}
groupsByUserId={groupsByUserIdQuery.data} members={members}
addMember={async (user: User) => { groupsByUserId={groupsByUserIdQuery.data}
await addMemberMutation.mutateAsync(user.id); addMember={async (user: User) => {
void membersQuery.refetch(); await addMemberMutation.mutateAsync(user.id);
}} void membersQuery.refetch();
removeMember={async (member: OrganizationMemberWithUserData) => { }}
await removeMemberMutation.mutateAsync(member.user_id); removeMember={setMemberToDelete}
void membersQuery.refetch(); updateMemberRoles={async (
}} member: OrganizationMemberWithUserData,
updateMemberRoles={async ( newRoles: string[],
member: OrganizationMemberWithUserData, ) => {
newRoles: string[], await updateMemberRolesMutation.mutateAsync({
) => { userId: member.user_id,
await updateMemberRolesMutation.mutateAsync({ roles: newRoles,
userId: member.user_id, });
roles: newRoles, }}
}); />
}}
/> <ConfirmDialog
type="delete"
open={memberToDelete !== undefined}
onClose={() => setMemberToDelete(undefined)}
title="Remove member"
confirmText="Remove"
onConfirm={async () => {
try {
if (memberToDelete) {
await removeMemberMutation.mutateAsync(memberToDelete?.user_id);
}
setMemberToDelete(undefined);
await membersQuery.refetch();
displaySuccess("User removed from organization successfully!");
} catch (error) {
setMemberToDelete(undefined);
displayError(
getErrorMessage(error, "Failed to remove user from organization"),
);
} finally {
setMemberToDelete(undefined);
}
}}
description={
<Stack>
<p>
Removing this member will:
<ul>
<li>Remove the member from all groups in this organization</li>
<li>Remove all user role assignments</li>
<li>
Orphan all the member's workspaces associated with this
organization
</li>
</ul>
</p>
<p css={styles.test}>
Are you sure you want to remove this member?
</p>
</Stack>
}
/>
</>
); );
}; };
const styles = {
test: {
paddingBottom: 20,
},
} satisfies Record<string, Interpolation<Theme>>;
export default OrganizationMembersPage; export default OrganizationMembersPage;
@@ -44,7 +44,7 @@ interface OrganizationMembersPageViewProps {
members: Array<OrganizationMemberTableEntry> | undefined; members: Array<OrganizationMemberTableEntry> | undefined;
groupsByUserId: GroupsByUserId | undefined; groupsByUserId: GroupsByUserId | undefined;
addMember: (user: User) => Promise<void>; addMember: (user: User) => Promise<void>;
removeMember: (member: OrganizationMemberWithUserData) => Promise<void>; removeMember: (member: OrganizationMemberWithUserData) => void;
updateMemberRoles: ( updateMemberRoles: (
member: OrganizationMemberWithUserData, member: OrganizationMemberWithUserData,
newRoles: string[], newRoles: string[],
@@ -134,19 +134,7 @@ export const OrganizationMembersPageView: FC<
<MoreMenuContent> <MoreMenuContent>
<MoreMenuItem <MoreMenuItem
danger danger
onClick={async () => { onClick={() => props.removeMember(member)}
try {
await props.removeMember(member);
displaySuccess("Member removed successfully.");
} catch (error) {
displayError(
getErrorMessage(
error,
"Failed to remove member.",
),
);
}
}}
> >
Remove Remove
</MoreMenuItem> </MoreMenuItem>