mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: integrate error handling across various components
- Added `useErrorHandler` hook to centralize error handling logic. - Updated components in the backoffice and portal applications to utilize the new error handling mechanism, replacing direct notify calls with `handleError` for improved error messaging. - Enhanced localization files to include generic error messages for better user feedback. - Refactored error handling in forms and API interactions to ensure consistent user experience across the application.
This commit is contained in:
@@ -22,7 +22,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
@@ -37,6 +37,7 @@ export function ForgotPasswordPage() {
|
||||
const [forgotTrigger, { isLoading }] = useApiMutation();
|
||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
const { handleError } = useErrorHandler();
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -59,11 +60,7 @@ export function ForgotPasswordPage() {
|
||||
try {
|
||||
await sendResetLink(values.email);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : 'Something went wrong');
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,11 +70,7 @@ export function ForgotPasswordPage() {
|
||||
await sendResetLink(sentTo);
|
||||
notify.success('Reset link sent again');
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : 'Something went wrong');
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { z } from "zod";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useApiMutation } from "@ema-platform/api";
|
||||
import { notify } from "@ema-platform/ui";
|
||||
import { notify, useErrorHandler } from "@ema-platform/ui";
|
||||
import { AuthShell } from "../components/AuthShell";
|
||||
import { loginSuccess, setUser, setCurrentProfile } from "../store/auth.slice";
|
||||
import type {
|
||||
@@ -52,6 +52,7 @@ export function LoginPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
const { handleError } = useErrorHandler();
|
||||
const [loginTrigger] = useApiMutation<LoginPayload>();
|
||||
const [meTrigger] = useApiMutation<AuthUser>();
|
||||
const [profileCheckTrigger] = useApiMutation<{
|
||||
@@ -118,11 +119,7 @@ export function LoginPage() {
|
||||
|
||||
navigate(loginRedirectPath);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : "Something went wrong");
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
} finally {
|
||||
localStorage.setItem("rememberMe", String(rememberMe));
|
||||
setIsLoading(false);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
@@ -48,6 +48,7 @@ export function OTPVerificationPage() {
|
||||
const [resendTrigger, { isLoading: resending }] = useApiMutation();
|
||||
const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
const { handleError } = useErrorHandler();
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -75,11 +76,7 @@ export function OTPVerificationPage() {
|
||||
notify.success('Phone number verified successfully');
|
||||
navigate(needsProfile ? '/profile-setup' : loginRedirectPath);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : 'Something went wrong');
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -96,11 +93,7 @@ export function OTPVerificationPage() {
|
||||
setSecondsLeft(RESEND_SECONDS);
|
||||
setServerError(null);
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : 'Something went wrong');
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { z } from 'zod';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { useErrorHandler } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { loginSuccess, setUser } from '../store/auth.slice';
|
||||
import type { AuthUser } from '../types/auth.types';
|
||||
@@ -69,6 +69,7 @@ export function SignupPage() {
|
||||
const { appName, loginRedirectPath } = useAuthConfig();
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
const { handleError } = useErrorHandler();
|
||||
const [signupTrigger, { isLoading: loading }] = useApiMutation<{
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
@@ -126,11 +127,7 @@ export function SignupPage() {
|
||||
});
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
(err instanceof Error ? err.message : 'Something went wrong');
|
||||
setServerError(msg);
|
||||
notify.error(msg);
|
||||
setServerError(handleError(err));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './lib/input/BilingualInput';
|
||||
export * from './lib/feedback/ConfirmModal';
|
||||
export * from './lib/feedback/ApiErrorAlert';
|
||||
export * from './lib/feedback/notify';
|
||||
export * from './lib/feedback/use-error-handler';
|
||||
export * from './lib/layout/AppHeader';
|
||||
export * from './lib/layout/AppSidebar';
|
||||
export * from './lib/layout/BrandAvatar';
|
||||
|
||||
72
libs/ui/src/lib/feedback/use-error-handler.ts
Normal file
72
libs/ui/src/lib/feedback/use-error-handler.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { notify } from './notify';
|
||||
|
||||
// Recursive: first non-empty string in a string | array | { message | error | detail }. Never throws.
|
||||
function extractMessage(value: unknown): string | null {
|
||||
if (value == null) return null;
|
||||
if (typeof value === 'string') return value.trim() || null;
|
||||
if (Array.isArray(value)) {
|
||||
const parts = value.map(extractMessage).filter(Boolean) as string[];
|
||||
return parts.length ? parts.join(', ') : null;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const o = value as Record<string, unknown>;
|
||||
return extractMessage(o.message) ?? extractMessage(o.error) ?? extractMessage(o.detail) ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// HTTP status OR RTK string status (FETCH_ERROR/TIMEOUT_ERROR/PARSING_ERROR/CUSTOM_ERROR) → i18n key.
|
||||
function statusKeyFor(status: number | string | undefined): string {
|
||||
if (status === 400 || status === 422) return 'msg.validationError';
|
||||
if (status === 401) return 'msg.authError';
|
||||
if (status === 403) return 'msg.permissionError';
|
||||
if (status === 404) return 'msg.notFoundError';
|
||||
if (status === 413) return 'msg.fileTooLarge';
|
||||
if (typeof status === 'number' && status >= 500) return 'msg.serverError';
|
||||
if (typeof status === 'string') return 'msg.networkError';
|
||||
return 'msg.genericError';
|
||||
}
|
||||
|
||||
function logError(err: unknown): void {
|
||||
if (err && typeof err === 'object' && 'status' in err) {
|
||||
console.error('[API ERROR]', (err as { status?: unknown }).status, (err as { data?: unknown }).data);
|
||||
return;
|
||||
}
|
||||
console.error('Error caught:', err);
|
||||
}
|
||||
|
||||
export function useErrorHandler() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Priority: backend message (FetchBaseQueryError.data) → Error.message → status/network fallback key.
|
||||
const getErrorMessage = useCallback(
|
||||
(err: unknown): string => {
|
||||
let status: number | string | undefined;
|
||||
if (err && typeof err === 'object' && ('status' in err || 'data' in err)) {
|
||||
status = (err as { status?: number | string }).status;
|
||||
const fromData = extractMessage((err as { data?: unknown }).data);
|
||||
if (fromData) return fromData;
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
const fromError = extractMessage(err.message);
|
||||
if (fromError) return fromError;
|
||||
}
|
||||
return t(statusKeyFor(status));
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleError = useCallback(
|
||||
(err: unknown): string => {
|
||||
logError(err);
|
||||
const message = getErrorMessage(err);
|
||||
notify.error(message);
|
||||
return message;
|
||||
},
|
||||
[getErrorMessage],
|
||||
);
|
||||
|
||||
return { getErrorMessage, handleError };
|
||||
}
|
||||
Reference in New Issue
Block a user