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.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||||
.tab-content { margin-top: 18px; animation: fadeIn .25s ease; }
|
.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 --------------------------------------------------------- */
|
/* ---- Responsive --------------------------------------------------------- */
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
:root { --sidebar-w: 0px; }
|
:root { --sidebar-w: 0px; }
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export default function RoleEditorPage() {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
|
const [messageOk, setMessageOk] = useState(true)
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||||
const [newRoleName, setNewRoleName] = useState('')
|
const [newRoleName, setNewRoleName] = useState('')
|
||||||
const [newRoleDesc, setNewRoleDesc] = useState('')
|
const [newRoleDesc, setNewRoleDesc] = useState('')
|
||||||
@@ -32,15 +33,13 @@ export default function RoleEditorPage() {
|
|||||||
|
|
||||||
const isAdmin = user?.is_admin === true
|
const isAdmin = user?.is_admin === true
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { fetchData() }, [])
|
||||||
fetchData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [rolesRes, permsRes] = await Promise.all([
|
const [rolesRes, permsRes] = await Promise.all([
|
||||||
api.get('/roles'),
|
api.get('/roles'),
|
||||||
api.get('/roles/permissions')
|
api.get('/roles/permissions'),
|
||||||
])
|
])
|
||||||
setRoles(rolesRes.data)
|
setRoles(rolesRes.data)
|
||||||
setPermissions(permsRes.data)
|
setPermissions(permsRes.data)
|
||||||
@@ -54,63 +53,67 @@ export default function RoleEditorPage() {
|
|||||||
const handlePermissionToggle = (permId: number) => {
|
const handlePermissionToggle = (permId: number) => {
|
||||||
if (!selectedRole) return
|
if (!selectedRole) return
|
||||||
const newPermIds = selectedRole.permission_ids.includes(permId)
|
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]
|
: [...selectedRole.permission_ids, permId]
|
||||||
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
|
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flashMessage = (msg: string, ok: boolean) => {
|
||||||
|
setMessage(msg); setMessageOk(ok)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!selectedRole) return
|
if (!selectedRole) return
|
||||||
setSaving(true)
|
setSaving(true); setMessage('')
|
||||||
setMessage('')
|
|
||||||
try {
|
try {
|
||||||
await api.post(`/roles/${selectedRole.id}/permissions`, {
|
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()
|
fetchData()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMessage(err.response?.data?.detail || 'Failed to save')
|
flashMessage(err.response?.data?.detail || 'Failed to save', false)
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteRole = async () => {
|
const handleDeleteRole = async () => {
|
||||||
if (!selectedRole || !confirm(`Are you sure you want to delete the "${selectedRole.name}" role?`)) return
|
if (!selectedRole) return
|
||||||
setSaving(true)
|
if (!confirm(`Delete the "${selectedRole.name}" role?`)) return
|
||||||
setMessage('')
|
setSaving(true); setMessage('')
|
||||||
try {
|
try {
|
||||||
await api.delete(`/roles/${selectedRole.id}`)
|
await api.delete(`/roles/${selectedRole.id}`)
|
||||||
setMessage('Role deleted successfully!')
|
flashMessage('Role deleted', true)
|
||||||
setSelectedRole(null)
|
setSelectedRole(null)
|
||||||
fetchData()
|
fetchData()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMessage(err.response?.data?.detail || 'Failed to delete role')
|
flashMessage(err.response?.data?.detail || 'Failed to delete role', false)
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false)
|
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 () => {
|
const handleCreateRole = async () => {
|
||||||
if (!newRoleName.trim()) return
|
if (!newRoleName.trim()) return
|
||||||
setCreating(true)
|
setCreating(true); setMessage('')
|
||||||
setMessage('')
|
|
||||||
try {
|
try {
|
||||||
await api.post('/roles', {
|
await api.post('/roles', {
|
||||||
name: newRoleName.trim(),
|
name: newRoleName.trim(),
|
||||||
description: newRoleDesc.trim() || null,
|
description: newRoleDesc.trim() || null,
|
||||||
is_global: false
|
is_global: false,
|
||||||
})
|
})
|
||||||
setMessage('Role created successfully!')
|
flashMessage('Role created', true)
|
||||||
setShowCreateForm(false)
|
setShowCreateForm(false)
|
||||||
setNewRoleName('')
|
setNewRoleName(''); setNewRoleDesc('')
|
||||||
setNewRoleDesc('')
|
|
||||||
fetchData()
|
fetchData()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setMessage(err.response?.data?.detail || 'Failed to create role')
|
flashMessage(err.response?.data?.detail || 'Failed to create role', false)
|
||||||
} finally {
|
} finally {
|
||||||
setCreating(false)
|
setCreating(false)
|
||||||
}
|
}
|
||||||
@@ -122,195 +125,127 @@ export default function RoleEditorPage() {
|
|||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, Permission[]>)
|
}, {} as Record<string, Permission[]>)
|
||||||
|
|
||||||
if (loading) return <div className="p-4">Loading...</div>
|
if (loading) return <div className="loading">Loading roles…</div>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="role-editor-page" style={{ padding: '20px' }}>
|
<div className="role-editor-page">
|
||||||
<h2>🔐 Role Editor</h2>
|
<h2>🔐 Role Editor</h2>
|
||||||
<p style={{ color: '#888', marginBottom: '20px' }}>
|
<p className="lead">Configure permissions for each role. Only admins can edit roles.</p>
|
||||||
Configure permissions for each role. Only admins can edit roles.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{isAdmin && !showCreateForm && (
|
{isAdmin && !showCreateForm && (
|
||||||
<button
|
<button className="btn-primary" onClick={() => setShowCreateForm(true)}
|
||||||
onClick={() => setShowCreateForm(true)}
|
style={{ marginBottom: 18 }}>
|
||||||
className="btn-primary"
|
|
||||||
style={{ marginBottom: '20px' }}
|
|
||||||
>
|
|
||||||
+ Create New Role
|
+ Create New Role
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCreateForm && (
|
{showCreateForm && (
|
||||||
<div style={{
|
<div className="role-editor-create">
|
||||||
border: '2px solid #007bff',
|
<h3>Create new role</h3>
|
||||||
borderRadius: '8px',
|
<label>Role name *</label>
|
||||||
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newRoleName}
|
value={newRoleName}
|
||||||
onChange={(e) => setNewRoleName(e.target.value)}
|
onChange={(e) => setNewRoleName(e.target.value)}
|
||||||
placeholder="e.g., developer, manager"
|
placeholder="e.g. developer, manager"
|
||||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<label>Description</label>
|
||||||
<div>
|
|
||||||
<label style={{ display: 'block', marginBottom: '4px', fontWeight: 500 }}>
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newRoleDesc}
|
value={newRoleDesc}
|
||||||
onChange={(e) => setNewRoleDesc(e.target.value)}
|
onChange={(e) => setNewRoleDesc(e.target.value)}
|
||||||
placeholder="Role description"
|
placeholder="(optional) short description"
|
||||||
style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="actions">
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<button className="btn-primary" disabled={creating || !newRoleName.trim()}
|
||||||
<button
|
onClick={handleCreateRole}>
|
||||||
onClick={handleCreateRole}
|
{creating ? 'Creating…' : 'Create'}
|
||||||
disabled={creating || !newRoleName.trim()}
|
|
||||||
className="btn-primary"
|
|
||||||
>
|
|
||||||
{creating ? 'Creating...' : 'Create'}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="btn-secondary"
|
||||||
onClick={() => {
|
onClick={() => { setShowCreateForm(false); setNewRoleName(''); setNewRoleDesc('') }}>
|
||||||
setShowCreateForm(false)
|
|
||||||
setNewRoleName('')
|
|
||||||
setNewRoleDesc('')
|
|
||||||
}}
|
|
||||||
className="btn-secondary"
|
|
||||||
style={{ padding: '8px 16px', borderRadius: '4px', border: '1px solid #ddd', background: '#fff' }}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
<div style={{
|
<div className={`role-editor-banner ${messageOk ? 'ok' : 'err'}`}>
|
||||||
padding: '10px',
|
<span>{messageOk ? '✓' : '✗'}</span>
|
||||||
marginBottom: '20px',
|
<span>{message}</span>
|
||||||
backgroundColor: message.includes('success') ? '#d4edda' : '#f8d7da',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}>
|
|
||||||
{message}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '20px' }}>
|
<div className="role-editor-grid">
|
||||||
{/* Role List */}
|
{/* Roles sidebar */}
|
||||||
<div style={{ width: '250px' }}>
|
<aside className="role-editor-sidebar">
|
||||||
<h3>Roles</h3>
|
<h3>Roles</h3>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
<div className="role-list">
|
||||||
{roles.map(role => (
|
{roles.map((role) => {
|
||||||
|
const active = selectedRole?.id === role.id
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={role.id}
|
key={role.id}
|
||||||
|
className={`role-card${active ? ' active' : ''}`}
|
||||||
onClick={() => setSelectedRole({ ...role })}
|
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>
|
<div className="role-name">
|
||||||
{role.is_global && <span style={{ fontSize: '12px', marginLeft: '8px', color: '#ff6b6b' }}>🌟</span>}
|
{role.name}
|
||||||
<div style={{ fontSize: '12px', color: '#666' }}>{role.description}</div>
|
{role.is_global && <span className="role-badge-global" title="global">★</span>}
|
||||||
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
|
|
||||||
{role.permission_ids.length} permissions
|
|
||||||
</div>
|
</div>
|
||||||
|
{role.description && <div className="role-desc">{role.description}</div>}
|
||||||
|
<div className="role-meta">{role.permission_ids.length} permissions</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
{/* Permission Editor */}
|
{/* Permission editor */}
|
||||||
<div style={{ flex: 1 }}>
|
<section className="role-editor-detail">
|
||||||
{selectedRole ? (
|
{selectedRole ? (
|
||||||
<>
|
<>
|
||||||
<h3>Permissions for: {selectedRole.name}</h3>
|
<h3>Permissions for <span className="name">{selectedRole.name}</span></h3>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
|
||||||
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||||
<div key={category} style={{
|
<div key={category} className="perm-group">
|
||||||
border: '1px solid #eee',
|
<div className="perm-group-header">{category}</div>
|
||||||
borderRadius: '8px',
|
<div className="perm-grid">
|
||||||
padding: '12px'
|
{perms.map((perm) => {
|
||||||
}}>
|
const checked = selectedRole.permission_ids.includes(perm.id)
|
||||||
<h4 style={{ margin: '0 0 10px 0', textTransform: 'capitalize' }}>
|
return (
|
||||||
{category}
|
<label key={perm.id} className={`perm-item${checked ? ' checked' : ''}`}>
|
||||||
</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'
|
|
||||||
}}>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedRole.permission_ids.includes(perm.id)}
|
checked={checked}
|
||||||
onChange={() => handlePermissionToggle(perm.id)}
|
onChange={() => handlePermissionToggle(perm.id)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className="perm-body">
|
||||||
<div style={{ fontWeight: 500 }}>{perm.name}</div>
|
<div className="perm-name">{perm.name}</div>
|
||||||
<div style={{ fontSize: '11px', color: '#666' }}>{perm.description}</div>
|
<div className="perm-desc">{perm.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
<div className="role-editor-actions">
|
||||||
<button
|
<button className="btn-primary" disabled={saving} onClick={handleSave}>
|
||||||
onClick={handleSave}
|
{saving ? 'Saving…' : 'Save changes'}
|
||||||
disabled={saving}
|
|
||||||
className="btn-primary"
|
|
||||||
style={{ marginTop: '20px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
|
||||||
</button>
|
</button>
|
||||||
{canDeleteRole && (
|
{canDeleteRole && (
|
||||||
<button
|
<button className="btn-danger" disabled={saving} onClick={handleDeleteRole}>
|
||||||
onClick={handleDeleteRole}
|
Delete role
|
||||||
disabled={saving}
|
|
||||||
style={{
|
|
||||||
marginTop: '20px',
|
|
||||||
padding: '10px 20px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid #dc3545',
|
|
||||||
backgroundColor: '#dc3545',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete Role
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ color: '#888', textAlign: 'center', marginTop: '40px' }}>
|
<div className="role-editor-empty">
|
||||||
Select a role to edit its permissions
|
◆ Select a role on the left to edit its permissions
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user