{{-- Row actions for the Roles DataTable — docs/wireframes-v12/role-management.html:
v12 shows view + edit. **The eye is not rendered and Delete is added**, and both departures are decisions rather than drift: * decision D10 — the eye links to `view-role.html`, which **does not exist in the wireframe pack**. It is a dead link on the client's own page, so no view screen was invented to give it somewhere to go; a role's whole state is its name, its flag and its permission grid, and the edit screen already draws all three. * decision D2 — v12 draws no delete icon and the client asked for one. It wears the same `.btn-icon` with the wireframe's own `.text-danger` utility, which is the colouring v12 gives a destructive icon button elsewhere: the same addition, for the same reason and in the same markup, that brokers/partials/actions.blade.php records. ## Three gates, and the two that are not permissions `roles.edit` and `roles.delete` gate their own control, so a holder of `roles.view` alone gets neither. On top of that: * **the Super Admin row gets nothing at all** (decision D1). That role is fully locked — no edit, no delete, ever — and the lock is on the NAME, compared against User::SUPER_ADMIN_ROLE. Hiding is only the visible half: RoleController answers 403 on its edit screen and RoleService refuses both writes against a FRESH row read inside the transaction, so a hand-rolled request is refused by the same rule that removed the button. * **a system role keeps its pencil and loses its bin** (decision D1, the wireframe's own helper sentence: "System roles cannot be deleted"). The flag stops deletion and nothing else — a system role may still be renamed and re-permissioned. When every control is gated away the cell draws the em-dash, which is this application's blank (recipe §9), rather than an empty `.action-group`. Bare permission gates and not a policy — CR-14 §0.2. Rendered by RolesDataTable through rawColumns(['actions']), which means this output is injected into the page unescaped. **Role names are user input** — a role can be called `');alert(1)//` — so the name reaches this partial in exactly one place, inside Js::from(), which emits it as a JSON string literal with < > & " escaped to \u sequences. `{{ }}` escapes for HTML and not for Javascript, and commit 3492ec1 is the stored XSS that proved the difference. Delete is a real POST form with a confirm() guard, never a link: a GET that destroys data would be triggered by any crawler or prefetch. It is a HARD delete — spatie's `roles` table has no soft deletes — which is why the server refuses it while any user still holds the role (decision D2) instead of letting the pivot's cascade quietly strip everybody's access. yajra hands the row's model in as $model. --}} @php /** @var \App\Models\Role $model */ $locked = $model->name === \App\Models\User::SUPER_ADMIN_ROLE; $showEdit = ! $locked && auth()->user()?->can('roles.edit') === true; $showDelete = ! $locked && ! $model->is_system && auth()->user()?->can('roles.delete') === true; @endphp @if ($showEdit || $showDelete)
@if ($showEdit) @endif @if ($showDelete) {{-- CR-16 item J2: a plain button, not a form with a confirm(). `data-url` is the destroy route rendered by route() — the client's own instruction — and `data-confirm` is the SAME lang sentence the confirm() showed, which the shared dialog writes with textContent. nrems-ajax-delete.js sends the DELETE and redraws this table in place. The `$showDelete` gate above is untouched: a locked or system role still renders no control at all, and RoleService refuses one anyway (item J9). --}} @endif
@else @endif