inou/portal/templates/edit_rbac.tmpl

185 lines
8.3 KiB
Cheetah

{{define "edit_rbac"}}
<div class="sg-container" style="justify-content: center;">
<div style="flex: 1; display: flex; align-items: flex-start; padding-top: 16px; justify-content: center;">
<div class="data-card" style="padding: 48px; max-width: 780px; width: 100%;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px;">
<div>
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 8px;">Edit permissions</h1>
<p style="color: var(--text-muted); font-weight: 300;">{{.GranteeName}}'s access to {{.TargetDossier.Name}}</p>
</div>
<a href="/dossier/{{.TargetDossier.DossierID}}" class="btn btn-secondary btn-small">Back</a>
</div>
{{if .Error}}<div class="error">{{.Error}}</div>{{end}}
{{if .Success}}<div class="success">{{.Success}}</div>{{end}}
<form action="/dossier/{{.TargetDossier.DossierID}}/rbac/{{.GranteeID}}" method="POST">
<input type="hidden" name="action" value="update">
<input type="hidden" name="role" id="roleHidden" value="{{.SelectedRole}}">
<!-- Role Selector -->
<div style="margin-bottom: 24px;">
<label style="font-weight: 600; margin-bottom: 8px; display: block;">Role</label>
<select id="roleSelect" class="sg-select" style="width: 100%;">
<option value="Custom"{{if eq .SelectedRole "Custom"}} selected{{end}}>Custom</option>
{{range .Roles}}
<option value="{{.Name}}" data-grants='{{.GrantsJSON}}'{{if eq $.SelectedRole .Name}} selected{{end}}>{{.Name}} &mdash; {{.Description}}</option>
{{end}}
</select>
</div>
<!-- Category Permission Table -->
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; table-layout: fixed;">
<colgroup>
<col style="width: auto;">
<col style="width: 72px;">
<col style="width: 72px;">
<col style="width: 72px;">
<col style="width: 72px;">
<col style="width: 72px;">
</colgroup>
<thead>
<tr style="border-bottom: 2px solid var(--border-light);">
<th style="text-align: left; padding: 8px 12px; font-weight: 600;">Category</th>
<th style="text-align: center; padding: 8px 4px; font-weight: 600;">All</th>
<th style="text-align: center; padding: 8px 4px; font-weight: 600;"><label class="col-toggle" data-op="r" title="Toggle all Read"><input type="checkbox" class="col-cb" data-op="r"> Read</label></th>
<th style="text-align: center; padding: 8px 4px; font-weight: 600;"><label class="col-toggle" data-op="w" title="Toggle all Write"><input type="checkbox" class="col-cb" data-op="w"> Write</label></th>
<th style="text-align: center; padding: 8px 4px; font-weight: 600;"><label class="col-toggle" data-op="d" title="Toggle all Delete"><input type="checkbox" class="col-cb" data-op="d"> Delete</label></th>
<th style="text-align: center; padding: 8px 4px; font-weight: 600;"><label class="col-toggle" data-op="m" title="Toggle all Manage"><input type="checkbox" class="col-cb" data-op="m"> Manage</label></th>
</tr>
</thead>
<tbody>
{{range .CategoriesRBAC}}
<tr style="border-bottom: 1px solid var(--border-light);" data-cat="{{.ID}}">
<td style="padding: 6px 12px; text-transform: capitalize;">{{.Name}}</td>
<td style="text-align: center; padding: 6px 4px;"><input type="checkbox" class="row-toggle"></td>
<td style="text-align: center; padding: 6px 4px;"><input type="checkbox" name="cat_{{.ID}}_r" value="1" class="perm-cb op-r" {{if .CanRead}}checked{{end}}></td>
<td style="text-align: center; padding: 6px 4px;"><input type="checkbox" name="cat_{{.ID}}_w" value="1" class="perm-cb op-w" {{if .CanWrite}}checked{{end}}></td>
<td style="text-align: center; padding: 6px 4px;"><input type="checkbox" name="cat_{{.ID}}_d" value="1" class="perm-cb op-d" {{if .CanDelete}}checked{{end}}></td>
<td style="text-align: center; padding: 6px 4px;"><input type="checkbox" name="cat_{{.ID}}_m" value="1" class="perm-cb op-m" {{if .CanManage}}checked{{end}}></td>
</tr>
{{end}}
</tbody>
</table>
<div style="display: flex; gap: 12px; margin-top: 32px;">
<a href="/dossier/{{.TargetDossier.DossierID}}" class="btn btn-secondary" style="flex: 1; text-align: center;">Cancel</a>
<button type="submit" class="btn btn-primary" style="flex: 1;">Save changes</button>
</div>
</form>
<!-- Revoke all access -->
<div style="border-top: 1px solid var(--border-light); padding-top: 32px; margin-top: 32px;">
<form action="/dossier/{{.TargetDossier.DossierID}}/rbac/{{.GranteeID}}" method="POST" onsubmit="return confirm('Revoke all access for {{.GranteeName}}?');">
<input type="hidden" name="action" value="revoke">
<button type="submit" class="btn btn-danger" style="width: 100%;">Revoke all access</button>
</form>
</div>
</div>
</div>
</div>
<style>
.col-toggle { cursor: pointer; user-select: none; display: inline-flex; align-items: center; gap: 4px; }
</style>
<script>
(function() {
const roleSelect = document.getElementById('roleSelect');
const roleHidden = document.getElementById('roleHidden');
const allCbs = document.querySelectorAll('.perm-cb');
const catIDs = [];
document.querySelectorAll('tr[data-cat]').forEach(tr => {
catIDs.push(parseInt(tr.dataset.cat));
});
// --- Role selection ---
roleSelect.addEventListener('change', function() {
roleHidden.value = this.value;
if (this.value === 'Custom') return;
const opt = this.options[this.selectedIndex];
const grantsJSON = opt.dataset.grants;
if (!grantsJSON) return;
const grants = JSON.parse(grantsJSON);
allCbs.forEach(cb => cb.checked = false);
let rootOps = '';
grants.forEach(g => { if (g.Category === 0) rootOps = g.Ops || ''; });
if (rootOps) catIDs.forEach(id => setOps(id, rootOps));
grants.forEach(g => { if (g.Category > 0) setOps(g.Category, g.Ops || ''); });
syncAll();
});
function setOps(catID, ops) {
const row = document.querySelector('tr[data-cat="' + catID + '"]');
if (!row) return;
const cbs = row.querySelectorAll('.perm-cb');
if (cbs[0]) cbs[0].checked = ops.includes('r');
if (cbs[1]) cbs[1].checked = ops.includes('w');
if (cbs[2]) cbs[2].checked = ops.includes('d');
if (cbs[3]) cbs[3].checked = ops.includes('m');
}
// --- Manual perm change → Custom ---
allCbs.forEach(cb => {
cb.addEventListener('change', function() {
roleSelect.value = 'Custom';
roleHidden.value = 'Custom';
syncRowToggle(this.closest('tr'));
syncColToggles();
});
});
// --- Row toggle ---
document.querySelectorAll('.row-toggle').forEach(toggle => {
toggle.addEventListener('change', function() {
const row = this.closest('tr');
row.querySelectorAll('.perm-cb').forEach(cb => cb.checked = this.checked);
roleSelect.value = 'Custom';
roleHidden.value = 'Custom';
syncColToggles();
});
});
// --- Column toggle (checkbox in header) ---
document.querySelectorAll('.col-cb').forEach(cb => {
cb.addEventListener('change', function() {
const op = this.dataset.op;
document.querySelectorAll('.op-' + op).forEach(c => c.checked = this.checked);
roleSelect.value = 'Custom';
roleHidden.value = 'Custom';
syncRowToggles();
});
});
// --- Sync helpers ---
function syncRowToggle(row) {
const cbs = row.querySelectorAll('.perm-cb');
const toggle = row.querySelector('.row-toggle');
if (toggle) toggle.checked = cbs.length > 0 && Array.from(cbs).every(cb => cb.checked);
}
function syncRowToggles() {
document.querySelectorAll('tr[data-cat]').forEach(syncRowToggle);
}
function syncColToggles() {
document.querySelectorAll('.col-cb').forEach(cb => {
const colCbs = document.querySelectorAll('.op-' + cb.dataset.op);
cb.checked = colCbs.length > 0 && Array.from(colCbs).every(c => c.checked);
});
}
function syncAll() {
syncRowToggles();
syncColToggles();
}
// --- Init ---
syncAll();
})();
</script>
{{end}}