Skip to content

Commit

Permalink
add: translations for adult reservee error
Browse files Browse the repository at this point in the history
Rework the error display mechanic to use a translation hook.
  • Loading branch information
joonatank committed Feb 14, 2025
1 parent ad08c2f commit 47820bf
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 279 deletions.
18 changes: 10 additions & 8 deletions apps/ui/components/common/StartApplicationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { breakpoints } from "common/src/common/style";
import ClientOnly from "common/src/ClientOnly";
import { useRouter } from "next/router";
import {
ApplicationCreateMutationInput,
type ApplicationCreateMutationInput,
useCreateApplicationMutation,
} from "@/gql/gql-types";
import { errorToast } from "common/src/common/toast";
import { getApplicationPath } from "@/modules/urls";
import { Flex, NoWrap } from "common/styles/util";
import { truncatedText } from "common/styles/cssFragments";
import { useMedia } from "react-use";
import { ignoreMaybeArray, toNumber } from "common/src/helpers";
import { useDisplayError } from "@/hooks/useDisplayError";

type Props = {
count: number;
Expand Down Expand Up @@ -77,9 +78,10 @@ function StartApplicationBar({

const [create, { loading: isSaving }] = useCreateApplicationMutation();

const createNewApplication = async (applicationRoundId: number) => {
const displayError = useDisplayError();
const createNewApplication = async (applicationRoundPk: number) => {
const input: ApplicationCreateMutationInput = {
applicationRound: applicationRoundId,
applicationRound: applicationRoundPk,
};
try {
const { data } = await create({
Expand All @@ -93,14 +95,14 @@ function StartApplicationBar({
throw new Error("create application mutation failed");
}
} catch (e) {
errorToast({ text: t("application:Intro.createFailedContent") });
displayError(e);
}
};

const onNext = () => {
const applicationRoundId = router.query.id;
if (typeof applicationRoundId === "string" && applicationRoundId !== "") {
createNewApplication(Number(applicationRoundId));
const applicationRoundPk = toNumber(ignoreMaybeArray(router.query.id));
if (applicationRoundPk) {
createNewApplication(applicationRoundPk);
} else {
throw new Error("Application round id is missing");
}
Expand Down
37 changes: 37 additions & 0 deletions apps/ui/hooks/useDisplayError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type ApiError, getApiErrors } from "common/src/apolloUtils";
import { errorToast } from "common/src/common/toast";
import { type TFunction, useTranslation } from "next-i18next";

/// formatErrorMessage
/// this should not check for missing keys
/// reason: if the key is missing it's a bug
export function formatErrorMessage(t: TFunction, err: ApiError): string {
if (err.code === "MUTATION_VALIDATION_ERROR") {
const validation_code =
"validation_code" in err
? err.validation_code
: "generic_validation_error";
return t(`errors:api.validation.${validation_code}`);
}
return t(`errors:api.${err.code}`);
}

export function useDisplayError() {
const { t } = useTranslation();

return function displayError(error: unknown) {
const errs = getApiErrors(error);
if (errs.length > 0) {
const msgs = errs.map((e) => formatErrorMessage(t, e));
for (const text of msgs) {
errorToast({
text,
});
}
} else {
errorToast({
text: t("errors:general_error"),
});
}
};
}
43 changes: 0 additions & 43 deletions apps/ui/modules/__tests__/util.test.ts

This file was deleted.

33 changes: 5 additions & 28 deletions apps/ui/modules/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { isSameDay, parseISO } from "date-fns";
import { i18n, TFunction } from "next-i18next";
import { i18n, type TFunction } from "next-i18next";
import { trim } from "lodash";
import type { ApolloError } from "@apollo/client";
import {
toApiDate,
toUIDate,
Expand Down Expand Up @@ -146,11 +145,13 @@ export const getAddressAlt = (ru: {
return trim(`${street}, ${city}`, ", ");
};

export const applicationErrorText = (
export function applicationErrorText(
t: TFunction,
key: string | undefined,
attrs: { [key: string]: string | number } = {}
): string => (key ? t(`application:error.${key}`, attrs) : "");
): string {
return key ? t(`application:error.${key}`, attrs) : "";
}

export const getReadableList = (list: string[]): string => {
if (list.length === 0) {
Expand All @@ -166,30 +167,6 @@ export const getReadableList = (list: string[]): string => {
return `${list.slice(0, -1).join(", ")} ${andStr} ${list[list.length - 1]}`;
};

export const printErrorMessages = (error: ApolloError): string => {
if (!error.graphQLErrors || error.graphQLErrors.length === 0) {
return "";
}

const { graphQLErrors: errors } = error;

// TODO add this case "No Reservation matches the given query."
// at least happens when mutating a reservation that doesn't exist
return errors
.reduce((acc, cur) => {
const code = cur?.extensions?.error_code
? // eslint-disable-next-line @typescript-eslint/no-base-to-string -- FIXME
i18n?.t(`errors:${cur?.extensions?.error_code}`)
: "";
const message =
code === cur?.extensions?.error_code || !cur?.extensions?.error_code
? i18n?.t("errors:general_error")
: code || "";
return message ? `${acc}${message}\n` : acc; /// contains non-breaking space
}, "")
.trim();
};

export const isTouchDevice = (): boolean =>
isBrowser && window?.matchMedia("(any-hover: none)").matches;

Expand Down
96 changes: 13 additions & 83 deletions apps/ui/pages/applications/[...params].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useEffect } from "react";
import { ApolloError } from "@apollo/client";
import React from "react";
import Error from "next/error";
import { FormProvider, useForm } from "react-hook-form";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { zodResolver } from "@hookform/resolvers/zod";
Expand All @@ -17,7 +15,6 @@ import {
convertApplication,
ApplicationFormSchemaRefined,
} from "@/components/application/Form";
import { getValidationErrors } from "common/src/apolloUtils";
import { useReservationUnitList } from "@/hooks";
import { useApplicationUpdate } from "@/hooks/useApplicationUpdate";
import { getCommonServerSideProps } from "@/modules/serverUtils";
Expand All @@ -28,70 +25,11 @@ import {
type ApplicationQuery,
type ApplicationQueryVariables,
} from "@/gql/gql-types";
import { errorToast } from "common/src/common/toast";
import { getApplicationPath } from "@/modules/urls";
import { useDisplayError } from "@/hooks/useDisplayError";

// TODO move this to a shared file
// and combine all the separate error handling functions to one
function getErrorMessages(error: unknown): string {
if (error == null) {
return "";
}
if (error instanceof ApolloError) {
const { graphQLErrors, networkError } = error;
if (networkError != null) {
if ("result" in networkError) {
if (typeof networkError.result === "string") {
return networkError.result;
}
if ("errors" in networkError.result) {
// TODO match to known error messages
// fallback to return unkown backend validation error (different from other unknown errors)
const { errors } = networkError.result;
// TODO separate validation errors: this is invalid MutationInput (probably a bug)
const VALIDATION_ERROR = "Variable '$input'";
const isValidationError =
errors.find((e: unknown) => {
if (typeof e !== "object" || e == null) {
return false;
}
if ("message" in e && typeof e.message === "string") {
return e.message.startsWith(VALIDATION_ERROR);
}
return false;
}) != null;
if (isValidationError) {
return "Validation error";
}
return "Unknown network error";
}
}
return networkError.message;
}
// Possible mutations errors (there are others too)
// 1. message: "Voi hakea vain 1-7 varausta viikossa."
// - code: "invalid"
// 2. message: "Reservations begin date cannot be before the application round's reservation period begin date."
// - code: ""
const mutationErrors = getValidationErrors(error);
if (mutationErrors.length > 0) {
return "Form validation error";
}
if (graphQLErrors.length > 0) {
return "Unknown GQL error";
}
}
if (typeof error === "string") {
return error;
}
if (typeof error === "object" && "message" in error) {
if (typeof error.message === "string") {
return error.message;
}
}
return "Unknown error";
}

type Props = Awaited<ReturnType<typeof getServerSideProps>>["props"];
type PropsNarrowed = Exclude<Props, { notFound: boolean }>;

Expand Down Expand Up @@ -163,7 +101,9 @@ function ApplicationRootPage({ slug, data }: PropsNarrowed): JSX.Element {
const { application, applicationRound } = data;
const router = useRouter();

const [update, { error }] = useApplicationUpdate();
const [update] = useApplicationUpdate();

const dislayError = useDisplayError();

const handleSave = async (appToSave: ApplicationFormValues) => {
// There should not be a situation where we are saving on this page without an application
Expand All @@ -178,11 +118,15 @@ function ApplicationRootPage({ slug, data }: PropsNarrowed): JSX.Element {

const saveAndNavigate =
(path: "page2" | "page3") => async (appToSave: ApplicationFormValues) => {
const pk = await handleSave(appToSave);
if (pk === 0) {
return;
try {
const pk = await handleSave(appToSave);
if (pk === 0) {
return;
}
router.push(getApplicationPath(pk, path));
} catch (e) {
dislayError(e);
}
router.push(getApplicationPath(pk, path));
};

const { reservationUnits: selectedReservationUnits } =
Expand All @@ -200,8 +144,6 @@ function ApplicationRootPage({ slug, data }: PropsNarrowed): JSX.Element {
formState: { isDirty },
} = form;

const { t } = useTranslation();

/* TODO removing form reset on page load for now
* the defaultValues should be enough and seems to work when loading an existing application
* this page is not saved + refreshed but goes to second page after save.
Expand All @@ -212,18 +154,6 @@ function ApplicationRootPage({ slug, data }: PropsNarrowed): JSX.Element {
const applicationRoundName =
applicationRound != null ? getTranslation(applicationRound, "name") : "-";

const errorMessage = getErrorMessages(error);
const errorTranslated =
errorMessage !== "" ? t(`errors:applicationMutation.${errorMessage}`) : "";

useEffect(() => {
if (errorTranslated !== "") {
errorToast({
text: errorTranslated,
});
}
}, [errorTranslated, t]);

return (
<FormProvider {...form}>
{slug === "page1" ? (
Expand Down
Loading

0 comments on commit 47820bf

Please sign in to comment.