diff --git a/src/app-components/Button/Button.tsx b/src/app-components/Button/Button.tsx index 21e9a6c351..c767f6a68a 100644 --- a/src/app-components/Button/Button.tsx +++ b/src/app-components/Button/Button.tsx @@ -1,9 +1,10 @@ import React, { forwardRef } from 'react'; import type { PropsWithChildren } from 'react'; -import { Button as DesignSystemButton, Spinner } from '@digdir/designsystemet-react'; +import { Button as DesignSystemButton } from '@digdir/designsystemet-react'; import type { ButtonProps as DesignSystemButtonProps } from '@digdir/designsystemet-react'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { useLanguage } from 'src/features/language/useLanguage'; export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | undefined; diff --git a/src/app-components/Table/Table.tsx b/src/app-components/Table/Table.tsx index 37fa5224e1..51e2834769 100644 --- a/src/app-components/Table/Table.tsx +++ b/src/app-components/Table/Table.tsx @@ -1,12 +1,13 @@ import React from 'react'; import type { ReactElement } from 'react'; -import { Button, Spinner, Table } from '@digdir/designsystemet-react'; +import { Button, Table } from '@digdir/designsystemet-react'; import cn from 'classnames'; import { format, isValid, parseISO } from 'date-fns'; import { pick } from 'dot-object'; import type { JSONSchema7 } from 'json-schema'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import classes from 'src/app-components/Table/Table.module.css'; import utilClasses from 'src/styles/utils.module.css'; import type { FormDataValue } from 'src/app-components/DynamicForm/DynamicForm'; diff --git a/src/app-components/error/FatalError/FatalError.tsx b/src/app-components/error/FatalError/FatalError.tsx new file mode 100644 index 0000000000..6086ae32e5 --- /dev/null +++ b/src/app-components/error/FatalError/FatalError.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type { HTMLAttributes, PropsWithChildren } from 'react'; + +/** + * The `data-fatal-error` signals that some unrecoverable error occured which should prevent PDF generation from happening as it would not include necessary information. + */ +export function FatalError({ children, ...props }: PropsWithChildren>) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app-components/error/FatalErrorEmpty/FatalErrorEmpty.tsx b/src/app-components/error/FatalErrorEmpty/FatalErrorEmpty.tsx new file mode 100644 index 0000000000..b272c885e5 --- /dev/null +++ b/src/app-components/error/FatalErrorEmpty/FatalErrorEmpty.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +/** + * The `data-fatal-error` signals that some unrecoverable error occured which should prevent PDF generation from happening as it would not include necessary information. + */ +export function FatalErrorEmpty() { + return ( +
+ ); +} diff --git a/src/components/atoms/AltinnContentIcon.tsx b/src/app-components/loading/AltinnContentLoader/AltinnContentIcon.tsx similarity index 100% rename from src/components/atoms/AltinnContentIcon.tsx rename to src/app-components/loading/AltinnContentLoader/AltinnContentIcon.tsx diff --git a/src/components/atoms/AltinnContentIconFormData.tsx b/src/app-components/loading/AltinnContentLoader/AltinnContentIconFormData.tsx similarity index 98% rename from src/components/atoms/AltinnContentIconFormData.tsx rename to src/app-components/loading/AltinnContentLoader/AltinnContentIconFormData.tsx index 01624e80c9..430e0d6ca4 100644 --- a/src/components/atoms/AltinnContentIconFormData.tsx +++ b/src/app-components/loading/AltinnContentLoader/AltinnContentIconFormData.tsx @@ -4,6 +4,7 @@ export function AltinnContentIconFormData() { return ( <> = {}) => { + const allProps = { + ...props, + }; + + rtlRender( + , + ); +}; + +describe('AltinnContentLoader', () => { + it('should show default loader when no variant is set', () => { + render(); + + expect(screen.getByTestId('AltinnContentIcon')).toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIconFormData')).not.toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIconReceipt')).not.toBeInTheDocument(); + }); + + it('should show form loader when variant=form', () => { + render({ variant: 'form' }); + + expect(screen.queryByTestId('AltinnContentIconFormData')).toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIconReceipt')).not.toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIcon')).not.toBeInTheDocument(); + }); + + it('should show receipt loader when variant=receipt', () => { + render({ variant: 'receipt' }); + + expect(screen.queryByTestId('AltinnContentIconReceipt')).toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIconFormData')).not.toBeInTheDocument(); + expect(screen.queryByTestId('AltinnContentIcon')).not.toBeInTheDocument(); + }); +}); diff --git a/src/app-components/loading/AltinnContentLoader/AltinnContentLoader.tsx b/src/app-components/loading/AltinnContentLoader/AltinnContentLoader.tsx new file mode 100644 index 0000000000..4a5b1571c9 --- /dev/null +++ b/src/app-components/loading/AltinnContentLoader/AltinnContentLoader.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import ContentLoader from 'react-content-loader'; + +import { AltinnContentIcon } from 'src/app-components/loading/AltinnContentLoader/AltinnContentIcon'; +import { AltinnContentIconFormData } from 'src/app-components/loading/AltinnContentLoader/AltinnContentIconFormData'; +import { AltinnContentIconReceipt } from 'src/app-components/loading/AltinnContentLoader/AltinnContentIconReceipt'; + +type LoaderVariant = 'default' | 'form' | 'receipt'; + +export interface IAltinnContentLoaderProps { + reason: string; + details?: string; + + variant?: LoaderVariant; + height?: number | string; + width?: number | string; +} + +interface LoaderIconProps { + variant?: LoaderVariant; +} + +function LoaderIcon({ variant }: LoaderIconProps) { + switch (variant) { + case 'form': + return ; + case 'receipt': + return ; + case 'default': + default: + return ; + } +} + +/** + * The `data-loading` signals that something is pending and we should not print PDF yet. + */ +export const AltinnContentLoader = ({ + reason, + details, + variant, + width = 400, + height = 200, +}: IAltinnContentLoaderProps) => ( +
+ + + +
+); diff --git a/src/app-components/loading/LoadingEmpty/LoadingEmpty.tsx b/src/app-components/loading/LoadingEmpty/LoadingEmpty.tsx new file mode 100644 index 0000000000..db4b27e367 --- /dev/null +++ b/src/app-components/loading/LoadingEmpty/LoadingEmpty.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +/** + * The `data-loading` signals that something is pending and we should not print PDF yet. + */ +export function LoadingEmpty() { + return ( +
+ ); +} diff --git a/src/app-components/loading/LoadingWrapper/LoadingWrapper.tsx b/src/app-components/loading/LoadingWrapper/LoadingWrapper.tsx new file mode 100644 index 0000000000..1c05c50da1 --- /dev/null +++ b/src/app-components/loading/LoadingWrapper/LoadingWrapper.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import type { HTMLAttributes, PropsWithChildren } from 'react'; + +/** + * The `data-loading` signals that something is pending and we should not print PDF yet. + */ +export function LoadingWrapper({ children, ...props }: PropsWithChildren>) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app-components/loading/Spinner/Spinner.tsx b/src/app-components/loading/Spinner/Spinner.tsx new file mode 100644 index 0000000000..37408139f0 --- /dev/null +++ b/src/app-components/loading/Spinner/Spinner.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Spinner as DesignSystemSpinner } from '@digdir/designsystemet-react'; + +/** + * The `data-loading` signals that something is pending and we should not print PDF yet. + */ +export function Spinner(props: Parameters[0]) { + return ( + + ); +} diff --git a/src/components/AltinnSpinner.tsx b/src/components/AltinnSpinner.tsx index 21e577d225..cbe4c9363a 100644 --- a/src/components/AltinnSpinner.tsx +++ b/src/components/AltinnSpinner.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Paragraph, Spinner } from '@digdir/designsystemet-react'; +import { Paragraph } from '@digdir/designsystemet-react'; import classNames from 'classnames'; import type { ArgumentArray } from 'classnames'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import classes from 'src/components/AltinnSpinner.module.css'; import { useLanguage } from 'src/features/language/useLanguage'; diff --git a/src/components/PDFGeneratorPreview/PDFGeneratorPreview.tsx b/src/components/PDFGeneratorPreview/PDFGeneratorPreview.tsx index 56da1fefad..c649324ce0 100644 --- a/src/components/PDFGeneratorPreview/PDFGeneratorPreview.tsx +++ b/src/components/PDFGeneratorPreview/PDFGeneratorPreview.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Dialog, Heading, Spinner } from '@digdir/designsystemet-react'; +import { Dialog, Heading } from '@digdir/designsystemet-react'; import { FilePdfIcon } from '@navikt/aksel-icons'; import { Button } from 'src/app-components/Button/Button'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import classes from 'src/features/devtools/components/PDFPreviewButton/PDFPreview.module.css'; import { useLaxInstanceId } from 'src/features/instance/InstanceContext'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; diff --git a/src/components/ReadyForPrint.tsx b/src/components/ReadyForPrint.tsx index 7c3492dda5..5f694b3a91 100644 --- a/src/components/ReadyForPrint.tsx +++ b/src/components/ReadyForPrint.tsx @@ -4,7 +4,8 @@ import { useIsFetching } from '@tanstack/react-query'; import { waitForAnimationFrames } from 'src/utils/waitForAnimationFrames'; -export const loadingClassName = 'loading'; +export const loadingAttribute = 'data-loading'; +const errorAttribute = 'data-fatal-error'; type ReadyType = 'print' | 'load'; const readyId: Record = { @@ -24,7 +25,8 @@ export function ReadyForPrint({ type }: { type: ReadyType }) { const isFetching = useIsFetching() > 0; - const hasLoaders = useHasElementsByClass(loadingClassName); + const hasLoaders = useHasElementsByAttribute(loadingAttribute); + const hasErrors = useHasElementsByAttribute(errorAttribute); React.useLayoutEffect(() => { if (assetsLoaded) { @@ -39,7 +41,7 @@ export function ReadyForPrint({ type }: { type: ReadyType }) { }); }, [assetsLoaded]); - if (!assetsLoaded || hasLoaders || isFetching || isPending) { + if (!assetsLoaded || hasLoaders || isFetching || isPending || (type === 'print' && hasErrors)) { return null; } @@ -74,13 +76,12 @@ async function waitForImages() { } while (nodes.some((node) => !node.complete)); } -export function useHasElementsByClass(className: string) { - const [hasElements, setHasElements] = useState(() => document.getElementsByClassName(className).length > 0); +export function useHasElementsByAttribute(attribute: string) { + const [hasElements, setHasElements] = useState(() => document.querySelector(`[${attribute}]`) != null); useEffect(() => { const updateCount = () => { - const newCount = document.getElementsByClassName(className).length; - setHasElements(newCount > 0); + setHasElements(document.querySelector(`[${attribute}]`) != null); }; const observer = new MutationObserver(updateCount); @@ -89,7 +90,7 @@ export function useHasElementsByClass(className: string) { childList: true, subtree: true, attributes: true, - attributeFilter: ['class'], + attributeFilter: [attribute], }); updateCount(); @@ -97,16 +98,7 @@ export function useHasElementsByClass(className: string) { return () => { observer.disconnect(); }; - }, [className]); + }, [attribute]); return hasElements; } - -export function BlockPrint() { - return ( -
- ); -} diff --git a/src/components/altinnError.tsx b/src/components/altinnError.tsx index 0b03c33033..6dc925ddc9 100644 --- a/src/components/altinnError.tsx +++ b/src/components/altinnError.tsx @@ -2,6 +2,7 @@ import React from 'react'; import cn from 'classnames'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import classes from 'src/components/altinnError.module.css'; import { Lang } from 'src/features/language/Lang'; import { altinnAppsIllustrationHelpCircleSvgUrl } from 'src/utils/urls/urlHelper'; @@ -29,7 +30,7 @@ export const AltinnError = ({ imageAlt, imageUrl, }: IAltinnErrorProps) => ( -
@@ -75,5 +76,5 @@ export const AltinnError = ({ src={imageUrl || altinnAppsIllustrationHelpCircleSvgUrl} />
-
+ ); diff --git a/src/components/molecules/AltinnContentLoader.test.tsx b/src/components/molecules/AltinnContentLoader.test.tsx deleted file mode 100644 index 62b03fd7ef..0000000000 --- a/src/components/molecules/AltinnContentLoader.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { render as rtlRender, screen } from '@testing-library/react'; - -import { AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader'; - -const render = (props = {}) => { - const allProps = { - ...props, - }; - - rtlRender( - , - ); -}; - -describe('AltinnContentLoader', () => { - it('should show default loader when no children are passed', () => { - render(); - - expect(screen.getByTestId('AltinnContentIcon')).toBeInTheDocument(); - }); - - it('should not show loader when children are passed', () => { - render({ children:
loader
}); - - expect(screen.queryByTestId('AltinnContentIcon')).not.toBeInTheDocument(); - expect(screen.getByTestId('custom-loader')).toBeInTheDocument(); - }); -}); diff --git a/src/components/molecules/AltinnContentLoader.tsx b/src/components/molecules/AltinnContentLoader.tsx deleted file mode 100644 index e7b3cead94..0000000000 --- a/src/components/molecules/AltinnContentLoader.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import ContentLoader from 'react-content-loader'; - -import { AltinnContentIcon } from 'src/components/atoms/AltinnContentIcon'; - -export interface IAltinnContentLoaderProps { - reason: string; - details?: string; - - height?: number | string; - width?: number | string; - children?: React.ReactNode; -} - -export const AltinnContentLoader = ({ - reason, - details, - width = 400, - height = 200, - children, -}: IAltinnContentLoaderProps) => ( -
- - {children ? children : } - -
-); diff --git a/src/components/presentation/BackNavigationButton.tsx b/src/components/presentation/BackNavigationButton.tsx index 48bfddc44f..d9d83cb6e1 100644 --- a/src/components/presentation/BackNavigationButton.tsx +++ b/src/components/presentation/BackNavigationButton.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; import { ArrowLeftIcon } from '@navikt/aksel-icons'; import { skipToken, useQuery } from '@tanstack/react-query'; import cn from 'classnames'; import { Button } from 'src/app-components/Button/Button'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import classes from 'src/components/presentation/BackNavigationButton.module.css'; import { useAppQueries } from 'src/core/contexts/AppQueriesProvider'; import { useIsProcessing } from 'src/core/contexts/processingContext'; diff --git a/src/core/loading/Loader.tsx b/src/core/loading/Loader.tsx index 41d0709eea..a1f6850e4a 100644 --- a/src/core/loading/Loader.tsx +++ b/src/core/loading/Loader.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { AltinnContentIconFormData } from 'src/components/atoms/AltinnContentIconFormData'; -import { AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader'; +import { AltinnContentLoader } from 'src/app-components/loading/AltinnContentLoader/AltinnContentLoader'; import { PresentationComponent, useHasPresentation } from 'src/components/presentation/Presentation'; import { LoadingProvider } from 'src/core/loading/LoadingContext'; import { Lang } from 'src/features/language/Lang'; @@ -37,11 +36,10 @@ export const Loader = (props: LoaderProps) => { const InnerLoader = ({ reason, details }: LoaderProps) => ( - - + /> ); diff --git a/src/core/loading/LoadingContext.tsx b/src/core/loading/LoadingContext.tsx index 899e321d9a..4ca9577054 100644 --- a/src/core/loading/LoadingContext.tsx +++ b/src/core/loading/LoadingContext.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; -import { BlockPrint } from 'src/components/ReadyForPrint'; import { createContext } from 'src/core/contexts/context'; interface Context { @@ -15,12 +14,7 @@ const { Provider, useCtx } = createContext({ }); export function LoadingProvider({ children, ...rest }: PropsWithChildren) { - return ( - <> - - {children} - - ); + return {children}; } export const useIsLoading = () => useCtx() !== undefined; diff --git a/src/core/ui/RenderStart.tsx b/src/core/ui/RenderStart.tsx index b90f5f15db..2cc23b8f8d 100644 --- a/src/core/ui/RenderStart.tsx +++ b/src/core/ui/RenderStart.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import type { PropsWithChildren } from 'react'; -import { loadingClassName, useHasElementsByClass } from 'src/components/ReadyForPrint'; +import { loadingAttribute, useHasElementsByAttribute } from 'src/components/ReadyForPrint'; import { useIsLoading } from 'src/core/loading/LoadingContext'; import { DevTools } from 'src/features/devtools/DevTools'; import { DataModelFetcher } from 'src/features/formData/FormDataReaders'; @@ -32,7 +32,7 @@ export function RenderStart({ children, devTools = true, dataModelFetcher = true function RunNavigationEffect() { const isLoading = useIsLoading(); - const hasLoaders = useHasElementsByClass(loadingClassName); + const hasLoaders = useHasElementsByAttribute(loadingAttribute); const navigationEffect = useNavigationEffect(); const location = useLocation().pathname; diff --git a/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx b/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx index ec5a14a03f..f44df57ae8 100644 --- a/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx +++ b/src/features/devtools/components/VersionSwitcher/VersionSwitcher.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { EXPERIMENTAL_Suggestion as Suggestion, Fieldset, Spinner } from '@digdir/designsystemet-react'; +import { EXPERIMENTAL_Suggestion as Suggestion, Fieldset } from '@digdir/designsystemet-react'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { Button } from 'src/app-components/Button/Button'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import comboboxClasses from 'src/styles/combobox.module.css'; import { optionFilter } from 'src/utils/options'; import { appFrontendCDNPath, appPath, frontendVersionsCDN } from 'src/utils/urls/appUrlHelper'; diff --git a/src/features/language/LanguageProvider.tsx b/src/features/language/LanguageProvider.tsx index bf1542e664..3370759eb9 100644 --- a/src/features/language/LanguageProvider.tsx +++ b/src/features/language/LanguageProvider.tsx @@ -48,6 +48,7 @@ export const LanguageProvider = ({ children }: PropsWithChildren) => { const [languageFromSelector, setWithLanguageSelector] = useLocalStorageState(['selectedLanguage', userId], null); const { data: appLanguages, error, isFetching } = useGetAppLanguageQuery(shouldFetchAppLanguages === true); + // TODO(Error handling): Should failing to fetch app languages cause PDF generation to fail? useEffect(() => { error && window.logError('Fetching app languages failed:\n', error); diff --git a/src/features/navigation/components/Page.tsx b/src/features/navigation/components/Page.tsx index 01162ccda0..6dc9cf16b7 100644 --- a/src/features/navigation/components/Page.tsx +++ b/src/features/navigation/components/Page.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; import { CheckmarkIcon, XMarkIcon } from '@navikt/aksel-icons'; import cn from 'classnames'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; diff --git a/src/features/navigation/components/PageGroup.tsx b/src/features/navigation/components/PageGroup.tsx index c5ff1b09b5..dee1d49edc 100644 --- a/src/features/navigation/components/PageGroup.tsx +++ b/src/features/navigation/components/PageGroup.tsx @@ -1,9 +1,9 @@ import React, { useLayoutEffect, useState } from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; import { CheckmarkIcon, ChevronDownIcon, InformationIcon, XMarkIcon } from '@navikt/aksel-icons'; import cn from 'classnames'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { ContextNotProvided } from 'src/core/contexts/context'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { useGetAltinnTaskType } from 'src/features/instance/useProcessQuery'; diff --git a/src/features/options/useGetOptions.ts b/src/features/options/useGetOptions.ts index c581f2435b..d30f89d8dc 100644 --- a/src/features/options/useGetOptions.ts +++ b/src/features/options/useGetOptions.ts @@ -255,6 +255,7 @@ export function useFilteredAndSortedOptions({ unsorted, valueType, item }: Filte ]); } +// TODO(Error handling): If fetching options fails, we just log and PDF generation will still succeed? export function useGetOptions( baseComponentId: string, valueType: OptionsValueType, diff --git a/src/features/pdf/PdfFromLayout.tsx b/src/features/pdf/PdfFromLayout.tsx index 096b447d3a..b46777a32a 100644 --- a/src/features/pdf/PdfFromLayout.tsx +++ b/src/features/pdf/PdfFromLayout.tsx @@ -5,9 +5,10 @@ import type { PropsWithChildren } from 'react'; import { Heading } from '@digdir/designsystemet-react'; import { Flex } from 'src/app-components/Flex/Flex'; +import { LoadingEmpty } from 'src/app-components/loading/LoadingEmpty/LoadingEmpty'; import { OrganisationLogo } from 'src/components/presentation/OrganisationLogo/OrganisationLogo'; import { DummyPresentation } from 'src/components/presentation/Presentation'; -import { BlockPrint, ReadyForPrint } from 'src/components/ReadyForPrint'; +import { ReadyForPrint } from 'src/components/ReadyForPrint'; import { SearchParams } from 'src/core/routing/types'; import { useAppName, useAppOwner } from 'src/core/texts/appTexts'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; @@ -61,7 +62,7 @@ function AutoGeneratePdfFromLayout() { const { data: pdfSettings, isFetching: pdfFormatIsLoading } = usePdfFormatQuery(true); if (pdfFormatIsLoading) { - return ; + return ; } return ( diff --git a/src/features/process/confirm/containers/Confirm.tsx b/src/features/process/confirm/containers/Confirm.tsx index 057b6dfc95..432da2739f 100644 --- a/src/features/process/confirm/containers/Confirm.tsx +++ b/src/features/process/confirm/containers/Confirm.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { AltinnContentIconReceipt } from 'src/components/atoms/AltinnContentIconReceipt'; -import { AltinnContentLoader } from 'src/components/molecules/AltinnContentLoader'; +import { AltinnContentLoader } from 'src/app-components/loading/AltinnContentLoader/AltinnContentLoader'; import { useAppName } from 'src/core/texts/appTexts'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { useInstanceDataQuery } from 'src/features/instance/InstanceContext'; @@ -22,12 +21,11 @@ export const Confirm = () => {
{missingRequirement ? ( - - + /> ) : ( { if (requirementMissing || !(instanceMetaObject && pdfDisplayAttachments)) { return ( - - + /> ); } diff --git a/src/layout/GenericComponent.tsx b/src/layout/GenericComponent.tsx index de0806d5de..6fffa87ba2 100644 --- a/src/layout/GenericComponent.tsx +++ b/src/layout/GenericComponent.tsx @@ -4,6 +4,8 @@ import type { SetURLSearchParams } from 'react-router-dom'; import classNames from 'classnames'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; +import { FatalErrorEmpty } from 'src/app-components/error/FatalErrorEmpty/FatalErrorEmpty'; import { Flex } from 'src/app-components/Flex/Flex'; import { SearchParams } from 'src/core/routing/types'; import { useIsNavigating } from 'src/core/routing/useIsNavigating'; @@ -190,11 +192,11 @@ const gridToClasses = (labelGrid: IGridStyling | undefined, classes: { [key: str export function ComponentErrorList({ baseComponentId, errors }: { baseComponentId: string; errors: string[] }) { if (!isDev()) { - return null; + return ; } return ( -
+

-

+ ); } diff --git a/src/layout/ImageUpload/ImageCanvas/ImagePreview.tsx b/src/layout/ImageUpload/ImageCanvas/ImagePreview.tsx index 0686868d44..5aab6fb930 100644 --- a/src/layout/ImageUpload/ImageCanvas/ImagePreview.tsx +++ b/src/layout/ImageUpload/ImageCanvas/ImagePreview.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; - +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { useLanguage } from 'src/features/language/useLanguage'; import classes from 'src/layout/ImageUpload/ImageCanvas/ImagePreview.module.css'; import type { UploadedAttachment } from 'src/features/attachments'; diff --git a/src/layout/NavigationBar/NavigationBarComponent.tsx b/src/layout/NavigationBar/NavigationBarComponent.tsx index 3b7b89c3cc..0cf3b62947 100644 --- a/src/layout/NavigationBar/NavigationBarComponent.tsx +++ b/src/layout/NavigationBar/NavigationBarComponent.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; import { CaretDownFillIcon } from '@navikt/aksel-icons'; import cn from 'classnames'; import { Flex } from 'src/app-components/Flex/Flex'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; import { Lang } from 'src/features/language/Lang'; diff --git a/src/layout/Option/OptionComponent.tsx b/src/layout/Option/OptionComponent.tsx index cf9bdb6dbe..f72bc51166 100644 --- a/src/layout/Option/OptionComponent.tsx +++ b/src/layout/Option/OptionComponent.tsx @@ -3,6 +3,7 @@ import React from 'react'; import cn from 'classnames'; import { HelpText } from 'src/app-components/HelpText/HelpText'; +import { LoadingEmpty } from 'src/app-components/loading/LoadingEmpty/LoadingEmpty'; import { getLabelId } from 'src/components/label/Label'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -57,8 +58,9 @@ function Text({ baseComponentId, usingLabel }: TextProps) { const { langAsString } = useLanguage(); const selectedOption = options.find((option) => option.value === value); const indexedId = useIndexedId(baseComponentId); + if (isFetching) { - return null; + return ; } return ( diff --git a/src/layout/SigneeList/SigneeListError.tsx b/src/layout/SigneeList/SigneeListError.tsx index ec5e803381..97dd1cf85f 100644 --- a/src/layout/SigneeList/SigneeListError.tsx +++ b/src/layout/SigneeList/SigneeListError.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { isAxiosError } from 'axios'; import { z, ZodError } from 'zod'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -22,7 +23,7 @@ export function SigneeListError({ error }: { error: Error }) { ); return ( -
+
-
+ ); } @@ -42,9 +43,17 @@ export function SigneeListError({ error }: { error: Error }) { if (parsed.success) { window.logErrorOnce(langAsString(error.message)); - return ; + return ( + + + + ); } } - return ; + return ( + + + + ); } diff --git a/src/layout/SigneeList/SigneeListSummary.tsx b/src/layout/SigneeList/SigneeListSummary.tsx index 11be17d5d2..1e4baabf2f 100644 --- a/src/layout/SigneeList/SigneeListSummary.tsx +++ b/src/layout/SigneeList/SigneeListSummary.tsx @@ -6,7 +6,9 @@ import { Divider, Paragraph } from '@digdir/designsystemet-react'; import { format } from 'date-fns'; import { nb } from 'date-fns/locale/nb'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import { Label } from 'src/app-components/Label/Label'; +import { LoadingWrapper } from 'src/app-components/loading/LoadingWrapper/LoadingWrapper'; import { Lang } from 'src/features/language/Lang'; import { type SigneeState, useSigneeList } from 'src/layout/SigneeList/api'; import classes from 'src/layout/SigneeList/SigneeListSummary.module.css'; @@ -31,29 +33,33 @@ export function SigneeListSummary({ targetBaseComponentId, titleOverride }: Sign if (isLoading) { return ( - - - - - + + + + + + + ); } if (error) { return ( - - - - - + + + + + + + ); } diff --git a/src/layout/SigningActions/PanelAwaitingCurrentUserSignature.tsx b/src/layout/SigningActions/PanelAwaitingCurrentUserSignature.tsx index 9cf880fc03..616d5cfd70 100644 --- a/src/layout/SigningActions/PanelAwaitingCurrentUserSignature.tsx +++ b/src/layout/SigningActions/PanelAwaitingCurrentUserSignature.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { Checkbox, Heading, Spinner, ValidationMessage } from '@digdir/designsystemet-react'; +import { Checkbox, Heading, ValidationMessage } from '@digdir/designsystemet-react'; import { Button } from 'src/app-components/Button/Button'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { Panel } from 'src/app-components/Panel/Panel'; import { useIsAuthorized } from 'src/features/instance/useProcessQuery'; import { UnknownError } from 'src/features/instantiate/containers/UnknownError'; diff --git a/src/layout/SigningActions/SigningActionsComponent.tsx b/src/layout/SigningActions/SigningActionsComponent.tsx index 2e51c3b22b..158e426bcd 100644 --- a/src/layout/SigningActions/SigningActionsComponent.tsx +++ b/src/layout/SigningActions/SigningActionsComponent.tsx @@ -1,8 +1,8 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; -import { Spinner } from '@digdir/designsystemet-react'; - +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { Panel } from 'src/app-components/Panel/Panel'; import { useIsAuthorized } from 'src/features/instance/useProcessQuery'; import { Lang } from 'src/features/language/Lang'; @@ -58,12 +58,14 @@ export function SigningActionsComponent({ baseComponentId }: PropsFromGenericCom if (signeeListError) { return ( - } - description={} - variant='error' - /> + + } + description={} + variant='error' + /> + ); } diff --git a/src/layout/SigningDocumentList/SigningDocumentListError.tsx b/src/layout/SigningDocumentList/SigningDocumentListError.tsx index 6c2d7c9d53..b7b99e91cd 100644 --- a/src/layout/SigningDocumentList/SigningDocumentListError.tsx +++ b/src/layout/SigningDocumentList/SigningDocumentListError.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { isAxiosError } from 'axios'; import { ZodError } from 'zod'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { problemDetailsSchema } from 'src/layout/SigneeList/SigneeListError'; @@ -17,7 +18,7 @@ export function SigningDocumentListError({ error }: { error: Error }) { ); return ( -
+
-
+ ); } @@ -38,9 +39,17 @@ export function SigningDocumentListError({ error }: { error: Error }) { if (parsed.success) { window.logErrorOnce(langAsString(error.message)); window.logErrorOnce(parsed); - return ; + return ( + + + + ); } } - return ; + return ( + + + + ); } diff --git a/src/layout/Subform/SubformComponent.tsx b/src/layout/Subform/SubformComponent.tsx index 51ee60494a..b3ebbeb2b6 100644 --- a/src/layout/Subform/SubformComponent.tsx +++ b/src/layout/Subform/SubformComponent.tsx @@ -1,12 +1,14 @@ import React from 'react'; import { useNavigation } from 'react-router-dom'; -import { Spinner, Table } from '@digdir/designsystemet-react'; +import { Table } from '@digdir/designsystemet-react'; import { PencilIcon, PlusIcon, TrashIcon } from '@navikt/aksel-icons'; import cn from 'classnames'; import { Button } from 'src/app-components/Button/Button'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import { Flex } from 'src/app-components/Flex/Flex'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { Caption } from 'src/components/form/caption/Caption'; import { useIsProcessing } from 'src/core/contexts/processingContext'; import { useDataTypeFromLayoutSet } from 'src/features/form/layout/LayoutsContext'; @@ -237,7 +239,9 @@ function SubformTableRow({ return ( - + + + ); diff --git a/src/layout/Subform/Summary/SubformSummaryComponent.tsx b/src/layout/Subform/Summary/SubformSummaryComponent.tsx index a492476b35..f137d33cab 100644 --- a/src/layout/Subform/Summary/SubformSummaryComponent.tsx +++ b/src/layout/Subform/Summary/SubformSummaryComponent.tsx @@ -1,8 +1,8 @@ import React from 'react'; import type { ReactNode } from 'react'; -import { Spinner } from '@digdir/designsystemet-react'; - +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { useDataTypeFromLayoutSet } from 'src/features/form/layout/LayoutsContext'; import { useInstanceDataElements } from 'src/features/instance/InstanceContext'; import { Lang } from 'src/features/language/Lang'; @@ -58,7 +58,11 @@ function SubformSummaryRow({ dataElement, baseComponentId }: { dataElement: IDat /> ); } else if (subformDataError) { - return ; + return ( + + + + ); } const content: (ReactNode | string)[] = tableColumns.map((entry, i) => ( diff --git a/src/layout/Subform/Summary/SubformSummaryComponent2.tsx b/src/layout/Subform/Summary/SubformSummaryComponent2.tsx index f8b418c98d..078991c937 100644 --- a/src/layout/Subform/Summary/SubformSummaryComponent2.tsx +++ b/src/layout/Subform/Summary/SubformSummaryComponent2.tsx @@ -4,8 +4,9 @@ import { Heading, Paragraph } from '@digdir/designsystemet-react'; import { Flex } from 'src/app-components/Flex/Flex'; import { Label, LabelInner } from 'src/components/label/Label'; -import { BlockPrint } from 'src/components/ReadyForPrint'; import { TaskOverrides } from 'src/core/contexts/TaskOverrides'; +import { DisplayError } from 'src/core/errorHandling/DisplayError'; +import { Loader } from 'src/core/loading/Loader'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { FormProvider } from 'src/features/form/FormContext'; import { useDataTypeFromLayoutSet, useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; @@ -93,7 +94,11 @@ const DoSummaryWrapper = ({ const subformDataSources = useExpressionDataSourcesForSubform(dataElement.dataType, subformData, entryDisplayName); if (isSubformDataFetching) { - return ; + return ; + } + + if (subformDataError) { + return ; } const subformEntryName = diff --git a/src/layout/Subform/Summary/SubformSummaryTable.tsx b/src/layout/Subform/Summary/SubformSummaryTable.tsx index eead09dc39..eae5716864 100644 --- a/src/layout/Subform/Summary/SubformSummaryTable.tsx +++ b/src/layout/Subform/Summary/SubformSummaryTable.tsx @@ -1,10 +1,12 @@ import React from 'react'; import { useNavigate, useNavigation } from 'react-router-dom'; -import { Paragraph, Spinner, Table } from '@digdir/designsystemet-react'; +import { Paragraph, Table } from '@digdir/designsystemet-react'; import classNames from 'classnames'; +import { FatalError } from 'src/app-components/error/FatalError/FatalError'; import { Flex } from 'src/app-components/Flex/Flex'; +import { Spinner } from 'src/app-components/loading/Spinner/Spinner'; import { Caption } from 'src/components/form/caption/Caption'; import { Label } from 'src/components/label/Label'; import { useDataTypeFromLayoutSet, useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; @@ -64,7 +66,9 @@ function SubformTableRow({ return ( - + + + ); diff --git a/test/e2e/integration/subform-test/pdf.ts b/test/e2e/integration/subform-test/pdf.ts index c78c8e8da6..8f7c602476 100644 --- a/test/e2e/integration/subform-test/pdf.ts +++ b/test/e2e/integration/subform-test/pdf.ts @@ -1,7 +1,9 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; +import { getInstanceIdRegExp } from 'src/utils/instanceIdRegExp'; import type { ILayoutSettings } from 'src/layout/common.generated'; import type { ILayoutCollection } from 'src/layout/layout'; +import type { IInstance } from 'src/types/shared'; const appFrontend = new AppFrontend(); @@ -100,6 +102,58 @@ describe('Subform test', () => { }); }); + it('should not show "#readyForPrint" if one subform fails', { retries: 0 }, () => { + fillTwoSubforms(); + + // Wait for page to load + cy.get('#finishedLoading').should('exist'); + cy.waitForNetworkIdle(500); + + // Intercept instance and capture data element id + const data = { dataElementIdToBlock: '' }; + cy.intercept({ method: 'GET', url: '**/instances/*/*', times: 1 }, (req) => { + req.on('response', (res) => { + const instance: IInstance = res.body; + const dataElementToBlock = instance.data.find((data) => data.dataType === 'moped'); + if (!dataElementToBlock) { + throw 'Could not find data element to block'; + } + data.dataElementIdToBlock = dataElementToBlock.id; + }); + }); + + // Block subform data element to provoke unknown error, intercept 3 times, 1 (main data) + 2 (subform data) + cy.intercept({ method: 'GET', url: '**/data/**includeRowId=true*', times: 3 }, (req) => { + if (req.url.includes(data.dataElementIdToBlock)) { + req.reply({ statusCode: 404, body: 'Not Found' }); + } + }); + + // Visit the PDF page and reload + cy.location('href').then((href) => { + const regex = getInstanceIdRegExp(); + const instanceId = regex.exec(href)?.[1]; + const before = href.split(regex)[0]; + const visitUrl = `${before}${instanceId}?pdf=1`; + cy.visit(visitUrl); + }); + cy.reload(); + + // Wait for page to load + cy.get('#finishedLoading').should('exist'); + cy.waitForNetworkIdle(500); + + // Check that we have an error and that #readyForPrint is not present + cy.findAllByRole('heading', { name: 'Ukjent feil' }).should('exist'); + cy.get('#readyForPrint').should('not.exist'); + + // To confirm we are on the PDF page, reload (which should now succeed) and check that #readyForPrint is visible + cy.reload(); + cy.waitForNetworkIdle(500); + cy.get('#readyForPrint').should('exist'); + cy.findByRole('heading', { name: 'Ukjent feil' }).should('not.exist'); + }); + it('should render PDF with summary2 layoutset with subform and subform table', { retries: 0 }, () => { const pdfLayoutName = 'CustomPDF'; cy.intercept('GET', '**/layoutsettings/**', (req) =>