#175: Admin-Personenliste — Aktions-Icons statt Textlinks #176
@ -60,3 +60,14 @@ test('user settings pass the axe WCAG A/AA scan', async ({ browser }) => {
|
||||
await expectClean(page, '/settings');
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('admin area passes the axe WCAG A/AA scan', async ({ browser }) => {
|
||||
const context = await contextForUser(browser, BASE, 'fixture-admin');
|
||||
const page = await context.newPage();
|
||||
await page.goto('/admin');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// Personenliste sichtbar, inkl. der Icon-Aktionen (issue #175).
|
||||
await page.locator('.user-manager__table .user-row').first().waitFor();
|
||||
await expectClean(page, '/admin');
|
||||
await context.close();
|
||||
});
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { AdminUserListView, AdminUserView } from '@dorfteich/shared';
|
||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { MailCheck, ShieldMinus, ShieldPlus, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useAuth } from '../auth/auth-context';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from '../lib/api';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@ -101,6 +103,12 @@ function UserRow({
|
||||
}): React.JSX.Element {
|
||||
const { t } = useTranslation('users');
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
const confirmRef = useRef<HTMLButtonElement>(null);
|
||||
// The icon button unmounts when the confirm step appears — hand focus over
|
||||
// so keyboard users land on the confirmation instead of losing focus.
|
||||
useEffect(() => {
|
||||
if (confirmingDelete) confirmRef.current?.focus();
|
||||
}, [confirmingDelete]);
|
||||
const disabled = user.status === 'DISABLED';
|
||||
|
||||
return (
|
||||
@ -118,31 +126,20 @@ function UserRow({
|
||||
<td className="user-row__actions">
|
||||
{!isSelf && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="linklike user-row__disable"
|
||||
onClick={() =>
|
||||
void run(() =>
|
||||
apiPatch(`/admin/users/${user.id}/disabled`, { disabled: !disabled }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{disabled ? t('actions.enable') : t('actions.disable')}
|
||||
</button>
|
||||
{user.status === 'PENDING_VERIFICATION' && (
|
||||
<button
|
||||
type="button"
|
||||
className="linklike"
|
||||
<IconButton
|
||||
className="user-row__resend"
|
||||
label={t('actions.resend')}
|
||||
onClick={() =>
|
||||
void run(() => apiPost(`/admin/users/${user.id}/resend-verification`))
|
||||
}
|
||||
>
|
||||
{t('actions.resend')}
|
||||
</button>
|
||||
<MailCheck aria-hidden />
|
||||
</IconButton>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="linklike"
|
||||
<IconButton
|
||||
className="user-row__admin"
|
||||
label={user.isSiteAdmin ? t('actions.revokeAdmin') : t('actions.grantAdmin')}
|
||||
onClick={() =>
|
||||
void run(() =>
|
||||
apiPatch(`/admin/users/${user.id}/site-admin`, {
|
||||
@ -151,11 +148,23 @@ function UserRow({
|
||||
)
|
||||
}
|
||||
>
|
||||
{user.isSiteAdmin ? t('actions.revokeAdmin') : t('actions.grantAdmin')}
|
||||
</button>
|
||||
{user.isSiteAdmin ? <ShieldMinus aria-hidden /> : <ShieldPlus aria-hidden />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="user-row__disable"
|
||||
label={disabled ? t('actions.enable') : t('actions.disable')}
|
||||
onClick={() =>
|
||||
void run(() =>
|
||||
apiPatch(`/admin/users/${user.id}/disabled`, { disabled: !disabled }),
|
||||
)
|
||||
}
|
||||
>
|
||||
{disabled ? <UserCheck aria-hidden /> : <UserX aria-hidden />}
|
||||
</IconButton>
|
||||
{confirmingDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
ref={confirmRef}
|
||||
className="linklike user-row__delete-confirm"
|
||||
title={t('actions.deleteConfirm', { name: user.displayName })}
|
||||
onClick={() => void run(() => apiDelete(`/admin/users/${user.id}`))}
|
||||
@ -163,13 +172,13 @@ function UserRow({
|
||||
{t('actions.delete')}?
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="linklike user-row__delete"
|
||||
<IconButton
|
||||
className="user-row__delete"
|
||||
label={t('actions.delete')}
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
>
|
||||
{t('actions.delete')}
|
||||
</button>
|
||||
<Trash2 aria-hidden />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -2985,7 +2985,7 @@ ul[data-type='task_list'] li p:last-of-type {
|
||||
|
||||
.user-row__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user