feat: enhance AdminRoles to manage permissions with loading state and role slug updates

This commit is contained in:
2026-01-22 15:23:50 -06:00
parent 554b599599
commit f2dd053320

View File

@@ -39,6 +39,7 @@ const AdminRoles = () => {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showPermissionsModal, setShowPermissionsModal] = useState(false);
const [expandedModules, setExpandedModules] = useState({});
const [savingPermissions, setSavingPermissions] = useState(false);
const [formData, setFormData] = useState({
code: '',
name: '',
@@ -46,6 +47,15 @@ const AdminRoles = () => {
permissions: []
});
const formatRoleSlug = (value) => (
value
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/_+/g, '-')
.replace(/^_+|_+$/g, '')
);
useEffect(() => {
fetchRoles();
fetchPermissions();
@@ -133,6 +143,7 @@ const AdminRoles = () => {
};
const handleSavePermissions = async () => {
setSavingPermissions(true);
try {
await api.put(`/admin/roles/${selectedRole.id}/permissions`, {
permission_codes: selectedPermissions
@@ -142,6 +153,8 @@ const AdminRoles = () => {
fetchRoles();
} catch (error) {
toast.error('Failed to update permissions');
} finally {
setSavingPermissions(false);
}
};
@@ -155,6 +168,14 @@ const AdminRoles = () => {
});
};
const addPermissions = (permissionCodes) => {
setSelectedPermissions(prev => [...new Set([...prev, ...permissionCodes])]);
};
const removePermissions = (permissionCodes) => {
setSelectedPermissions(prev => prev.filter(code => !permissionCodes.includes(code)));
};
const toggleModule = (module) => {
setExpandedModules(prev => ({
...prev,
@@ -282,8 +303,28 @@ const AdminRoles = () => {
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Role Code *</Label>
<Label>Role Name *</Label>
<Input
placeholder="e.g., Content Editor, Finance Manager"
value={formData.name}
onChange={(e) => {
const nextName = e.target.value;
setFormData(prev => {
const prevAuto = formatRoleSlug(prev.name);
const isAuto = !prev.code || prev.code === prevAuto;
return {
...prev,
name: nextName,
code: isAuto ? formatRoleSlug(nextName) : prev.code
};
});
}}
/>
</div>
<div>
<Label>Role Slug *</Label>
<Input
placeholder="e.g., content_editor, finance_manager"
value={formData.code}
@@ -294,15 +335,6 @@ const AdminRoles = () => {
</p>
</div>
<div>
<Label>Role Name *</Label>
<Input
placeholder="e.g., Content Editor, Finance Manager"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
@@ -382,10 +414,6 @@ const AdminRoles = () => {
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Role Code</Label>
<Input value={selectedRole?.code || ''} disabled />
</div>
<div>
<Label>Role Name *</Label>
@@ -395,6 +423,11 @@ const AdminRoles = () => {
/>
</div>
<div>
<Label>Role Slug</Label>
<Input value={selectedRole?.code || ''} disabled />
</div>
<div>
<Label>Description</Label>
<Textarea
@@ -426,11 +459,20 @@ const AdminRoles = () => {
</DialogHeader>
<div className="border rounded-lg p-4">
{Object.entries(groupedPermissions).map(([module, perms]) => (
{Object.entries(groupedPermissions).map(([module, perms]) => {
const moduleCodes = perms.map(perm => perm.code);
const selectedCount = moduleCodes.filter(code => selectedPermissions.includes(code)).length;
const hasPermissions = moduleCodes.length > 0;
const isAllSelected = hasPermissions && selectedCount === moduleCodes.length;
const isNoneSelected = selectedCount === 0;
return (
<div key={module} className="mb-6">
<div className="flex items-center justify-between mb-3">
<button
type="button"
onClick={() => toggleModule(module)}
className="flex items-center w-full text-left font-medium text-lg mb-3 hover:text-blue-600"
className="flex items-center text-left font-medium text-lg hover:text-blue-600"
>
{expandedModules[module] ? (
<ChevronUp className="w-5 h-5 mr-2" />
@@ -439,6 +481,25 @@ const AdminRoles = () => {
)}
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
</button>
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => addPermissions(moduleCodes)}
disabled={!hasPermissions || isAllSelected}
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
Select all
</button>
<button
type="button"
onClick={() => removePermissions(moduleCodes)}
disabled={!hasPermissions || isNoneSelected}
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
>
Deselect all
</button>
</div>
</div>
{expandedModules[module] && (
<div className="space-y-3 ml-7">
{perms.map(perm => (
@@ -459,15 +520,16 @@ const AdminRoles = () => {
</div>
)}
</div>
))}
);
})}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowPermissionsModal(false)}>
Cancel
</Button>
<Button onClick={handleSavePermissions}>
Save Permissions
<Button onClick={handleSavePermissions} disabled={savingPermissions}>
{savingPermissions ? 'Saving...' : 'Save Permissions'}
</Button>
</DialogFooter>
</DialogContent>
@@ -491,6 +553,15 @@ const AdminRoles = () => {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{savingPermissions && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-xl shadow-lg px-6 py-5 text-center">
<div className="mx-auto h-10 w-10 animate-spin rounded-full border-4 border-[var(--neutral-800)] border-t-transparent" />
<p className="mt-4 text-sm font-medium text-gray-700">Saving permissions...</p>
</div>
</div>
)}
</div>
);
};