From 922472fd6197f88d9204091a01b4f7bd6c42dc2d Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sat, 8 Feb 2025 21:45:37 +0530 Subject: [PATCH 1/3] Skip retries for client errors to fail fast --- src/App.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 51e33c2209d..70c7b21d71b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,21 +18,34 @@ import AuthUserProvider from "@/Providers/AuthUserProvider"; import HistoryAPIProvider from "@/Providers/HistoryAPIProvider"; import Routers from "@/Routers"; import { handleHttpError } from "@/Utils/request/errorHandler"; +import { HTTPError } from "@/Utils/request/types"; import { PubSubProvider } from "./Utils/pubsubContext"; const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: 2, + retry(failureCount, error) { + // Fail-fast by skipping retries for non-5xx HTTP errors. + if (error instanceof HTTPError && error.status < 500) { + return false; + } + + // Retries at most 2 times for HTTP 5xx errors or Network errors. + return failureCount < 2; + }, refetchOnWindowFocus: false, }, }, queryCache: new QueryCache({ - onError: handleHttpError, + onError: (error, query) => { + handleHttpError(error, query.meta); + }, }), mutationCache: new MutationCache({ - onError: handleHttpError, + onError: (error, _vars, _ctx, mutation) => { + handleHttpError(error, mutation.meta); + }, }), }); From 842f978bacba0f416c2f462e0f0ece955d8e681c Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Sat, 8 Feb 2025 21:49:30 +0530 Subject: [PATCH 2/3] Update global error handler Fixes #9882 --- src/Providers/AuthUserProvider.tsx | 2 +- src/Utils/request/errorHandler.ts | 220 +++++++++++++-------- src/Utils/request/types.ts | 17 +- src/Utils/request/uploadFile.ts | 1 - src/Utils/request/useValueErrorHandler.tsx | 70 +++++++ src/components/Auth/Login.tsx | 46 ++--- src/components/Patient/allergy/list.tsx | 12 +- src/hooks/useAuthUser.ts | 4 +- 8 files changed, 239 insertions(+), 133 deletions(-) create mode 100644 src/Utils/request/useValueErrorHandler.tsx diff --git a/src/Providers/AuthUserProvider.tsx b/src/Providers/AuthUserProvider.tsx index 2a24aea06c7..f7491a0cb7d 100644 --- a/src/Providers/AuthUserProvider.tsx +++ b/src/Providers/AuthUserProvider.tsx @@ -67,7 +67,7 @@ export default function AuthUserProvider({ } }, [tokenRefreshQuery.data, tokenRefreshQuery.isError]); - const { mutateAsync: signIn, isPending: isAuthenticating } = useMutation({ + const { mutate: signIn, isPending: isAuthenticating } = useMutation({ mutationFn: mutate(routes.login), onSuccess: (data: JwtTokenObtainPair) => { setAccessToken(data.access); diff --git a/src/Utils/request/errorHandler.ts b/src/Utils/request/errorHandler.ts index 8d07981307d..46dbb560b77 100644 --- a/src/Utils/request/errorHandler.ts +++ b/src/Utils/request/errorHandler.ts @@ -1,122 +1,170 @@ +import { MutationMeta, QueryMeta } from "@tanstack/react-query"; import { t } from "i18next"; import { navigate } from "raviger"; +import React from "react"; import { toast } from "sonner"; -import * as Notifications from "@/Utils/Notifications"; -import { HTTPError, StructuredError } from "@/Utils/request/types"; +import { HTTPError } from "@/Utils/request/types"; -export function handleHttpError(error: Error) { - // Skip handling silent errors and AbortError - if (("silent" in error && error.silent) || error.name === "AbortError") { +type Meta = QueryMeta | MutationMeta | undefined; + +export type HttpErrorHandler = (error: HTTPError, meta: Meta) => boolean | void; + +const httpErrorHandlers: HttpErrorHandler[] = []; + +/** + * Registers a handler that will be called for all HTTP errors. + * The latest registered handler will be called first. + * @param handler - The handler to register. + */ +export function registerHttpErrorHandler(handler: HttpErrorHandler) { + httpErrorHandlers.splice(0, 0, handler); +} + +/** + * Unregisters a handler that was previously registered using + * `registerHttpErrorHandler`. + * @param handler - The handler to unregister. + */ +export function unregisterHttpErrorHandler(handler: HttpErrorHandler) { + httpErrorHandlers.splice(httpErrorHandlers.indexOf(handler), 1); +} + +/** + * Registers a handler that will be called for all HTTP errors. + * + * When the handler returns `true`, the error is considered handled and no + * other handlers will be called. + * + * The error handler will be unregistered when the component unmounts. + * @param handler - The handler to register. + */ +export function useHttpErrorHandler(handler: HttpErrorHandler) { + const handlerRef = React.useRef(handler); + + React.useEffect(() => { + registerHttpErrorHandler(handlerRef.current); + return () => unregisterHttpErrorHandler(handlerRef.current); + }, [handlerRef]); +} + +/** + * Handles HTTP errors. + * @param error - The error to handle. + */ +export function handleHttpError(error: Error, meta?: Meta) { + // If the error is an AbortError, skip further handling. + if (error.name === "AbortError") { return; } - if (!(error instanceof HTTPError)) { - toast.error(error.message || t("something_went_wrong")); + // If the error is silenced, skip further handling. + // + // Voluntarily kept this check before the HTTPError instance check as errors + // from plugins may not be an instance of HTTPError, but plugins could choose + // to set the `silent` property regardless. + if ("silent" in error && error.silent) { return; } - const cause = error.cause; - - if (isNotFound(error)) { - toast.error((cause?.detail as string) || t("not_found")); + // If the error is not an HTTPError, show a generic error message and skip + // further handling. + if (!(error instanceof HTTPError)) { + toast.error(error.message || t("something_went_wrong")); return; } - if (isSessionExpired(cause)) { - handleSessionExpired(); + // Session expired handler is always called before any other handlers. + if (sessionExpiredHandler(error, meta)) { return; } - if (isBadRequest(error)) { - const errs = cause?.errors; - if (isPydanticError(errs)) { - handlePydanticErrors(errs); + // Other handlers are called in the order they were registered. + for (const handler of httpErrorHandlers) { + if (handler(error, meta)) { return; } + } - if (isStructuredError(cause)) { - handleStructuredErrors(cause); + // Default / fallback handlers are called last. + for (const handler of [detailHandler, notFoundHandler, badRequestHandler]) { + if (handler(error, meta)) { return; } - - Notifications.BadRequest({ errs }); - return; } - toast.error((cause?.detail as string) || t("something_went_wrong")); -} - -function isSessionExpired(error: HTTPError["cause"]) { - return ( - // If Authorization header is not valid - error?.code === "token_not_valid" || - // If Authorization header is not provided - error?.detail === "Authentication credentials were not provided." - ); + // If no handler handled the error, show a generic error message. + toast.error(t("something_went_wrong")); } -function handleSessionExpired() { - if (!location.pathname.startsWith("/session-expired")) { - navigate(`/session-expired?redirect=${window.location.href}`); +/** + * Handles HTTP errors with a `detail` property by showing it as an error. + */ +const detailHandler: HttpErrorHandler = ({ cause }) => { + if (cause && "detail" in cause && typeof cause.detail === "string") { + toast.error(cause.detail); + return true; } -} +}; -function isBadRequest(error: HTTPError) { - return error.status === 400 || error.status === 406; -} +/** + * Handles HTTP 400 Bad Request errors by showing a generic error message. + */ +const badRequestHandler: HttpErrorHandler = ({ status, cause }) => { + if (status !== 400) { + return false; + } -function isNotFound(error: HTTPError) { - return error.status === 404; -} + if (cause && "errors" in cause) { + for (const error of cause.errors) { + if ("type" in error && error.type === "value_error") { + // If error has a ctx property with an error property, show the error. + if (error.ctx && "error" in error.ctx) { + toast.error(error.ctx.error); + continue; + } + + if (typeof error.msg === "string") { + toast.error(error.msg); + continue; + } + } + } + } -type PydanticError = { - type: string; - loc?: string[]; - msg: string | Record; - input?: unknown; - url?: string; + return true; }; -function isStructuredError(err: HTTPError["cause"]): err is StructuredError { - return typeof err === "object" && !Array.isArray(err); -} +/** + * Handles Session Expired / Invalid Token errors by redirecting to the + * session expired page. + */ +const sessionExpiredHandler: HttpErrorHandler = ({ cause }) => { + if (!cause || !("code" in cause) || !("detail" in cause)) { + return; + } -function handleStructuredErrors(cause: StructuredError) { - for (const value of Object.values(cause)) { - if (Array.isArray(value)) { - value.forEach((err) => toast.error(err)); - return; - } - if (typeof value === "string") { - toast.error(value); - return; - } + if ( + cause.code !== "token_not_valid" && + cause.detail !== "Authentication credentials were not provided." + ) { + return; } -} -function isPydanticError(errors: unknown): errors is PydanticError[] { - return ( - Array.isArray(errors) && - errors.every( - (error) => typeof error === "object" && error !== null && "type" in error, - ) - ); -} + // If the user is not already on the session expired page, navigate to it. + if (!location.pathname.startsWith("/session-expired")) { + navigate(`/session-expired?redirect=${window.location.href}`); + } -function handlePydanticErrors(errors: PydanticError[]) { - errors.map(({ type, loc, msg }) => { - const message = typeof msg === "string" ? msg : Object.values(msg)[0]; - if (!loc) { - toast.error(message); - return; - } - type = type - .replace("_", " ") - .replace(/\b\w/g, (char) => char.toUpperCase()); - toast.error(message, { - description: `${type}: '${loc.join(".")}'`, - duration: 8000, - }); - }); -} + return true; +}; + +/** + * Handles HTTP 404 Not Found errors by showing a generic error message. + */ +const notFoundHandler: HttpErrorHandler = ({ status }) => { + if (status === 404) { + toast.error(t("not_found")); + return true; + } +}; diff --git a/src/Utils/request/types.ts b/src/Utils/request/types.ts index 2592208b926..fee73ae52b7 100644 --- a/src/Utils/request/types.ts +++ b/src/Utils/request/types.ts @@ -54,14 +54,23 @@ export interface ApiCallOptions> { headers?: HeadersInit; } -export type StructuredError = Record; +export type BadRequestValueError = { + type: "value_error"; + loc: string[]; + msg: string; + input: unknown; + ctx?: { error?: string }; + url: string; +}; -type HTTPErrorCause = StructuredError | Record | undefined; +export type BadRequestErrorResponse = + | { code: string; detail: string } + | { errors: (BadRequestValueError | object)[] }; export class HTTPError extends Error { status: number; silent: boolean; - cause?: HTTPErrorCause; + cause?: BadRequestErrorResponse | undefined; constructor({ message, @@ -77,7 +86,7 @@ export class HTTPError extends Error { super(message, { cause }); this.status = status; this.silent = silent; - this.cause = cause; + this.cause = cause as BadRequestErrorResponse | undefined; } } diff --git a/src/Utils/request/uploadFile.ts b/src/Utils/request/uploadFile.ts index 9e62eefb799..39bc00ed4b8 100644 --- a/src/Utils/request/uploadFile.ts +++ b/src/Utils/request/uploadFile.ts @@ -33,7 +33,6 @@ const uploadFile = async ( } Notification.BadRequest({ errs: error.errors }); reject(new Error("Client error")); - reject(new Error("Client error")); } else { resolve(); } diff --git a/src/Utils/request/useValueErrorHandler.tsx b/src/Utils/request/useValueErrorHandler.tsx new file mode 100644 index 00000000000..aa1dbadaefe --- /dev/null +++ b/src/Utils/request/useValueErrorHandler.tsx @@ -0,0 +1,70 @@ +import { hashKey } from "@tanstack/react-query"; + +import { useHttpErrorHandler } from "@/Utils/request/errorHandler"; + +import { BadRequestValueError, HTTPError } from "./types"; + +type ValueErrorMatch = + | { loc: string[]; msg: string } + | { loc: string[]; msg?: undefined } + | { loc?: undefined; msg: string }; + +/** + * Pops a ValueError from the cause. + * @param cause - The cause of the error (from the HTTPError instance) + * @param match - The match to find in the cause. + * @returns The ValueError that was popped, or null if no match was found. + */ +export const popValueError = ( + cause: HTTPError["cause"], + match: ValueErrorMatch, +) => { + if (!cause || !("errors" in cause)) { + return null; + } + + const matchedIndex = cause.errors.findIndex((error) => { + if (!("type" in error) || error.type !== "value_error") { + return false; + } + if (match.loc && JSON.stringify(error.loc) !== JSON.stringify(match.loc)) { + return false; + } + if (match.msg && error.msg !== match.msg) { + return false; + } + return true; + }); + + const error = cause.errors[matchedIndex]; + + if (matchedIndex === -1) { + return null; + } + + cause.errors.splice(matchedIndex, 1); + + return error as BadRequestValueError; +}; + +/** + * A wrapper around useHttpErrorHandler that pops a ValueError from the cause + * and calls the onMatch callback if a match is found. + */ +export function useValueErrorHandler(opts: { + match: ValueErrorMatch; + onMatch: (error: BadRequestValueError) => void; + meta?: Record; +}) { + useHttpErrorHandler(({ cause }, meta) => { + if (opts.meta && hashKey([meta]) !== hashKey([opts.meta])) { + return false; + } + + const error = popValueError(cause, opts.match); + if (error) { + opts.onMatch(error); + return true; + } + }); +} diff --git a/src/components/Auth/Login.tsx b/src/components/Auth/Login.tsx index d049cee06d9..591e162b4ff 100644 --- a/src/components/Auth/Login.tsx +++ b/src/components/Auth/Login.tsx @@ -36,8 +36,9 @@ import { LocalStorageKeys } from "@/common/constants"; import FiltersCache from "@/Utils/FiltersCache"; import ViewCache from "@/Utils/ViewCache"; import routes from "@/Utils/request/api"; +import { useHttpErrorHandler } from "@/Utils/request/errorHandler"; import mutate from "@/Utils/request/mutate"; -import { HTTPError } from "@/Utils/request/types"; +import { useValueErrorHandler } from "@/Utils/request/useValueErrorHandler"; import { TokenData } from "@/types/auth/otpToken"; interface OtpLoginData { @@ -45,17 +46,6 @@ interface OtpLoginData { otp: string; } -interface OtpError { - type: string; - loc: string[]; - msg: string; - input: string; - ctx: { - error: string; - }; - url: string; -} - interface OtpValidationError { otp?: string; [key: string]: string | undefined; @@ -107,15 +97,12 @@ const Login = (props: LoginProps) => { setOtpError(""); toast.success(t("send_otp_success")); }, - onError: (error: any) => { - const errors = error?.data || []; - if (Array.isArray(errors) && errors.length > 0) { - const firstError = errors[0] as OtpError; - setOtpError(firstError.msg); - } else { - setOtpError(t("send_otp_error")); - } - }, + }); + + useValueErrorHandler({ + match: { loc: ["phone_number"] }, + onMatch: () => setOtpError(t("phone_number_validation_error")), + meta: { key: "send_op" }, }); // Verify OTP Mutation @@ -212,6 +199,15 @@ const Login = (props: LoginProps) => { return form; }; + // Handles HTTP 429 errors - Captcha due to too many requests + useHttpErrorHandler(({ status }) => { + if (status === 429) { + setCaptcha(true); + return true; + } + setCaptcha(false); + }); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); ViewCache.invalidateAll(); @@ -219,13 +215,7 @@ const Login = (props: LoginProps) => { if (!validated) return; FiltersCache.invalidateAll(); - try { - await signIn(validated); - } catch (error) { - if (error instanceof HTTPError) { - setCaptcha(error.status == 429); - } - } + signIn(validated); }; const validateForgetData = () => { diff --git a/src/components/Patient/allergy/list.tsx b/src/components/Patient/allergy/list.tsx index c3f5854be99..ddbcaaab127 100644 --- a/src/components/Patient/allergy/list.tsx +++ b/src/components/Patient/allergy/list.tsx @@ -7,7 +7,7 @@ import { LeafIcon, } from "lucide-react"; import { Link } from "raviger"; -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode, useState } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -132,16 +132,6 @@ export function AllergyList({ ? `${note.slice(0, MAX_NOTE_LENGTH)}..` : note; - useEffect(() => { - console.log( - "Allergy Note:", - allergy.note, - isLongNote, - displayNote, - note.length, - ); - }, [allergy.note, isLongNote, displayNote, note.length]); - return ( Promise; + signIn: (creds: LoginCredentials) => void; isAuthenticating: boolean; signOut: () => Promise; patientLogin: (tokenData: TokenData, redirectUrl: string) => void; From c4c4fb0ac6c2397f14aa3d03263f224a69eb9f2a Mon Sep 17 00:00:00 2001 From: Jacob John Jeevan Date: Wed, 19 Feb 2025 15:40:53 +0530 Subject: [PATCH 3/3] rm unnecessary onErrors, modify existing ones --- public/locale/en.json | 2 + .../Encounter/CreateEncounterForm.tsx | 6 --- .../Patient/LinkDepartmentsSheet.tsx | 12 ------ .../PatientDetailsTab/PatientUsers.tsx | 12 ------ .../Questionnaire/CloneQuestionnaireSheet.tsx | 10 +++-- .../Questionnaire/QuestionnaireForm.tsx | 8 ++-- .../CreateFacilityOrganizationSheet.tsx | 6 --- .../components/EditFacilityUserRoleSheet.tsx | 12 ------ .../components/LinkFacilityUserSheet.tsx | 6 --- .../components/EditUserRoleSheet.tsx | 12 ------ .../Organization/components/LinkUserSheet.tsx | 6 --- src/pages/Patients/VerifyPatient.tsx | 7 ---- .../PublicAppointments/PatientSelect.tsx | 3 -- src/pages/PublicAppointments/Schedule.tsx | 42 ++++++++++++------- 14 files changed, 37 insertions(+), 107 deletions(-) diff --git a/public/locale/en.json b/public/locale/en.json index 7a455f5a214..7dc753cf5b3 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -943,11 +943,13 @@ "environment": "Environment", "error_404": "Error 404", "error_deleting_shifting": "Error while deleting Shifting record", + "error_failed_to_clone_questionnaire": "Failed to clone questionnaire. Please try again.", "error_fetching_facility_data": "Error while fetching facility data", "error_fetching_slots_data": "Error while fetching slots data", "error_fetching_user_data": "Error while fetching user data", "error_fetching_user_details": "Error while fetching user details: ", "error_loading_questionnaire_response": "Error loading questionnaire response", + "error_slug_already_in_use": "This slug is already in use. Please choose a different one.", "error_updating_encounter": "Error to Updating Encounter", "error_verifying_otp": "Error while verifying OTP, Please request a new OTP", "error_while_deleting_record": "Error while deleting record", diff --git a/src/components/Encounter/CreateEncounterForm.tsx b/src/components/Encounter/CreateEncounterForm.tsx index 488cbc585fd..f9f684505a6 100644 --- a/src/components/Encounter/CreateEncounterForm.tsx +++ b/src/components/Encounter/CreateEncounterForm.tsx @@ -156,12 +156,6 @@ export default function CreateEncounterForm({ onSuccess?.(); navigate(`/facility/${facilityId}/encounter/${data.id}/updates`); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); function onSubmit(data: z.infer) { diff --git a/src/components/Patient/LinkDepartmentsSheet.tsx b/src/components/Patient/LinkDepartmentsSheet.tsx index d3f98027b06..6b30daedc8b 100644 --- a/src/components/Patient/LinkDepartmentsSheet.tsx +++ b/src/components/Patient/LinkDepartmentsSheet.tsx @@ -117,12 +117,6 @@ export default function LinkDepartmentsSheet({ setOpen(false); onUpdate?.(); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const { mutate: removeOrganization, isPending: isRemoving } = useMutation({ @@ -149,12 +143,6 @@ export default function LinkDepartmentsSheet({ toast.success("Organization removed successfully"); onUpdate?.(); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); return ( diff --git a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx index 5f0e90e8297..0cb118704ad 100644 --- a/src/components/Patient/PatientDetailsTab/PatientUsers.tsx +++ b/src/components/Patient/PatientDetailsTab/PatientUsers.tsx @@ -75,12 +75,6 @@ function AddUserSheet({ patientId }: AddUserSheetProps) { setSelectedUser(undefined); setSelectedRole(""); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const handleAddUser = () => { @@ -226,12 +220,6 @@ export const PatientUsers = (props: PatientProps) => { }); toast.success("User removed successfully"); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const ManageUsers = () => { diff --git a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx index 3a61ce1910a..c8b41842161 100644 --- a/src/components/Questionnaire/CloneQuestionnaireSheet.tsx +++ b/src/components/Questionnaire/CloneQuestionnaireSheet.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { Building, Check, Loader2 } from "lucide-react"; import { useNavigate } from "raviger"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -46,6 +47,7 @@ export default function CloneQuestionnaireSheet({ const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [selectedIds, setSelectedIds] = useState([]); + const { t } = useTranslation(); const { data: availableOrganizations, isLoading: isLoadingOrganizations } = useQuery({ @@ -67,11 +69,11 @@ export default function CloneQuestionnaireSheet({ navigate(`/questionnaire/${data.slug}`); setOpen(false); }, - onError: (error: any) => { - if (error.response?.status === 400) { - setError("This slug is already in use. Please choose a different one."); + onError: (error) => { + if (error.status === 400) { + setError(t("error_slug_already_in_use")); } else { - setError("Failed to clone questionnaire. Please try again."); + setError(t("error_failed_to_clone_questionnaire")); } }, }); diff --git a/src/components/Questionnaire/QuestionnaireForm.tsx b/src/components/Questionnaire/QuestionnaireForm.tsx index 6c8716058de..7357dd5298d 100644 --- a/src/components/Questionnaire/QuestionnaireForm.tsx +++ b/src/components/Questionnaire/QuestionnaireForm.tsx @@ -94,11 +94,9 @@ export function QuestionnaireForm({ toast.success(t("questionnaire_submitted_successfully")); onSubmit?.(); }, - onError: (error) => { - const errorData = error.cause; - if (errorData?.results) { - handleSubmissionError(errorData.results as ValidationErrorResponse[]); - } + onError: (error: any) => { + const errorData = error.cause.results as ValidationErrorResponse[]; + handleSubmissionError(errorData); toast.error(t("questionnaire_submission_failed")); }, }); diff --git a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx index 0726a6d44fe..aa0a1774d58 100644 --- a/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/CreateFacilityOrganizationSheet.tsx @@ -68,12 +68,6 @@ export default function CreateFacilityOrganizationSheet({ setDescription(""); setOrgType("dept"); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const handleSubmit = () => { diff --git a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx index 3d96f2210a7..90ab2e20ca2 100644 --- a/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/EditFacilityUserRoleSheet.tsx @@ -81,12 +81,6 @@ export default function EditUserRoleSheet({ toast.success(t("user_role_update_success")); setOpen(false); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); const { mutate: removeRole } = useMutation({ @@ -105,12 +99,6 @@ export default function EditUserRoleSheet({ toast.success(t("user_removed_success")); setOpen(false); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); const handleUpdateRole = () => { diff --git a/src/pages/Facility/settings/organizations/components/LinkFacilityUserSheet.tsx b/src/pages/Facility/settings/organizations/components/LinkFacilityUserSheet.tsx index c0cb464b2b1..d6ab42e1555 100644 --- a/src/pages/Facility/settings/organizations/components/LinkFacilityUserSheet.tsx +++ b/src/pages/Facility/settings/organizations/components/LinkFacilityUserSheet.tsx @@ -85,12 +85,6 @@ export default function LinkFacilityUserSheet({ setSelectedUser(undefined); setSelectedRole(""); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const handleAddUser = () => { diff --git a/src/pages/Organization/components/EditUserRoleSheet.tsx b/src/pages/Organization/components/EditUserRoleSheet.tsx index a70e4633340..2903aff574b 100644 --- a/src/pages/Organization/components/EditUserRoleSheet.tsx +++ b/src/pages/Organization/components/EditUserRoleSheet.tsx @@ -82,12 +82,6 @@ export default function EditUserRoleSheet({ toast.success(t("user_role_update_success")); setOpen(false); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); const { mutate: removeRole } = useMutation({ @@ -102,12 +96,6 @@ export default function EditUserRoleSheet({ toast.success(t("user_removed_success")); setOpen(false); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); const handleUpdateRole = () => { diff --git a/src/pages/Organization/components/LinkUserSheet.tsx b/src/pages/Organization/components/LinkUserSheet.tsx index 967783d61be..9838fe47d70 100644 --- a/src/pages/Organization/components/LinkUserSheet.tsx +++ b/src/pages/Organization/components/LinkUserSheet.tsx @@ -83,12 +83,6 @@ export default function LinkUserSheet({ setSelectedUser(undefined); setSelectedRole(""); }, - onError: (error) => { - const errorData = error.cause as { errors: { msg: string }[] }; - errorData.errors.forEach((er) => { - toast.error(er.msg); - }); - }, }); const handleAddUser = () => { diff --git a/src/pages/Patients/VerifyPatient.tsx b/src/pages/Patients/VerifyPatient.tsx index 01f1feb744b..9b747ffe9f0 100644 --- a/src/pages/Patients/VerifyPatient.tsx +++ b/src/pages/Patients/VerifyPatient.tsx @@ -3,7 +3,6 @@ import { AlertCircle, CalendarIcon } from "lucide-react"; import { Link, useQueryParams } from "raviger"; import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { toast } from "sonner"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -42,12 +41,6 @@ export default function VerifyPatient(props: { facilityId: string }) { isError, } = useMutation({ mutationFn: mutate(routes.patient.search_retrieve), - onError: (error) => { - const errorData = error.cause as { errors: { msg: string[] } }; - errorData.errors.msg.forEach((er) => { - toast.error(er); - }); - }, }); const { data: encounters } = useQuery>({ diff --git a/src/pages/PublicAppointments/PatientSelect.tsx b/src/pages/PublicAppointments/PatientSelect.tsx index d73b1956419..7d50714f906 100644 --- a/src/pages/PublicAppointments/PatientSelect.tsx +++ b/src/pages/PublicAppointments/PatientSelect.tsx @@ -86,9 +86,6 @@ export default function PatientSelect({ replace: true, }); }, - onError: (error) => { - toast.error(error?.message || t("failed_to_create_appointment")); - }, }); const patients = patientData?.results; diff --git a/src/pages/PublicAppointments/Schedule.tsx b/src/pages/PublicAppointments/Schedule.tsx index fa93b45f1d8..f638fabacd5 100644 --- a/src/pages/PublicAppointments/Schedule.tsx +++ b/src/pages/PublicAppointments/Schedule.tsx @@ -91,9 +91,12 @@ export function ScheduleAppointment(props: AppointmentsProps) { }), }); - if (facilityError) { - toast.error(t("error_fetching_facility_data")); - } + useEffect(() => { + if (facilityError) { + toast.error(t("error_fetching_facility_data")); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [facilityError]); const { data: userData, error: userError } = useQuery({ queryKey: ["user", facilityId, staffId], @@ -103,9 +106,12 @@ export function ScheduleAppointment(props: AppointmentsProps) { enabled: !!facilityId && !!staffId, }); - if (userError) { - toast.error(t("error_fetching_user_data")); - } + useEffect(() => { + if (userError) { + toast.error(t("error_fetching_user_data")); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userError]); const slotsQuery = useQuery<{ results: TokenSlot[] }>({ queryKey: ["slots", facilityId, staffId, selectedDate], @@ -123,17 +129,21 @@ export function ScheduleAppointment(props: AppointmentsProps) { enabled: !!selectedDate && !!tokenData.token, }); - if (slotsQuery.error) { - if ( - slotsQuery.error.cause?.errors && - Array.isArray(slotsQuery.error.cause.errors) && - slotsQuery.error.cause.errors[0][0] === "Resource is not schedulable" - ) { - toast.error(t("user_not_available_for_appointments")); - } else { - toast.error(t("error_fetching_slots_data")); + useEffect(() => { + if (slotsQuery.error) { + const errorData = slotsQuery.error.cause as { errors: { msg: string }[] }; + if ( + errorData.errors && + Array.isArray(errorData.errors) && + errorData.errors[0].msg === "Resource is not schedulable" + ) { + toast.error(t("user_not_available_for_appointments")); + } else { + toast.error(t("error_fetching_slots_data")); + } } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [slotsQuery.error]); const { mutate: createAppointment, isPending: isCreatingAppointment } = useMutation({