diff --git a/frontend/src/components/common/async-loading-boundary/__snapshots__/async-loading-boundary.test.tsx.snap b/frontend/src/components/common/async-loading-boundary/__snapshots__/async-loading-boundary.test.tsx.snap new file mode 100644 index 000000000..8079c47b1 --- /dev/null +++ b/frontend/src/components/common/async-loading-boundary/__snapshots__/async-loading-boundary.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Async loading boundary shows a waiting spinner if loading 1`] = ` +
+
+ +
+
+`; + +exports[`Async loading boundary shows an error if error is given with loading 1`] = ` +
+ +
+`; + +exports[`Async loading boundary shows an error if error is given without loading 1`] = ` +
+ +
+`; + +exports[`Async loading boundary shows the children if not loading and no error 1`] = ` +
+ children +
+`; diff --git a/frontend/src/components/common/async-loading-boundary/__snapshots__/custom-async-loading-boundary.test.tsx.snap b/frontend/src/components/common/async-loading-boundary/__snapshots__/custom-async-loading-boundary.test.tsx.snap new file mode 100644 index 000000000..c01a834d0 --- /dev/null +++ b/frontend/src/components/common/async-loading-boundary/__snapshots__/custom-async-loading-boundary.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Custom error async loading boundary shows a waiting spinner if loading 1`] = ` +
+ wait +
+`; + +exports[`Custom error async loading boundary shows an error if error is given with loading 1`] = ` +
+ error +
+`; + +exports[`Custom error async loading boundary shows an error if error is given without loading 1`] = ` +
+ error +
+`; + +exports[`Custom error async loading boundary shows the children if not loading and no error 1`] = ` +
+ children +
+`; diff --git a/frontend/src/components/common/async-loading-boundary/async-loading-boundary.test.tsx b/frontend/src/components/common/async-loading-boundary/async-loading-boundary.test.tsx new file mode 100644 index 000000000..56e2ee685 --- /dev/null +++ b/frontend/src/components/common/async-loading-boundary/async-loading-boundary.test.tsx @@ -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( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows a waiting spinner if loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows an error if error is given without loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows an error if error is given with loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/frontend/src/components/common/async-loading-boundary.tsx b/frontend/src/components/common/async-loading-boundary/async-loading-boundary.tsx similarity index 71% rename from frontend/src/components/common/async-loading-boundary.tsx rename to frontend/src/components/common/async-loading-boundary/async-loading-boundary.tsx index fc2fd7761..336e3897b 100644 --- a/frontend/src/components/common/async-loading-boundary.tsx +++ b/frontend/src/components/common/async-loading-boundary/async-loading-boundary.tsx @@ -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 { useTranslation() - if (error !== undefined && error !== false) { - if (errorComponent) { - return {errorComponent} - } - return ( + + const errorComponent = useMemo(() => { + return error ? ( + ) : ( + ) - } else if (loading) { - return - } else { - return {children} - } + }, [componentName, error]) + + return ( + }> + {children} + + ) } diff --git a/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.test.tsx b/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.test.tsx new file mode 100644 index 000000000..8b15ca5d4 --- /dev/null +++ b/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.test.tsx @@ -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( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows a waiting spinner if loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows an error if error is given without loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) + + it('shows an error if error is given with loading', () => { + const view = render( + + children + + ) + expect(view.container).toMatchSnapshot() + }) +}) diff --git a/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.tsx b/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.tsx new file mode 100644 index 000000000..738feae53 --- /dev/null +++ b/frontend/src/components/common/async-loading-boundary/custom-async-loading-boundary.tsx @@ -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> = ({ + loading, + error, + errorComponent, + loadingComponent, + children +}) => { + useTranslation() + if (error) { + return {errorComponent} + } else if (loading) { + return {loadingComponent} + } else { + return {children} + } +} diff --git a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx index 779e8e7a5..082eff093 100644 --- a/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx +++ b/frontend/src/components/common/note-loading-boundary/note-loading-boundary.tsx @@ -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> = ({ children }) => { - const [{ error, loading }, loadNoteFromServer] = useLoadNoteFromServer() +export const NoteLoadingBoundary: React.FC = ({ children }) => { + const [{ error, loading, value }, loadNoteFromServer] = useLoadNoteFromServer() useEffect(() => { loadNoteFromServer() }, [loadNoteFromServer]) - if (loading) { - return - } else if (error) { + const errorComponent = useMemo(() => { + if (error === undefined) { + return <> + } return ( @@ -35,7 +37,15 @@ export const NoteLoadingBoundary: React.FC> = ({ chil ) - } else { - return {children} - } + }, [error, loadNoteFromServer]) + + return ( + }> + {children} + + ) } diff --git a/frontend/src/components/common/user-avatar/user-avatar-for-username.tsx b/frontend/src/components/common/user-avatar/user-avatar-for-username.tsx index 0c3a9a170..a778fecbb 100644 --- a/frontend/src/components/common/user-avatar/user-avatar-for-username.tsx +++ b/frontend/src/components/common/user-avatar/user-avatar-for-username.tsx @@ -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' diff --git a/frontend/src/components/editor-page/document-bar/revisions/revision-list.tsx b/frontend/src/components/editor-page/document-bar/revisions/revision-list.tsx index f89281294..c85b73561 100644 --- a/frontend/src/components/editor-page/document-bar/revisions/revision-list.tsx +++ b/frontend/src/components/editor-page/document-bar/revisions/revision-list.tsx @@ -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' diff --git a/frontend/src/components/editor-page/document-bar/revisions/revision-viewer.tsx b/frontend/src/components/editor-page/document-bar/revisions/revision-viewer.tsx index 23509b294..03c2a9804 100644 --- a/frontend/src/components/editor-page/document-bar/revisions/revision-viewer.tsx +++ b/frontend/src/components/editor-page/document-bar/revisions/revision-viewer.tsx @@ -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' diff --git a/frontend/src/components/intro-page/intro-custom-content.tsx b/frontend/src/components/intro-page/intro-custom-content.tsx index 391934be7..83e357b1b 100644 --- a/frontend/src/components/intro-page/intro-custom-content.tsx +++ b/frontend/src/components/intro-page/intro-custom-content.tsx @@ -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' diff --git a/frontend/src/extensions/extra-integrations/abcjs/abc-frame.tsx b/frontend/src/extensions/extra-integrations/abcjs/abc-frame.tsx index 9fdbde262..10da79e35 100644 --- a/frontend/src/extensions/extra-integrations/abcjs/abc-frame.tsx +++ b/frontend/src/extensions/extra-integrations/abcjs/abc-frame.tsx @@ -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' diff --git a/frontend/src/extensions/extra-integrations/flowchart/flowchart.tsx b/frontend/src/extensions/extra-integrations/flowchart/flowchart.tsx index 6fc8171fb..8e0434abc 100644 --- a/frontend/src/extensions/extra-integrations/flowchart/flowchart.tsx +++ b/frontend/src/extensions/extra-integrations/flowchart/flowchart.tsx @@ -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' diff --git a/frontend/src/extensions/extra-integrations/graphviz/graphviz-frame.tsx b/frontend/src/extensions/extra-integrations/graphviz/graphviz-frame.tsx index d0b03c130..0233fee58 100644 --- a/frontend/src/extensions/extra-integrations/graphviz/graphviz-frame.tsx +++ b/frontend/src/extensions/extra-integrations/graphviz/graphviz-frame.tsx @@ -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' diff --git a/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code.tsx b/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code.tsx index f08b01cd8..8b4cc81d5 100644 --- a/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code.tsx +++ b/frontend/src/extensions/extra-integrations/highlighted-code-fence/highlighted-code.tsx @@ -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' diff --git a/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-chart.tsx b/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-chart.tsx index 57369c161..58fe1f839 100644 --- a/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-chart.tsx +++ b/frontend/src/extensions/extra-integrations/vega-lite/vega-lite-chart.tsx @@ -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' diff --git a/frontend/src/pages/new.tsx b/frontend/src/pages/new.tsx index f64a647d7..938427f5a 100644 --- a/frontend/src/pages/new.tsx +++ b/frontend/src/pages/new.tsx @@ -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 ( - } errorComponent={ { /> }> {value ? : null} - + ) }