diff --git a/src/App.tsx b/src/App.tsx index 65e7881f01..9b13cde9e6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { ProcessWrapper } from 'src/components/wrappers/ProcessWrapper'; +import { ProcessWrapperWrapper } from 'src/components/wrappers/ProcessWrapper'; import { Entrypoint } from 'src/features/entrypoint/Entrypoint'; import { InstanceProvider } from 'src/features/instance/InstanceContext'; import { PartySelection } from 'src/features/instantiate/containers/PartySelection'; @@ -10,7 +10,7 @@ import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/Ins export const App = () => ( } /> ( element={} /> - + } /> diff --git a/src/__mocks__/getAttachmentsMock.ts b/src/__mocks__/getAttachmentsMock.ts index 5852d67889..800f90089d 100644 --- a/src/__mocks__/getAttachmentsMock.ts +++ b/src/__mocks__/getAttachmentsMock.ts @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import type { UploadedAttachment } from 'src/features/attachments'; +import type { IData } from 'src/types/shared'; const getRandomFileSize = () => Math.floor(Math.random() * (2500 - 250 + 1)) + 250; @@ -13,29 +14,43 @@ export const getAttachmentsMock = ({ count = 3, fileSize }: IGetAttachmentsMock const out: UploadedAttachment[] = []; for (let i = 0; i < count; i++) { - out.push({ - error: undefined, - uploaded: true, - deleting: false, - updating: false, - data: { - id: uuidv4(), - dataType: 'file', - size: fileSize || getRandomFileSize(), - filename: `attachment-name-${i}`, - tags: [`attachment-tag-${i}`], - created: new Date().toISOString(), - createdBy: 'test', - lastChanged: new Date().toISOString(), - lastChangedBy: 'test', - blobStoragePath: 'test', - contentType: 'test', - locked: false, - instanceGuid: 'test', - refs: [], - }, - }); + out.push( + getAttachmentMock({ + data: getAttachmentDataMock({ + size: fileSize || getRandomFileSize(), + filename: `attachment-name-${i}`, + tags: [`attachment-tag-${i}`], + }), + }), + ); } return out; }; + +export const getAttachmentDataMock = (overrides: Partial = {}): IData => ({ + id: uuidv4(), + dataType: 'file', + size: getRandomFileSize(), + filename: 'attachment-name', + tags: ['attachment-tag-'], + created: new Date().toISOString(), + createdBy: 'test', + lastChanged: new Date().toISOString(), + lastChangedBy: 'test', + blobStoragePath: 'test', + contentType: 'test', + locked: false, + instanceGuid: 'test', + refs: [], + ...overrides, +}); + +export const getAttachmentMock = (overrides: Partial = {}): UploadedAttachment => ({ + error: undefined, + uploaded: true, + deleting: false, + updating: false, + data: getAttachmentDataMock(overrides.data), + ...overrides, +}); diff --git a/src/components/form/Form.test.tsx b/src/components/form/Form.test.tsx index 107fd93073..38ba3d2a71 100644 --- a/src/components/form/Form.test.tsx +++ b/src/components/form/Form.test.tsx @@ -6,6 +6,7 @@ import { getFormLayoutStateMock } from 'src/__mocks__/getFormLayoutStateMock'; import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { Form } from 'src/components/form/Form'; import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; +import { PageNavigationRouter } from 'src/test/routerUtils'; import type { CompExternal, ILayout } from 'src/layout/layout'; import type { CompSummaryExternal } from 'src/layout/Summary/config.generated'; import type { RootState } from 'src/redux/store'; @@ -219,6 +220,11 @@ describe('Form', () => { async function render(layout = mockComponents, customState: Partial = {}) { await renderWithInstanceAndLayout({ renderer: () =>
, + router: PageNavigationRouter('FormLayout'), + queries: { + fetchLayouts: () => Promise.resolve({}), + fetchLayoutSettings: () => Promise.resolve({ pages: { order: ['FormLayout', '2', '3'] } }), + }, reduxState: { ...getInitialStateMock(), ...customState, diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index 89293fa989..f3a8c349a1 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -8,6 +8,7 @@ import { ErrorReport } from 'src/components/message/ErrorReport'; import { ReadyForPrint } from 'src/components/ReadyForPrint'; import { useLanguage } from 'src/features/language/useLanguage'; import { useAppSelector } from 'src/hooks/useAppSelector'; +import { useNavigatePage } from 'src/hooks/useNavigatePage'; import { GenericComponent } from 'src/layout/GenericComponent'; import { getFieldName } from 'src/utils/formComponentUtils'; import { extractBottomButtons, hasRequiredFields } from 'src/utils/formLayout'; @@ -15,24 +16,18 @@ import { useExprContext } from 'src/utils/layout/ExprContext'; import { getFormHasErrors, missingFieldsInLayoutValidations } from 'src/utils/validation/validation'; export function Form() { - const nodes = useExprContext(); const langTools = useLanguage(); + const { currentPageId } = useNavigatePage(); const validations = useAppSelector((state) => state.formValidations.validations); - const hasErrors = useAppSelector((state) => getFormHasErrors(state.formValidations.validations)); - const page = nodes?.current(); - const pageKey = page?.top.myKey; + const nodes = useExprContext(); - const [mainNodes, errorReportNodes] = React.useMemo(() => { - if (!page) { - return [[], []]; - } - return hasErrors ? extractBottomButtons(page) : [page.children(), []]; - }, [page, hasErrors]); + const page = nodes?.all?.()?.[currentPageId]; + const hasErrors = useAppSelector((state) => getFormHasErrors(state.formValidations.validations)); const requiredFieldsMissing = React.useMemo(() => { - if (validations && pageKey && validations[pageKey]) { + if (validations && validations[currentPageId]) { const requiredValidationTextResources: string[] = []; - page.flat(true).forEach((node) => { + page?.flat(true).forEach((node) => { const trb = node.item.textResourceBindings; const fieldName = getFieldName(trb, langTools); if ('required' in node.item && node.item.required && trb && 'requiredValidation' in trb) { @@ -40,15 +35,18 @@ export function Form() { } }); - return missingFieldsInLayoutValidations(validations[pageKey], requiredValidationTextResources, langTools); + return missingFieldsInLayoutValidations(validations[currentPageId], requiredValidationTextResources, langTools); } return false; - }, [validations, pageKey, page, langTools]); + }, [validations, currentPageId, page, langTools]); - if (!page) { - return null; - } + const [mainNodes, errorReportNodes] = React.useMemo(() => { + if (!page) { + return [[], []]; + } + return hasErrors ? extractBottomButtons(page) : [page.children(), []]; + }, [page, hasErrors]); return ( <> @@ -63,7 +61,7 @@ export function Form() { spacing={3} alignItems='flex-start' > - {mainNodes.map((n) => ( + {mainNodes?.map((n) => ( { const dispatch = useAppDispatch(); - const currentView = useAppSelector((state) => state.formLayout.uiConfig.currentView); + const { currentPageId, navigateToPage } = useNavigatePage(); + const { setFocusId } = usePageNavigationContext(); const [errorsMapped, errorsUnmapped] = useAppSelector(selectMappedUnmappedErrors); const allNodes = useExprContext(); const hasErrors = errorsUnmapped.length > 0 || errorsMapped.length > 0; @@ -59,12 +62,8 @@ export const ErrorReport = ({ nodes }: IErrorReportProps) => { return; } - if (currentView !== error.layout) { - dispatch( - FormLayoutActions.updateCurrentView({ - newView: error.layout, - }), - ); + if (currentPageId !== error.layout) { + navigateToPage(error.layout); } const allParents = componentNode?.parents() || []; @@ -116,17 +115,14 @@ export const ErrorReport = ({ nodes }: IErrorReportProps) => { FormLayoutActions.updateRepeatingGroupsEditIndex({ group: parentNode.item.id, index: childNode.rowIndex, + currentPageId, }), ); } } // Set focus - dispatch( - FormLayoutActions.updateFocus({ - focusComponentId: error.componentId, - }), - ); + setFocusId(error.componentId); }; const errorMessage = (message: string) => diff --git a/src/components/presentation/NavBar.test.tsx b/src/components/presentation/NavBar.test.tsx index edc8f03743..f4ee4736d2 100644 --- a/src/components/presentation/NavBar.test.tsx +++ b/src/components/presentation/NavBar.test.tsx @@ -5,87 +5,76 @@ import { userEvent } from '@testing-library/user-event'; import mockAxios from 'jest-mock-axios'; import { getFormLayoutStateMock } from 'src/__mocks__/getFormLayoutStateMock'; -import { getUiConfigStateMock } from 'src/__mocks__/getUiConfigStateMock'; import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; import { NavBar } from 'src/components/presentation/NavBar'; -import { renderWithoutInstanceAndLayout } from 'src/test/renderWithProviders'; +import { mockWindow } from 'src/test/mockWindow'; +import { renderWithInstanceAndLayout } from 'src/test/renderWithProviders'; +import { ProcessTaskType } from 'src/types'; import type { IRawTextResource } from 'src/features/language/textResources'; +import type { PresentationType } from 'src/types'; import type { IAppLanguage } from 'src/types/shared'; afterEach(() => mockAxios.reset()); interface RenderNavBarProps { - showBackArrow: boolean; + currentPageId?: string; hideCloseButton: boolean; - showLanguageSelector: boolean; languageResponse?: IAppLanguage[]; + showLanguageSelector: boolean; textResources?: IRawTextResource[]; + type?: ProcessTaskType | PresentationType; + initialPage?: string; } const render = async ({ hideCloseButton, - showBackArrow, showLanguageSelector, languageResponse, + type = ProcessTaskType.Data, + initialPage, textResources = [], }: RenderNavBarProps) => { - const mockClose = jest.fn(); - const mockBack = jest.fn(); - const mockAppLanguageChange = jest.fn(); - - await renderWithoutInstanceAndLayout({ - renderer: () => ( - - ), + await renderWithInstanceAndLayout({ + renderer: () => , reduxState: { ...getInitialStateMock(), - formLayout: getFormLayoutStateMock({ - uiConfig: getUiConfigStateMock({ - hideCloseButton, - showLanguageSelector, - }), - }), + formLayout: getFormLayoutStateMock({}), }, + initialPage, queries: { fetchAppLanguages: () => languageResponse ? Promise.resolve(languageResponse) : Promise.reject(new Error('No languages mocked')), fetchTextResources: () => Promise.resolve({ language: 'nb', resources: textResources }), + fetchLayoutSettings: () => + Promise.resolve({ pages: { hideCloseButton, showLanguageSelector, order: ['1', '2', '3'] } }), }, reduxGateKeeper: (action) => 'type' in action && action.type === 'deprecated/setCurrentLanguage', }); - - return { mockClose, mockBack, mockAppLanguageChange }; }; describe('NavBar', () => { + const { mockAssign } = mockWindow(); it('should render nav', async () => { await render({ hideCloseButton: true, - showBackArrow: false, showLanguageSelector: false, }); screen.getByRole('navigation', { name: /Appnavigasjon/i }); }); it('should render close button', async () => { - const { mockClose } = await render({ + await render({ hideCloseButton: false, - showBackArrow: false, showLanguageSelector: false, }); const closeButton = screen.getByRole('button', { name: /Lukk Skjema/i }); await userEvent.click(closeButton); - expect(mockClose).toHaveBeenCalled(); + expect(mockAssign).toHaveBeenCalled(); }); it('should hide close button and back button', async () => { await render({ hideCloseButton: true, - showBackArrow: false, showLanguageSelector: false, }); expect(screen.queryAllByRole('button')).toHaveLength(0); @@ -93,19 +82,17 @@ describe('NavBar', () => { }); it('should render back button', async () => { - const { mockBack } = await render({ + await render({ hideCloseButton: true, - showBackArrow: true, showLanguageSelector: false, + type: ProcessTaskType.Data, + initialPage: 'Task_1/2', }); - const backButton = screen.getByTestId('form-back-button'); - await userEvent.click(backButton); - expect(mockBack).toHaveBeenCalled(); + expect(screen.getByTestId('form-back-button')).toBeInTheDocument(); }); it('should render and change app language', async () => { await render({ hideCloseButton: false, - showBackArrow: true, showLanguageSelector: true, languageResponse: [{ language: 'en' }, { language: 'nb' }], }); @@ -122,7 +109,6 @@ describe('NavBar', () => { it('should render app language with custom labels', async () => { await render({ hideCloseButton: false, - showBackArrow: true, showLanguageSelector: true, textResources: [ { id: 'language.selector.label', value: 'Velg språk test' }, diff --git a/src/components/presentation/NavBar.tsx b/src/components/presentation/NavBar.tsx index 9de95eab36..c758af50b3 100644 --- a/src/components/presentation/NavBar.tsx +++ b/src/components/presentation/NavBar.tsx @@ -6,41 +6,69 @@ import cn from 'classnames'; import { LanguageSelector } from 'src/components/presentation/LanguageSelector'; import classes from 'src/components/presentation/NavBar.module.css'; -import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice'; +import { usePageNavigationContext } from 'src/features/form/layout/PageNavigationContext'; +import { useUiConfigContext } from 'src/features/form/layout/UiConfigContext'; import { useLanguage } from 'src/features/language/useLanguage'; -import { useAppDispatch } from 'src/hooks/useAppDispatch'; -import { useAppSelector } from 'src/hooks/useAppSelector'; +import { useCurrentParty } from 'src/features/party/PartiesProvider'; +import { useNavigatePage } from 'src/hooks/useNavigatePage'; +import { PresentationType, ProcessTaskType } from 'src/types'; +import { httpGet } from 'src/utils/network/networking'; +import { getRedirectUrl } from 'src/utils/urls/appUrlHelper'; +import { returnUrlFromQueryParameter, returnUrlToMessagebox } from 'src/utils/urls/urlHelper'; export interface INavBarProps { - handleClose: () => void; - handleBack: (e: any) => void; - showBackArrow?: boolean; + type: PresentationType | ProcessTaskType; } const expandIconStyle = { transform: 'rotate(45deg)' }; -export const NavBar = (props: INavBarProps) => { - const dispatch = useAppDispatch(); +export const NavBar = ({ type }: INavBarProps) => { const { langAsString } = useLanguage(); - const { hideCloseButton, showLanguageSelector, showExpandWidthButton, expandedWidth } = useAppSelector( - (state) => state.formLayout.uiConfig, - ); + const { navigateToPage, previous } = useNavigatePage(); + + const { returnToView } = usePageNavigationContext(); + const party = useCurrentParty(); + const { hideCloseButton, showLanguageSelector, showExpandWidthButton, expandedWidth, toggleExpandedWidth } = + useUiConfigContext(); - const handleExpand = () => { - dispatch(FormLayoutActions.toggleExpandedWidth()); + const handleBackArrowButton = () => { + if (returnToView) { + navigateToPage(returnToView); + } else if (previous !== undefined && (type === ProcessTaskType.Data || type === PresentationType.Stateless)) { + navigateToPage(previous); + } }; + const handleModalCloseButton = () => { + const queryParameterReturnUrl = returnUrlFromQueryParameter(); + const messageBoxUrl = returnUrlToMessagebox(window.location.origin, party?.partyId); + if (!queryParameterReturnUrl && messageBoxUrl) { + window.location.assign(messageBoxUrl); + return; + } + + if (queryParameterReturnUrl) { + httpGet(getRedirectUrl(queryParameterReturnUrl)) + .then((response) => response) + .catch(() => messageBoxUrl) + .then((returnUrl) => { + window.location.assign(returnUrl); + }); + } + }; + + const showBackArrow = !!previous && (type === ProcessTaskType.Data || type === PresentationType.Stateless); return (