Integrate role deletion response shape

Ensures the role delete API returns the standardized `{ message, success, status_code }` success payload.

Made-with: Cursor
This commit is contained in:
Yared Yemane 2026-04-16 01:26:58 -07:00
parent 23d100cdde
commit 28af7994f8
3 changed files with 152 additions and 13 deletions

View File

@ -5,6 +5,7 @@ import type {
GetRolesParams,
CreateRoleRequest,
CreateRoleResponse,
DeleteRoleResponse,
SetRolePermissionsRequest,
GetPermissionsResponse,
} from "../types/rbac.types"
@ -26,3 +27,6 @@ export const setRolePermissions = (roleId: number, data: SetRolePermissionsReque
export const getAllPermissions = () =>
http.get<GetPermissionsResponse>("/rbac/permissions")
export const deleteRole = (roleId: number) =>
http.delete<DeleteRoleResponse>(`/rbac/roles/${roleId}`)

View File

@ -1,8 +1,18 @@
import { useEffect, useMemo, useState } from "react"
import { useNavigate } from "react-router-dom"
import {
Plus, Search, Shield, ShieldCheck, ChevronLeft, ChevronRight,
AlertCircle, Eye, X, Pencil, Check,
Plus,
Search,
Shield,
ShieldCheck,
ChevronLeft,
ChevronRight,
AlertCircle,
Eye,
X,
Pencil,
Check,
Trash2,
} from "lucide-react"
import { Button } from "../../components/ui/button"
import { Card, CardContent } from "../../components/ui/card"
@ -12,7 +22,14 @@ import { Textarea } from "../../components/ui/textarea"
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
} from "../../components/ui/dialog"
import { getRoles, getRoleDetail, getAllPermissions, setRolePermissions, updateRole } from "../../api/rbac.api"
import {
getRoles,
getRoleDetail,
getAllPermissions,
setRolePermissions,
updateRole,
deleteRole,
} from "../../api/rbac.api"
import type { Role, RoleDetail, RolePermission } from "../../types/rbac.types"
import { cn } from "../../lib/utils"
import { toast } from "sonner"
@ -36,6 +53,11 @@ export function RolesListPage() {
const [detailOpen, setDetailOpen] = useState(false)
const [detailLoading, setDetailLoading] = useState(false)
// Delete modal state
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null)
const [deleteLoading, setDeleteLoading] = useState(false)
// Role info editing state
const [editingRole, setEditingRole] = useState(false)
const [editName, setEditName] = useState("")
@ -97,6 +119,45 @@ export function RolesListPage() {
}
}
const handleDeleteRoleClick = (role: Role) => {
setRoleToDelete(role)
setDeleteDialogOpen(true)
}
const handleCancelDeleteRole = () => {
setDeleteDialogOpen(false)
setRoleToDelete(null)
}
const handleConfirmDeleteRole = async () => {
if (!roleToDelete) return
setDeleteLoading(true)
try {
const res = await deleteRole(roleToDelete.id)
toast.success(res.data.message ?? "Role deleted successfully")
// Close dialogs if the deleted role is currently opened.
if (selectedRole?.id === roleToDelete.id) {
setDetailOpen(false)
setSelectedRole(null)
setEditingPermissions(false)
setEditingRole(false)
setPermSearch("")
}
setRoleToDelete(null)
setDeleteDialogOpen(false)
setPage(1) // trigger list refresh via the existing effect
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response?.data?.message ??
"Failed to delete role."
toast.error(message)
} finally {
setDeleteLoading(false)
}
}
// Enter role info edit mode
const handleEditRole = () => {
if (!selectedRole) return
@ -360,7 +421,23 @@ export function RolesListPage() {
</div>
<div className="flex items-center justify-between">
<span className="text-[11px] text-grayScale-400">Open details to view permissions</span>
<span className="text-[11px] text-grayScale-400">
Open details to view permissions
</span>
<div className="flex items-center gap-2">
{!role.is_system && (
<Button
type="button"
variant="destructive"
size="icon"
className="h-8 w-8"
onClick={() => handleDeleteRoleClick(role)}
disabled={deleteLoading}
aria-label={`Delete role ${role.name}`}
>
<Trash2 className="h-4 w-4" />
</Button>
)}
<Button
variant="outline"
size="sm"
@ -371,6 +448,7 @@ export function RolesListPage() {
View
</Button>
</div>
</div>
</CardContent>
</Card>
))}
@ -703,6 +781,55 @@ export function RolesListPage() {
)}
</DialogContent>
</Dialog>
{/* Delete role dialog */}
<Dialog
open={deleteDialogOpen}
onOpenChange={(open) => {
setDeleteDialogOpen(open)
if (!open) handleCancelDeleteRole()
}}
>
<DialogContent className="max-w-sm">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-red-600">
<Trash2 className="h-5 w-5" />
Delete Role
</DialogTitle>
<DialogDescription>
Are you sure you want to delete this role? This action cannot be undone.
</DialogDescription>
</DialogHeader>
{roleToDelete && (
<div className="rounded-lg bg-red-50 border border-red-100 p-3">
<p className="text-sm font-medium text-red-700">{roleToDelete.name}</p>
<p className="text-xs text-red-500 mt-0.5">Role #{roleToDelete.id}</p>
</div>
)}
<div className="flex justify-end gap-2 pt-2">
<Button
variant="outline"
size="sm"
onClick={handleCancelDeleteRole}
disabled={deleteLoading}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
className="gap-1.5"
disabled={deleteLoading || !roleToDelete}
onClick={handleConfirmDeleteRole}
>
{deleteLoading ? <SpinnerIcon className="h-3.5 w-3.5" /> : <Trash2 className="h-3.5 w-3.5" />}
{deleteLoading ? "Deleting..." : "Delete"}
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}

View File

@ -60,6 +60,14 @@ export interface CreateRoleResponse {
metadata: unknown
}
export interface DeleteRoleResponse {
message: string
success: boolean
status_code: number
// Some backends may include extra fields; keep it optional for compatibility.
metadata?: unknown
}
export interface SetRolePermissionsRequest {
permission_ids: number[]
}