#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 expectClean(page, '/settings');
|
||||||
await context.close();
|
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 type { AdminUserListView, AdminUserView } from '@dorfteich/shared';
|
||||||
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useAuth } from '../auth/auth-context';
|
import { useAuth } from '../auth/auth-context';
|
||||||
|
import { IconButton } from '../components/IconButton';
|
||||||
import { apiDelete, apiGet, apiPatch, apiPost } from '../lib/api';
|
import { apiDelete, apiGet, apiPatch, apiPost } from '../lib/api';
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
@ -101,6 +103,12 @@ function UserRow({
|
|||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const { t } = useTranslation('users');
|
const { t } = useTranslation('users');
|
||||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
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';
|
const disabled = user.status === 'DISABLED';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -118,31 +126,20 @@ function UserRow({
|
|||||||
<td className="user-row__actions">
|
<td className="user-row__actions">
|
||||||
{!isSelf && (
|
{!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' && (
|
{user.status === 'PENDING_VERIFICATION' && (
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
className="user-row__resend"
|
||||||
className="linklike"
|
label={t('actions.resend')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void run(() => apiPost(`/admin/users/${user.id}/resend-verification`))
|
void run(() => apiPost(`/admin/users/${user.id}/resend-verification`))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('actions.resend')}
|
<MailCheck aria-hidden />
|
||||||
</button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
className="user-row__admin"
|
||||||
className="linklike"
|
label={user.isSiteAdmin ? t('actions.revokeAdmin') : t('actions.grantAdmin')}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void run(() =>
|
void run(() =>
|
||||||
apiPatch(`/admin/users/${user.id}/site-admin`, {
|
apiPatch(`/admin/users/${user.id}/site-admin`, {
|
||||||
@ -151,11 +148,23 @@ function UserRow({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{user.isSiteAdmin ? t('actions.revokeAdmin') : t('actions.grantAdmin')}
|
{user.isSiteAdmin ? <ShieldMinus aria-hidden /> : <ShieldPlus aria-hidden />}
|
||||||
</button>
|
</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 ? (
|
{confirmingDelete ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
ref={confirmRef}
|
||||||
className="linklike user-row__delete-confirm"
|
className="linklike user-row__delete-confirm"
|
||||||
title={t('actions.deleteConfirm', { name: user.displayName })}
|
title={t('actions.deleteConfirm', { name: user.displayName })}
|
||||||
onClick={() => void run(() => apiDelete(`/admin/users/${user.id}`))}
|
onClick={() => void run(() => apiDelete(`/admin/users/${user.id}`))}
|
||||||
@ -163,13 +172,13 @@ function UserRow({
|
|||||||
{t('actions.delete')}?
|
{t('actions.delete')}?
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<IconButton
|
||||||
type="button"
|
className="user-row__delete"
|
||||||
className="linklike user-row__delete"
|
label={t('actions.delete')}
|
||||||
onClick={() => setConfirmingDelete(true)}
|
onClick={() => setConfirmingDelete(true)}
|
||||||
>
|
>
|
||||||
{t('actions.delete')}
|
<Trash2 aria-hidden />
|
||||||
</button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2985,7 +2985,7 @@ ul[data-type='task_list'] li p:last-of-type {
|
|||||||
|
|
||||||
.user-row__actions {
|
.user-row__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user