diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 6187593e70c7ad471ab02555a480d1d7d5b79ca6..fc1eda768f0f416362340d3e7127db622eca2bed 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -154,10 +154,11 @@ "error": "Error", "invitationCreationFailedHeader": "Failed to create invite", "errorStatusCode": "Status code: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "The server reported:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Message:<1>{{errorBodyText}}</1>", "contactHelp": "Contact help through the link in the footer if the problem persists.", + "errorLoadOusRoleTypeHeading": "Error loading form data", + "errorLoadOusRoleType": "Could not load organizational units and/or role type from server", "unknown": "An unknown error has occurred. If the problem persists, contact support.", - "contactHelp": "Contact help through the link in the footer if the problem persists.", "invitationDataFetchFailed": "Failed to fetch invitation data", "guestRegistrationFailed": "Failed to register your data", "codes": { diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 4d1855ffe9800332aed8db0bafa13fd7081e7472..79d565aa28768ffe6a6e95fec7709a679504f0cb 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -154,10 +154,11 @@ "error": "Feil", "invitationCreationFailedHeader": "Kunne ikke opprette invitasjon", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Melding:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", "unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte.", - "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", + "errorLoadOusRoleTypeHeading": "Feil under lasting av skjemadata", + "errorLoadOusRoleType": "Kunne ikke laste organisasjons og/eller rolletype data fra server", "invitationDataFetchFailed": "Klarte ikke å hente invitasjonsdata", "guestRegistrationFailed": "Klarte ikke å registrere dataene dine", "codes": { diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 23ad489b0b76af9166004dda686044e4d52fd062..c49aa0b3406db333e16c365a6cb2d5bb9651ceb0 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -155,10 +155,11 @@ "error": "Feil", "invitationCreationFailedHeader": "Kunne ikkje opprette invitasjon", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", - "genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>", + "genericServerErrorBody": "Melding:<1>{{errorBodyText}}</1>", "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", "unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte.",, - "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.", + "errorLoadOusRoleTypeHeading": "Feil under lasting av skjemadata", + "errorLoadOusRoleType": "Kunne ikkje laste organisasjons og/eller rolletype data frå server", "invitationDataFetchFailed": "Klarte ikkje å hente invitasjonsdata", "guestRegistrationFailed": "Klarte ikkje å registrere dataene dine", "codes": { diff --git a/frontend/src/components/errorReport/index.tsx b/frontend/src/components/errorReport/index.tsx index 32d0f67fe1f4aadb1f5c03b69cd87000a2361753..f95a9b33dd126ce42905ec633454e2f41051a3da 100644 --- a/frontend/src/components/errorReport/index.tsx +++ b/frontend/src/components/errorReport/index.tsx @@ -2,7 +2,7 @@ import { Alert, AlertTitle } from '@mui/material' import { Trans, useTranslation } from 'react-i18next' import React from 'react' -export interface ErrorReportProps { +export type ServerErrorReportData = { errorHeading: string statusCode?: number statusText?: string @@ -14,8 +14,8 @@ export default function ServerErrorReport({ statusCode, statusText, errorBodyText, -}: ErrorReportProps) { - const [t] = useTranslation(['common']) +}: ServerErrorReportData) { + const { t } = useTranslation(['common']) return ( <Alert severity="error"> <AlertTitle>{errorHeading}</AlertTitle> @@ -27,7 +27,7 @@ export default function ServerErrorReport({ <br /> {errorBodyText !== undefined && ( <Trans i18nKey="error.genericServerErrorBody" t={t}> - The server reported: + Message: <blockquote>{{ errorBodyText }}</blockquote> </Trans> )} diff --git a/frontend/src/routes/guest/register/index.tsx b/frontend/src/routes/guest/register/index.tsx index 3b8bd5b022442fcd09f8d6927bd371aa94f9d97f..d1adc3e554da35240b7ab474384fcf8017291060 100644 --- a/frontend/src/routes/guest/register/index.tsx +++ b/frontend/src/routes/guest/register/index.tsx @@ -22,7 +22,7 @@ import GuestRegisterStep from './steps/register' import GuestConsentStep from './steps/consent' import GuestSuccessStep from './steps/success' import ServerErrorReport, { - ErrorReportProps, + ServerErrorReportData, } from '../../../components/errorReport' enum Step { @@ -79,9 +79,9 @@ export default function GuestRegister() { const [guestConsentData, setGuestConsentData] = useState<GuestConsentData | null>(null) const [fetchInvitationDataError, setFetchInvitationDataError] = - useState<ErrorReportProps | null>(null) + useState<ServerErrorReportData | null>(null) const [submitGuestDataError, setSubmitGuestDataError] = - useState<ErrorReportProps | null>(null) + useState<ServerErrorReportData | null>(null) const fetchInvitationData = async () => { const response = await fetch('/api/ui/v1/invited/', fetchJsonOpts()) diff --git a/frontend/src/routes/sponsor/register/stepRegistration.tsx b/frontend/src/routes/sponsor/register/stepRegistration.tsx index 3b7e98aed639820b29d584a3a034cb7c26ef19ca..9486b9ca60efea9cd81a0cb3f582750b40763a11 100644 --- a/frontend/src/routes/sponsor/register/stepRegistration.tsx +++ b/frontend/src/routes/sponsor/register/stepRegistration.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react' +import React, { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Box, Button } from '@mui/material' @@ -14,7 +14,11 @@ import SubmitState from './submitState' import SponsorGuestButtons from '../../components/sponsorGuestButtons' import { submitJsonOpts } from '../../../utils' import StepSubmitSuccess from './stepSubmitSuccess' -import ServerErrorReport from '../../../components/errorReport' +import ServerErrorReport, { + ServerErrorReportData, +} from '../../../components/errorReport' +import useOus from '../../../hooks/useOus' +import useRoleTypes from '../../../hooks/useRoleTypes' enum Steps { RegisterStep = 0, @@ -22,12 +26,6 @@ enum Steps { SuccessStep = 2, } -interface SubmitErrorData { - statusCode: number - statusText: string - bodyText: string -} - /** * * This component controls the invite process where the sponsor @@ -52,7 +50,16 @@ export default function StepRegistration() { const [activeStep, setActiveStep] = useState(0) const personFormRef = useRef<PersonFormMethods>(null) const [submitState, setSubmitState] = useState(SubmitState.NotSubmitted) - const [errorReport, setErrorReport] = useState<SubmitErrorData>() + const [submitErrorReport, setSubmitErrorReport] = + useState<ServerErrorReportData>() + const [formDataErrorReport, setFormDataErrorReport] = + useState<ServerErrorReportData>() + + // The organizational unit and role types are not used by this component, but + // loading them here anyways to make sure that they can be loaded using the + // hooks. If they cannot, an error message will be shown to the user + const ous = useOus() + const roleTypes = useRoleTypes() const handleNext = () => { if (activeStep === 0) { @@ -91,10 +98,11 @@ export default function StepRegistration() { if (!res.ok) { res.text().then((text) => { setSubmitState(SubmitState.SubmitFailure) - setErrorReport({ + setSubmitErrorReport({ + errorHeading: t('error.invitationCreationFailedHeader'), statusCode: res.status, statusText: res.statusText, - bodyText: text, + errorBodyText: text, }) }) } else { @@ -130,6 +138,20 @@ export default function StepRegistration() { history.push('/') } + useEffect(() => { + if (ous.length === 0 || roleTypes.length === 0) { + // These arrays should have values. There is no information + // about the status code at this level, since the values come + // from hooks and any server errors are handled there + setFormDataErrorReport({ + errorHeading: t('error.errorLoadOusRoleTypeHeading'), + statusCode: undefined, + statusText: undefined, + errorBodyText: t('error.errorLoadOusRoleType'), + }) + } + }, []) + return ( <Page> <SponsorGuestButtons registerNewGuestActive /> @@ -203,14 +225,23 @@ export default function StepRegistration() { {activeStep === Steps.SuccessStep && <StepSubmitSuccess />} {submitState === SubmitState.SubmitFailure && - errorReport !== undefined && ( + submitErrorReport !== undefined && ( <ServerErrorReport - errorHeading={t('error.invitationCreationFailedHeader')} - statusCode={errorReport?.statusCode} - statusText={errorReport?.statusText} - errorBodyText={errorReport?.bodyText} + errorHeading={submitErrorReport?.errorHeading} + statusCode={submitErrorReport?.statusCode} + statusText={submitErrorReport?.statusText} + errorBodyText={submitErrorReport?.errorBodyText} /> )} + + {formDataErrorReport !== undefined && ( + <ServerErrorReport + errorHeading={formDataErrorReport.errorHeading} + statusCode={undefined} + statusText={undefined} + errorBodyText={formDataErrorReport.errorBodyText} + /> + )} </Page> ) } diff --git a/frontend/src/test-utils.tsx b/frontend/src/test-utils.tsx index 7be8884de2a2feb948ca530b2b2464225e5e5be5..fa8739024553136211812cbfeb356c9840f2fd8c 100644 --- a/frontend/src/test-utils.tsx +++ b/frontend/src/test-utils.tsx @@ -19,13 +19,71 @@ export * from '@testing-library/react' // override render method export { customRender as render } -// Mock react-i18next module to return a translation that just returns the key -jest.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (value: string) => value, - i18n: { - language: 'en', - changeLanguage: () => new Promise(() => {}), - }, - }), -})) +// The reason for this complex mock of react-i18next is to make the Trans-component work when running tests +jest.mock('react-i18next', (): object => { + // Need to require React at this level as well to avoid getting an error + // when running the tests saying that the module factory is not allowed + // to reference out-of-scope variables + // eslint-disable-next-line @typescript-eslint/no-shadow,global-require + const React = require('react') + + const hasChildren = (node: any): boolean => + node && (node.children || (node.props && node.props.children)) + + const getChildrenFromProps = (node: any): any => + node.props && node.props.children ? node.props.children : null + + const getChildren = (node: any): any => + node && node.children ? node.children : getChildrenFromProps(node) + + const renderNodes = (reactNodes: any): any => { + if (typeof reactNodes === 'string') { + return reactNodes + } + + return Object.keys(reactNodes).map((key, i) => { + const child = reactNodes[key] + + if (typeof child === 'string') { + return child + } + + if (hasChildren(child)) { + const inner = renderNodes(getChildren(child)) + // eslint-disable-next-line react/no-array-index-key + return React.cloneElement(child, { ...child.props, key: i }, inner) + } + + const isElement = React.isValidElement(child) + if (typeof child === 'object' && !isElement) { + return Object.keys(child).reduce( + (str, childKey) => `${str}${child[childKey]}`, + '' + ) + } + + return child + }) + } + + const useMock: any = [(k: any) => k, {}] + // Mock react-i18next module to return a translation that just returns the key + useMock.t = (k: any) => k + useMock.i18n = { + // Return "en" as the selected language if the code asks for it + language: 'en', + changeLanguage: () => new Promise(() => {}), + } + + return { + withTranslation: + () => + (Component: any): any => + (props: any): any => + <Component t={(k: any): any => k} {...props} />, + Trans: ({ children }: any) => renderNodes(children), + Translation: ({ children }: any) => + children((k: any): any => k, { i18n: {} }), + useTranslation: () => useMock, + } +})