feat(role-editor): "Use as template" — copy another role's perm set
Adds a select+button next to the permission editor: pick any other role from the dropdown, click "Use as template" → all checkboxes are replaced with that role's permission set. Local-only (no API call); the user still hits "Save changes" to persist. Includes a banner confirming the load with the source role name + perm count. Selector excludes the currently-edited role. Hidden for non-admins. UI: dark card row matching the Foundry Deck token system (--bg-card, --text-dim mono label, --bg-sink select with --accent focus border). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
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 {
|
.role-editor-actions {
|
||||||
display: flex; gap: 10px; margin-top: 22px;
|
display: flex; gap: 10px; margin-top: 22px;
|
||||||
padding-top: 18px; border-top: var(--hair);
|
padding-top: 18px; border-top: var(--hair);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function RoleEditorPage() {
|
|||||||
const [newRoleName, setNewRoleName] = useState('')
|
const [newRoleName, setNewRoleName] = useState('')
|
||||||
const [newRoleDesc, setNewRoleDesc] = useState('')
|
const [newRoleDesc, setNewRoleDesc] = useState('')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [templateRoleId, setTemplateRoleId] = useState<string>('')
|
||||||
|
|
||||||
const isAdmin = user?.is_admin === true
|
const isAdmin = user?.is_admin === true
|
||||||
|
|
||||||
@@ -58,6 +59,20 @@ export default function RoleEditorPage() {
|
|||||||
setSelectedRole({ ...selectedRole, permission_ids: newPermIds })
|
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) => {
|
const flashMessage = (msg: string, ok: boolean) => {
|
||||||
setMessage(msg); setMessageOk(ok)
|
setMessage(msg); setMessageOk(ok)
|
||||||
}
|
}
|
||||||
@@ -206,6 +221,35 @@ export default function RoleEditorPage() {
|
|||||||
{selectedRole ? (
|
{selectedRole ? (
|
||||||
<>
|
<>
|
||||||
<h3>Permissions for <span className="name">{selectedRole.name}</span></h3>
|
<h3>Permissions for <span className="name">{selectedRole.name}</span></h3>
|
||||||
|
|
||||||
|
{isAdmin && roles.length > 1 && (
|
||||||
|
<div className="role-editor-template">
|
||||||
|
<label htmlFor="tpl-select">Copy from template:</label>
|
||||||
|
<select
|
||||||
|
id="tpl-select"
|
||||||
|
value={templateRoleId}
|
||||||
|
onChange={(e) => setTemplateRoleId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">— select a role —</option>
|
||||||
|
{roles
|
||||||
|
.filter((r) => r.id !== selectedRole.id)
|
||||||
|
.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
{r.name} ({r.permission_ids.length} perms)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
disabled={!templateRoleId}
|
||||||
|
onClick={handleApplyTemplate}
|
||||||
|
title="Replace current checkboxes with the template role's permission set (local; not saved until you click Save changes)"
|
||||||
|
>
|
||||||
|
Use as template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
{Object.entries(groupedPermissions).map(([category, perms]) => (
|
||||||
<div key={category} className="perm-group">
|
<div key={category} className="perm-group">
|
||||||
<div className="perm-group-header">{category}</div>
|
<div className="perm-group-header">{category}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user