ui: move activate/inactivate/delete to user list table actions column

- New actions column (right of state) with inline buttons per row
- Manage form keeps save user + clear only (no duplicate action buttons)
- Themed to match admin dialog: JetBrains Mono, uppercase, square borders
- Delete uses danger styling
This commit is contained in:
2026-06-25 14:19:11 +00:00
parent 451732c867
commit 570b7d1dba
+63 -5
View File
@@ -831,6 +831,38 @@ body {
letter-spacing: 0.06em; letter-spacing: 0.06em;
color: var(--ink-dim); color: var(--ink-dim);
} }
.user-list-table .user-actions-cell {
white-space: nowrap;
display: flex;
gap: 0.3rem;
justify-content: flex-end;
}
.user-list-table .user-actions-cell button {
font-family: var(--font);
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0.2rem 0.4rem;
min-height: 1.7rem;
cursor: pointer;
border: 1px solid var(--border-light);
background: var(--input-bg);
color: var(--ink);
border-radius: 0;
-webkit-appearance: none;
}
.user-list-table .user-actions-cell button:hover {
background: var(--hover);
border-color: var(--accent);
color: var(--accent);
}
.user-list-table .user-actions-cell button.danger {
color: var(--danger);
}
.user-list-table .user-actions-cell button.danger:hover {
border-color: var(--danger);
color: var(--danger);
}
.user-state-active { color: var(--accent3); } .user-state-active { color: var(--accent3); }
.user-state-inactive { color: var(--danger); } .user-state-inactive { color: var(--danger); }
/* OAuth client list — compact, matches user table density */ /* OAuth client list — compact, matches user table density */
@@ -1384,9 +1416,6 @@ body {
<input type="password" id="admin-password" placeholder="required for new users; leave blank to keep unchanged" autocomplete="new-password"> <input type="password" id="admin-password" placeholder="required for new users; leave blank to keep unchanged" autocomplete="new-password">
<div class="manage-user-actions"> <div class="manage-user-actions">
<button class="primary" type="button" onclick="saveManagedUser()">save user</button> <button class="primary" type="button" onclick="saveManagedUser()">save user</button>
<button type="button" onclick="setManagedUserActive(false)">inactivate</button>
<button type="button" onclick="setManagedUserActive(true)">activate</button>
<button type="button" onclick="deleteManagedUser()">delete</button>
<button type="button" onclick="clearManageUserForm()">clear</button> <button type="button" onclick="clearManageUserForm()">clear</button>
</div> </div>
</div> </div>
@@ -1714,14 +1743,20 @@ function renderUserList() {
el.innerHTML = '<div class="empty-state">no users</div>'; el.innerHTML = '<div class="empty-state">no users</div>';
return; return;
} }
el.innerHTML = `<table class="user-list-table"><thead><tr><th>user id</th><th>name</th><th>role</th><th>state</th></tr></thead><tbody>${ el.innerHTML = `<table class="user-list-table"><thead><tr><th>user id</th><th>name</th><th>role</th><th>state</th><th style="text-align:right">actions</th></tr></thead><tbody>${
state.users.map(u => { state.users.map(u => {
const active = u.active !== false; const active = u.active !== false;
const uid = escapeHtml(u.user_id || '');
return `<tr> return `<tr>
<td class="user-id">${escapeHtml(u.user_id || '')}</td> <td class="user-id">${uid}</td>
<td>${escapeHtml(u.display_name || '')}</td> <td>${escapeHtml(u.display_name || '')}</td>
<td>${escapeHtml(u.role || '')}</td> <td>${escapeHtml(u.role || '')}</td>
<td class="${active ? 'user-state-active' : 'user-state-inactive'}">${active ? 'active' : 'inactive'}</td> <td class="${active ? 'user-state-active' : 'user-state-inactive'}">${active ? 'active' : 'inactive'}</td>
<td class="user-actions-cell">
<button type="button" onclick="userListAction('${uid}', 'activate')">activate</button>
<button type="button" onclick="userListAction('${uid}', 'inactivate')">inactivate</button>
<button type="button" class="danger" onclick="userListAction('${uid}', 'delete')">delete</button>
</td>
</tr>`; </tr>`;
}).join('') }).join('')
}</tbody></table>`; }</tbody></table>`;
@@ -1785,6 +1820,29 @@ async function setManagedUserActive(active) {
} }
} }
async function userListAction(userId, action) {
if (!userId) return;
if (action === 'delete') {
if (!confirm(`Delete user "${userId}"? If delete fails, inactivate instead.`)) return;
try {
await api('DELETE', `/users/${encodeURIComponent(userId)}`);
showToast(`user deleted · ${userId}`, 'success');
await loadUsers();
} catch (e) {
if (e.message !== 'unauthorized') showToast('delete failed: ' + e.message, 'error');
}
return;
}
const active = action === 'activate';
try {
await api('PATCH', `/users/${encodeURIComponent(userId)}`, { active });
showToast(active ? `activated · ${userId}` : `inactivated · ${userId}`, 'success');
await loadUsers();
} catch (e) {
if (e.message !== 'unauthorized') showToast('update failed: ' + e.message, 'error');
}
}
async function deleteManagedUser() { async function deleteManagedUser() {
const userId = document.getElementById('admin-manage-select').value || document.getElementById('admin-user-id').value.trim(); const userId = document.getElementById('admin-manage-select').value || document.getElementById('admin-user-id').value.trim();
if (!userId) { if (!userId) {