fix(web): Formularweg und Listenladen der Benutzerverwaltung melden Fehler sichtbar
WINDOWS #36, Aufgabe 2/3: handleSubmit und fetchUsers verschluckten abgewiesene Antworten und Verbindungsfehler ebenso wie der Loeschweg aus Aufgabe 1. formError zeigt jetzt den Servertext oder eine Ersatzmeldung im offenen Formulardialog; loadError verhindert die irrefuehrende Meldung "Keine Benutzer gefunden", wenn das Laden selbst gescheitert ist. Beide Zustaende werden beim Oeffnen eines neuen Dialogs zurueckgesetzt, damit eine alte Meldung nicht in den naechsten Aufruf hinueberwandert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -65,6 +65,8 @@ export default function AdminUsersPage() {
|
|||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
const [formError, setFormError] = useState<string | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
const [detailsUser, setDetailsUser] = useState<User | null>(null);
|
const [detailsUser, setDetailsUser] = useState<User | null>(null);
|
||||||
const [formData, setFormData] = useState<UserFormData>({
|
const [formData, setFormData] = useState<UserFormData>({
|
||||||
username: '',
|
username: '',
|
||||||
@@ -79,19 +81,22 @@ export default function AdminUsersPage() {
|
|||||||
currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN';
|
currentUser?.role === 'ADMIN' || currentUser?.role === 'SUPER_ADMIN';
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
|
setLoadError(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_URL}/users`, {
|
const res = await fetch(`${API_URL}/users`, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setUsers(await res.json());
|
setUsers(await res.json());
|
||||||
|
} else {
|
||||||
|
setLoadError(t('errors.loadFailed'));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
setLoadError(t('errors.loadFailed'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAccess) {
|
if (hasAccess) {
|
||||||
@@ -110,6 +115,7 @@ export default function AdminUsersPage() {
|
|||||||
displayName: '',
|
displayName: '',
|
||||||
role: 'USER',
|
role: 'USER',
|
||||||
});
|
});
|
||||||
|
setFormError(null);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,11 +128,13 @@ export default function AdminUsersPage() {
|
|||||||
displayName: user.displayName ?? '',
|
displayName: user.displayName ?? '',
|
||||||
role: user.role,
|
role: user.role,
|
||||||
});
|
});
|
||||||
|
setFormError(null);
|
||||||
setShowForm(true);
|
setShowForm(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setFormError(null);
|
||||||
|
|
||||||
const url = editingUser
|
const url = editingUser
|
||||||
? `${API_URL}/users/${editingUser.id}`
|
? `${API_URL}/users/${editingUser.id}`
|
||||||
@@ -156,9 +164,14 @@ export default function AdminUsersPage() {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setShowForm(false);
|
setShowForm(false);
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
const detail = await readApiMessage(res);
|
||||||
|
setFormError(
|
||||||
|
detail ? t('errors.serverRejected', { detail }) : t('errors.generic'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
setFormError(t('errors.network'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -215,10 +228,19 @@ export default function AdminUsersPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{loadError && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"
|
||||||
|
>
|
||||||
|
{loadError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Users table */}
|
{/* Users table */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-muted-foreground">{tCommon('loading')}</p>
|
<p className="text-muted-foreground">{tCommon('loading')}</p>
|
||||||
) : users.length === 0 ? (
|
) : loadError ? null : users.length === 0 ? (
|
||||||
<p className="text-muted-foreground">{t('noUsers')}</p>
|
<p className="text-muted-foreground">{t('noUsers')}</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto rounded-md border border-border">
|
<div className="overflow-x-auto rounded-md border border-border">
|
||||||
@@ -400,6 +422,14 @@ export default function AdminUsersPage() {
|
|||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{formError && (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
className="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive"
|
||||||
|
>
|
||||||
|
{formError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex justify-end gap-3 pt-2">
|
<div className="flex justify-end gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -285,3 +285,189 @@ describe('AdminUsersPage — Loeschweg (WINDOWS #36, Aufgabe 1)', () => {
|
|||||||
expect(usersCalls.length).toBeGreaterThanOrEqual(2);
|
expect(usersCalls.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('AdminUsersPage — Formularweg und Listenladen (WINDOWS #36, Aufgabe 2)', () => {
|
||||||
|
it('zeigt den Servertext im offenen Formular, wenn das Speichern abgewiesen wird', async () => {
|
||||||
|
stubAdmin();
|
||||||
|
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
||||||
|
if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) });
|
||||||
|
}
|
||||||
|
if (typeof url === 'string' && init?.method === 'PATCH') {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
json: () => Promise.resolve({ message: 'Cannot modify a SUPER_ADMIN user' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<AdminUsersPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('user.zwei')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByText('Bearbeiten');
|
||||||
|
await userEvent.click(editButtons[0]);
|
||||||
|
|
||||||
|
const usernameInput = await screen.findByDisplayValue('admin.eins');
|
||||||
|
|
||||||
|
const saveButton = screen.getByText('Speichern');
|
||||||
|
await userEvent.click(saveButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
// Formular bleibt offen.
|
||||||
|
expect(usernameInput).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('verbindet Pruefmeldungen aus einem Feld mit Komma im Rahmensatz', async () => {
|
||||||
|
stubAdmin();
|
||||||
|
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
||||||
|
if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) });
|
||||||
|
}
|
||||||
|
if (typeof url === 'string' && init?.method === 'PATCH') {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({ message: ['username must be longer', 'email must be an email'] }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<AdminUsersPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('user.zwei')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByText('Bearbeiten');
|
||||||
|
await userEvent.click(editButtons[0]);
|
||||||
|
await screen.findByDisplayValue('admin.eins');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Speichern'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
'Der Server hat die Aktion abgelehnt: username must be longer, email must be an email',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('zeigt die Netzmeldung im offenen Formular, wenn die Verbindung beim Speichern scheitert', async () => {
|
||||||
|
stubAdmin();
|
||||||
|
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
||||||
|
if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) });
|
||||||
|
}
|
||||||
|
if (typeof url === 'string' && init?.method === 'PATCH') {
|
||||||
|
return Promise.reject(new Error('network down'));
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<AdminUsersPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('user.zwei')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByText('Bearbeiten');
|
||||||
|
await userEvent.click(editButtons[0]);
|
||||||
|
const usernameInput = await screen.findByDisplayValue('admin.eins');
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Speichern'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Der Server ist nicht erreichbar. Bitte erneut versuchen.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(usernameInput).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('meldet ein gescheitertes Laden im Listenkopf statt faelschlich "Keine Benutzer gefunden" zu zeigen', async () => {
|
||||||
|
stubAdmin();
|
||||||
|
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
||||||
|
if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 500,
|
||||||
|
json: () => Promise.reject(new Error('not json')),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<AdminUsersPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Die Benutzerliste konnte nicht geladen werden. Bitte laden Sie die Seite neu.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.queryByText('Keine Benutzer gefunden')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('laesst eine Meldung nicht das Schliessen und erneute Oeffnen des Formulars ueberdauern', async () => {
|
||||||
|
stubAdmin();
|
||||||
|
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
||||||
|
if (typeof url === 'string' && url.endsWith('/users') && (!init || init.method === undefined)) {
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve(mockUsers) });
|
||||||
|
}
|
||||||
|
if (typeof url === 'string' && init?.method === 'PATCH') {
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
json: () => Promise.resolve({ message: 'Cannot modify a SUPER_ADMIN user' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||||
|
});
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
render(<AdminUsersPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('user.zwei')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtons = screen.getAllByText('Bearbeiten');
|
||||||
|
await userEvent.click(editButtons[0]);
|
||||||
|
await screen.findByDisplayValue('admin.eins');
|
||||||
|
await userEvent.click(screen.getByText('Speichern'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('Abbrechen'));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByDisplayValue('admin.eins')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const editButtonsAgain = screen.getAllByText('Bearbeiten');
|
||||||
|
await userEvent.click(editButtonsAgain[0]);
|
||||||
|
await screen.findByDisplayValue('admin.eins');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText('Der Server hat die Aktion abgelehnt: Cannot modify a SUPER_ADMIN user'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user