style(role-editor): rewrite UI on Foundry Deck dark tokens
Replace hardcoded light-theme inline colors (#fff/#f8f9fa/#e8f5e9/#888…) with semantic CSS classes that consume the existing design tokens (--bg-card/--ember/--ember-soft/--accent/--text-dim/--steel/etc.). Fixes the unreadable role names, faded checkmark cards, and washed-out success banner — everything was rendering "white-on-cream" against the dark Foundry Deck background. Visual structure now: - two-column grid (sidebar 280px / detail flex) - role-card: dark surface, ember-soft + 3px ember edge + glow on selected, Saira Condensed display name + mono permission count - perm-group card with dashed steel header - perm-item: bg-sink default; ember-soft + accent border + ember-tint check glyph on checked; native checkbox restyled with the ember palette - banner pill: success-green or danger-red token, mono text - create-role card: ember left-edge, mono uppercase labels - delete uses .btn-danger (already in the token set) No state/logic changes — same fetch + toggle + save flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
162
src/index.css
162
src/index.css
@@ -513,6 +513,168 @@ dd { font-size: .92rem; font-family: 'JetBrains Mono', monospace; }
|
||||
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
.tab-content { margin-top: 18px; animation: fadeIn .25s ease; }
|
||||
|
||||
/* ---- Role Editor -------------------------------------------------------- */
|
||||
.role-editor-page { animation: deck-in .35s cubic-bezier(.16,1,.3,1) both; }
|
||||
.role-editor-page h2 { display: flex; align-items: center; gap: 10px; }
|
||||
.role-editor-page .lead {
|
||||
color: var(--text-dim); font-size: .82rem;
|
||||
text-transform: uppercase; letter-spacing: .12em; margin: 8px 0 22px;
|
||||
}
|
||||
|
||||
.role-editor-banner {
|
||||
border: var(--hair); border-radius: var(--radius);
|
||||
padding: 12px 16px; margin-bottom: 18px;
|
||||
font-family: 'JetBrains Mono', monospace; font-size: .85rem;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
animation: fadeIn .25s ease;
|
||||
}
|
||||
.role-editor-banner.ok { background: rgba(70,180,135,.10); border-color: var(--success); color: var(--success); }
|
||||
.role-editor-banner.err { background: rgba(226,85,60,.10); border-color: var(--danger); color: var(--danger); }
|
||||
|
||||
.role-editor-create {
|
||||
background: var(--bg-card); border: var(--hair); border-radius: var(--radius);
|
||||
padding: 22px 22px 18px; margin-bottom: 22px;
|
||||
position: relative; max-width: 480px;
|
||||
animation: fadeIn .2s ease;
|
||||
}
|
||||
.role-editor-create::before {
|
||||
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
||||
background: var(--ember); border-radius: var(--radius) 0 0 var(--radius);
|
||||
}
|
||||
.role-editor-create h3 { margin-bottom: 14px; font-size: 1.05rem; letter-spacing: .04em; }
|
||||
.role-editor-create label {
|
||||
display: block; margin-bottom: 5px;
|
||||
font-size: .72rem; text-transform: uppercase; letter-spacing: .12em;
|
||||
color: var(--text-dim); font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.role-editor-create input {
|
||||
width: 100%; padding: 9px 12px; margin-bottom: 14px;
|
||||
background: var(--bg-sink); color: var(--text);
|
||||
border: var(--hair); border-radius: var(--radius);
|
||||
font-family: inherit; font-size: .9rem; transition: .15s;
|
||||
}
|
||||
.role-editor-create input:focus { border-color: var(--accent); outline: none; }
|
||||
.role-editor-create .actions { display: flex; gap: 8px; }
|
||||
|
||||
/* ---- two-column body ---- */
|
||||
.role-editor-grid {
|
||||
display: grid; grid-template-columns: 280px 1fr; gap: 22px;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.role-editor-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.role-editor-sidebar h3,
|
||||
.role-editor-detail h3 {
|
||||
font-size: .82rem; text-transform: uppercase; letter-spacing: .14em;
|
||||
color: var(--text-dim); margin-bottom: 12px;
|
||||
border-bottom: var(--hair); padding-bottom: 8px;
|
||||
}
|
||||
.role-editor-detail h3 .name {
|
||||
color: var(--accent); font-family: 'JetBrains Mono', monospace;
|
||||
letter-spacing: .04em; text-transform: none;
|
||||
}
|
||||
|
||||
.role-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.role-card {
|
||||
position: relative; padding: 12px 14px;
|
||||
background: var(--bg-card); border: var(--hair); border-radius: var(--radius);
|
||||
cursor: pointer; transition: .15s; overflow: hidden;
|
||||
}
|
||||
.role-card:hover { background: var(--bg-hover); border-color: var(--border-bright); }
|
||||
.role-card.active {
|
||||
background: var(--ember-soft); border-color: var(--accent);
|
||||
box-shadow: var(--glow);
|
||||
}
|
||||
.role-card.active::before {
|
||||
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
|
||||
background: var(--ember);
|
||||
}
|
||||
.role-card .role-name {
|
||||
font-family: 'Saira Condensed', sans-serif; font-weight: 700;
|
||||
font-size: .98rem; color: var(--text); letter-spacing: .04em;
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.role-card .role-desc {
|
||||
color: var(--text-dim); font-size: .78rem; margin-top: 3px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.role-card .role-meta {
|
||||
color: var(--steel); font-size: .68rem; margin-top: 6px;
|
||||
font-family: 'JetBrains Mono', monospace; letter-spacing: .04em;
|
||||
}
|
||||
.role-card .role-badge-global {
|
||||
font-size: .72rem; color: var(--warning);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* ---- permissions ---- */
|
||||
.perm-group {
|
||||
background: var(--bg-card); border: var(--hair); border-radius: var(--radius);
|
||||
padding: 14px 16px 16px; margin-bottom: 14px;
|
||||
}
|
||||
.perm-group-header {
|
||||
font-family: 'Saira Condensed', sans-serif; font-weight: 600;
|
||||
font-size: .85rem; text-transform: uppercase; letter-spacing: .14em;
|
||||
color: var(--steel); margin-bottom: 12px;
|
||||
padding-bottom: 8px; border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.perm-grid {
|
||||
display: grid; gap: 8px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
}
|
||||
.perm-item {
|
||||
display: flex; align-items: flex-start; gap: 10px;
|
||||
padding: 9px 11px;
|
||||
background: var(--bg-sink); border: 1px solid var(--border); border-radius: 3px;
|
||||
cursor: pointer; transition: .12s;
|
||||
user-select: none;
|
||||
}
|
||||
.perm-item:hover { background: var(--bg-hover); border-color: var(--border-bright); }
|
||||
.perm-item.checked {
|
||||
background: var(--ember-soft);
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px rgba(255,122,31,.18);
|
||||
}
|
||||
.perm-item input[type="checkbox"] {
|
||||
appearance: none; -webkit-appearance: none;
|
||||
width: 14px; height: 14px; margin-top: 2px;
|
||||
background: var(--bg); border: 1px solid var(--border-bright);
|
||||
border-radius: 2px; cursor: pointer; flex: 0 0 14px;
|
||||
position: relative; transition: .12s;
|
||||
}
|
||||
.perm-item input[type="checkbox"]:checked {
|
||||
background: var(--accent); border-color: var(--accent);
|
||||
}
|
||||
.perm-item input[type="checkbox"]:checked::after {
|
||||
content: ''; position: absolute; left: 3px; top: 0px;
|
||||
width: 4px; height: 8px;
|
||||
border: solid #1a0d02; border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.perm-item .perm-body { min-width: 0; }
|
||||
.perm-item .perm-name {
|
||||
font-family: 'JetBrains Mono', monospace; font-size: .82rem;
|
||||
color: var(--text); font-weight: 500; word-break: break-word;
|
||||
}
|
||||
.perm-item.checked .perm-name { color: var(--accent); }
|
||||
.perm-item .perm-desc {
|
||||
color: var(--text-dim); font-size: .72rem; line-height: 1.35; margin-top: 2px;
|
||||
}
|
||||
|
||||
.role-editor-actions {
|
||||
display: flex; gap: 10px; margin-top: 22px;
|
||||
padding-top: 18px; border-top: var(--hair);
|
||||
}
|
||||
|
||||
.role-editor-empty {
|
||||
background: var(--bg-card); border: 1px dashed var(--border);
|
||||
border-radius: var(--radius); padding: 40px 28px;
|
||||
text-align: center; color: var(--text-dim);
|
||||
font-family: 'JetBrains Mono', monospace; font-size: .85rem;
|
||||
}
|
||||
|
||||
/* ---- Responsive --------------------------------------------------------- */
|
||||
@media (max-width: 860px) {
|
||||
:root { --sidebar-w: 0px; }
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function RoleEditorPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [messageOk, setMessageOk] = useState(true)
|
||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||
const [newRoleName, setNewRoleName] = useState('')
|
||||
const [newRoleDesc, setNewRoleDesc] = useState('')
|
||||
@@ -32,15 +33,13 @@ export default function RoleEditorPage() {
|
||||
|
||||
const isAdmin = user?.is_admin === true
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [])
|
||||
useEffect(() => { fetchData() }, [])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [rolesRes, permsRes] = await Promise.all([
|
||||
api.get('/roles'),
|
||||
api.get('/roles/permissions')
|
||||
api.get('/roles/permissions'),
|
||||
])
|
||||
setRoles(rolesRes.data)
|
||||
setPermissions(permsRes.data)
|
||||
@@ -54,63 +53,67 @@ export default function RoleEditorPage() {
|
||||
const handlePermissionToggle = (permId: number) => {
|
||||
if (!selectedRole) return
|
||||
const newPermIds = selectedRole.permission_ids.includes(permId)
|
||||
? selectedRole.permission_ids.filter(id => id !== permId)
|
||||
? selectedRole.permission_ids.filter((id) => id !== permId)
|
||||
: [...selectedRole.permission_ids, permId]
|
||||
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
|
||||
}
|
||||
|
||||
const flashMessage = (msg: string, ok: boolean) => {
|
||||
setMessage(msg); setMessageOk(ok)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!selectedRole) return
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
setSaving(true); setMessage('')
|
||||
try {
|
||||
await api.post(`/roles/${selectedRole.id}/permissions`, {
|
||||
permission_ids: selectedRole.permission_ids
|
||||
permission_ids: selectedRole.permission_ids,
|
||||
})
|
||||
setMessage('Saved successfully!')
|
||||
flashMessage('Saved successfully', true)
|
||||
fetchData()
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to save')
|
||||
flashMessage(err.response?.data?.detail || 'Failed to save', false)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteRole = async () => {
|
||||
if (!selectedRole || !confirm(`Are you sure you want to delete the "${selectedRole.name}" role?`)) return
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
if (!selectedRole) return
|
||||
if (!confirm(`Delete the "${selectedRole.name}" role?`)) return
|
||||
setSaving(true); setMessage('')
|
||||
try {
|
||||
await api.delete(`/roles/${selectedRole.id}`)
|
||||
setMessage('Role deleted successfully!')
|
||||
flashMessage('Role deleted', true)
|
||||
setSelectedRole(null)
|
||||
fetchData()
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to delete role')
|
||||
flashMessage(err.response?.data?.detail || 'Failed to delete role', false)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const canDeleteRole = selectedRole && !['admin', 'guest', 'account-manager'].includes(selectedRole.name) && isAdmin
|
||||
const canDeleteRole =
|
||||
selectedRole &&
|
||||
!['admin', 'guest', 'account-manager'].includes(selectedRole.name) &&
|
||||
isAdmin
|
||||
|
||||
const handleCreateRole = async () => {
|
||||
if (!newRoleName.trim()) return
|
||||
setCreating(true)
|
||||
setMessage('')
|
||||
setCreating(true); setMessage('')
|
||||
try {
|
||||
await api.post('/roles', {
|
||||
name: newRoleName.trim(),
|
||||
description: newRoleDesc.trim() || null,
|
||||
is_global: false
|
||||
is_global: false,
|
||||
})
|
||||
setMessage('Role created successfully!')
|
||||
flashMessage('Role created', true)
|
||||
setShowCreateForm(false)
|
||||
setNewRoleName('')
|
||||
setNewRoleDesc('')
|
||||
setNewRoleName(''); setNewRoleDesc('')
|
||||
fetchData()
|
||||
} catch (err: any) {
|
||||
setMessage(err.response?.data?.detail || 'Failed to create role')
|
||||
flashMessage(err.response?.data?.detail || 'Failed to create role', false)
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
@@ -122,195 +125,127 @@ export default function RoleEditorPage() {
|
||||
return acc
|
||||
}, {} as Record<string, Permission[]>)
|
||||
|
||||
if (loading) return <div className="p-4">Loading...</div>
|
||||
if (loading) return <div className="loading">Loading roles…</div>
|
||||
|
||||
return (
|
||||
<div className="role-editor-page" style={{ padding: '20px' }}>
|
||||
<div className="role-editor-page">
|
||||
<h2>🔐 Role Editor</h2>
|
||||
<p style={{ color: '#888', marginBottom: '20px' }}>
|
||||
Configure permissions for each role. Only admins can edit roles.
|
||||
</p>
|
||||
<p className="lead">Configure permissions for each role. Only admins can edit roles.</p>
|
||||
|
||||
{isAdmin && !showCreateForm && (
|
||||
<button
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
className="btn-primary"
|
||||
style={{ marginBottom: '20px' }}
|
||||
>
|
||||
<button className="btn-primary" onClick={() => setShowCreateForm(true)}
|
||||
style={{ marginBottom: 18 }}>
|
||||
+ Create New Role
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showCreateForm && (
|
||||
<div style={{
|
||||
border: '2px solid #007bff',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
backgroundColor: '#f8f9fa'
|
||||
}}>
|
||||
<h3 style={{ marginTop: 0 }}>Create New Role</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: '400px' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 500 }}>
|
||||
Role Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRoleName}
|
||||
onChange={(e) => setNewRoleName(e.target.value)}
|
||||
placeholder="e.g., developer, manager"
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 500 }}>
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRoleDesc}
|
||||
onChange={(e) => setNewRoleDesc(e.target.value)}
|
||||
placeholder="Role description"
|
||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleCreateRole}
|
||||
disabled={creating || !newRoleName.trim()}
|
||||
className="btn-primary"
|
||||
>
|
||||
{creating ? 'Creating...' : 'Create'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowCreateForm(false)
|
||||
setNewRoleName('')
|
||||
setNewRoleDesc('')
|
||||
}}
|
||||
className="btn-secondary"
|
||||
style={{ padding: '8px 16px', borderRadius: '4px', border: '1px solid #ddd', background: '#fff' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div className="role-editor-create">
|
||||
<h3>Create new role</h3>
|
||||
<label>Role name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRoleName}
|
||||
onChange={(e) => setNewRoleName(e.target.value)}
|
||||
placeholder="e.g. developer, manager"
|
||||
/>
|
||||
<label>Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newRoleDesc}
|
||||
onChange={(e) => setNewRoleDesc(e.target.value)}
|
||||
placeholder="(optional) short description"
|
||||
/>
|
||||
<div className="actions">
|
||||
<button className="btn-primary" disabled={creating || !newRoleName.trim()}
|
||||
onClick={handleCreateRole}>
|
||||
{creating ? 'Creating…' : 'Create'}
|
||||
</button>
|
||||
<button className="btn-secondary"
|
||||
onClick={() => { setShowCreateForm(false); setNewRoleName(''); setNewRoleDesc('') }}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div style={{
|
||||
padding: '10px',
|
||||
marginBottom: '20px',
|
||||
backgroundColor: message.includes('success') ? '#d4edda' : '#f8d7da',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
{message}
|
||||
<div className={`role-editor-banner ${messageOk ? 'ok' : 'err'}`}>
|
||||
<span>{messageOk ? '✓' : '✗'}</span>
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: '20px' }}>
|
||||
{/* Role List */}
|
||||
<div style={{ width: '250px' }}>
|
||||
<div className="role-editor-grid">
|
||||
{/* Roles sidebar */}
|
||||
<aside className="role-editor-sidebar">
|
||||
<h3>Roles</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{roles.map(role => (
|
||||
<div
|
||||
key={role.id}
|
||||
onClick={() => setSelectedRole({ ...role })}
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: selectedRole?.id === role.id ? '2px solid #007bff' : '1px solid #ddd',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: selectedRole?.id === role.id ? '#f0f8ff' : 'white'
|
||||
}}
|
||||
>
|
||||
<strong>{role.name}</strong>
|
||||
{role.is_global && <span style={{ fontSize: '12px', marginLeft: '8px', color: '#ff6b6b' }}>🌟</span>}
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>{role.description}</div>
|
||||
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
|
||||
{role.permission_ids.length} permissions
|
||||
<div className="role-list">
|
||||
{roles.map((role) => {
|
||||
const active = selectedRole?.id === role.id
|
||||
return (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`role-card${active ? ' active' : ''}`}
|
||||
onClick={() => setSelectedRole({ ...role })}
|
||||
>
|
||||
<div className="role-name">
|
||||
{role.name}
|
||||
{role.is_global && <span className="role-badge-global" title="global">★</span>}
|
||||
</div>
|
||||
{role.description && <div className="role-desc">{role.description}</div>}
|
||||
<div className="role-meta">{role.permission_ids.length} permissions</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Permission Editor */}
|
||||
<div style={{ flex: 1 }}>
|
||||
{/* Permission editor */}
|
||||
<section className="role-editor-detail">
|
||||
{selectedRole ? (
|
||||
<>
|
||||
<h3>Permissions for: {selectedRole.name}</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||
<div key={category} style={{
|
||||
border: '1px solid #eee',
|
||||
borderRadius: '8px',
|
||||
padding: '12px'
|
||||
}}>
|
||||
<h4 style={{ margin: '0 0 10px 0', textTransform: 'capitalize' }}>
|
||||
{category}
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '8px' }}>
|
||||
{perms.map(perm => (
|
||||
<label key={perm.id} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
cursor: 'pointer',
|
||||
padding: '6px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: selectedRole.permission_ids.includes(perm.id) ? '#e8f5e9' : 'transparent'
|
||||
}}>
|
||||
<h3>Permissions for <span className="name">{selectedRole.name}</span></h3>
|
||||
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||
<div key={category} className="perm-group">
|
||||
<div className="perm-group-header">{category}</div>
|
||||
<div className="perm-grid">
|
||||
{perms.map((perm) => {
|
||||
const checked = selectedRole.permission_ids.includes(perm.id)
|
||||
return (
|
||||
<label key={perm.id} className={`perm-item${checked ? ' checked' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRole.permission_ids.includes(perm.id)}
|
||||
checked={checked}
|
||||
onChange={() => handlePermissionToggle(perm.id)}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{perm.name}</div>
|
||||
<div style={{ fontSize: '11px', color: '#666' }}>{perm.description}</div>
|
||||
<div className="perm-body">
|
||||
<div className="perm-name">{perm.name}</div>
|
||||
<div className="perm-desc">{perm.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="btn-primary"
|
||||
style={{ marginTop: '20px', marginRight: '10px' }}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
{canDeleteRole && (
|
||||
<button
|
||||
onClick={handleDeleteRole}
|
||||
disabled={saving}
|
||||
style={{
|
||||
marginTop: '20px',
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #dc3545',
|
||||
backgroundColor: '#dc3545',
|
||||
color: 'white',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Delete Role
|
||||
</div>
|
||||
))}
|
||||
<div className="role-editor-actions">
|
||||
<button className="btn-primary" disabled={saving} onClick={handleSave}>
|
||||
{saving ? 'Saving…' : 'Save changes'}
|
||||
</button>
|
||||
)}
|
||||
{canDeleteRole && (
|
||||
<button className="btn-danger" disabled={saving} onClick={handleDeleteRole}>
|
||||
Delete role
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: '#888', textAlign: 'center', marginTop: '40px' }}>
|
||||
Select a role to edit its permissions
|
||||
<div className="role-editor-empty">
|
||||
◆ Select a role on the left to edit its permissions
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user