mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user