diff --git a/src/App.tsx b/src/App.tsx index 29a2b07b6d..7c2122d2ea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,7 @@ import { FormProvider } from 'src/features/form/FormContext'; import { InstanceProvider } from 'src/features/instance/InstanceContext'; import { PartySelection } from 'src/features/instantiate/containers/PartySelection'; import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/InstanceSelection'; -import { PDFWrapper } from 'src/features/pdf/PDFWrapper'; +import { PdfWrapper } from 'src/features/pdf/PdfWrapper'; import { FixWrongReceiptType } from 'src/features/receipt/FixWrongReceiptType'; import { DefaultReceipt } from 'src/features/receipt/ReceiptContainer'; import { TaskKeys } from 'src/hooks/useNavigatePage'; @@ -83,11 +83,11 @@ export const App = () => ( +
- + } /> diff --git a/src/components/presentation/Presentation.tsx b/src/components/presentation/Presentation.tsx index debfae8091..07354b552a 100644 --- a/src/components/presentation/Presentation.tsx +++ b/src/components/presentation/Presentation.tsx @@ -121,5 +121,9 @@ export const useHasPresentation = () => useHasProvider(); * for loaders, this can be used to prevent the loader from creating a presentation. */ export function DummyPresentation({ children }: PropsWithChildren) { - return {children}; + return ( + + {children} + + ); } diff --git a/src/components/wrappers/ProcessWrapper.tsx b/src/components/wrappers/ProcessWrapper.tsx index c2c3ea228c..e27bb809f1 100644 --- a/src/components/wrappers/ProcessWrapper.tsx +++ b/src/components/wrappers/ProcessWrapper.tsx @@ -17,8 +17,10 @@ import { getProcessNextMutationKey, getTargetTaskFromProcess } from 'src/feature import { useGetTaskTypeById, useProcessQuery } from 'src/features/instance/useProcessQuery'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { Confirm } from 'src/features/processEnd/confirm/containers/Confirm'; -import { Feedback } from 'src/features/processEnd/feedback/Feedback'; +import { PdfWrapper } from 'src/features/pdf/PdfWrapper'; +import { Confirm } from 'src/features/process/confirm/containers/Confirm'; +import { Feedback } from 'src/features/process/feedback/Feedback'; +import { ServiceTask } from 'src/features/process/service/ServiceTask'; import { useNavigationParam } from 'src/hooks/navigation'; import { TaskKeys, useIsValidTaskId, useNavigateToTask, useStartUrl } from 'src/hooks/useNavigatePage'; import { useWaitForQueries } from 'src/hooks/useWaitForQueries'; @@ -143,6 +145,16 @@ export function ProcessWrapper({ children }: PropsWithChildren) { ); } + if (taskType === ProcessTaskType.Service) { + return ( + + + + + + ); + } + if (taskType === ProcessTaskType.Data) { return children; } diff --git a/src/core/routing/types.ts b/src/core/routing/types.ts index 9ae686519e..3819be44e0 100644 --- a/src/core/routing/types.ts +++ b/src/core/routing/types.ts @@ -4,5 +4,6 @@ export enum SearchParams { ExitSubform = 'exitSubform', Validate = 'validate', Pdf = 'pdf', + PdfForTask = 'task', BackToPage = 'backToPage', } diff --git a/src/features/instance/useProcessQuery.ts b/src/features/instance/useProcessQuery.ts index b31a47c792..48fef670f9 100644 --- a/src/features/instance/useProcessQuery.ts +++ b/src/features/instance/useProcessQuery.ts @@ -79,6 +79,10 @@ export function useGetTaskTypeById() { return ProcessTaskType.Archived; } + if (task?.elementType === 'ServiceTask') { + return ProcessTaskType.Service; + } + const altinnTaskType = task?.altinnTaskType; if (altinnTaskType && isProcessTaskType(altinnTaskType)) { return altinnTaskType; diff --git a/src/features/pdf/PDFView.module.css b/src/features/pdf/PDFView.module.css index ddc492d3c3..1e7a12ce7f 100644 --- a/src/features/pdf/PDFView.module.css +++ b/src/features/pdf/PDFView.module.css @@ -29,10 +29,19 @@ margin-bottom: var(--ds-size-12); } +.pageBreak { + height: 0; + width: 0; +} + @media print { .hideInPrint { display: none; } + + .pageBreak { + page-break-before: always; + } } @media screen { diff --git a/src/features/pdf/PDFWrapper.test.tsx b/src/features/pdf/PDFWrapper.test.tsx index d45a55683d..6f396080cb 100644 --- a/src/features/pdf/PDFWrapper.test.tsx +++ b/src/features/pdf/PDFWrapper.test.tsx @@ -11,7 +11,7 @@ import { getProcessDataMock } from 'src/__mocks__/getProcessDataMock'; import { PresentationComponent } from 'src/components/presentation/Presentation'; import { FormProvider } from 'src/features/form/FormContext'; import { InstanceProvider } from 'src/features/instance/InstanceContext'; -import { PDFWrapper } from 'src/features/pdf/PDFWrapper'; +import { PdfWrapper } from 'src/features/pdf/PdfWrapper'; import { fetchApplicationMetadata, fetchInstanceData, fetchProcessState } from 'src/queries/queries'; import { InstanceRouter, renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; import type { AppQueries } from 'src/queries/types'; @@ -52,11 +52,11 @@ const render = async (renderAs: RenderAs, queriesOverride?: Partial) renderer: () => ( - + - + ), diff --git a/src/features/pdf/PdfView2.tsx b/src/features/pdf/PdfFromLayout.tsx similarity index 67% rename from src/features/pdf/PdfView2.tsx rename to src/features/pdf/PdfFromLayout.tsx index 90f887958e..096b447d3a 100644 --- a/src/features/pdf/PdfView2.tsx +++ b/src/features/pdf/PdfFromLayout.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; import type { PropsWithChildren } from 'react'; import { Heading } from '@digdir/designsystemet-react'; @@ -7,6 +8,7 @@ import { Flex } from 'src/app-components/Flex/Flex'; import { OrganisationLogo } from 'src/components/presentation/OrganisationLogo/OrganisationLogo'; import { DummyPresentation } from 'src/components/presentation/Presentation'; import { BlockPrint, 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'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; @@ -24,30 +26,77 @@ import { AllSubformSummaryComponent2 } from 'src/layout/Subform/Summary/SubformS import { SummaryComponentFor } from 'src/layout/Summary/SummaryComponent'; import { ComponentSummary } from 'src/layout/Summary2/SummaryComponent2/ComponentSummary'; import { SummaryComponent2 } from 'src/layout/Summary2/SummaryComponent2/SummaryComponent2'; +import { TaskSummaryWrapper } from 'src/layout/Summary2/SummaryComponent2/TaskSummaryWrapper'; import { useIsHiddenMulti } from 'src/utils/layout/hidden'; import { useExternalItem } from 'src/utils/layout/hooks'; import { NodesInternal } from 'src/utils/layout/NodesContext'; import { useItemIfType } from 'src/utils/layout/useNodeItem'; import type { IPdfFormat } from 'src/features/pdf/types'; -export const PDFView2 = () => { - const order = usePageOrder(); - const { data: pdfSettings, isFetching: pdfFormatIsLoading } = usePdfFormatQuery(true); +export function PdfFromLayout() { const pdfLayoutName = usePdfLayoutName(); + if (pdfLayoutName) { + return ( + + + + ); + } + + return ; +} + +function AutoGeneratePdfFromLayout() { + const [params] = useSearchParams(); + const taskIds = params.getAll(SearchParams.PdfForTask); + if (taskIds.length > 0) { + throw new Error( + `Unexpected search param ${SearchParams.PdfForTask} provided. This mode does not support passing ` + + `${SearchParams.PdfForTask} as a search param, but will auto-generate a PDF from the ` + + `current layout-set instead. To use the multi-task mode, you cannot have a layout-set ` + + `set up for the current task.`, + ); + } + + const { data: pdfSettings, isFetching: pdfFormatIsLoading } = usePdfFormatQuery(true); if (pdfFormatIsLoading) { return ; } - if (pdfLayoutName) { - // Render all components directly if given a separate PDF layout - return ( + return ( + - +
+ +
+ +
+
+ ); +} + +export function PdfForServiceTask() { + const [params] = useSearchParams(); + const taskIds = params.getAll(SearchParams.PdfForTask); + if (taskIds.length === 0) { + throw new Error( + `No task ids provided (this mode requires passing one or multiple ${SearchParams.PdfForTask} as a search param)`, ); } + return ; +} + +function AutoGeneratePdfFromTasks({ taskIds }: { taskIds: string[] }) { return ( @@ -61,20 +110,22 @@ export const PDFView2 = () => { }} /> - {order - .filter((pageKey) => !pdfSettings?.excludedPages.includes(pageKey)) - .map((pageKey) => ( - - ))} - + {taskIds.map((taskId, idx) => ( + + {idx > 0 &&
} + {/* Settings intentionally omitted, as this is new functionality + and PDF settings are deprecated at this point. */} + + + + ))} ); -}; +} function PdfWrapping({ children }: PropsWithChildren) { const orgLogoEnabled = Boolean(useApplicationMetadata().logoOptions); @@ -139,20 +190,32 @@ function PlainPage({ pageKey }: { pageKey: string }) { ); } +function AllPages({ pdfSettings }: { pdfSettings: IPdfFormat | undefined }) { + const order = usePageOrder(); + const visiblePages = getPdfVisiblePages(order, pdfSettings); + + return ( + <> + {visiblePages.map((pageKey) => ( + + ))} + + ); +} + +function getPdfVisiblePages(pages: string[], pdfSettings: IPdfFormat | undefined): string[] { + if (!pdfSettings?.excludedPages) { + return pages; + } + return pages.filter((pageKey) => !pdfSettings.excludedPages.includes(pageKey)); +} + function PdfForPage({ pageKey, pdfSettings }: { pageKey: string; pdfSettings: IPdfFormat | undefined }) { - const lookups = useLayoutLookups(); - const children = useMemo(() => { - const topLevel = lookups.topLevelComponents[pageKey] ?? []; - return topLevel.filter((baseId) => { - const component = lookups.getComponent(baseId); - const def = getComponentDef(component.type); - return ( - component.type !== 'Subform' && - !pdfSettings?.excludedComponents.includes(baseId) && - def.shouldRenderInAutomaticPDF(component as never) - ); - }); - }, [lookups, pageKey, pdfSettings?.excludedComponents]); + const children = useTopLevelComponentsToAutoRender(pageKey, pdfSettings); const hidden = useIsHiddenMulti(children); return ( @@ -179,6 +242,22 @@ function PdfForPage({ pageKey, pdfSettings }: { pageKey: string; pdfSettings: IP ); } +function useTopLevelComponentsToAutoRender(pageKey: string, pdfSettings: IPdfFormat | undefined): string[] { + const lookups = useLayoutLookups(); + return useMemo(() => { + const topLevel = lookups.topLevelComponents[pageKey] ?? []; + return topLevel.filter((baseId) => { + const component = lookups.getComponent(baseId); + const def = getComponentDef(component.type); + return ( + component.type !== 'Subform' && + !pdfSettings?.excludedComponents.includes(baseId) && + def.shouldRenderInAutomaticPDF(component as never) + ); + }); + }, [lookups, pageKey, pdfSettings?.excludedComponents]); +} + function PdfForNode({ baseComponentId }: { baseComponentId: string }) { const component = useExternalItem(baseComponentId); const item = useItemIfType(baseComponentId, 'Summary2'); diff --git a/src/features/pdf/PDFWrapper.tsx b/src/features/pdf/PdfWrapper.tsx similarity index 72% rename from src/features/pdf/PDFWrapper.tsx rename to src/features/pdf/PdfWrapper.tsx index c34d43acfc..38930b65ea 100644 --- a/src/features/pdf/PDFWrapper.tsx +++ b/src/features/pdf/PdfWrapper.tsx @@ -4,9 +4,12 @@ import type { PropsWithChildren } from 'react'; import cn from 'classnames'; import { useDevToolsStore } from 'src/features/devtools/data/DevToolsStore'; +import { useGetTaskTypeById } from 'src/features/instance/useProcessQuery'; +import { PdfForServiceTask, PdfFromLayout } from 'src/features/pdf/PdfFromLayout'; import classes from 'src/features/pdf/PDFView.module.css'; -import { PDFView2 } from 'src/features/pdf/PdfView2'; +import { useNavigationParam } from 'src/hooks/navigation'; import { useIsPdf } from 'src/hooks/useIsPdf'; +import { ProcessTaskType } from 'src/types'; export const usePdfModeActive = (): boolean => { const previewPDF = useDevToolsStore((state) => state.pdfPreview); @@ -14,11 +17,14 @@ export const usePdfModeActive = (): boolean => { return pdfIsSetInUrl || previewPDF; }; -export function PDFWrapper({ children }: PropsWithChildren) { +export function PdfWrapper({ children }: PropsWithChildren) { const previewPDF = useDevToolsStore((state) => state.pdfPreview); const setPdfPreview = useDevToolsStore((state) => state.actions.setPdfPreview); const renderInstead = useIsPdf(); + const taskId = useNavigationParam('taskId'); + const taskType = useGetTaskTypeById()(taskId); + useEffect(() => { if (previewPDF) { waitForPrint().then((success) => { @@ -32,8 +38,10 @@ export function PDFWrapper({ children }: PropsWithChildren) { } }, [previewPDF, setPdfPreview]); + const PdfComponent = taskType === ProcessTaskType.Service ? PdfForServiceTask : PdfFromLayout; + if (renderInstead) { - return ; + return ; } return ( @@ -42,7 +50,7 @@ export function PDFWrapper({ children }: PropsWithChildren) { {previewPDF && (
- +
)} diff --git a/src/features/processEnd/confirm/containers/Confirm.test.tsx b/src/features/process/confirm/containers/Confirm.test.tsx similarity index 94% rename from src/features/processEnd/confirm/containers/Confirm.test.tsx rename to src/features/process/confirm/containers/Confirm.test.tsx index 8826bd3f32..da0568d830 100644 --- a/src/features/processEnd/confirm/containers/Confirm.test.tsx +++ b/src/features/process/confirm/containers/Confirm.test.tsx @@ -4,7 +4,7 @@ import { screen } from '@testing-library/react'; import { getInstanceDataMock } from 'src/__mocks__/getInstanceDataMock'; import { getPartyWithSubunitMock } from 'src/__mocks__/getPartyMock'; -import { Confirm } from 'src/features/processEnd/confirm/containers/Confirm'; +import { Confirm } from 'src/features/process/confirm/containers/Confirm'; import { fetchInstanceData } from 'src/queries/queries'; import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; diff --git a/src/features/processEnd/confirm/containers/Confirm.tsx b/src/features/process/confirm/containers/Confirm.tsx similarity index 94% rename from src/features/processEnd/confirm/containers/Confirm.tsx rename to src/features/process/confirm/containers/Confirm.tsx index 71d160cde6..057b6dfc95 100644 --- a/src/features/processEnd/confirm/containers/Confirm.tsx +++ b/src/features/process/confirm/containers/Confirm.tsx @@ -6,7 +6,7 @@ import { useAppName } from 'src/core/texts/appTexts'; import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { useInstanceDataQuery } from 'src/features/instance/InstanceContext'; import { useInstanceOwnerParty, usePartiesAllowedToInstantiate } from 'src/features/party/PartiesProvider'; -import { ConfirmPage } from 'src/features/processEnd/confirm/containers/ConfirmPage'; +import { ConfirmPage } from 'src/features/process/confirm/containers/ConfirmPage'; export const Confirm = () => { const instance = useInstanceDataQuery().data; diff --git a/src/features/processEnd/confirm/containers/ConfirmPage.test.tsx b/src/features/process/confirm/containers/ConfirmPage.test.tsx similarity index 98% rename from src/features/processEnd/confirm/containers/ConfirmPage.test.tsx rename to src/features/process/confirm/containers/ConfirmPage.test.tsx index 017f3e33e7..9d7f58140c 100644 --- a/src/features/processEnd/confirm/containers/ConfirmPage.test.tsx +++ b/src/features/process/confirm/containers/ConfirmPage.test.tsx @@ -8,7 +8,7 @@ import { AxiosResponse } from 'axios'; import { getApplicationMetadataMock } from 'src/__mocks__/getApplicationMetadataMock'; import { getInstanceDataMock } from 'src/__mocks__/getInstanceDataMock'; import { getPartyMock, getPartyWithSubunitMock } from 'src/__mocks__/getPartyMock'; -import { ConfirmPage, type IConfirmPageProps } from 'src/features/processEnd/confirm/containers/ConfirmPage'; +import { ConfirmPage, type IConfirmPageProps } from 'src/features/process/confirm/containers/ConfirmPage'; import { doProcessNext } from 'src/queries/queries'; import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; import { IProcess } from 'src/types/shared'; diff --git a/src/features/processEnd/confirm/containers/ConfirmPage.tsx b/src/features/process/confirm/containers/ConfirmPage.tsx similarity index 98% rename from src/features/processEnd/confirm/containers/ConfirmPage.tsx rename to src/features/process/confirm/containers/ConfirmPage.tsx index 51d0bd40e6..2842733c1b 100644 --- a/src/features/processEnd/confirm/containers/ConfirmPage.tsx +++ b/src/features/process/confirm/containers/ConfirmPage.tsx @@ -8,7 +8,7 @@ import { useProcessNext } from 'src/features/instance/useProcessNext'; import { useIsAuthorized } from 'src/features/instance/useProcessQuery'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { returnConfirmSummaryObject } from 'src/features/processEnd/confirm/helpers/returnConfirmSummaryObject'; +import { returnConfirmSummaryObject } from 'src/features/process/confirm/helpers/returnConfirmSummaryObject'; import { filterOutDataModelRefDataAsPdfAndAppOwnedDataTypes, getAttachmentsWithDataType, diff --git a/src/features/processEnd/confirm/helpers/returnConfirmSummaryObject.test.ts b/src/features/process/confirm/helpers/returnConfirmSummaryObject.test.ts similarity index 98% rename from src/features/processEnd/confirm/helpers/returnConfirmSummaryObject.test.ts rename to src/features/process/confirm/helpers/returnConfirmSummaryObject.test.ts index 83b412b5c9..8d9b071988 100644 --- a/src/features/processEnd/confirm/helpers/returnConfirmSummaryObject.test.ts +++ b/src/features/process/confirm/helpers/returnConfirmSummaryObject.test.ts @@ -1,5 +1,5 @@ import { staticUseLanguageForTests } from 'src/features/language/useLanguage'; -import { returnConfirmSummaryObject } from 'src/features/processEnd/confirm/helpers/returnConfirmSummaryObject'; +import { returnConfirmSummaryObject } from 'src/features/process/confirm/helpers/returnConfirmSummaryObject'; import type { IParty } from 'src/types/shared'; const langTools = staticUseLanguageForTests({ language: {} }); diff --git a/src/features/processEnd/confirm/helpers/returnConfirmSummaryObject.ts b/src/features/process/confirm/helpers/returnConfirmSummaryObject.ts similarity index 100% rename from src/features/processEnd/confirm/helpers/returnConfirmSummaryObject.ts rename to src/features/process/confirm/helpers/returnConfirmSummaryObject.ts diff --git a/src/features/processEnd/feedback/Feedback.tsx b/src/features/process/feedback/Feedback.tsx similarity index 100% rename from src/features/processEnd/feedback/Feedback.tsx rename to src/features/process/feedback/Feedback.tsx diff --git a/src/features/process/service/ServiceTask.module.css b/src/features/process/service/ServiceTask.module.css new file mode 100644 index 0000000000..ec5d2ba4d5 --- /dev/null +++ b/src/features/process/service/ServiceTask.module.css @@ -0,0 +1,5 @@ +.buttons { + margin-top: var(--button-margin-top); + display: flex; + gap: 1rem; +} diff --git a/src/features/process/service/ServiceTask.tsx b/src/features/process/service/ServiceTask.tsx new file mode 100644 index 0000000000..4c2bf5440b --- /dev/null +++ b/src/features/process/service/ServiceTask.tsx @@ -0,0 +1,93 @@ +import React from 'react'; + +import { Heading, Paragraph } from '@digdir/designsystemet-react'; + +import { Button } from 'src/app-components/Button/Button'; +import { ReadyForPrint } from 'src/components/ReadyForPrint'; +import { useAppOwner } from 'src/core/texts/appTexts'; +import { useProcessNext } from 'src/features/instance/useProcessNext'; +import { useIsAuthorized } from 'src/features/instance/useProcessQuery'; +import { Lang } from 'src/features/language/Lang'; +import { useLanguage } from 'src/features/language/useLanguage'; +import classes from 'src/features/process/service/ServiceTask.module.css'; +import { getPageTitle } from 'src/utils/getPageTitle'; + +export function ServiceTask() { + const langTools = useLanguage(); + const appOwner = useAppOwner(); + + return ( + <> + {`${getPageTitle('', langTools.langAsString('service_task.title'), appOwner)}`} +
+ + + + + + + + , + , + ]} + /> + +
+ + +
+
+ + + ); +} + +const RetryButton = () => { + const canRetry = useIsAuthorized()('write'); + const { mutateAsync: processRetry, isPending: isRetrying } = useProcessNext(); + + return ( + + ); +}; + +const BackButton = () => { + const canReject = useIsAuthorized()('reject'); + const { mutateAsync: processReject, isPending: isRejecting } = useProcessNext({ action: 'reject' }); + + if (!canReject) { + return null; + } + + return ( + + ); +}; diff --git a/src/features/receipt/ReceiptContainer.tsx b/src/features/receipt/ReceiptContainer.tsx index e65dec4c1e..4b77c6fc80 100644 --- a/src/features/receipt/ReceiptContainer.tsx +++ b/src/features/receipt/ReceiptContainer.tsx @@ -15,7 +15,7 @@ import { useInstanceDataQuery } from 'src/features/instance/InstanceContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useInstanceOwnerParty } from 'src/features/party/PartiesProvider'; -import { getInstanceSender } from 'src/features/processEnd/confirm/helpers/returnConfirmSummaryObject'; +import { getInstanceSender } from 'src/features/process/confirm/helpers/returnConfirmSummaryObject'; import { FixWrongReceiptType } from 'src/features/receipt/FixWrongReceiptType'; import { useNavigationParam } from 'src/hooks/navigation'; import { diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index 92663fc4ed..0457dd3706 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -505,5 +505,11 @@ export function en() { 'signing_document_list.attachment_type_form': 'Form', 'signing_document_list.download': 'Download', 'signing_document_list_summary.header': 'Signed documents', + 'service_task.title': 'Something went wrong', + 'service_task.body': 'An error occurred during automatic processing of the form.', + 'service_task.help_text': + 'You can try to process again by clicking "{0}". If the problem persists, contact customer service at {1}.', + 'service_task.retry_button': 'Try again', + 'service_task.back_button': 'Go back', }; } diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 18f85f6752..7fc23b0bc5 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -506,5 +506,11 @@ export function nb() { 'signing_document_list.attachment_type_form': 'Skjema', 'signing_document_list.download': 'Last ned', 'signing_document_list_summary.header': 'Signerte dokumenter', + 'service_task.title': 'Noe gikk galt', + 'service_task.body': 'En feil oppstod under automatisk behandling av skjemaet.', + 'service_task.help_text': + 'Du kan prøve å utføre behandlingen på nytt ved å klikke på "{0}". Hvis problemet vedvarer, ta kontakt med oss på brukerservice {1}.', + 'service_task.retry_button': 'Prøv igjen', + 'service_task.back_button': 'Gå tilbake', } satisfies FixedLanguageList; } diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 5e6c9adcc8..59f3e7e5e4 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -503,5 +503,11 @@ export function nn() { 'signing_document_list.attachment_type_form': 'Skjema', 'signing_document_list.download': 'Last ned', 'signing_document_list_summary.header': 'Signerte dokument', + 'service_task.title': 'Noko gjekk gale', + 'service_task.body': 'Ein feil oppstod under automatisk handsaming av skjemaet.', + 'service_task.help_text': + 'Du kan prøve å utføre handsaminga på nytt ved å klikke på "{0}". Om problemet held fram, ta kontakt med oss på brukarservice {1}.', + 'service_task.retry_button': 'Prøv igjen', + 'service_task.back_button': 'Gå tilbake', } satisfies FixedLanguageList; } diff --git a/src/layout/FileUpload/AttachmentSummaryComponent2.tsx b/src/layout/FileUpload/AttachmentSummaryComponent2.tsx index 11873a93ef..3fb6e54669 100644 --- a/src/layout/FileUpload/AttachmentSummaryComponent2.tsx +++ b/src/layout/FileUpload/AttachmentSummaryComponent2.tsx @@ -5,7 +5,7 @@ import { Paragraph } from '@digdir/designsystemet-react'; import { Label } from 'src/components/label/Label'; import { Lang } from 'src/features/language/Lang'; import { useOptionsFor } from 'src/features/options/useOptionsFor'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { useIsMobileOrTablet } from 'src/hooks/useDeviceWidths'; import { FileTable } from 'src/layout/FileUpload/FileUploadTable/FileTable'; import classes from 'src/layout/FileUpload/FileUploadTable/FileTableComponent.module.css'; diff --git a/src/layout/FileUpload/FileUploadTable/AttachmentFileName.tsx b/src/layout/FileUpload/FileUploadTable/AttachmentFileName.tsx index ed9a364e9f..39d138e2e2 100644 --- a/src/layout/FileUpload/FileUploadTable/AttachmentFileName.tsx +++ b/src/layout/FileUpload/FileUploadTable/AttachmentFileName.tsx @@ -6,7 +6,7 @@ import { FileCsvIcon, FileExcelIcon, FileIcon, FilePdfIcon, FileWordIcon } from import { isAttachmentUploaded } from 'src/features/attachments'; import { useLaxInstanceId } from 'src/features/instance/InstanceContext'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import classes from 'src/layout/FileUpload/FileUploadTable/AttachmentFileName.module.css'; import { getFileEnding, removeFileEnding } from 'src/layout/FileUpload/utils/fileEndings'; import { getDataElementUrl } from 'src/utils/urls/appUrlHelper'; diff --git a/src/layout/FileUpload/FileUploadTable/FileTable.tsx b/src/layout/FileUpload/FileUploadTable/FileTable.tsx index 7b606261f7..be497d2b5e 100644 --- a/src/layout/FileUpload/FileUploadTable/FileTable.tsx +++ b/src/layout/FileUpload/FileUploadTable/FileTable.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { isAttachmentUploaded } from 'src/features/attachments'; import { Lang } from 'src/features/language/Lang'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import classes from 'src/layout/FileUpload/FileUploadTable/FileTableComponent.module.css'; import { FileTableRow } from 'src/layout/FileUpload/FileUploadTable/FileTableRow'; import { FileTableRowProvider } from 'src/layout/FileUpload/FileUploadTable/FileTableRowContext'; diff --git a/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx b/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx index 5abf1109d8..5bfd05e916 100644 --- a/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx +++ b/src/layout/FileUpload/FileUploadTable/FileTableRow.tsx @@ -8,7 +8,7 @@ import { isAttachmentUploaded } from 'src/features/attachments'; import { FileScanResults } from 'src/features/attachments/types'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { AttachmentFileName } from 'src/layout/FileUpload/FileUploadTable/AttachmentFileName'; import { FileTableButtons } from 'src/layout/FileUpload/FileUploadTable/FileTableButtons'; import classes from 'src/layout/FileUpload/FileUploadTable/FileTableRow.module.css'; diff --git a/src/layout/Grid/GridSummary.tsx b/src/layout/Grid/GridSummary.tsx index b6f8c80032..a2c187332b 100644 --- a/src/layout/Grid/GridSummary.tsx +++ b/src/layout/Grid/GridSummary.tsx @@ -9,7 +9,7 @@ import { useDisplayData } from 'src/features/displayData/useDisplayData'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/unifiedValidationsForNode'; import { validationsOfSeverity } from 'src/features/validation/utils'; import { useIsMobile } from 'src/hooks/useDeviceWidths'; diff --git a/src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.tsx b/src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.tsx index 5dc282b7ef..47992b727b 100644 --- a/src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.tsx +++ b/src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.tsx @@ -9,7 +9,7 @@ import { useDisplayData } from 'src/features/displayData/useDisplayData'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/unifiedValidationsForNode'; import { validationsOfSeverity } from 'src/features/validation/utils'; import { useIsMobile } from 'src/hooks/useDeviceWidths'; diff --git a/src/layout/Subform/SubformWrapper.tsx b/src/layout/Subform/SubformWrapper.tsx index 2bbd461010..15ea3556bf 100644 --- a/src/layout/Subform/SubformWrapper.tsx +++ b/src/layout/Subform/SubformWrapper.tsx @@ -7,7 +7,7 @@ import { TaskOverrides } from 'src/core/contexts/TaskOverrides'; import { Loader } from 'src/core/loading/Loader'; import { FormProvider } from 'src/features/form/FormContext'; import { useDataTypeFromLayoutSet } from 'src/features/form/layout/LayoutsContext'; -import { PDFWrapper } from 'src/features/pdf/PDFWrapper'; +import { PdfWrapper } from 'src/features/pdf/PdfWrapper'; import { useNavigationParam } from 'src/hooks/navigation'; import { useNavigatePage } from 'src/hooks/useNavigatePage'; import { useItemWhenType } from 'src/utils/layout/useNodeItem'; @@ -22,11 +22,11 @@ export function SubformWrapper({ baseComponentId, children }: PropsWithChildren< export function SubformForm() { return ( - + - + ); } diff --git a/src/layout/Subform/Summary/SubformSummaryTable.tsx b/src/layout/Subform/Summary/SubformSummaryTable.tsx index 4c3d82739f..eead09dc39 100644 --- a/src/layout/Subform/Summary/SubformSummaryTable.tsx +++ b/src/layout/Subform/Summary/SubformSummaryTable.tsx @@ -11,7 +11,7 @@ import { useDataTypeFromLayoutSet, useLayoutLookups } from 'src/features/form/la import { useInstanceDataElements } from 'src/features/instance/InstanceContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { isSubformValidation } from 'src/features/validation'; import { useComponentValidationsFor } from 'src/features/validation/selectors/componentValidationsForNode'; import { useAllNavigationParams, useIsSubformPage } from 'src/hooks/navigation'; diff --git a/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx b/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx index 5ad657f951..17e4a67286 100644 --- a/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx +++ b/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx @@ -7,7 +7,7 @@ import { useTaskOverrides } from 'src/core/contexts/TaskOverrides'; import { useSetReturnToView, useSetSummaryNodeOfOrigin } from 'src/features/form/layout/PageNavigationContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; +import { usePdfModeActive } from 'src/features/pdf/PdfWrapper'; import { useIsMobile } from 'src/hooks/useDeviceWidths'; import { useCurrentView, useNavigateToComponent } from 'src/hooks/useNavigatePage'; import { useSummaryProp } from 'src/layout/Summary2/summaryStoreContext'; diff --git a/src/types/index.ts b/src/types/index.ts index 4bfa48d988..97cd0988b0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -24,6 +24,7 @@ export interface IExpandedWidthLayouts { export enum ProcessTaskType { Unknown = 'unknown', + Service = 'service', Data = 'data', Archived = 'ended', Confirm = 'confirmation', diff --git a/src/types/shared.ts b/src/types/shared.ts index 3853c245a2..6effda0735 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -197,7 +197,7 @@ export interface IProcess { currentTask?: ITask; ended?: string | null; endEvent?: string | null; - processTasks?: Pick[]; + processTasks?: IProcessTask[]; } export interface IProfile { @@ -237,13 +237,16 @@ export const ELEMENT_TYPE = { type ElementType = (typeof ELEMENT_TYPE)[keyof typeof ELEMENT_TYPE]; -export type ITask = { +interface IProcessTask { + altinnTaskType: string; + elementId: string; + elementType?: ElementType; // Appears in versions after https://github.com/Altinn/app-lib-dotnet/pull/745 +} + +export interface ITask extends IProcessTask { flow: number; started: string; - elementId: string; name: string; - altinnTaskType: string; - elementType?: ElementType; ended?: string | null; validated?: IValidated | null; @@ -251,7 +254,7 @@ export type ITask = { write?: boolean | null; actions?: IProcessActions | null; userActions?: IUserAction[]; -}; +} export type IProcessActions = { [k in IActionType]?: boolean; diff --git a/test/README.md b/test/README.md index ecdfb02ebe..fde7b82fc9 100755 --- a/test/README.md +++ b/test/README.md @@ -36,16 +36,18 @@ npx cypress run --env environment=tt02 -s 'test/e2e/integration/*/*.ts' 1. Clone one or more of the apps we've made automatic tests for: -- [ttd/frontend-test](https://dev.altinn.studio/repos/ttd/frontend-test) - [ttd/anonymous-stateless-app](https://dev.altinn.studio/repos/ttd/anonymous-stateless-app) -- [ttd/stateless-app](https://dev.altinn.studio/repos/ttd/stateless-app) -- [ttd/signing-test](https://dev.altinn.studio/repos/ttd/signing-test) -- [ttd/expression-validation-test](https://dev.altinn.studio/repos/ttd/expression-validation-test) -- [ttd/payment-test](https://dev.altinn.studio/repos/ttd/payment-test) - [ttd/component-library](https://altinn.studio/repos/ttd/component-library.git) +- [ttd/expression-validation-test](https://dev.altinn.studio/repos/ttd/expression-validation-test) +- [ttd/frontend-test](https://dev.altinn.studio/repos/ttd/frontend-test) - [ttd/multiple-datamodels-test](https://dev.altinn.studio/repos/ttd/multiple-datamodels-test) -- [ttd/subform-test](https://dev.altinn.studio/repos/ttd/subform-test) - [ttd/navigation-test-subform](https://dev.altinn.studio/repos/ttd/navigation-test-subform) +- [ttd/payment-test](https://dev.altinn.studio/repos/ttd/payment-test) +- [ttd/service-task](https://altinn.studio/repos/ttd/service-task) +- [ttd/signering-brukerstyrt](https://altinn.studio/repos/ttd/signering-brukerstyrt) +- [ttd/signing-test](https://dev.altinn.studio/repos/ttd/signing-test) +- [ttd/stateless-app](https://dev.altinn.studio/repos/ttd/stateless-app) +- [ttd/subform-test](https://dev.altinn.studio/repos/ttd/subform-test) 3. Start the app you want to test: diff --git a/test/e2e/integration/service-task/service-task.ts b/test/e2e/integration/service-task/service-task.ts new file mode 100644 index 0000000000..a37124ad93 --- /dev/null +++ b/test/e2e/integration/service-task/service-task.ts @@ -0,0 +1,141 @@ +import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; + +import { SearchParams } from 'src/core/routing/types'; +import type { ILayoutSets } from 'src/layout/common.generated'; + +const appFrontend = new AppFrontend(); + +describe('Service task', () => { + it('should display the default service task layout when failing', { retries: 0 }, () => { + startAppAndFillToFailure(); + + // This message comes from a custom layout-set showing an error in this service-task + cy.findByText('Uff da! Her tryna denne service-tasken, men det var jo du som valgte at det skulle skje.').should( + 'be.visible', + ); + + assertAndDismissNotification('Service task fail returned a failed result!'); + + // This layout-set does not have a pdfLayoutName, so it will auto-generate a PDF + cy.testPdf({ + snapshotName: 'service-task-with-layout', + returnToForm: true, + enableResponseFuzzing: false, + callback: () => { + // This is not a great-looking PDF, but when overriding it is the responsibility of the + // developer to make a good PDF. + cy.findByText( + 'Uff da! Her tryna denne service-tasken, men det var jo du som valgte at det skulle skje.', + ).should('be.visible'); + }, + }); + + goBackAndAchieveSuccess(); + }); + + it('should display something sensible when there is no layout-set for the service task', { retries: 0 }, () => { + cy.intercept('**/layoutsets', (req) => { + req.on('response', (res) => { + // We do some trickery here. Ordinarily it's the PDF service task we would test, but we can't really reach + // that from the test (it will only be reached from the real PDF generator). However, the 'Fail' task is also + // a service task, so we can remove the custom layout-set here to simulate what happens in the PDF generator + // for a PDF-generating service-task. + const layoutSets: ILayoutSets = JSON.parse(res.body); + layoutSets.sets = layoutSets.sets.filter((set) => set.id !== 'Fail'); + res.send(layoutSets); + }); + }).as('LayoutSets'); + + startAppAndFillToFailure(); + + cy.findByText(/En feil oppstod under automatisk behandling av skjemaet/).should('be.visible'); + cy.findByText(/Du kan prøve å utføre behandlingen på nytt/).should('be.visible'); + cy.visualTesting('service-task-no-layout-set'); + assertAndDismissNotification('Service task fail returned a failed result!'); + + cy.testPdf({ + snapshotName: 'service-task-with-multiple-tasks', + returnToForm: true, + enableResponseFuzzing: false, + buildUrl: (href) => { + const queryArgs = new URLSearchParams(); + queryArgs.append(SearchParams.Pdf, '1'); + for (const task of ['Task_Utfylling1', 'Task_Utfylling2']) { + queryArgs.append(SearchParams.PdfForTask, task); + } + + const baseUrl = href.split('?')[0]; + return `${baseUrl}?${queryArgs.toString()}`; + }, + callback: () => { + cy.expectPageBreaks(2); + cy.findByText('En hilsen fra Task_Utfylling1').should('be.visible'); + cy.findByText('Lykkeønsker fra et underskjema').should('be.visible'); + cy.findByText('Himling med øyne og skuldertrekk fra Task_Utfylling2').should('be.visible'); + }, + }); + + goBackAndAchieveSuccess(); + }); +}); + +function startAppAndFillToFailure() { + cy.startAppInstance(appFrontend.apps.serviceTask, { cyUser: 'manager' }); + cy.findByRole('textbox', { name: 'En tekst i Task_Utfylling1' }).type('En hilsen fra Task_Utfylling1'); + cy.waitUntilSaved(); + + cy.get('#subform-Subform-z8we7d-add-button').click(); + cy.get('#finishedLoading').should('exist'); + cy.findByRole('textbox', { name: 'Subform tekstfelt' }).type('Lykkeønsker fra et underskjema'); + cy.findByRole('button', { name: 'Ferdig' }).click(); + + cy.findByRole('textbox', { name: 'En tekst i Task_Utfylling1' }).should( + 'have.value', + 'En hilsen fra Task_Utfylling1', + ); + cy.waitUntilSaved(); + cy.findByRole('button', { name: 'Neste' }).click(); + + cy.findByRole('heading', { name: 'Task_Utfylling2' }).should('be.visible'); + cy.get('#finishedLoading').should('exist'); + cy.findByRole('textbox', { name: 'En tekst i Task_Utfylling2' }).type( + 'Himling med øyne og skuldertrekk fra Task_Utfylling2', + ); + cy.findByRole('textbox', { name: 'En tekst i Task_Utfylling2' }).should( + 'have.value', + 'Himling med øyne og skuldertrekk fra Task_Utfylling2', + ); + cy.findByRole('radiogroup', { name: /Skal Task_Fail servicetask feile\?/ }) + .findByRole('radio', { name: 'Ja' }) + .click(); + cy.waitUntilSaved(); + cy.findByRole('button', { name: 'Neste' }).click(); +} + +function assertAndDismissNotification(notificationText: string) { + cy.findByRole('region', { name: /Notifications/ }).should('be.visible'); + cy.findByRole('region', { name: /Notifications/ }) + .findByText(notificationText) + .should('exist'); + cy.findByRole('region', { name: /Notifications/ }) + .findByRole('button', { name: 'close' }) + .click(); + cy.findByRole('region', { name: /Notifications/ }).should('not.be.visible'); +} + +function goBackAndAchieveSuccess() { + cy.waitUntilSaved(); + cy.findByRole('button', { name: 'Prøv igjen' }).should('be.visible'); + cy.findByRole('button', { name: 'Gå tilbake' }).click(); + + cy.findByRole('radiogroup', { name: /Skal Task_Fail servicetask feile\?/ }) + .findByRole('radio', { name: 'Nei' }) + .click(); + cy.waitUntilSaved(); + cy.findByRole('button', { name: 'Neste' }).click(); + + cy.findByText('Skjemaet er sendt inn').should('be.visible'); + cy.findAllByRole('link', { name: /\.pdf$/ }).should('have.length', 2); + cy.findByRole('link', { name: /Autogenerert PDF av Task_Utfylling1 og Task_Utfylling2\.pdf$/ }).should('be.visible'); + cy.findByRole('link', { name: /PDF basert på layout-set\.pdf$/ }).should('be.visible'); +} diff --git a/test/e2e/pageobjects/app-frontend.ts b/test/e2e/pageobjects/app-frontend.ts index 6e09b06800..2980a812c5 100644 --- a/test/e2e/pageobjects/app-frontend.ts +++ b/test/e2e/pageobjects/app-frontend.ts @@ -2,38 +2,41 @@ import texts from 'test/e2e/fixtures/texts.json'; export class AppFrontend { public apps = { - /** @see https://dev.altinn.studio/repos/ttd/frontend-test */ - frontendTest: 'frontend-test', - - /** @see https://dev.altinn.studio/repos/ttd/stateless-app */ - stateless: 'stateless-app', - /** @see https://dev.altinn.studio/repos/ttd/anonymous-stateless-app */ anonymousStateless: 'anonymous-stateless-app', - /** @see https://dev.altinn.studio/repos/ttd/signing-test */ - signingTest: 'signing-test', + /** @see https://altinn.studio/repos/ttd/component-library.git */ + componentLibrary: 'component-library', /** @see https://dev.altinn.studio/repos/ttd/expression-validation-test */ expressionValidationTest: 'expression-validation-test', - /** @see https://dev.altinn.studio/repos/ttd/payment-test */ - paymentTest: 'payment-test', - - /** @see https://altinn.studio/repos/ttd/component-library.git */ - componentLibrary: 'component-library', + /** @see https://dev.altinn.studio/repos/ttd/frontend-test */ + frontendTest: 'frontend-test', /** @see https://dev.altinn.studio/repos/ttd/multiple-datamodels-test */ multipleDatamodelsTest: 'multiple-datamodels-test', - /** @see https://dev.altinn.studio/repos/ttd/subform-test */ - subformTest: 'subform-test', - /** @see https://dev.altinn.studio/repos/ttd/navigation-test-subform */ navigationTestSubform: 'navigation-test-subform', - /** @see https://dev.altinn.studio/repos/ttd/signering-brukerstyrt */ + /** @see https://dev.altinn.studio/repos/ttd/payment-test */ + paymentTest: 'payment-test', + + /** @see https://altinn.studio/repos/ttd/service-task */ + serviceTask: 'service-task', + + /** @see https://altinn.studio/repos/ttd/signering-brukerstyrt */ signeringBrukerstyrt: 'signering-brukerstyrt', + + /** @see https://dev.altinn.studio/repos/ttd/signing-test */ + signingTest: 'signing-test', + + /** @see https://dev.altinn.studio/repos/ttd/stateless-app */ + stateless: 'stateless-app', + + /** @see https://dev.altinn.studio/repos/ttd/subform-test */ + subformTest: 'subform-test', }; //Start app instance page diff --git a/test/e2e/support/custom.ts b/test/e2e/support/custom.ts index 397d23f6c9..4732146c17 100644 --- a/test/e2e/support/custom.ts +++ b/test/e2e/support/custom.ts @@ -653,7 +653,7 @@ Cypress.Commands.add( cy.getCurrentViewportSize().as('testPdfViewportSize'); // Make sure instantiation is completed before we get the url - cy.location('hash', { log: false }).should('contain', '#/instance/'); + cy.location('hash', { log: false }).should('contain', '#/instance/').as('hashBeforePdf'); // Make sure we blur any selected component before reload to trigger save cy.get('body').click({ log: false }); @@ -734,9 +734,12 @@ Cypress.Commands.add( }); cy.get('body').invoke('css', 'margin', ''); - cy.location('href').then((href) => { - cy.visit(href.replace('?pdf=1', '')); + cy.get('@hashBeforePdf').then((hashBeforePdf) => { + cy.window().then((win) => { + win.location.hash = hashBeforePdf.toString(); + }); }); + cy.get('#readyForPrint').should('not.exist'); cy.get('#finishedLoading').should('exist'); } diff --git a/test/e2e/support/global.ts b/test/e2e/support/global.ts index 54ac6acf80..32eee62e15 100644 --- a/test/e2e/support/global.ts +++ b/test/e2e/support/global.ts @@ -20,7 +20,7 @@ export type StartAppInstanceOptions = { // Tenor user to log in as (alternative to user) tenorUser?: TenorUser | null; - authenticationLevel?: string; + authenticationLevel?: '0' | '1' | '2'; // JavaScript code to evaluate before starting the app instance (evaluates in the browser, in context of the app). // The code runs inside an async function, and if it ends with a return value, that value will assumed to be a