Refactor query message toast
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { Text } from '@radix-ui/themes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast } from 'react-toastify/unstyled';
|
||||
import { SearchParamKeys } from '../lib/constants';
|
||||
import { useQueryMessage } from './useQueryMessage';
|
||||
import { QueryMessageCode, QueryMessageType } from '../lib/QueryMessages';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export enum ResponseErrorToastId {
|
||||
NetworkError = 'network-error',
|
||||
@@ -19,95 +22,99 @@ export type DefaultResponseErrorHandlerOptions = {
|
||||
* @param err error value
|
||||
* @returns {boolean} true if the error was handled, false otherwise
|
||||
*/
|
||||
const defaultResponseErrorHandler =
|
||||
(navigate: ReturnType<typeof useNavigate>, location: ReturnType<typeof useLocation>) =>
|
||||
(err: unknown, options?: DefaultResponseErrorHandlerOptions): boolean => {
|
||||
if (!(err instanceof AxiosError) && !options?.disableHandleUnexpectedErrors) {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Unexpected Error:</Text>
|
||||
<br /> An unexpected error occurred. Please try again later.
|
||||
</div>,
|
||||
{
|
||||
position: 'top-center',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(err instanceof AxiosError)) return false;
|
||||
|
||||
if (err.message === 'canceled') {
|
||||
// request was aborted, ignore but return true to indicate it was handled
|
||||
return !options?.disableIgnoreCanceledRequests;
|
||||
}
|
||||
|
||||
if (err.message === 'Network Error') {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Network Error:</Text>
|
||||
<br /> Unable to reach the server. Please check your internet connection and try again.
|
||||
</div>,
|
||||
{
|
||||
toastId: ResponseErrorToastId.NetworkError,
|
||||
position: 'top-center',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle 401 Unauthorized globally
|
||||
if (err.status === 401 && !options?.disableUnauthorizedHandling) {
|
||||
// store current path for redirect after login
|
||||
const currentPath = location.pathname + location.search;
|
||||
const searchParam = new URLSearchParams();
|
||||
searchParam.set(SearchParamKeys.Redirect, currentPath);
|
||||
searchParam.set(SearchParamKeys.Message, 'Session expired, please log in again');
|
||||
navigate(`/login?${searchParam.toString()}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (err.status === 403) {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Forbidden:</Text>
|
||||
<br /> You do not have permission to perform this action.
|
||||
</div>,
|
||||
{
|
||||
position: 'top-center',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function useResponseErrorHandler(): {
|
||||
defaultResponseErrorHandler: ReturnType<typeof defaultResponseErrorHandler>;
|
||||
defaultResponseErrorHandler: typeof defaultResponseErrorHandler;
|
||||
} {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
return { defaultResponseErrorHandler: defaultResponseErrorHandler(navigate, location) };
|
||||
const { toSearchParamQueryMessage } = useQueryMessage();
|
||||
|
||||
const defaultResponseErrorHandler = useCallback(
|
||||
(err: unknown, options?: DefaultResponseErrorHandlerOptions): boolean => {
|
||||
if (!(err instanceof AxiosError) && !options?.disableHandleUnexpectedErrors) {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Unexpected Error:</Text>
|
||||
<br /> An unexpected error occurred. Please try again later.
|
||||
</div>,
|
||||
{
|
||||
position: 'top-center',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!(err instanceof AxiosError)) return false;
|
||||
|
||||
if (err.message === 'canceled') {
|
||||
// request was aborted, ignore but return true to indicate it was handled
|
||||
return !options?.disableIgnoreCanceledRequests;
|
||||
}
|
||||
|
||||
if (err.message === 'Network Error') {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Network Error:</Text>
|
||||
<br /> Unable to reach the server. Please check your internet connection and try again.
|
||||
</div>,
|
||||
{
|
||||
toastId: ResponseErrorToastId.NetworkError,
|
||||
position: 'top-center',
|
||||
autoClose: false,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// handle 401 Unauthorized globally
|
||||
if (err.status === 401 && !options?.disableUnauthorizedHandling) {
|
||||
// store current path for redirect after login
|
||||
const currentPath = location.pathname + location.search;
|
||||
const searchParam = new URLSearchParams();
|
||||
searchParam.set(SearchParamKeys.Redirect, currentPath);
|
||||
searchParam.set(SearchParamKeys.Message, toSearchParamQueryMessage(QueryMessageCode.SessionExpired, QueryMessageType.Info));
|
||||
navigate(`/login?${searchParam.toString()}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (err.status === 403) {
|
||||
toast.error(
|
||||
<div>
|
||||
<Text weight="bold">Forbidden:</Text>
|
||||
<br /> You do not have permission to perform this action.
|
||||
</div>,
|
||||
{
|
||||
position: 'top-center',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
}
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
[location, navigate, toSearchParamQueryMessage]
|
||||
);
|
||||
|
||||
return { defaultResponseErrorHandler };
|
||||
}
|
||||
|
||||
111
apps/frontend/app/hooks/useQueryMessage.tsx
Normal file
111
apps/frontend/app/hooks/useQueryMessage.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useCallback, useEffect, useRef, type ReactNode } from 'react';
|
||||
import { useLocation, useSearchParams } from 'react-router';
|
||||
import { toast } from 'react-toastify/unstyled';
|
||||
import { SearchParamKeys } from '../lib/constants';
|
||||
import { CODE_TO_MESSAGE_MAP, QueryMessageCode, QueryMessageType } from '../lib/QueryMessages';
|
||||
|
||||
type QueryMessageString = `${QueryMessageCode}__${QueryMessageType}`;
|
||||
|
||||
export type QueryMessage = {
|
||||
type: QueryMessageType;
|
||||
code: QueryMessageCode;
|
||||
message: ReactNode;
|
||||
};
|
||||
|
||||
export type UseQueryMessageOptions = {
|
||||
displayMessages?: boolean;
|
||||
};
|
||||
|
||||
export type UseQueryMessageReturn = {
|
||||
setQueryMessage: (messageCode: QueryMessageCode, messageType: QueryMessageType) => void;
|
||||
clearQueryMessage: () => void;
|
||||
toSearchParamQueryMessage: (message: QueryMessageCode, type: QueryMessageType) => QueryMessageString;
|
||||
};
|
||||
|
||||
export function useQueryMessage(
|
||||
{ displayMessages }: UseQueryMessageOptions = {
|
||||
displayMessages: true,
|
||||
}
|
||||
): UseQueryMessageReturn {
|
||||
const location = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const messageStr = useRef<QueryMessageString | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset messageStr when location changes to allow re-displaying the same message on navigation
|
||||
messageStr.current = null;
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const queryMessageStr = searchParams.get(SearchParamKeys.Message);
|
||||
if (!(queryMessageStr && queryMessageStr !== messageStr.current)) return;
|
||||
const [queryMessage, queryMessageString] = toQueryMessage(queryMessageStr) ?? [null, null];
|
||||
if (!queryMessage) return;
|
||||
|
||||
messageStr.current = queryMessageString;
|
||||
if (displayMessages) {
|
||||
toast[queryMessage.type](queryMessage.code, {
|
||||
position: 'top-center',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: false,
|
||||
progress: undefined,
|
||||
theme: 'colored',
|
||||
toastId: 'login-route-info-message',
|
||||
});
|
||||
}
|
||||
}, [displayMessages, searchParams]);
|
||||
|
||||
const setQueryMessage = useCallback(
|
||||
(messageCode: QueryMessageCode, messageType: QueryMessageType) => {
|
||||
const queryMessageString: QueryMessageString = `${messageCode}__${messageType}`;
|
||||
messageStr.current = queryMessageString;
|
||||
setSearchParams((prev) => {
|
||||
prev.set(SearchParamKeys.Message, queryMessageString);
|
||||
return prev;
|
||||
});
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
const clearQueryMessage = useCallback(() => {
|
||||
messageStr.current = null;
|
||||
setSearchParams((prev) => {
|
||||
prev.delete(SearchParamKeys.Message);
|
||||
return prev;
|
||||
});
|
||||
}, [setSearchParams]);
|
||||
|
||||
const toSearchParamQueryMessage = useCallback((message: QueryMessageCode, type: QueryMessageType): QueryMessageString => {
|
||||
return `${message}__${type}`;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setQueryMessage,
|
||||
clearQueryMessage,
|
||||
toSearchParamQueryMessage,
|
||||
};
|
||||
}
|
||||
|
||||
function isValidQueryMessageCode(code: string): code is QueryMessageCode {
|
||||
return Object.values(QueryMessageCode).includes(code as QueryMessageCode);
|
||||
}
|
||||
|
||||
function isValidQueryMessageType(type: string): type is QueryMessageType {
|
||||
return Object.values(QueryMessageType).includes(type as QueryMessageType);
|
||||
}
|
||||
|
||||
function toQueryMessage(value: string): [QueryMessage, QueryMessageString] | null {
|
||||
const [code, type] = value.split('__');
|
||||
if (!isValidQueryMessageCode(code) || !isValidQueryMessageType(type)) return null;
|
||||
return [
|
||||
{
|
||||
code: code,
|
||||
type: type,
|
||||
message: CODE_TO_MESSAGE_MAP[code],
|
||||
},
|
||||
`${code}__${type}`,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user