diff --git a/server/routers/role/deleteRole.ts b/server/routers/role/deleteRole.ts index 20cb98fe..12f8ff88 100644 --- a/server/routers/role/deleteRole.ts +++ b/server/routers/role/deleteRole.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { roles } from "@server/db/schema"; +import { roles, userOrgs } from "@server/db/schema"; import { eq } from "drizzle-orm"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; @@ -13,6 +13,10 @@ const deleteRoleSchema = z.object({ roleId: z.string().transform(Number).pipe(z.number().int().positive()), }); +const deelteRoleBodySchema = z.object({ + roleId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + export async function deleteRole( req: Request, res: Response, @@ -29,7 +33,27 @@ export async function deleteRole( ); } + const parsedBody = deelteRoleBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + const { roleId } = parsedParams.data; + const { roleId: newRoleId } = parsedBody.data; + + if (roleId === newRoleId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Cannot delete a role and assign the same role` + ) + ); + } const role = await db .select() @@ -55,20 +79,30 @@ export async function deleteRole( ); } - const deletedRole = await db - .delete(roles) - .where(eq(roles.roleId, roleId)) - .returning(); + const newRole = await db + .select() + .from(roles) + .where(eq(roles.roleId, newRoleId)) + .limit(1); - if (deletedRole.length === 0) { + if (newRole.length === 0) { return next( createHttpError( HttpCode.NOT_FOUND, - `Role with ID ${roleId} not found` + `Role with ID ${newRoleId} not found` ) ); } + // move all users from the userOrgs table with roleId to newRoleId + await db + .update(userOrgs) + .set({ roleId: newRoleId }) + .where(eq(userOrgs.roleId, roleId)); + + // delete the old role + await db.delete(roles).where(eq(roles.roleId, roleId)); + return response(res, { data: null, success: true, diff --git a/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx b/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx new file mode 100644 index 00000000..706eb0a6 --- /dev/null +++ b/src/app/[orgId]/settings/access/roles/components/DeleteRoleForm.tsx @@ -0,0 +1,220 @@ +"use client"; + +import api from "@app/api"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@app/components/ui/form"; +import { useToast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AxiosResponse } from "axios"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle, +} from "@app/components/Credenza"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { ListRolesResponse } from "@server/routers/role"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@app/components/ui/select"; +import { RoleRow } from "./RolesTable"; + +type CreateRoleFormProps = { + open: boolean; + roleToDelete: RoleRow; + setOpen: (open: boolean) => void; + afterDelete?: () => void; +}; + +const formSchema = z.object({ + newRoleId: z.string({ message: "New role is required" }), +}); + +export default function DeleteRoleForm({ + open, + roleToDelete, + setOpen, + afterDelete, +}: CreateRoleFormProps) { + const { toast } = useToast(); + const { org } = useOrgContext(); + + const [loading, setLoading] = useState(false); + const [roles, setRoles] = useState([]); + + useEffect(() => { + async function fetchRoles() { + const res = await api + .get>( + `/org/${org?.org.orgId}/roles` + ) + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: "Failed to fetch roles", + description: + e.message || + "An error occurred while fetching the roles", + }); + }); + + if (res?.status === 200) { + setRoles( + res.data.data.roles.filter( + (r) => r.roleId !== roleToDelete.roleId + ) + ); + } + } + + fetchRoles(); + }, []); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + newRoleId: "", + }, + }); + + async function onSubmit(values: z.infer) { + setLoading(true); + + const res = await api + .delete(`/role/${roleToDelete.roleId}`, { + data: { + roleId: values.newRoleId, + }, + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Failed to remove role", + description: + e.response?.data?.message || + "An error occurred while removing the role.", + }); + }); + + if (res && res.status === 200) { + toast({ + variant: "default", + title: "Role removed", + description: "The role has been successfully removed.", + }); + + if (open) { + setOpen(false); + } + + if (afterDelete) { + afterDelete(); + } + } + + setLoading(false); + } + + return ( + <> + { + setOpen(val); + setLoading(false); + form.reset(); + }} + > + + + Remove Role + + Remove a role from the organization + + + +

+ You're about to delete the{" "} + {roleToDelete.name} role. You cannot undo + this action. +

+

+ Before deleting this role, please select a new role + to transfer existing members to. +

+
+ + ( + + Role + + + + )} + /> + + +
+ + + + + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index a8218e23..7e39c3a2 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -17,6 +17,7 @@ import { useToast } from "@app/hooks/useToast"; import { RolesDataTable } from "./RolesDataTable"; import { Role } from "@server/db/schema"; import CreateRoleForm from "./CreateRoleForm"; +import DeleteRoleForm from "./DeleteRoleForm"; export type RoleRow = Role; @@ -97,35 +98,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) { }, ]; - async function removeRole() { - if (roleToRemove) { - const res = await api - .delete(`/role/${roleToRemove.roleId}`) - .catch((e) => { - toast({ - variant: "destructive", - title: "Failed to remove role", - description: - e.message ?? - "An error occurred while removing the role.", - }); - }); - - if (res && res.status === 200) { - toast({ - variant: "default", - title: "Role removed", - description: `The role ${roleToRemove.name} has been removed from the organization.`, - }); - - setRoles((prev) => - prev.filter((role) => role.roleId !== roleToRemove.roleId) - ); - } - } - setIsDeleteModalOpen(false); - } - return ( <> - { - setIsDeleteModalOpen(val); - setUserToRemove(null); - }} - dialog={ -
-

- Are you sure you want to remove the role{" "} - {roleToRemove?.name} from the organization? -

- -

- You cannot undo this action. Please select a new - role to move existing users to after deletion. -

- -

- To confirm, please type the name of the role below. -

-
- } - buttonText="Confirm remove role" - onConfirm={removeRole} - string={roleToRemove?.name ?? ""} - title="Remove role from organization" - /> + {roleToRemove && ( + { + setRoles((prev) => + prev.filter((r) => r.roleId !== roleToRemove.roleId) + ); + setUserToRemove(null); + }} + /> + )}