cms/resources/js/components/admin/RoleManager.svelte

300 lines
14 KiB
Svelte
Raw Normal View History

<script>
let { roles = [], permissions = {}, adminPath = 'loom', status = null, errors = [] } = $props();
let displayRoles = $state([]);
$effect(() => {
displayRoles = roles;
});
let savingPermission = $state(false);
let newRoleName = $state('');
let newRoleSlug = $state('');
let newRoleDescription = $state('');
let editingRole = $state(null);
let editRoleName = $state('');
let editRoleSlug = $state('');
let editRoleDescription = $state('');
// Convert grouped permissions to a flat list for easier processing
let allPermissions = $derived(Object.values(permissions).flat());
let groupedPermissions = $derived(permissions);
async function togglePermission(roleId, permId, isActive) {
savingPermission = true;
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const baseUrl = window.location.origin.endsWith('/') ? window.location.origin : `${window.location.origin}/`;
const path = adminPath.startsWith('/') ? adminPath.substring(1) : adminPath;
const url = `${baseUrl}${path}/roles/${roleId}/permissions`;
const formData = new FormData();
formData.append('_token', csrfToken);
formData.append('permission_id', permId);
if (isActive) {
formData.append('active', 'on');
}
const response = await fetch(url, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: formData
});
const data = await response.json();
if (data.success && data.role) {
// Update local state
const index = displayRoles.findIndex(r => r.id === roleId);
if (index !== -1) {
displayRoles[index] = data.role;
}
} else if (data.errors) {
alert(data.errors.join('\n'));
}
} catch (error) {
console.error('Failed to update permission:', error);
alert('Failed to update permission. Please try again.');
} finally {
savingPermission = false;
}
}
function handleNameInput(e) {
newRoleSlug = e.target.value.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
}
function handleEditNameInput(e) {
editRoleSlug = e.target.value.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
}
function startEditing(role) {
editingRole = role;
editRoleName = role.name;
editRoleSlug = role.slug;
editRoleDescription = role.description || '';
}
function cancelEditing() {
editingRole = null;
}
function deleteRole(id, name) {
if (confirm(`Are you sure you want to delete the role "${name}"? This cannot be undone.`)) {
const form = document.createElement('form');
form.method = 'POST';
const baseUrl = window.location.origin.endsWith('/') ? window.location.origin : `${window.location.origin}/`;
const path = adminPath.startsWith('/') ? adminPath.substring(1) : adminPath;
const url = `${baseUrl}${path}/roles/${id}`;
form.action = url;
const methodInput = document.createElement('input');
methodInput.type = 'hidden';
methodInput.name = '_method';
methodInput.value = 'DELETE';
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = '_token';
tokenInput.value = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
form.appendChild(methodInput);
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
}
}
function updatePermissions(roleId) {
const form = document.getElementById(`perms-form-${roleId}`);
form.submit();
}
function checkboxAction(node) {
if (typeof jQuery !== 'undefined') {
jQuery(node).checkbox({
onChange: function() {
const input = node.querySelector('input');
const [_, roleId, permId] = input.id.split('-');
togglePermission(parseInt(roleId), parseInt(permId), input.checked);
}
});
}
}
</script>
<div class="ui container">
<div class="ui grid">
<div class="sixteen wide column">
<h1 class="ui header">
<i class="shield alternate icon"></i>
<div class="content">
Roles & Permissions
<div class="sub header">Define roles and assign granular authorizations to them</div>
</div>
{#if savingPermission}
<div class="ui active mini inline loader" style="margin-left: 10px;"></div>
{/if}
</h1>
</div>
<div class="sixteen wide column">
{#if status}
<div class="ui positive message">
<i class="close icon"></i>
<p>{status}</p>
</div>
{/if}
{#if errors && errors.length > 0}
<div class="ui negative message">
<i class="close icon"></i>
<ul class="list">
{#each errors as error}
<li>{error}</li>
{/each}
</ul>
</div>
{/if}
</div>
<div class="six wide column">
{#if editingRole}
<div class="ui blue segment">
<h3 class="ui header">Edit Role: {editingRole.name}</h3>
<form class="ui form" method="POST" action="{window.location.origin.endsWith('/') ? window.location.origin : window.location.origin + '/'}{adminPath.startsWith('/') ? adminPath.substring(1) : adminPath}/roles/{editingRole.id}">
<input type="hidden" name="_token" value={document.querySelector('meta[name="csrf-token"]').getAttribute('content')}>
<input type="hidden" name="_method" value="PUT">
<div class="field required">
<label for="edit_name">Role Name</label>
<input id="edit_name" type="text" name="name" bind:value={editRoleName} oninput={handleEditNameInput} placeholder="Editor" required>
</div>
<div class="field required">
<label for="edit_slug">Slug</label>
<input id="edit_slug" type="text" name="slug" bind:value={editRoleSlug} placeholder="editor" required>
</div>
<div class="field">
<label for="edit_description">Description</label>
<textarea id="edit_description" name="description" bind:value={editRoleDescription} rows="2" placeholder="Briefly describe what this role can do"></textarea>
</div>
<div class="two fields">
<div class="field">
<button type="button" class="ui fluid button" onclick={cancelEditing}>
Cancel
</button>
</div>
<div class="field">
<button type="submit" class="ui primary fluid button">
Update Role
</button>
</div>
</div>
</form>
</div>
{:else}
<div class="ui segment">
<h3 class="ui header">Create New Role</h3>
<form class="ui form" method="POST" action="{window.location.origin.endsWith('/') ? window.location.origin : window.location.origin + '/'}{adminPath.startsWith('/') ? adminPath.substring(1) : adminPath}/roles">
<input type="hidden" name="_token" value={document.querySelector('meta[name="csrf-token"]').getAttribute('content')}>
<div class="field required">
<label for="new_name">Role Name</label>
<input id="new_name" type="text" name="name" bind:value={newRoleName} oninput={handleNameInput} placeholder="Editor" required>
</div>
<div class="field required">
<label for="new_slug">Slug</label>
<input id="new_slug" type="text" name="slug" bind:value={newRoleSlug} placeholder="editor" required>
</div>
<div class="field">
<label for="new_description">Description</label>
<textarea id="new_description" name="description" bind:value={newRoleDescription} rows="2" placeholder="Briefly describe what this role can do"></textarea>
</div>
<button type="submit" class="ui primary fluid button">
<i class="plus icon"></i> Create Role
</button>
</form>
</div>
{/if}
</div>
<div class="ten wide column">
<div class="ui segment">
<h3 class="ui header">Existing Roles</h3>
<div class="ui divided list">
{#each displayRoles as role}
<div class="item" style="padding: 1em 0;">
<div class="right floated content">
{#if !role.is_protected}
<button class="ui mini blue icon button" onclick={() => startEditing(role)} title="Edit Role">
<i class="edit icon"></i>
</button>
<button class="ui mini red icon button" onclick={() => deleteRole(role.id, role.name)} title="Delete Role">
<i class="trash icon"></i>
</button>
{:else}
<div class="ui mini basic label"><i class="lock icon"></i> Protected</div>
{/if}
</div>
<div class="content">
<div class="header">{role.name} <code style="font-weight: normal; font-size: 0.8em; margin-left: 0.5em;">({role.slug})</code></div>
<div class="description">{role.description || 'No description provided.'}</div>
</div>
</div>
{/each}
</div>
</div>
</div>
<div class="sixteen wide column">
<div class="ui segment">
<h2 class="ui dividing header">Authorization Assignment Grid</h2>
<div style="overflow-x: auto;">
<table class="ui celled definition table">
<thead>
<tr>
<th></th>
{#each displayRoles as role}
<th class="center aligned">{role.name}</th>
{/each}
</tr>
</thead>
<tbody>
{#each Object.entries(groupedPermissions) as [resource, perms]}
<tr class="active">
<td colspan={displayRoles.length + 1}>
<strong>{resource.charAt(0).toUpperCase() + resource.slice(1)} Management</strong>
</td>
</tr>
{#each perms as perm}
<tr>
<td>{perm.name}</td>
{#each displayRoles as role}
<td class="center aligned">
{#if role.is_protected && role.slug === 'admin'}
<i class="green check circle icon" title="Admin has all permissions"></i>
{:else}
<div class="ui fitted toggle checkbox {savingPermission ? 'disabled' : ''}" use:checkboxAction>
<input
id="perm-{role.id}-{perm.id}"
type="checkbox"
name="active"
checked={role.permissions.some(p => p.id === perm.id)}
disabled={role.is_protected || savingPermission}
>
<label for="perm-{role.id}-{perm.id}"></label>
</div>
{/if}
</td>
{/each}
</tr>
{/each}
{/each}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>