mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-29 06:26:34 +00:00
refactor(webui): share the API-error → message mapping across account forms
The three account forms duplicated the getApiErrorCode + ERROR_BY_CODE lookup pattern verbatim, and each carried a dead `AuthRequired` entry that the axios interceptor already handles (hard redirect) before the form's catch can render it. - add resolveApiErrorMessage(err, map, fallback) next to the axios error helpers - email/name/password forms use it and drop the unreachable AuthRequired entry - unit-test the helper's precedence (mapped code > server msg > fallback) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
789caf0dfe
commit
9b9a74bec0
@@ -8,7 +8,7 @@ const { patchUser, put, refreshAuthInfo } = vi.hoisted(() => ({
|
||||
refreshAuthInfo: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// Keep the pure helpers (`getApiErrorCode`, `getApiErrorMessage`) real — only the network call is stubbed.
|
||||
// Keep the real error-mapping helper (`resolveApiErrorMessage`) — only the network call is stubbed.
|
||||
vi.mock('@/lib/axios', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/axios')>();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { InputPassword } from '@/components/ui/input-password';
|
||||
import { api, getApiErrorCode, getApiErrorMessage } from '@/lib/axios';
|
||||
import { api, resolveApiErrorMessage } from '@/lib/axios';
|
||||
import { useUser } from '@/providers/user-provider';
|
||||
|
||||
const emailChangeSchema = z.object({
|
||||
@@ -24,7 +24,6 @@ const emailChangeSchema = z.object({
|
||||
});
|
||||
|
||||
const ERROR_BY_CODE: Record<string, string> = {
|
||||
AuthRequired: 'Authentication required',
|
||||
'Users.ChangeEmailCurrentUser.EmailAlreadyExists': 'Email address is already in use',
|
||||
'Users.ChangeEmailCurrentUser.InvalidCurrentPassword': 'Current password is incorrect',
|
||||
'Users.ChangeEmailCurrentUser.InvalidEmail': 'New email does not meet requirements',
|
||||
@@ -68,8 +67,7 @@ export function EmailChangeForm({ isModal = true, onCancel, onSuccess }: EmailCh
|
||||
|
||||
onSuccess?.();
|
||||
} catch (err: unknown) {
|
||||
const code = getApiErrorCode(err);
|
||||
setError((code && ERROR_BY_CODE[code]) ?? getApiErrorMessage(err, 'Failed to update email'));
|
||||
setError(resolveApiErrorMessage(err, ERROR_BY_CODE, 'Failed to update email'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const { patchUser, put, refreshAuthInfo } = vi.hoisted(() => ({
|
||||
refreshAuthInfo: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// Keep the pure helpers (`getApiErrorCode`, `getApiErrorMessage`) real — only the network call is stubbed.
|
||||
// Keep the real error-mapping helper (`resolveApiErrorMessage`) — only the network call is stubbed.
|
||||
vi.mock('@/lib/axios', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/axios')>();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { api, getApiErrorCode, getApiErrorMessage } from '@/lib/axios';
|
||||
import { api, resolveApiErrorMessage } from '@/lib/axios';
|
||||
import { useUser } from '@/providers/user-provider';
|
||||
|
||||
const nameChangeSchema = z.object({
|
||||
@@ -20,7 +20,6 @@ const nameChangeSchema = z.object({
|
||||
});
|
||||
|
||||
const ERROR_BY_CODE: Record<string, string> = {
|
||||
AuthRequired: 'Authentication required',
|
||||
'Users.ChangeNameCurrentUser.InvalidName': 'New name does not meet requirements',
|
||||
'Users.NotFound': 'User not found',
|
||||
};
|
||||
@@ -57,8 +56,7 @@ export function NameChangeForm({ isModal = true, onCancel, onSuccess }: NameChan
|
||||
|
||||
onSuccess?.();
|
||||
} catch (err: unknown) {
|
||||
const code = getApiErrorCode(err);
|
||||
setError((code && ERROR_BY_CODE[code]) ?? getApiErrorMessage(err, 'Failed to update name'));
|
||||
setError(resolveApiErrorMessage(err, ERROR_BY_CODE, 'Failed to update name'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { put } = vi.hoisted(() => ({ put: vi.fn() }));
|
||||
|
||||
// Keep the pure helpers (`getApiErrorCode`, `getApiErrorMessage`) real — only the network call is stubbed.
|
||||
// Keep the real error-mapping helper (`resolveApiErrorMessage`) — only the network call is stubbed.
|
||||
vi.mock('@/lib/axios', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/axios')>();
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { FormSubmitButton } from '@/components/ui/form-submit-button';
|
||||
import { InputPassword } from '@/components/ui/input-password';
|
||||
import { api, getApiErrorCode, getApiErrorMessage } from '@/lib/axios';
|
||||
import { api, resolveApiErrorMessage } from '@/lib/axios';
|
||||
|
||||
const passwordChangeSchema = z
|
||||
.object({
|
||||
@@ -48,7 +48,6 @@ const passwordChangeSchema = z
|
||||
});
|
||||
|
||||
const ERROR_BY_CODE: Record<string, string> = {
|
||||
AuthRequired: 'Authentication required',
|
||||
'Users.ChangePasswordCurrentUser.InvalidCurrentPassword': 'Current password is incorrect',
|
||||
'Users.ChangePasswordCurrentUser.InvalidNewPassword': 'New password does not meet requirements',
|
||||
'Users.ChangePasswordCurrentUser.InvalidPassword': 'Password validation failed',
|
||||
@@ -98,8 +97,7 @@ export function PasswordChangeForm({
|
||||
|
||||
onSuccess?.();
|
||||
} catch (err: unknown) {
|
||||
const code = getApiErrorCode(err);
|
||||
setError((code && ERROR_BY_CODE[code]) ?? getApiErrorMessage(err, 'Failed to change password'));
|
||||
setError(resolveApiErrorMessage(err, ERROR_BY_CODE, 'Failed to change password'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveApiErrorMessage } from './axios';
|
||||
|
||||
const apiError = (code: string, msg: string) => ({ response: { data: { code, msg, status: 'error' } } });
|
||||
const messages = { 'Users.NotFound': 'User not found' };
|
||||
|
||||
describe('resolveApiErrorMessage', () => {
|
||||
it('prefers a mapped code over the raw server message', () => {
|
||||
expect(resolveApiErrorMessage(apiError('Users.NotFound', 'user not found'), messages, 'fallback')).toBe(
|
||||
'User not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the server message for an unmapped code', () => {
|
||||
expect(resolveApiErrorMessage(apiError('Some.Other.Code', 'raw server message'), messages, 'fallback')).toBe(
|
||||
'raw server message',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses the fallback when there is neither a mapped code nor a server message', () => {
|
||||
expect(resolveApiErrorMessage({}, messages, 'fallback')).toBe('fallback');
|
||||
});
|
||||
});
|
||||
@@ -255,5 +255,15 @@ export const getApiErrorCode = (error: unknown): string | undefined => {
|
||||
return responseData && typeof responseData === 'object' ? (responseData as ApiErrorResponse).code : undefined;
|
||||
};
|
||||
|
||||
export const resolveApiErrorMessage = (
|
||||
error: unknown,
|
||||
messagesByCode: Record<string, string>,
|
||||
fallback: string,
|
||||
): string => {
|
||||
const code = getApiErrorCode(error);
|
||||
|
||||
return (code && messagesByCode[code]) ?? getApiErrorMessage(error, fallback);
|
||||
};
|
||||
|
||||
export default axios;
|
||||
export { axios };
|
||||
|
||||
Reference in New Issue
Block a user