From 04bb0c6f9484671f0ddb7649671831b9ba4eee41 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sun, 24 May 2026 19:38:18 +0100 Subject: [PATCH] =?UTF-8?q?feat(role-editor):=20"Use=20as=20template"=20?= =?UTF-8?q?=E2=80=94=20copy=20another=20role's=20perm=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/index.css | 26 +++++++++++++++++++++ src/pages/RoleEditorPage.tsx | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) 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}