feat(async-loading-boundary): extract custom error component into separate component

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-12-29 12:25:06 +01:00
parent 6eb6b6a25f
commit 26c1f1bcaa
17 changed files with 265 additions and 37 deletions

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Async loading boundary shows a waiting spinner if loading 1`] = `
<div>
<div
class="m-3 d-flex align-items-center justify-content-center"
>
<i
class="fa fa-spinner fa-spin "
/>
</div>
</div>
`;
exports[`Async loading boundary shows an error if error is given with loading 1`] = `
<div>
<div
class="fade alert alert-danger show"
role="alert"
>
common.errorWhileLoading
</div>
</div>
`;
exports[`Async loading boundary shows an error if error is given without loading 1`] = `
<div>
<div
class="fade alert alert-danger show"
role="alert"
>
common.errorWhileLoading
</div>
</div>
`;
exports[`Async loading boundary shows the children if not loading and no error 1`] = `
<div>
children
</div>
`;

View file

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Custom error async loading boundary shows a waiting spinner if loading 1`] = `
<div>
wait
</div>
`;
exports[`Custom error async loading boundary shows an error if error is given with loading 1`] = `
<div>
error
</div>
`;
exports[`Custom error async loading boundary shows an error if error is given without loading 1`] = `
<div>
error
</div>
`;
exports[`Custom error async loading boundary shows the children if not loading and no error 1`] = `
<div>
children
</div>
`;

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n'
import { AsyncLoadingBoundary } from './async-loading-boundary'
import { render } from '@testing-library/react'
describe('Async loading boundary', () => {
beforeAll(() => mockI18n())
it('shows the children if not loading and no error', () => {
const view = render(
<AsyncLoadingBoundary loading={false} componentName={'test'}>
children
</AsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows a waiting spinner if loading', () => {
const view = render(
<AsyncLoadingBoundary loading={true} componentName={'test'}>
children
</AsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows an error if error is given without loading', () => {
const view = render(
<AsyncLoadingBoundary loading={false} error={new Error('error')} componentName={'test'}>
children
</AsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows an error if error is given with loading', () => {
const view = render(
<AsyncLoadingBoundary loading={true} error={new Error('error')} componentName={'test'}>
children
</AsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -3,9 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { WaitSpinner } from './wait-spinner/wait-spinner'
import type { PropsWithChildren, ReactNode } from 'react'
import React, { Fragment } from 'react'
import { WaitSpinner } from '../wait-spinner/wait-spinner'
import { CustomAsyncLoadingBoundary } from './custom-async-loading-boundary'
import type { PropsWithChildren } from 'react'
import React, { Fragment, useMemo } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
@ -13,7 +14,6 @@ export interface AsyncLoadingBoundaryProps {
loading: boolean
error?: Error | boolean
componentName: string
errorComponent?: ReactNode
}
/**
@ -30,22 +30,27 @@ export const AsyncLoadingBoundary: React.FC<PropsWithChildren<AsyncLoadingBounda
loading,
error,
componentName,
errorComponent,
children
}) => {
useTranslation()
if (error !== undefined && error !== false) {
if (errorComponent) {
return <Fragment>{errorComponent}</Fragment>
}
return (
const errorComponent = useMemo(() => {
return error ? (
<Alert variant={'danger'}>
<Trans i18nKey={'common.errorWhileLoading'} values={{ name: componentName }} />
</Alert>
) : (
<Fragment></Fragment>
)
} else if (loading) {
return <WaitSpinner />
} else {
return <Fragment>{children}</Fragment>
}
}, [componentName, error])
return (
<CustomAsyncLoadingBoundary
loading={loading}
error={error}
errorComponent={errorComponent}
loadingComponent={<WaitSpinner />}>
{children}
</CustomAsyncLoadingBoundary>
)
}

View file

@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n'
import { CustomAsyncLoadingBoundary } from './custom-async-loading-boundary'
import { render } from '@testing-library/react'
describe('Custom error async loading boundary', () => {
beforeAll(() => mockI18n())
it('shows the children if not loading and no error', () => {
const view = render(
<CustomAsyncLoadingBoundary loading={false} errorComponent={'error'} loadingComponent={'wait'}>
children
</CustomAsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows a waiting spinner if loading', () => {
const view = render(
<CustomAsyncLoadingBoundary loading={true} errorComponent={'error'} loadingComponent={'wait'}>
children
</CustomAsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows an error if error is given without loading', () => {
const view = render(
<CustomAsyncLoadingBoundary
loading={false}
error={new Error('error')}
errorComponent={'error'}
loadingComponent={'wait'}>
children
</CustomAsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
it('shows an error if error is given with loading', () => {
const view = render(
<CustomAsyncLoadingBoundary
loading={true}
error={new Error('error')}
errorComponent={'error'}
loadingComponent={'wait'}>
children
</CustomAsyncLoadingBoundary>
)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren, ReactNode } from 'react'
import React, { Fragment } from 'react'
import { useTranslation } from 'react-i18next'
export interface CustomErrorAsyncLoadingBoundaryProps {
loading: boolean
error?: Error | boolean
errorComponent: ReactNode
loadingComponent: ReactNode
}
/**
* Indicates that a component currently loading or an error occurred.
* It's meant to be used in combination with useAsync from react-use.
*
* @param loading Indicates that the component is currently loading. Setting this will show a spinner instead of the children.
* @param error Indicates that an error occurred during the loading process. Setting this to any non-null value will show an error message instead of the children.
* @param componentName The name of the component that is currently loading. It will be shown in the error message.
* @param errorComponent Optional component that will be used in case of an error instead of the default alert message.
* @param children The child {@link ReactElement elements} that are only shown if the component isn't in loading or error state
*/
export const CustomAsyncLoadingBoundary: React.FC<PropsWithChildren<CustomErrorAsyncLoadingBoundaryProps>> = ({
loading,
error,
errorComponent,
loadingComponent,
children
}) => {
useTranslation()
if (error) {
return <Fragment>{errorComponent}</Fragment>
} else if (loading) {
return <Fragment>{loadingComponent}</Fragment>
} else {
return <Fragment>{children}</Fragment>
}
}

View file

@ -5,11 +5,12 @@
*/
import { LoadingScreen } from '../../application-loader/loading-screen/loading-screen'
import { CommonErrorPage } from '../../error-pages/common-error-page'
import { CustomAsyncLoadingBoundary } from '../async-loading-boundary/custom-async-loading-boundary'
import { ShowIf } from '../show-if/show-if'
import { CreateNonExistingNoteHint } from './create-non-existing-note-hint'
import { useLoadNoteFromServer } from './hooks/use-load-note-from-server'
import type { PropsWithChildren } from 'react'
import React, { Fragment, useEffect } from 'react'
import React, { useEffect, useMemo } from 'react'
/**
* Loads the note identified by the note-id in the URL.
@ -18,16 +19,17 @@ import React, { Fragment, useEffect } from 'react'
*
* @param children The react elements that will be shown when the loading was successful.
*/
export const NoteLoadingBoundary: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
const [{ error, loading }, loadNoteFromServer] = useLoadNoteFromServer()
export const NoteLoadingBoundary: React.FC<PropsWithChildren> = ({ children }) => {
const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer()
useEffect(() => {
loadNoteFromServer()
}, [loadNoteFromServer])
if (loading) {
return <LoadingScreen />
} else if (error) {
const errorComponent = useMemo(() => {
if (error === undefined) {
return <></>
}
return (
<CommonErrorPage titleI18nKey={`${error.message}.title`} descriptionI18nKey={`${error.message}.description`}>
<ShowIf condition={error.message === 'api.note.notFound'}>
@ -35,7 +37,15 @@ export const NoteLoadingBoundary: React.FC<PropsWithChildren<unknown>> = ({ chil
</ShowIf>
</CommonErrorPage>
)
} else {
return <Fragment>{children}</Fragment>
}
}, [error, loadNoteFromServer])
return (
<CustomAsyncLoadingBoundary
loading={loading || !value}
error={error}
errorComponent={errorComponent}
loadingComponent={<LoadingScreen />}>
{children}
</CustomAsyncLoadingBoundary>
)
}

View file

@ -5,7 +5,7 @@
*/
import { getUser } from '../../../api/users'
import type { UserInfo } from '../../../api/users/types'
import { AsyncLoadingBoundary } from '../async-loading-boundary'
import { AsyncLoadingBoundary } from '../async-loading-boundary/async-loading-boundary'
import type { UserAvatarProps } from './user-avatar'
import { UserAvatar } from './user-avatar'
import React from 'react'