move to new role before delete

This commit is contained in:
Milo Schwartz 2024-11-10 21:52:50 -05:00
parent 02a762a693
commit 4ebfd86854
3 changed files with 275 additions and 64 deletions

View file

@ -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,

View file

@ -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<ListRolesResponse["roles"]>([]);
useEffect(() => {
async function fetchRoles() {
const res = await api
.get<AxiosResponse<ListRolesResponse>>(
`/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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
newRoleId: "",
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
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 (
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
setLoading(false);
form.reset();
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>Remove Role</CredenzaTitle>
<CredenzaDescription>
Remove a role from the organization
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<p className="mb-1">
You're about to delete the{" "}
<b>{roleToDelete.name}</b> role. You cannot undo
this action.
</p>
<p className="mb-4">
Before deleting this role, please select a new role
to transfer existing members to.
</p>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="remove-role-form"
>
<FormField
control={form.control}
name="newRoleId"
render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select role" />
</SelectTrigger>
</FormControl>
<SelectContent>
{roles.map((role) => (
<SelectItem
key={role.roleId}
value={role.roleId.toString()}
>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<Button
type="submit"
form="remove-role-form"
loading={loading}
disabled={loading}
>
Remove Role
</Button>
<CredenzaClose asChild>
<Button variant="outline">Close</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

View file

@ -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 (
<>
<CreateRoleForm
@ -136,34 +108,19 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
}}
/>
<ConfirmDeleteDialog
open={isDeleteModalOpen}
setOpen={(val) => {
setIsDeleteModalOpen(val);
setUserToRemove(null);
}}
dialog={
<div>
<p className="mb-2">
Are you sure you want to remove the role{" "}
<b>{roleToRemove?.name}</b> from the organization?
</p>
<p className="mb-2">
You cannot undo this action. Please select a new
role to move existing users to after deletion.
</p>
<p>
To confirm, please type the name of the role below.
</p>
</div>
}
buttonText="Confirm remove role"
onConfirm={removeRole}
string={roleToRemove?.name ?? ""}
title="Remove role from organization"
/>
{roleToRemove && (
<DeleteRoleForm
open={isDeleteModalOpen}
setOpen={setIsDeleteModalOpen}
roleToDelete={roleToRemove}
afterDelete={() => {
setRoles((prev) =>
prev.filter((r) => r.roleId !== roleToRemove.roleId)
);
setUserToRemove(null);
}}
/>
)}
<RolesDataTable
columns={columns}