diff --git a/src/index.css b/src/index.css index ce08e2e..d99a9b1 100644 --- a/src/index.css +++ b/src/index.css @@ -663,6 +663,32 @@ dd { font-size: .92rem; font-family: 'JetBrains Mono', monospace; } color: var(--text-dim); font-size: .72rem; line-height: 1.35; margin-top: 2px; } +.role-editor-template { + display: flex; align-items: center; gap: 10px; + background: var(--bg-card); border: var(--hair); border-radius: var(--radius); + padding: 10px 14px; margin-bottom: 14px; + font-size: .82rem; +} +.role-editor-template label { + color: var(--text-dim); text-transform: uppercase; + letter-spacing: .12em; font-size: .72rem; + font-family: 'JetBrains Mono', monospace; + white-space: nowrap; +} +.role-editor-template select { + flex: 1; min-width: 0; + padding: 7px 10px; + background: var(--bg-sink); color: var(--text); + border: var(--hair); border-radius: var(--radius); + font-family: 'JetBrains Mono', monospace; font-size: .82rem; + cursor: pointer; transition: .12s; +} +.role-editor-template select:hover { border-color: var(--border-bright); } +.role-editor-template select:focus { border-color: var(--accent); outline: none; } +.role-editor-template button:disabled { + opacity: .4; cursor: not-allowed; border-color: var(--border); +} + .role-editor-actions { display: flex; gap: 10px; margin-top: 22px; padding-top: 18px; border-top: var(--hair); diff --git a/src/pages/RoleEditorPage.tsx b/src/pages/RoleEditorPage.tsx index 8808694..997f596 100644 --- a/src/pages/RoleEditorPage.tsx +++ b/src/pages/RoleEditorPage.tsx @@ -30,6 +30,7 @@ export default function RoleEditorPage() { const [newRoleName, setNewRoleName] = useState('') const [newRoleDesc, setNewRoleDesc] = useState('') const [creating, setCreating] = useState(false) + const [templateRoleId, setTemplateRoleId] = useState('') const isAdmin = user?.is_admin === true @@ -58,6 +59,20 @@ export default function RoleEditorPage() { setSelectedRole({ ...selectedRole, permission_ids: newPermIds }) } + // Copy a source role's permission_ids over the current selection. + // Replaces (not merges) — the user explicitly clicked "use as template". + // Local-only: nothing persists until they hit "Save changes". + const handleApplyTemplate = () => { + if (!selectedRole || !templateRoleId) return + const src = roles.find((r) => String(r.id) === templateRoleId) + if (!src) return + setSelectedRole({ ...selectedRole, permission_ids: [...src.permission_ids] }) + flashMessage( + `Loaded ${src.permission_ids.length} permissions from "${src.name}" — not saved yet`, + true, + ) + } + const flashMessage = (msg: string, ok: boolean) => { setMessage(msg); setMessageOk(ok) } @@ -206,6 +221,35 @@ export default function RoleEditorPage() { {selectedRole ? ( <>

Permissions for {selectedRole.name}

+ + {isAdmin && roles.length > 1 && ( +
+ + + +
+ )} + {Object.entries(groupedPermissions).map(([category, perms]) => (
{category}