mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-04-14 12:05:10 +00:00
feat(async-loading-boundary): extract custom error component into separate component
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
6eb6b6a25f
commit
26c1f1bcaa
17 changed files with 265 additions and 37 deletions
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
import { getAllRevisions } from '../../../../api/revisions'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary/async-loading-boundary'
|
||||
import { RevisionListEntry } from './revision-list-entry'
|
||||
import { DateTime } from 'luxon'
|
||||
import React, { useMemo } from 'react'
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { getRevision } from '../../../../api/revisions'
|
||||
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||
import { useDarkModeState } from '../../../../hooks/common/use-dark-mode-state'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../common/async-loading-boundary/async-loading-boundary'
|
||||
import { invertUnifiedPatch } from './invert-unified-patch'
|
||||
import { Optional } from '@mrdrogdrog/optional'
|
||||
import { applyPatch, parsePatch } from 'diff'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Logger } from '../../utils/logger'
|
||||
import { AsyncLoadingBoundary } from '../common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../common/async-loading-boundary/async-loading-boundary'
|
||||
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
|
||||
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
|
||||
import { fetchFrontPageContent } from './requests'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
|
||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
||||
import { WaitSpinner } from '../../../components/common/wait-spinner/wait-spinner'
|
||||
import type { CodeProps } from '../../../components/markdown-renderer/replace-components/code-block-component-replacer'
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import fontStyles from '../../../../global-styles/variables.module.scss'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
|
||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
||||
import type { CodeProps } from '../../../components/markdown-renderer/replace-components/code-block-component-replacer'
|
||||
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
|
||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
||||
import type { CodeProps } from '../../../components/markdown-renderer/replace-components/code-block-component-replacer'
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
|
||||
import { CopyToClipboardButton } from '../../../components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button'
|
||||
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary'
|
||||
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
|
||||
import { ShowIf } from '../../../components/common/show-if/show-if'
|
||||
import type { CodeProps } from '../../../components/markdown-renderer/replace-components/code-block-component-replacer'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { createNote } from '../api/notes'
|
||||
import { AsyncLoadingBoundary } from '../components/common/async-loading-boundary'
|
||||
import { LoadingScreen } from '../components/application-loader/loading-screen/loading-screen'
|
||||
import { CustomAsyncLoadingBoundary } from '../components/common/async-loading-boundary/custom-async-loading-boundary'
|
||||
import { Redirect } from '../components/common/redirect'
|
||||
import { CommonErrorPage } from '../components/error-pages/common-error-page'
|
||||
import { useSingleStringUrlParameter } from '../hooks/common/use-single-string-url-parameter'
|
||||
|
@ -22,10 +23,10 @@ export const NewNotePage: NextPage = () => {
|
|||
}, [newContent])
|
||||
|
||||
return (
|
||||
<AsyncLoadingBoundary
|
||||
<CustomAsyncLoadingBoundary
|
||||
loading={loading}
|
||||
componentName={'NewNotePage'}
|
||||
error={error}
|
||||
loadingComponent={<LoadingScreen />}
|
||||
errorComponent={
|
||||
<CommonErrorPage
|
||||
titleI18nKey={'errors.noteCreationFailed.title'}
|
||||
|
@ -33,7 +34,7 @@ export const NewNotePage: NextPage = () => {
|
|||
/>
|
||||
}>
|
||||
{value ? <Redirect to={`/n/${value.metadata.primaryAddress}`} /> : null}
|
||||
</AsyncLoadingBoundary>
|
||||
</CustomAsyncLoadingBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue