Skip to content

Commit

Permalink
Refactor/1508 rewrite page navigation (#1682)
Browse files Browse the repository at this point in the history
* refactor: Add routes for pageLayouts. Show pages of a form with said routes. Navigation updated to use routes
* refactor: Add taskId to url navigation
* Add navigateToCurrentProcess button
* refactor: get pageorder by using hook instead of redux
* refactor: fix showing of confirmation, receipt, and feedback steps. Fix small bug with progress component
* refactor: fix returnToView. Add state to context
* Add returnToView and scrollPosition to PageNavigationContext
* Add support for navigating users to the last visited page stored in local storage
* Add UiConfigContext. Fix more tests after refactoring navigation
* refactor: e2e-tests after refactoring to use router based page navigation
* fix: bug with layouts becoming hidden with any expression
* refactor: layoutsettings to custom context
* refactor: adapt code after merging with new app-flow
* refactor: add page routing for stateless apps
* fix: remove keepScroll functionality until validation is in place
* fix: issue with back to summary page being shown after navigating to next step
* fix: bug with add items in sub group repeating groups
* fix: bug with datamodel not being accessible during confirmation/receipt steps
* fix: summary tests
* fix: temporarily skip validation cypress tests while waiting for 1506
* fix: remove currentView from devtools
* fix: remove use of redux based currentView in repGroupDeleteRow
* fix: rewrite PresentationComponent to have prop stating whether to render navbar or not
* fix: unit tests after refactor
* fix: bug with mergeAndSort function
* fix: issue with validation currentView in GenericComponent
* refactor: remove unused sagas related to page navigation
* fix: bug with navigating away from instantiation-selection
* fix: bug with RepeatingGroupsLikertContainer tests
* fix: issue with receipt redirecting
* fix: redirect to page and processEnd. Use currentPage from url in ExprContext
* Update process mutation data when navigating to next step
* fix: bug with overwriting queryData in InstanceContext
* fix: unit test for NavigationBar component
* refactor: types on ProcessContext
* refactor: use lang component for generic help page when navigating
* refactor: remove uiconfig options that are available in UiConfigContext from redux
* fix: redirect to start for stateless-apps on invalid pages
* fix: add process task error messages for all types of process steps
* fix: remove unnecessary routes in stateless apps
* fix: use enums to represent fixed routes
* fix: issue with useLayoutQuery
* refactor: move LayoutProvider, UiConfigProvider and LayoutSettingsProvider to renderWithInstanceAndLayout
* docs: add comment about useTaskType
* refactor: add test for bug with validation and new page navigation
* fix: remove unnecessary redirect to confirmation page from receipt page
* refactor: remove duplicate import

---------

Co-authored-by: Ole Martin Handeland <git@olemartin.org>
  • Loading branch information
mikaelrss and Ole Martin Handeland authored Dec 7, 2023
1 parent bf1edfa commit b11facd
Show file tree
Hide file tree
Showing 96 changed files with 1,471 additions and 1,368 deletions.
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,7 +10,7 @@ import { InstanceSelectionWrapper } from 'src/features/instantiate/selection/Ins
export const App = () => (
<Routes>
<Route
path='/'
path='*'
element={<Entrypoint />}
/>
<Route
Expand All @@ -22,10 +22,10 @@ export const App = () => (
element={<PartySelection />}
/>
<Route
path='/instance/:partyId/:instanceGuid'
path='/instance/:partyId/:instanceGuid/*'
element={
<InstanceProvider>
<ProcessWrapper />
<ProcessWrapperWrapper />
</InstanceProvider>
}
/>
Expand Down
59 changes: 37 additions & 22 deletions src/__mocks__/getAttachmentsMock.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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> = {}): 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> = {}): UploadedAttachment => ({
error: undefined,
uploaded: true,
deleting: false,
updating: false,
data: getAttachmentDataMock(overrides.data),
...overrides,
});
6 changes: 6 additions & 0 deletions src/components/form/Form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -219,6 +220,11 @@ describe('Form', () => {
async function render(layout = mockComponents, customState: Partial<IRuntimeState> = {}) {
await renderWithInstanceAndLayout({
renderer: () => <Form />,
router: PageNavigationRouter('FormLayout'),
queries: {
fetchLayouts: () => Promise.resolve({}),
fetchLayoutSettings: () => Promise.resolve({ pages: { order: ['FormLayout', '2', '3'] } }),
},
reduxState: {
...getInitialStateMock(),
...customState,
Expand Down
34 changes: 16 additions & 18 deletions src/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,45 @@ 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';
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) {
requiredValidationTextResources.push(langTools.langAsString(trb.requiredValidation, [fieldName]));
}
});

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 (
<>
Expand All @@ -63,7 +61,7 @@ export function Form() {
spacing={3}
alignItems='flex-start'
>
{mainNodes.map((n) => (
{mainNodes?.map((n) => (
<GenericComponent
key={n.item.id}
node={n}
Expand Down
20 changes: 8 additions & 12 deletions src/components/message/ErrorReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import { createSelector } from 'reselect';
import { FullWidthWrapper } from 'src/components/form/FullWidthWrapper';
import classes from 'src/components/message/ErrorReport.module.css';
import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
import { usePageNavigationContext } from 'src/features/form/layout/PageNavigationContext';
import { Lang } from 'src/features/language/Lang';
import { useAppDispatch } from 'src/hooks/useAppDispatch';
import { useAppSelector } from 'src/hooks/useAppSelector';
import { useNavigatePage } from 'src/hooks/useNavigatePage';
import { getParsedLanguageFromText } from 'src/language/sharedLanguage';
import { AsciiUnitSeparator } from 'src/layout/FileUpload/utils/asciiUnitSeparator';
import { GenericComponent } from 'src/layout/GenericComponent';
Expand Down Expand Up @@ -39,7 +41,8 @@ const selectMappedUnmappedErrors = createSelector(selectValidations, createMappe

export const ErrorReport = ({ nodes }: IErrorReportProps) => {
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;
Expand All @@ -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() || [];
Expand Down Expand Up @@ -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) =>
Expand Down
60 changes: 23 additions & 37 deletions src/components/presentation/NavBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,107 +5,94 @@ 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: () => (
<NavBar
handleClose={mockClose}
handleBack={mockBack}
showBackArrow={showBackArrow}
/>
),
await renderWithInstanceAndLayout({
renderer: () => <NavBar type={type} />,
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);
expect(screen.queryByTestId('form-back-button')).toBeNull();
});

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' }],
});
Expand All @@ -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' },
Expand Down
Loading

0 comments on commit b11facd

Please sign in to comment.