From 3283affa6725ba1ee4f65042e736ab1f082497dc Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sun, 26 Jul 2026 17:41:26 +0200 Subject: [PATCH] =?UTF-8?q?#175:=20Admin-Personenliste=20=E2=80=94=20Aktio?= =?UTF-8?q?ns-Icons=20statt=20Textlinks,=20Reihenfolge=20Admin/Deaktiviere?= =?UTF-8?q?n/L=C3=B6schen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Zeilen-Aktionen der Personenverwaltung sind jetzt IconButtons (lucide): MailCheck (Bestätigung erneut senden, nur bei Ausstehend), ShieldPlus/ShieldMinus (Zum Admin machen / Admin entfernen), UserX/UserCheck (Deaktivieren/Aktivieren), Trash2 (Löschen) — in dieser Reihenfolge. Das zweistufige Löschen bleibt: die Bestätigung ist weiterhin ein roter Text-Button und erhält beim Umschalten den Fokus (kein Fokusverlust, ADR 0017). Lokalisierte Namen kommen unverändert aus users.json via IconButton (aria-label+title), Icons aria-hidden. Der Admin-Bereich ist neu im a11y-CI-Pack (axe WCAG A/AA auf /admin). Co-Authored-By: Claude Fable 5 --- apps/web/e2e/a11y.spec.ts | 11 ++++++ apps/web/src/pages/UserManager.tsx | 63 +++++++++++++++++------------- apps/web/src/styles/base.css | 2 +- 3 files changed, 48 insertions(+), 28 deletions(-) diff --git a/apps/web/e2e/a11y.spec.ts b/apps/web/e2e/a11y.spec.ts index de33d80..7b232d4 100644 --- a/apps/web/e2e/a11y.spec.ts +++ b/apps/web/e2e/a11y.spec.ts @@ -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(); +}); diff --git a/apps/web/src/pages/UserManager.tsx b/apps/web/src/pages/UserManager.tsx index ff94694..2c0c833 100644 --- a/apps/web/src/pages/UserManager.tsx +++ b/apps/web/src/pages/UserManager.tsx @@ -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(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({ {!isSelf && ( <> - {user.status === 'PENDING_VERIFICATION' && ( - + + )} - + {user.isSiteAdmin ? : } + + + void run(() => + apiPatch(`/admin/users/${user.id}/disabled`, { disabled: !disabled }), + ) + } + > + {disabled ? : } + {confirmingDelete ? ( ) : ( - + + )} )} diff --git a/apps/web/src/styles/base.css b/apps/web/src/styles/base.css index 30456f2..e242224 100644 --- a/apps/web/src/styles/base.css +++ b/apps/web/src/styles/base.css @@ -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); } -- 2.45.2