From 1ce7ece523dfad5e26ad38b4a86838003bd6f57c Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Fri, 10 Nov 2023 13:58:23 +0100 Subject: [PATCH 1/4] refactor: remove tracks functionality --- src/__mocks__/uiConfigStateMock.ts | 3 +- src/codegen/Common.ts | 4 - src/components/presentation/Header.test.tsx | 8 +- src/components/wrappers/Presentation.tsx | 22 ++- .../DevNavigationButtons.tsx | 6 +- src/features/expressions/ExprContext.ts | 2 +- .../layout/fetch/fetchFormLayoutSagas.test.ts | 6 - .../layout/fetch/fetchFormLayoutSagas.ts | 3 - src/features/layout/formLayoutSlice.test.ts | 7 +- src/features/layout/formLayoutSlice.ts | 31 ++--- src/features/layout/formLayoutTypes.ts | 15 +-- .../update/updateFormLayoutSagas.test.ts | 92 ++++--------- .../layout/update/updateFormLayoutSagas.ts | 127 +++--------------- src/hooks/usePdfPage.ts | 14 +- .../Group/RepeatingGroupsTable.test.tsx | 2 +- .../Group/SummaryGroupComponent.test.tsx | 3 +- ...epeatingGroupsLikertContainerTestUtils.tsx | 3 +- .../NavigationBarComponent.test.tsx | 2 +- .../NavigationBar/NavigationBarComponent.tsx | 2 +- .../NavigationButtonsComponent.test.tsx | 10 +- .../NavigationButtonsComponent.tsx | 40 +----- src/selectors/getLayoutOrder.test.ts | 8 +- src/selectors/getLayoutOrder.ts | 40 ++++-- src/types/index.ts | 12 +- src/utils/formLayout.ts | 27 ---- src/utils/layout/ExprContext.tsx | 11 +- src/utils/layout/LayoutPage.ts | 4 +- src/utils/urls/appUrlHelper.test.ts | 15 --- src/utils/urls/appUrlHelper.ts | 8 -- .../integration/frontend-test/validation.ts | 4 +- 30 files changed, 141 insertions(+), 390 deletions(-) diff --git a/src/__mocks__/uiConfigStateMock.ts b/src/__mocks__/uiConfigStateMock.ts index 0d02f38d91..0365895a09 100644 --- a/src/__mocks__/uiConfigStateMock.ts +++ b/src/__mocks__/uiConfigStateMock.ts @@ -2,7 +2,7 @@ import type { IUiConfig } from 'src/types'; export const getUiConfigStateMock = (customStates?: Partial): IUiConfig => ({ focus: null, - tracks: { + pageOrderConfig: { hidden: [], hiddenExpr: {}, order: ['FormLayout'], @@ -23,7 +23,6 @@ export const getUiConfigStateMock = (customStates?: Partial): IUiConf }, }, currentView: 'FormLayout', - navigationConfig: {}, excludePageFromPdf: [], excludeComponentFromPdf: [], ...customStates, diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index d953884063..eb4bf0e285 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -35,14 +35,11 @@ const common = { .setDescription('Expression that will hide the page/form layout if true') .optional({ default: false }), ), - new CG.prop('navigation', CG.common('ILayoutNavigation').optional()), ), ), ) .setTitle('Altinn layout') .setDescription('Schema that describes the layout configuration for Altinn applications.'), - ILayoutNavigation: () => - new CG.obj(new CG.prop('next', new CG.str().optional()), new CG.prop('previous', new CG.str().optional())), ILabelSettings: () => new CG.obj( @@ -109,7 +106,6 @@ const common = { Triggers: () => new CG.enum( 'validation', - 'calculatePageOrder', 'validatePage', 'validateCurrentAndPreviousPages', 'validateAllPages', diff --git a/src/components/presentation/Header.test.tsx b/src/components/presentation/Header.test.tsx index 44a0719aea..52cc04560f 100644 --- a/src/components/presentation/Header.test.tsx +++ b/src/components/presentation/Header.test.tsx @@ -47,8 +47,8 @@ describe('Header', () => { ...mockFormLayout.uiConfig, showProgress: true, currentView: '3', - tracks: { - ...mockFormLayout.uiConfig.tracks, + pageOrderConfig: { + ...mockFormLayout.uiConfig.pageOrderConfig, order: ['1', '2', '3', '4', '5', '6'], }, }, @@ -67,8 +67,8 @@ describe('Header', () => { ...mockFormLayout.uiConfig, showProgress: true, currentView: '3', - tracks: { - ...mockFormLayout.uiConfig.tracks, + pageOrderConfig: { + ...mockFormLayout.uiConfig.pageOrderConfig, order: ['1', '2', '3', '4', '5', '6'], }, }, diff --git a/src/components/wrappers/Presentation.tsx b/src/components/wrappers/Presentation.tsx index 80e2c2225b..04edebe929 100644 --- a/src/components/wrappers/Presentation.tsx +++ b/src/components/wrappers/Presentation.tsx @@ -12,10 +12,9 @@ import { FormLayoutActions } from 'src/features/layout/formLayoutSlice'; import { useAppDispatch } from 'src/hooks/useAppDispatch'; import { useAppSelector } from 'src/hooks/useAppSelector'; import { useLanguage } from 'src/hooks/useLanguage'; -import { getLayoutOrderFromTracks } from 'src/selectors/getLayoutOrder'; +import { selectPreviousAndNextPage } from 'src/selectors/getLayoutOrder'; import { AltinnAppTheme } from 'src/theme/altinnAppTheme'; import { PresentationType, ProcessTaskType } from 'src/types'; -import { getNextView } from 'src/utils/formLayout'; import { httpGet } from 'src/utils/network/networking'; import { getRedirectUrl } from 'src/utils/urls/appUrlHelper'; import { returnUrlFromQueryParameter, returnUrlToMessagebox } from 'src/utils/urls/urlHelper'; @@ -34,16 +33,8 @@ export const PresentationComponent = (props: IPresentationProvidedProps) => { const instance = useAppSelector((state) => state.instanceData?.instance); const userParty = useAppSelector((state) => state.profile.profile?.party); const { expandedWidth } = useAppSelector((state) => state.formLayout.uiConfig); + const { previous } = useAppSelector(selectPreviousAndNextPage); - const previousFormPage: string = useAppSelector((state) => - getNextView( - state.formLayout.uiConfig.navigationConfig && - state.formLayout.uiConfig.navigationConfig[state.formLayout.uiConfig.currentView], - getLayoutOrderFromTracks(state.formLayout.uiConfig.tracks), - state.formLayout.uiConfig.currentView, - true, - ), - ); const returnToView = useAppSelector((state) => state.formLayout.uiConfig.returnToView); const handleBackArrowButton = () => { @@ -53,10 +44,13 @@ export const PresentationComponent = (props: IPresentationProvidedProps) => { newView: returnToView, }), ); - } else if (props.type === ProcessTaskType.Data || props.type === PresentationType.Stateless) { + } else if ( + previous !== undefined && + (props.type === ProcessTaskType.Data || props.type === PresentationType.Stateless) + ) { dispatch( FormLayoutActions.updateCurrentView({ - newView: previousFormPage, + newView: previous, }), ); } @@ -105,7 +99,7 @@ export const PresentationComponent = (props: IPresentationProvidedProps) => { handleClose={handleModalCloseButton} handleBack={handleBackArrowButton} showBackArrow={ - !!previousFormPage && (props.type === ProcessTaskType.Data || props.type === PresentationType.Stateless) + !!previous && (props.type === ProcessTaskType.Data || props.type === PresentationType.Stateless) } />
{ - const { currentView, tracks } = useAppSelector((state) => state.formLayout.uiConfig); + const { currentView, pageOrderConfig } = useAppSelector((state) => state.formLayout.uiConfig); const ctx = useExprContext(); const dispatch = useDispatch(); - const order = tracks?.order ?? []; + const order = pageOrderConfig?.order ?? []; const allPages = ctx?.allPageKeys() || []; function handleChange(newView: string) { @@ -21,7 +21,7 @@ export const DevNavigationButtons = () => { } function isHidden(page: string) { - return tracks?.hidden.includes(page); + return pageOrderConfig?.hidden.includes(page); } function isHiddenLegacy(page: string) { diff --git a/src/features/expressions/ExprContext.ts b/src/features/expressions/ExprContext.ts index db0a48c789..53db234b53 100644 --- a/src/features/expressions/ExprContext.ts +++ b/src/features/expressions/ExprContext.ts @@ -31,7 +31,7 @@ export interface PrettyErrorsOptions { } /** - * The expression context object is passed around when executing/evaluating a expression, and is + * The expression context object is passed around when executing/evaluating an expression, and is * a toolbox for expressions to resolve lookups in data sources, getting the current node, etc. */ export class ExprContext { diff --git a/src/features/layout/fetch/fetchFormLayoutSagas.test.ts b/src/features/layout/fetch/fetchFormLayoutSagas.test.ts index 5396d2528c..f5e7347db8 100644 --- a/src/features/layout/fetch/fetchFormLayoutSagas.test.ts +++ b/src/features/layout/fetch/fetchFormLayoutSagas.test.ts @@ -89,7 +89,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { page1: [] }, - navigationConfig: { page1: undefined }, hiddenLayoutsExpressions: { ...hiddenExprPage1 }, layoutSetId: null, }), @@ -115,7 +114,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { FormLayout: [] }, - navigationConfig: {}, hiddenLayoutsExpressions: { FormLayout: hiddenExprPage1['page1'] }, layoutSetId: null, }), @@ -161,7 +159,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { page1: [], page2: [] }, - navigationConfig: { page1: undefined, page2: undefined }, hiddenLayoutsExpressions: { ...hiddenExprPage1, page2: undefined, @@ -193,7 +190,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { page1: [], page2: [] }, - navigationConfig: { page1: undefined, page2: undefined }, hiddenLayoutsExpressions: { ...hiddenExprPage1, ...hiddenExprPage2, @@ -222,7 +218,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { page1: [] }, - navigationConfig: { page1: undefined }, hiddenLayoutsExpressions: { ...hiddenExprPage1 }, layoutSetId: null, }), @@ -248,7 +243,6 @@ describe('fetchFormLayoutSagas', () => { .put( FormLayoutActions.fetchFulfilled({ layouts: { page1: [] }, - navigationConfig: { page1: undefined }, hiddenLayoutsExpressions: { ...hiddenExprPage1 }, layoutSetId: null, }), diff --git a/src/features/layout/fetch/fetchFormLayoutSagas.ts b/src/features/layout/fetch/fetchFormLayoutSagas.ts index b810947aee..e17c750017 100644 --- a/src/features/layout/fetch/fetchFormLayoutSagas.ts +++ b/src/features/layout/fetch/fetchFormLayoutSagas.ts @@ -56,7 +56,6 @@ export function* fetchLayoutSaga(): SagaIterator { getLayoutsUrl(layoutSetId || null), ); const layouts: ILayouts = {}; - const navigationConfig: any = {}; const hiddenLayoutsExpressions: IHiddenLayoutsExternal = {}; let firstLayoutKey: string; if ('data' in layoutResponse && 'layout' in layoutResponse.data && layoutResponse.data.layout) { @@ -81,7 +80,6 @@ export function* fetchLayoutSaga(): SagaIterator { const file: ILayoutFileExternal = layoutResponse[key]; layouts[key] = cleanLayout(file.data.layout); hiddenLayoutsExpressions[key] = file.data.hidden; - navigationConfig[key] = file.data.navigation; }); } @@ -100,7 +98,6 @@ export function* fetchLayoutSaga(): SagaIterator { yield put( FormLayoutActions.fetchFulfilled({ layouts, - navigationConfig, hiddenLayoutsExpressions, layoutSetId: layoutSetId ?? null, }), diff --git a/src/features/layout/formLayoutSlice.test.ts b/src/features/layout/formLayoutSlice.test.ts index 8bc5f81781..3c4dfc2d28 100644 --- a/src/features/layout/formLayoutSlice.test.ts +++ b/src/features/layout/formLayoutSlice.test.ts @@ -6,7 +6,6 @@ describe('layoutSlice', () => { describe('fetchLayoutFulfilled', () => { const layouts = {}; - const navigationConfig = {}; const hiddenLayoutsExpressions = {}; it('should set layout state accordingly', () => { @@ -14,15 +13,13 @@ describe('layoutSlice', () => { initialState, FormLayoutActions.fetchFulfilled({ layouts, - navigationConfig, hiddenLayoutsExpressions, layoutSetId: null, }), ); expect(nextState.layouts).toEqual(layouts); - expect(nextState.uiConfig.tracks.order).toEqual(Object.keys(layouts)); - expect(nextState.uiConfig.navigationConfig).toEqual(navigationConfig); + expect(nextState.uiConfig.pageOrderConfig.order).toEqual(Object.keys(layouts)); }); it('should reset repeatingGroups if set', () => { @@ -41,7 +38,6 @@ describe('layoutSlice', () => { stateWithRepGroups, FormLayoutActions.fetchFulfilled({ layouts, - navigationConfig, hiddenLayoutsExpressions, layoutSetId: null, }), @@ -59,7 +55,6 @@ describe('layoutSlice', () => { stateWithError, FormLayoutActions.fetchFulfilled({ layouts, - navigationConfig, hiddenLayoutsExpressions, layoutSetId: null, }), diff --git a/src/features/layout/formLayoutSlice.ts b/src/features/layout/formLayoutSlice.ts index 63bb8c1db6..d10a17a9fb 100644 --- a/src/features/layout/formLayoutSlice.ts +++ b/src/features/layout/formLayoutSlice.ts @@ -12,10 +12,9 @@ import { repGroupAddRowSaga } from 'src/features/layout/repGroups/repGroupAddRow import { repGroupDeleteRowSaga } from 'src/features/layout/repGroups/repGroupDeleteRowSaga'; import { updateRepeatingGroupEditIndexSaga } from 'src/features/layout/repGroups/updateRepeatingGroupEditIndexSaga'; import { - calculatePageOrderAndMoveToNextPageSaga, findAndMoveToNextVisibleLayout, + moveToNextPageSaga, updateCurrentViewSaga, - watchInitialCalculatePageOrderAndMoveToNextPageSaga, } from 'src/features/layout/update/updateFormLayoutSagas'; import { createSagaSlice } from 'src/redux/sagaSlice'; import type * as LayoutTypes from 'src/features/layout/formLayoutTypes'; @@ -41,8 +40,7 @@ export const initialState: ILayoutState = { repeatingGroups: null, receiptLayoutName: undefined, currentView: 'FormLayout', - navigationConfig: {}, - tracks: { + pageOrderConfig: { hidden: [], hiddenExpr: {}, order: null, @@ -76,18 +74,16 @@ export const formLayoutSlice = () => { return { name: 'formLayout', initialState, - extraSagas: [watchInitialCalculatePageOrderAndMoveToNextPageSaga], actions: { fetch: mkAction({ saga: () => watchFetchFormLayoutSaga, }), fetchFulfilled: mkAction({ reducer: (state, action) => { - const { layouts, navigationConfig, hiddenLayoutsExpressions, layoutSetId } = action.payload; + const { layouts, hiddenLayoutsExpressions, layoutSetId } = action.payload; state.layouts = layouts; - state.uiConfig.navigationConfig = navigationConfig; - state.uiConfig.tracks.order = Object.keys(layouts); - state.uiConfig.tracks.hiddenExpr = hiddenLayoutsExpressions; + state.uiConfig.pageOrderConfig.order = Object.keys(layouts); + state.uiConfig.pageOrderConfig.hiddenExpr = hiddenLayoutsExpressions; state.error = null; state.uiConfig.repeatingGroups = null; state.layoutSetId = layoutSetId; @@ -121,7 +117,7 @@ export const formLayoutSlice = () => { updateCommonPageSettings(state, settings.pages); const order = settings.pages.order; if (order) { - state.uiConfig.tracks.order = order; + state.uiConfig.pageOrderConfig.order = order; if (state.uiConfig.currentViewCacheKey) { let currentView: string; const lastVisitedPage = localStorage.getItem(state.uiConfig.currentViewCacheKey); @@ -241,21 +237,14 @@ export const formLayoutSlice = () => { } }, }), - calculatePageOrderAndMoveToNextPage: mkAction({ - takeEvery: calculatePageOrderAndMoveToNextPageSaga, + moveToNextPage: mkAction({ + takeEvery: moveToNextPageSaga, }), - calculatePageOrderAndMoveToNextPageFulfilled: - mkAction({ - reducer: (state, action) => { - const { order } = action.payload; - state.uiConfig.tracks.order = order; - }, - }), - calculatePageOrderAndMoveToNextPageRejected: genericReject, + moveToNextPageRejected: genericReject, updateHiddenLayouts: mkAction({ takeEvery: findAndMoveToNextVisibleLayout, reducer: (state, action) => { - state.uiConfig.tracks.hidden = action.payload.hiddenLayouts; + state.uiConfig.pageOrderConfig.hidden = action.payload.hiddenLayouts; }, }), initRepeatingGroups: mkAction({ diff --git a/src/features/layout/formLayoutTypes.ts b/src/features/layout/formLayoutTypes.ts index 6ab738bc79..c77220df43 100644 --- a/src/features/layout/formLayoutTypes.ts +++ b/src/features/layout/formLayoutTypes.ts @@ -1,13 +1,7 @@ import type { IFormData } from 'src/features/formData'; import type { Triggers } from 'src/layout/common.generated'; import type { ILayouts } from 'src/layout/layout'; -import type { - IHiddenLayoutsExternal, - ILayoutSets, - ILayoutSettings, - INavigationConfig, - TriggersPageValidation, -} from 'src/types'; +import type { IHiddenLayoutsExternal, ILayoutSets, ILayoutSettings, TriggersPageValidation } from 'src/types'; export interface IFormLayoutActionRejected { error: Error | null; @@ -16,7 +10,6 @@ export interface IFormLayoutActionRejected { export interface IFetchLayoutFulfilled { layouts: ILayouts; - navigationConfig?: INavigationConfig; hiddenLayoutsExpressions: IHiddenLayoutsExternal; layoutSetId: string | null; } @@ -86,16 +79,12 @@ export interface IKeepComponentScrollPos { offsetTop: number | undefined; } -export interface ICalculatePageOrderAndMoveToNextPage { +export interface IMoveToNextPage { runValidations?: TriggersPageValidation; skipMoveToNext?: boolean; keepScrollPos?: IKeepComponentScrollPos; } -export interface ICalculatePageOrderAndMoveToNextPageFulfilled { - order: string[]; -} - export interface IHiddenLayoutsUpdate { hiddenLayouts: string[]; } diff --git a/src/features/layout/update/updateFormLayoutSagas.test.ts b/src/features/layout/update/updateFormLayoutSagas.test.ts index 94db6b2bc1..c032b75017 100644 --- a/src/features/layout/update/updateFormLayoutSagas.test.ts +++ b/src/features/layout/update/updateFormLayoutSagas.test.ts @@ -3,17 +3,19 @@ import { select } from 'redux-saga/effects'; import { expectSaga } from 'redux-saga-test-plan'; import type { PayloadAction } from '@reduxjs/toolkit'; +import { getFormLayoutStateMock } from 'src/__mocks__/formLayoutStateMock'; import { getInitialStateMock } from 'src/__mocks__/initialStateMock'; +import { getUiConfigStateMock } from 'src/__mocks__/uiConfigStateMock'; import { FormLayoutActions } from 'src/features/layout/formLayoutSlice'; import { - calculatePageOrderAndMoveToNextPageSaga, findAndMoveToNextVisibleLayout, + moveToNextPageSaga, selectAllLayouts, selectCurrentLayout, } from 'src/features/layout/update/updateFormLayoutSagas'; import { selectLayoutOrder } from 'src/selectors/getLayoutOrder'; import type { IApplicationMetadata } from 'src/features/applicationMetadata'; -import type { ICalculatePageOrderAndMoveToNextPage } from 'src/features/layout/formLayoutTypes'; +import type { IMoveToNextPage } from 'src/features/layout/formLayoutTypes'; import type { IRuntimeState } from 'src/types'; describe('updateLayoutSagas', () => { @@ -21,22 +23,28 @@ describe('updateLayoutSagas', () => { mockAxios.reset(); }); - describe('calculatePageOrderAndMoveToNextPageSaga', () => { - const state = getInitialStateMock(); - const orderResponse = ['page-1', 'FormLayout', 'page-3']; + describe('moveToNextPageSaga', () => { + const state = getInitialStateMock({ + formLayout: getFormLayoutStateMock({ + uiConfig: getUiConfigStateMock({ + currentView: 'FormLayout', + returnToView: undefined, + pageOrderConfig: { + order: ['page-1', 'FormLayout', 'page-3'], + hidden: [], + hiddenExpr: {}, + }, + }), + }), + }); it('should fetch pageOrder and update state accordingly', () => { - const action: PayloadAction = { + const action: PayloadAction = { type: 'test', payload: {}, }; - const exp = expectSaga(calculatePageOrderAndMoveToNextPageSaga, action) + return expectSaga(moveToNextPageSaga, action) .provide([[select(), state]]) - .put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled({ - order: orderResponse, - }), - ) .put( FormLayoutActions.updateCurrentView({ newView: 'page-3', @@ -45,35 +53,22 @@ describe('updateLayoutSagas', () => { }), ) .run(); - - mockAxios.mockResponse({ data: orderResponse }); - - return exp; }); it('should not update current view if skipMoveToNext is true', () => { - const action: PayloadAction = { + const action: PayloadAction = { type: 'test', payload: { skipMoveToNext: true, }, }; - const exp = expectSaga(calculatePageOrderAndMoveToNextPageSaga, action) + return expectSaga(moveToNextPageSaga, action) .provide([[select(), state]]) - .put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled({ - order: orderResponse, - }), - ) .run(); - - mockAxios.mockResponse({ data: orderResponse }); - - return exp; }); it('stateless: should fetch pageOrder and update state accordingly', () => { - const action: PayloadAction = { + const action: PayloadAction = { type: 'test', payload: { keepScrollPos: { @@ -100,13 +95,8 @@ describe('updateLayoutSagas', () => { }, }, }; - const exp = expectSaga(calculatePageOrderAndMoveToNextPageSaga, action) + return expectSaga(moveToNextPageSaga, action) .provide([[select(), stateWithStatelessApp]]) - .put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled({ - order: orderResponse, - }), - ) .put( FormLayoutActions.updateCurrentView({ newView: 'page-3', @@ -118,14 +108,10 @@ describe('updateLayoutSagas', () => { }), ) .run(); - - mockAxios.mockResponse({ data: orderResponse }); - - return exp; }); it('should set new page to returnToView if set in state', () => { - const action: PayloadAction = { + const action: PayloadAction = { type: 'test', payload: {}, }; @@ -139,13 +125,8 @@ describe('updateLayoutSagas', () => { }, }, }; - const exp = expectSaga(calculatePageOrderAndMoveToNextPageSaga, action) + return expectSaga(moveToNextPageSaga, action) .provide([[select(), stateWithReturnToView]]) - .put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled({ - order: orderResponse, - }), - ) .put( FormLayoutActions.updateCurrentView({ newView: 'return-here', @@ -154,27 +135,6 @@ describe('updateLayoutSagas', () => { }), ) .run(); - mockAxios.mockResponse({ data: orderResponse }); - - return exp; - }); - - it('should call rejected action if fetching of order fails', () => { - const action = { type: 'test', payload: {} }; - const error = new Error('mock'); - - const exp = expectSaga(calculatePageOrderAndMoveToNextPageSaga, action) - .provide([[select(), state]]) - .put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected({ - error, - }), - ) - .run(); - - mockAxios.mockError(error); - - return exp; }); }); diff --git a/src/features/layout/update/updateFormLayoutSagas.ts b/src/features/layout/update/updateFormLayoutSagas.ts index ed2b461824..bb67ea9aa8 100644 --- a/src/features/layout/update/updateFormLayoutSagas.ts +++ b/src/features/layout/update/updateFormLayoutSagas.ts @@ -1,23 +1,19 @@ -import { all, call, put, select, take } from 'redux-saga/effects'; +import { call, put, select, take } from 'redux-saga/effects'; import type { PayloadAction } from '@reduxjs/toolkit'; -import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import type { AxiosRequestConfig } from 'axios'; import type { SagaIterator } from 'redux-saga'; import { FormDataActions } from 'src/features/formData/formDataSlice'; import { FormLayoutActions } from 'src/features/layout/formLayoutSlice'; -import { QueueActions } from 'src/features/queue/queueSlice'; import { ValidationActions } from 'src/features/validation/validationSlice'; import { staticUseLanguageFromState } from 'src/hooks/useLanguage'; import { Triggers } from 'src/layout/common.generated'; -import { getLayoutOrderFromTracks, selectLayoutOrder } from 'src/selectors/getLayoutOrder'; -import { getCurrentDataTypeForApplication, getCurrentTaskDataElementId, isStatelessApp } from 'src/utils/appMetadata'; -import { convertDataBindingToModel } from 'src/utils/databindings'; -import { getLayoutsetForDataElement } from 'src/utils/layout'; +import { getLayoutOrderFromPageOrderConfig, selectLayoutOrder } from 'src/selectors/getLayoutOrder'; +import { getCurrentTaskDataElementId } from 'src/utils/appMetadata'; import { ResolvedNodesSelector } from 'src/utils/layout/hierarchy'; -import { httpPost } from 'src/utils/network/networking'; import { httpGet } from 'src/utils/network/sharedNetworking'; import { waitFor } from 'src/utils/sagas'; -import { getCalculatePageOrderUrl, getDataValidationUrl } from 'src/utils/urls/appUrlHelper'; +import { getDataValidationUrl } from 'src/utils/urls/appUrlHelper'; import { mapValidationIssues } from 'src/utils/validation/backendValidation'; import { containsErrors, @@ -25,7 +21,7 @@ import { filterValidationObjectsByPage, validationContextFromState, } from 'src/utils/validation/validationHelpers'; -import type { ICalculatePageOrderAndMoveToNextPage, IUpdateCurrentView } from 'src/features/layout/formLayoutTypes'; +import type { IMoveToNextPage, IUpdateCurrentView } from 'src/features/layout/formLayoutTypes'; import type { IRuntimeState, IUiConfig } from 'src/types'; import type { LayoutPages } from 'src/utils/layout/LayoutPages'; import type { BackendValidationIssue } from 'src/utils/validation/types'; @@ -34,7 +30,7 @@ export const selectFormLayoutState = (state: IRuntimeState) => state.formLayout; export const selectFormData = (state: IRuntimeState) => state.formData.formData; export const selectFormLayouts = (state: IRuntimeState) => state.formLayout.layouts; export const selectAttachmentState = (state: IRuntimeState) => state.attachments; -export const selectAllLayouts = (state: IRuntimeState) => state.formLayout.uiConfig.tracks.order; +export const selectAllLayouts = (state: IRuntimeState) => state.formLayout.uiConfig.pageOrderConfig.order; export const selectCurrentLayout = (state: IRuntimeState) => state.formLayout.uiConfig.currentView; const selectUiConfig = (state: IRuntimeState) => state.formLayout.uiConfig; @@ -176,89 +172,29 @@ export function* updateCurrentViewSaga({ } } -export function* calculatePageOrderAndMoveToNextPageSaga({ +export function* moveToNextPageSaga({ payload: { runValidations, skipMoveToNext, keepScrollPos }, -}: PayloadAction): SagaIterator { +}: PayloadAction): SagaIterator { try { const state: IRuntimeState = yield select(); - const layoutSets = state.formLayout.layoutsets; const currentView = state.formLayout.uiConfig.currentView; - const formData = convertDataBindingToModel(state.formData.formData); if (!state.applicationMetadata.applicationMetadata) { - yield put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected({ - error: null, - }), - ); + yield put(FormLayoutActions.moveToNextPageRejected({ error: null })); return; } - let layoutSetId: string | null = null; - const dataTypeId = - getCurrentDataTypeForApplication({ - application: state.applicationMetadata.applicationMetadata, - instance: state.instanceData.instance, - layoutSets: state.formLayout.layoutsets, - }) || null; - - const appIsStateless = isStatelessApp(state.applicationMetadata.applicationMetadata); - if (appIsStateless) { - layoutSetId = state.applicationMetadata.applicationMetadata.onEntry?.show || null; - } else { - const instance = state.instanceData.instance; - if (layoutSets != null) { - layoutSetId = getLayoutsetForDataElement(instance, dataTypeId || undefined, layoutSets) || null; - } - } - const layoutOrderResponse: AxiosResponse = yield call( - httpPost, - getCalculatePageOrderUrl(appIsStateless), - { - params: { - currentPage: currentView, - layoutSetId, - dataTypeId, - }, - headers: { - 'Content-Type': 'application/json', - }, - }, - formData, - ); - const layoutOrder = layoutOrderResponse.data ? layoutOrderResponse.data : null; - yield put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageFulfilled({ - order: layoutOrder, - }), - ); if (skipMoveToNext) { return; } + const returnToView = state.formLayout.uiConfig.returnToView; - const newOrder = - getLayoutOrderFromTracks({ - ...state.formLayout.uiConfig.tracks, - order: layoutOrder, - }) || []; - const newView = returnToView || newOrder[newOrder.indexOf(currentView) + 1]; - yield put( - FormLayoutActions.updateCurrentView({ - newView, - runValidations, - keepScrollPos, - }), - ); + const layoutOrder = getLayoutOrderFromPageOrderConfig(state.formLayout.uiConfig.pageOrderConfig) || []; + const newView = returnToView || layoutOrder[layoutOrder.indexOf(currentView) + 1]; + + yield put(FormLayoutActions.updateCurrentView({ newView, runValidations, keepScrollPos })); } catch (error) { - if (error?.response?.status === 404) { - // We accept that the app does noe have defined a calculate page order as this is not default for older apps - } else { - yield put( - FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected({ - error, - }), - ); - } + yield put(FormLayoutActions.moveToNextPageRejected({ error })); } } @@ -295,34 +231,3 @@ export function* findAndMoveToNextVisibleLayout(): SagaIterator { ); } } - -export function* watchInitialCalculatePageOrderAndMoveToNextPageSaga(): SagaIterator { - while (true) { - yield all([ - take(QueueActions.startInitialDataTaskQueue), - take(FormLayoutActions.fetchFulfilled), - take(FormLayoutActions.fetchSettingsFulfilled), - ]); - const state: IRuntimeState = yield select(); - const layouts = state.formLayout.layouts || {}; - const pageTriggers = state.formLayout.uiConfig.pageTriggers; - const appHasCalculateTrigger = - pageTriggers?.includes(Triggers.CalculatePageOrder) || - Object.keys(layouts).some( - (layout) => - layouts[layout]?.some( - (element) => - element.type === 'NavigationButtons' && element.triggers?.includes(Triggers.CalculatePageOrder), - ), - ); - if (appHasCalculateTrigger) { - yield put( - FormLayoutActions.calculatePageOrderAndMoveToNextPage({ - skipMoveToNext: true, - }), - ); - } else { - yield put(FormLayoutActions.calculatePageOrderAndMoveToNextPageRejected({ error: null })); - } - } -} diff --git a/src/hooks/usePdfPage.ts b/src/hooks/usePdfPage.ts index a7eb44252e..51382daba2 100644 --- a/src/hooks/usePdfPage.ts +++ b/src/hooks/usePdfPage.ts @@ -10,7 +10,7 @@ import type { IPdfFormat } from 'src/features/pdf/types'; import type { CompInstanceInformationExternal } from 'src/layout/InstanceInformation/config.generated'; import type { HierarchyDataSources, ILayout } from 'src/layout/layout'; import type { CompSummaryExternal } from 'src/layout/Summary/config.generated'; -import type { IRepeatingGroups, ITracks } from 'src/types'; +import type { IPageOrderConfig, IRepeatingGroups } from 'src/types'; import type { LayoutPage } from 'src/utils/layout/LayoutPage'; import type { LayoutPages } from 'src/utils/layout/LayoutPages'; @@ -19,7 +19,7 @@ const PDF_LAYOUT_NAME = '__pdf__'; export const usePdfPage = (): LayoutPage | null => { const layoutPages = useExprContext(); const dataSources = useAppSelector(dataSourcesFromState); - const tracks = useAppSelector((state) => state.formLayout.uiConfig.tracks); + const pageOrderConfig = useAppSelector((state) => state.formLayout.uiConfig.pageOrderConfig); const repeatingGroups = useAppSelector((state) => state.formLayout.uiConfig.repeatingGroups); const pdfLayoutName = useAppSelector((state) => state.formLayout.uiConfig.pdfLayoutName); @@ -32,10 +32,10 @@ export const usePdfPage = (): LayoutPage | null => { const automaticPdfPage = useMemo(() => { if (readyForPrint && method === 'auto') { - return generateAutomaticPage(pdfFormat!, tracks!, layoutPages!, dataSources, repeatingGroups!); + return generateAutomaticPage(pdfFormat!, pageOrderConfig!, layoutPages!, dataSources, repeatingGroups!); } return null; - }, [readyForPrint, method, pdfFormat, tracks, layoutPages, dataSources, repeatingGroups]); + }, [readyForPrint, method, pdfFormat, pageOrderConfig, layoutPages, dataSources, repeatingGroups]); if (!readyForPrint) { return null; @@ -50,7 +50,7 @@ export const usePdfPage = (): LayoutPage | null => { function generateAutomaticPage( pdfFormat: IPdfFormat, - tracks: ITracks, + pageOrderConfig: IPageOrderConfig, layoutPages: LayoutPages, dataSources: HierarchyDataSources, repeatingGroups: IRepeatingGroups, @@ -75,8 +75,8 @@ function generateAutomaticPage( const excludedPages = new Set(pdfFormat?.excludedPages); const excludedComponents = new Set(pdfFormat?.excludedComponents); - const hiddenPages = new Set(tracks.hidden); - const pageOrder = tracks.order; + const hiddenPages = new Set(pageOrderConfig.hidden); + const pageOrder = pageOrderConfig.order; // Iterate over all pages, and add all components that should be included in the automatic PDF as summary components Object.entries(layoutPages.all()) diff --git a/src/layout/Group/RepeatingGroupsTable.test.tsx b/src/layout/Group/RepeatingGroupsTable.test.tsx index 19433f8946..ab5604cfcd 100644 --- a/src/layout/Group/RepeatingGroupsTable.test.tsx +++ b/src/layout/Group/RepeatingGroupsTable.test.tsx @@ -40,7 +40,7 @@ const getLayout = (group: CompGroupRepeatingExternal, components: CompOrGroupExt }, currentView: 'FormLayout', focus: undefined, - tracks: { + pageOrderConfig: { order: ['FormLayout'], hidden: [], hiddenExpr: {}, diff --git a/src/layout/Group/SummaryGroupComponent.test.tsx b/src/layout/Group/SummaryGroupComponent.test.tsx index aa1484878c..60ab94e9e0 100644 --- a/src/layout/Group/SummaryGroupComponent.test.tsx +++ b/src/layout/Group/SummaryGroupComponent.test.tsx @@ -78,8 +78,7 @@ describe('SummaryGroupComponent', () => { }, }, currentView: 'page1', - navigationConfig: {}, - tracks: { + pageOrderConfig: { order: ['page1'], hidden: [], hiddenExpr: {}, diff --git a/src/layout/Likert/RepeatingGroupsLikertContainerTestUtils.tsx b/src/layout/Likert/RepeatingGroupsLikertContainerTestUtils.tsx index f07ab4faa4..6c1d1f1421 100644 --- a/src/layout/Likert/RepeatingGroupsLikertContainerTestUtils.tsx +++ b/src/layout/Likert/RepeatingGroupsLikertContainerTestUtils.tsx @@ -129,8 +129,7 @@ const createLayout = ( }, currentView: 'FormLayout', focus: null, - navigationConfig: {}, - tracks: { + pageOrderConfig: { order: null, hidden: [], hiddenExpr: {}, diff --git a/src/layout/NavigationBar/NavigationBarComponent.test.tsx b/src/layout/NavigationBar/NavigationBarComponent.test.tsx index b33097ae10..d16ed78372 100644 --- a/src/layout/NavigationBar/NavigationBarComponent.test.tsx +++ b/src/layout/NavigationBar/NavigationBarComponent.test.tsx @@ -30,7 +30,7 @@ const render = ({ dispatch = jest.fn() }: Props = {}) => { layoutsets: null, layoutSetId: null, uiConfig: { - tracks: { + pageOrderConfig: { order: ['page1', 'page2', 'page3'], hiddenExpr: {}, hidden: [], diff --git a/src/layout/NavigationBar/NavigationBarComponent.tsx b/src/layout/NavigationBar/NavigationBarComponent.tsx index 3a04b421a0..31a5d23f16 100644 --- a/src/layout/NavigationBar/NavigationBarComponent.tsx +++ b/src/layout/NavigationBar/NavigationBarComponent.tsx @@ -190,7 +190,7 @@ export const NavigationBarComponent = ({ node }: INavigationBar) => { {pageIds.map((pageId, index) => (
  • { focus: null, hiddenFields: [], repeatingGroups: {}, - tracks: { + pageOrderConfig: { order: ['layout1', 'layout2'], hidden: [], hiddenExpr: {}, }, excludePageFromPdf: [], excludeComponentFromPdf: [], - navigationConfig: { - layout1: { - next: 'layout2', - }, - layout2: { - previous: 'layout1', - }, - }, }, }); diff --git a/src/layout/NavigationButtons/NavigationButtonsComponent.tsx b/src/layout/NavigationButtons/NavigationButtonsComponent.tsx index 28ea203236..761365fbbc 100644 --- a/src/layout/NavigationButtons/NavigationButtonsComponent.tsx +++ b/src/layout/NavigationButtons/NavigationButtonsComponent.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { shallowEqual } from 'react-redux'; import { Button } from '@digdir/design-system-react'; import { Grid } from '@material-ui/core'; @@ -8,16 +7,12 @@ import { FormLayoutActions } from 'src/features/layout/formLayoutSlice'; import { useAppDispatch } from 'src/hooks/useAppDispatch'; import { useAppSelector } from 'src/hooks/useAppSelector'; import { useLanguage } from 'src/hooks/useLanguage'; -import { Triggers } from 'src/layout/common.generated'; import classes from 'src/layout/NavigationButtons/NavigationButtonsComponent.module.css'; -import { getLayoutOrderFromTracks, selectLayoutOrder } from 'src/selectors/getLayoutOrder'; +import { selectLayoutOrder, selectPreviousAndNextPage } from 'src/selectors/getLayoutOrder'; import { reducePageValidations } from 'src/types'; -import { getNextView } from 'src/utils/formLayout'; import { LayoutPage } from 'src/utils/layout/LayoutPage'; import type { IKeepComponentScrollPos } from 'src/features/layout/formLayoutTypes'; import type { PropsFromGenericComponent } from 'src/layout'; -import type { ILayoutNavigation } from 'src/layout/common.generated'; -import type { IRuntimeState } from 'src/types'; export type INavigationButtons = PropsFromGenericComponent<'NavigationButtons'>; export function NavigationButtonsComponent({ node }: INavigationButtons) { @@ -34,7 +29,7 @@ export function NavigationButtonsComponent({ node }: INavigationButtons) { const orderedLayoutKeys = useAppSelector(selectLayoutOrder); const returnToView = useAppSelector((state) => state.formLayout.uiConfig.returnToView); const pageTriggers = useAppSelector((state) => state.formLayout.uiConfig.pageTriggers); - const { next, previous } = useAppSelector((state) => getNavigationConfigForCurrentView(state), shallowEqual); + const { next, previous } = useAppSelector(selectPreviousAndNextPage); const activeTriggers = triggers || pageTriggers; const nextTextKey = returnToView ? 'form_filler.back_to_summary' : textResourceBindings?.next || 'next'; const backTextKey = textResourceBindings?.back || 'back'; @@ -68,25 +63,16 @@ export function NavigationButtonsComponent({ node }: INavigationButtons) { offsetTop: getScrollPosition(), }; - if (activeTriggers?.includes(Triggers.CalculatePageOrder)) { + const goToView = + returnToView || next || (orderedLayoutKeys && orderedLayoutKeys[orderedLayoutKeys.indexOf(currentView) + 1]); + if (goToView) { dispatch( - FormLayoutActions.calculatePageOrderAndMoveToNextPage({ + FormLayoutActions.updateCurrentView({ + newView: goToView, runValidations, keepScrollPos: keepScrollPosAction, }), ); - } else { - const goToView = - returnToView || next || (orderedLayoutKeys && orderedLayoutKeys[orderedLayoutKeys.indexOf(currentView) + 1]); - if (goToView) { - dispatch( - FormLayoutActions.updateCurrentView({ - newView: goToView, - runValidations, - keepScrollPos: keepScrollPosAction, - }), - ); - } } }; @@ -137,15 +123,3 @@ export function NavigationButtonsComponent({ node }: INavigationButtons) { ); } - -function getNavigationConfigForCurrentView(state: IRuntimeState): ILayoutNavigation { - const currentView = state.formLayout.uiConfig.currentView; - const navConfig = - state.formLayout.uiConfig.navigationConfig && state.formLayout.uiConfig.navigationConfig[currentView]; - const order = getLayoutOrderFromTracks(state.formLayout.uiConfig.tracks); - - return { - previous: getNextView(navConfig, order, currentView, true), - next: getNextView(navConfig, order, currentView), - }; -} diff --git a/src/selectors/getLayoutOrder.test.ts b/src/selectors/getLayoutOrder.test.ts index cf679643e5..e1a36e865e 100644 --- a/src/selectors/getLayoutOrder.test.ts +++ b/src/selectors/getLayoutOrder.test.ts @@ -1,9 +1,9 @@ -import { getLayoutOrderFromTracks } from 'src/selectors/getLayoutOrder'; +import { getLayoutOrderFromPageOrderConfig } from 'src/selectors/getLayoutOrder'; -describe('getLayoutOrderFromTracks', () => { +describe('getLayoutOrderFromPageOrderConfig', () => { it('should hide a layout after expressions have been evaluated', () => { expect( - getLayoutOrderFromTracks({ + getLayoutOrderFromPageOrderConfig({ order: ['first', 'second', 'third'], hidden: ['second'], hiddenExpr: {}, @@ -13,7 +13,7 @@ describe('getLayoutOrderFromTracks', () => { it('should not affect the order sent from the server', () => { expect( - getLayoutOrderFromTracks({ + getLayoutOrderFromPageOrderConfig({ order: ['4', '3', '2', '1'], hidden: ['2', '3'], hiddenExpr: {}, diff --git a/src/selectors/getLayoutOrder.ts b/src/selectors/getLayoutOrder.ts index 3891076eb7..d3e77515ac 100644 --- a/src/selectors/getLayoutOrder.ts +++ b/src/selectors/getLayoutOrder.ts @@ -1,20 +1,44 @@ import { createSelector } from 'reselect'; import type { RootState } from 'src/redux/store'; -import type { ITracks } from 'src/types'; +import type { IPageOrderConfig } from 'src/types'; /** - * Given the ITracks state, this returns the final order for layouts + * Given the IPageOrderConfig state, this returns the final order for layouts */ -export function getLayoutOrderFromTracks(tracks: ITracks): string[] | null { - if (tracks.order === null) { +export function getLayoutOrderFromPageOrderConfig(pageOrderConfig: IPageOrderConfig): string[] | null { + if (pageOrderConfig.order === null) { return null; } - const hiddenSet = new Set(tracks.hidden); - return [...tracks.order].filter((layout) => !hiddenSet.has(layout)); + const hiddenSet = new Set(pageOrderConfig.hidden); + return [...pageOrderConfig.order].filter((layout) => !hiddenSet.has(layout)); } -const selectTracks = (state: RootState) => state.formLayout.uiConfig.tracks; +/** + * Given the current view and the layout order, this returns the next and previous page + */ +function getNextAndPreviousPageFromState( + currentView: string, + layoutOrder: string[], +): { next?: string; previous?: string } { + const currentViewIndex = layoutOrder?.indexOf(currentView); + const nextView = currentViewIndex !== -1 ? currentViewIndex + 1 : 0; + const previousView = currentViewIndex !== -1 ? currentViewIndex - 1 : 0; + + return { + next: layoutOrder?.[nextView], + previous: layoutOrder?.[previousView], + }; +} + +export const selectPageOrderConfig = (state: RootState) => state.formLayout.uiConfig.pageOrderConfig; +const selectCurrentView = (state: RootState) => state.formLayout.uiConfig.currentView; + +export const selectLayoutOrder = createSelector(selectPageOrderConfig, getLayoutOrderFromPageOrderConfig); -export const selectLayoutOrder = createSelector(selectTracks, (tracks) => getLayoutOrderFromTracks(tracks)); +export const selectPreviousAndNextPage = createSelector( + selectCurrentView, + selectLayoutOrder, + getNextAndPreviousPageFromState, +); diff --git a/src/types/index.ts b/src/types/index.ts index f24fde28a3..08ed832880 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,7 @@ import { Triggers } from 'src/layout/common.generated'; import type { ExprVal, ExprValToActualOrExpr } from 'src/features/expressions/types'; import type { IFormData } from 'src/features/formData'; import type { IKeepComponentScrollPos } from 'src/features/layout/formLayoutTypes'; -import type { ILayoutNavigation, IMapping } from 'src/layout/common.generated'; +import type { IMapping } from 'src/layout/common.generated'; import type { RootState } from 'src/redux/store'; export interface ILayoutSets { @@ -38,10 +38,6 @@ export interface IComponentsSettings { excludeFromPdf?: string[]; } -export interface INavigationConfig { - [id: string]: ILayoutNavigation | undefined; -} - export interface IRepeatingGroup { index: number; baseGroupId?: string; @@ -88,8 +84,7 @@ export interface IUiConfig { focus: string | null | undefined; hiddenFields: string[]; repeatingGroups: IRepeatingGroups | null; - navigationConfig?: INavigationConfig; - tracks: ITracks; + pageOrderConfig: IPageOrderConfig; excludePageFromPdf: string[] | null; excludeComponentFromPdf: string[] | null; pdfLayoutName?: string; @@ -104,9 +99,8 @@ export interface IUiConfig { /** * This state includes everything needed to calculate which layouts should be shown, and their order. - * @see https://docs.altinn.studio/app/development/ux/pages/tracks/ */ -export interface ITracks { +export interface IPageOrderConfig { /** * The main 'order' is the list of layouts available, or which layouts the server tells us to display. If a layout * is not in this list, it should be considered hidden. It will be null until layouts have been fetched. diff --git a/src/utils/formLayout.ts b/src/utils/formLayout.ts index bee080f109..07d285b997 100644 --- a/src/utils/formLayout.ts +++ b/src/utils/formLayout.ts @@ -1,5 +1,4 @@ import { groupIsRepeatingExt, groupIsRepeatingLikertExt } from 'src/layout/Group/tools'; -import type { ILayoutNavigation } from 'src/layout/common.generated'; import type { CompGroupExternal, IGroupEditPropertiesLikert } from 'src/layout/Group/config.generated'; import type { CompExternal, ILayout } from 'src/layout/layout'; import type { ILayoutSets, IRepeatingGroups } from 'src/types'; @@ -186,32 +185,6 @@ function getIndexForNestedRepeatingGroup( return -1; } -export function getNextView( - navOptions: ILayoutNavigation | undefined, - layoutOrder: string[] | null, - currentView: string, - goBack?: boolean, -) { - let result; - if (navOptions) { - if (goBack && navOptions.previous) { - return navOptions.previous; - } - - if (!goBack && navOptions.next) { - return navOptions.next; - } - } - - if (layoutOrder) { - const currentViewIndex = layoutOrder.indexOf(currentView); - const newViewIndex = goBack ? currentViewIndex - 1 : currentViewIndex + 1; - result = layoutOrder[newViewIndex]; - } - - return result; -} - export function removeRepeatingGroupFromUIConfig( repeatingGroups: IRepeatingGroups, repeatingGroupId: string, diff --git a/src/utils/layout/ExprContext.tsx b/src/utils/layout/ExprContext.tsx index 876284b8da..576827745d 100644 --- a/src/utils/layout/ExprContext.tsx +++ b/src/utils/layout/ExprContext.tsx @@ -8,6 +8,7 @@ import { import { FormLayoutActions } from 'src/features/layout/formLayoutSlice'; import { useAppDispatch } from 'src/hooks/useAppDispatch'; import { useAppSelector } from 'src/hooks/useAppSelector'; +import { selectPageOrderConfig } from 'src/selectors/getLayoutOrder'; import { runConditionalRenderingRules } from 'src/utils/conditionalRendering'; import { _private, selectDataSourcesFromState } from 'src/utils/layout/hierarchy'; import { BaseLayoutNode } from 'src/utils/layout/LayoutNode'; @@ -82,7 +83,7 @@ export function useResolvedNode(selector: string | undefined | T | LayoutNode */ function useLegacyHiddenComponents(resolvedNodes: LayoutPages | undefined) { const _currentHiddenFields = useAppSelector((state) => state.formLayout.uiConfig.hiddenFields); - const tracks = useAppSelector((state) => state.formLayout.uiConfig.tracks); + const pageOrderConfig = useAppSelector(selectPageOrderConfig); const formData = useAppSelector((state) => state.formData.formData); const rules = useAppSelector((state) => state.formDynamics.conditionalRendering); const repeatingGroups = useAppSelector((state) => state.formLayout.uiConfig.repeatingGroups); @@ -94,8 +95,8 @@ function useLegacyHiddenComponents(resolvedNodes: LayoutPages | undefined) { return; } - const currentHiddenLayouts = new Set(tracks.hidden); - const futureHiddenLayouts = runExpressionsForLayouts(resolvedNodes, tracks.hiddenExpr, dataSources); + const currentHiddenLayouts = new Set(pageOrderConfig.hidden); + const futureHiddenLayouts = runExpressionsForLayouts(resolvedNodes, pageOrderConfig.hiddenExpr, dataSources); if (shouldUpdate(currentHiddenLayouts, futureHiddenLayouts)) { dispatch( @@ -145,7 +146,7 @@ function useLegacyHiddenComponents(resolvedNodes: LayoutPages | undefined) { repeatingGroups, resolvedNodes, rules, - tracks.hidden, - tracks.hiddenExpr, + pageOrderConfig.hidden, + pageOrderConfig.hiddenExpr, ]); } diff --git a/src/utils/layout/LayoutPage.ts b/src/utils/layout/LayoutPage.ts index 10d89724d9..4b336c02b1 100644 --- a/src/utils/layout/LayoutPage.ts +++ b/src/utils/layout/LayoutPage.ts @@ -159,9 +159,9 @@ export class LayoutPage implements LayoutObject { return false; } - const { order } = uiConfig.tracks || {}; + const { order } = uiConfig.pageOrderConfig || {}; if (!order) { - // If no tracks are provided, then we can't determine if this is hidden or not + // If no pageOrderConfig is provided, then we can't determine if this is hidden or not return false; } diff --git a/src/utils/urls/appUrlHelper.test.ts b/src/utils/urls/appUrlHelper.test.ts index a7f29861cc..2024a7c15d 100644 --- a/src/utils/urls/appUrlHelper.test.ts +++ b/src/utils/urls/appUrlHelper.test.ts @@ -2,7 +2,6 @@ import { dataElementUrl, fileTagUrl, fileUploadUrl, - getCalculatePageOrderUrl, getCreateInstancesUrl, getDataListsUrl, getDataValidationUrl, @@ -409,20 +408,6 @@ describe('Frontend urlHelper.ts', () => { }); }); - describe('getCalculatePageOrderUrl', () => { - it('should return stateful url if stateless is false', () => { - const result = getCalculatePageOrderUrl(false); - - expect(result).toBe('https://local.altinn.cloud/ttd/test/instances/12345/instanceId-1234/pages/order'); - }); - - it('should return stateless url if stateless is true', () => { - const result = getCalculatePageOrderUrl(true); - - expect(result).toBe('https://local.altinn.cloud/ttd/test/v1/pages/order'); - }); - }); - describe('getLayoutsUrl', () => { it('should return default when no parameter is passed', () => { const result = getLayoutsUrl(null); diff --git a/src/utils/urls/appUrlHelper.ts b/src/utils/urls/appUrlHelper.ts index c4da0389f3..0ed1ac49bc 100644 --- a/src/utils/urls/appUrlHelper.ts +++ b/src/utils/urls/appUrlHelper.ts @@ -154,14 +154,6 @@ export const getRulehandlerUrl = (layoutset?: string) => { return `${appPath}/api/rulehandler/${layoutset}`; }; -export const getCalculatePageOrderUrl = (stateless: boolean) => { - if (stateless) { - return `${appPath}/v1/pages/order`; - } else { - return `${appPath}/instances/${window.instanceId}/pages/order`; - } -}; - export const getPartyValidationUrl = (partyId: string) => `${appPath}/api/v1/parties/validateInstantiation?partyId=${partyId}`; diff --git a/test/e2e/integration/frontend-test/validation.ts b/test/e2e/integration/frontend-test/validation.ts index feae3960ee..ca99e64cbe 100644 --- a/test/e2e/integration/frontend-test/validation.ts +++ b/test/e2e/integration/frontend-test/validation.ts @@ -557,7 +557,7 @@ describe('Validation', () => { cy.get(appFrontend.changeOfName.uploadedTable).find('tbody > tr').should('have.length', 1); }); - it('Submitting should be rejected if validation fails on field hidden by tracks', () => { + it('Submitting should be rejected if validation fails on field hidden by pageOrderConfig', () => { cy.goto('changename'); cy.fillOut('changename'); @@ -606,7 +606,7 @@ describe('Validation', () => { cy.intercept('POST', '**/pages/order*', (req) => { req.reply((res) => { res.send({ - // Always reply with all pages, as none should be hidden using tracks here + // Always reply with all pages, as none should be hidden using pageOrderConfig here body: ['prefill', 'repeating', 'repeating2', 'hide', 'summary'], }); }); From 121db86089ca7df76254e6a97aaa4e7c68c9b7fe Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Fri, 10 Nov 2023 15:54:21 +0100 Subject: [PATCH 2/4] refactor: use reselct selectors for selectors returning new object references --- src/components/message/ErrorReport.tsx | 15 +++++++++++---- src/layout/index.ts | 5 ++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/message/ErrorReport.tsx b/src/components/message/ErrorReport.tsx index ee21521b81..2785ea0c60 100644 --- a/src/components/message/ErrorReport.tsx +++ b/src/components/message/ErrorReport.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Panel, PanelVariant } from '@altinn/altinn-design-system'; import { Grid } from '@material-ui/core'; +import { createSelector } from 'reselect'; import { FullWidthWrapper } from 'src/components/form/FullWidthWrapper'; import classes from 'src/components/message/ErrorReport.module.css'; @@ -15,7 +16,9 @@ import { LayoutNodeForGroup } from 'src/layout/Group/LayoutNodeForGroup'; import { AsciiUnitSeparator } from 'src/utils/attachment'; import { useExprContext } from 'src/utils/layout/ExprContext'; import { getMappedErrors, getUnmappedErrors } from 'src/utils/validation/validation'; +import type { IRuntimeState } from 'src/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; +import type { IValidations } from 'src/utils/validation/types'; import type { FlatError } from 'src/utils/validation/validation'; export interface IErrorReportProps { @@ -27,13 +30,17 @@ const ArrowForwardSvg = ` state.formValidations.validations; +const createMappedAndUnmappedErrors = (validations: IValidations): [FlatError[], string[]] => [ + getMappedErrors(validations), + getUnmappedErrors(validations), +]; +const selectMappedUnmappedErrors = createSelector(selectValidations, createMappedAndUnmappedErrors); + export const ErrorReport = ({ nodes }: IErrorReportProps) => { const dispatch = useAppDispatch(); const currentView = useAppSelector((state) => state.formLayout.uiConfig.currentView); - const [errorsMapped, errorsUnmapped] = useAppSelector((state) => [ - getMappedErrors(state.formValidations.validations), - getUnmappedErrors(state.formValidations.validations), - ]); + const [errorsMapped, errorsUnmapped] = useAppSelector(selectMappedUnmappedErrors); const allNodes = useExprContext(); const hasErrors = errorsUnmapped.length > 0 || errorsMapped.length > 0; const { lang } = useLanguage(); diff --git a/src/layout/index.ts b/src/layout/index.ts index 267359da86..ad2ba7b02b 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -1,5 +1,7 @@ import { createContext, useMemo } from 'react'; +import { createSelector } from 'reselect'; + import { useAllOptions } from 'src/features/options/useAllOptions'; import { useAppSelector } from 'src/hooks/useAppSelector'; import { type IUseLanguage, staticUseLanguageFromState } from 'src/hooks/useLanguage'; @@ -177,10 +179,11 @@ function getDisplayDataPropsFromState(state: IRuntimeState): Omit props); export function useDisplayDataProps(): DisplayDataProps { const options = useAllOptions(); - const props = useAppSelector(getDisplayDataPropsFromState); + const props = useAppSelector(selectDisplayDataProps); return useMemo(() => ({ options, ...props }), [options, props]); } From 07a57fe6ea6c3047c0eb3814e89128a7b8f09a15 Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Fri, 10 Nov 2023 16:18:05 +0100 Subject: [PATCH 3/4] test: fix e2e-tests after refactoring code --- .../frontend-test/auto-save-behavior.ts | 42 ----- .../frontend-test/calculate-page-order.ts | 151 +----------------- 2 files changed, 1 insertion(+), 192 deletions(-) diff --git a/test/e2e/integration/frontend-test/auto-save-behavior.ts b/test/e2e/integration/frontend-test/auto-save-behavior.ts index 0402eef430..65bada38be 100644 --- a/test/e2e/integration/frontend-test/auto-save-behavior.ts +++ b/test/e2e/integration/frontend-test/auto-save-behavior.ts @@ -68,48 +68,6 @@ describe('Auto save behavior', () => { }); }); - it('onChangePage: Should save data when NavigationButton has triggered calculatePageOrder', () => { - cy.interceptLayoutSetsUiSettings({ autoSaveBehavior: 'onChangePage' }); - cy.interceptLayout( - 'group', - (component) => { - if (component.type === 'NavigationButtons') { - if (!component.triggers) { - component.triggers = [Triggers.CalculatePageOrder]; - } else if (!component.triggers?.includes(Triggers.CalculatePageOrder)) { - component.triggers.push(Triggers.CalculatePageOrder); - } - } - }, - (layoutSet) => { - layoutSet.hide.data.hidden = ['equals', ['component', 'choose-group-prefills'], 'stor']; - layoutSet.repeating.data.hidden = ['equals', ['component', 'choose-group-prefills'], 'stor']; - }, - ); - - cy.goto('group'); - cy.intercept('POST', '**/pages/order*').as('getPageOrder'); - cy.intercept('PUT', '**/data/**').as('putFormData'); - cy.get(appFrontend.navMenuButtons).should('have.length', 4); - - // This test relies on Cypress being fast enough to click the 'next' button before the next page is hidden - cy.get(appFrontend.group.prefill.stor).dsCheck(); - // Double click to check that the request is cancelled and still navigates to next page - cy.get(appFrontend.nextButton).dblclick(); - - // Wait for both endpoints to be called - cy.wait('@getPageOrder'); - cy.wait('@putFormData'); - - // Clicking the next button above did nothing, because the next page was hidden as a result of clicking the - // checkbox. We'll click again to make sure navigation works again. - cy.get(appFrontend.navMenuButtons).should('have.length', 2); - cy.get(appFrontend.navMenuCurrent).should('have.text', '1. prefill'); - - cy.get(appFrontend.nextButton).click(); - cy.get(appFrontend.navMenuCurrent).should('have.text', '2. summary'); - }); - [Triggers.ValidatePage, Triggers.ValidateAllPages].forEach((trigger) => { it(`should run save before single field validation with navigation trigger ${trigger || 'undefined'}`, () => { cy.interceptLayoutSetsUiSettings({ autoSaveBehavior: 'onChangePage' }); diff --git a/test/e2e/integration/frontend-test/calculate-page-order.ts b/test/e2e/integration/frontend-test/calculate-page-order.ts index bc83695dd1..9e746f3f7f 100644 --- a/test/e2e/integration/frontend-test/calculate-page-order.ts +++ b/test/e2e/integration/frontend-test/calculate-page-order.ts @@ -1,161 +1,12 @@ import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; -import { Triggers } from 'src/layout/common.generated'; -import type { CompInputExternal } from 'src/layout/Input/config.generated'; - const appFrontend = new AppFrontend(); describe('Calculate Page Order', () => { - it('Testing combinations of old and new hidden pages functionalities', () => { - cy.interceptLayout( - 'group', - () => { - // Intentionally empty - }, - (layoutSet) => { - // Adding a new required field to the prefill page, just so that we have something to stop us from submitting - layoutSet.prefill.data.layout.push({ - id: 'yet-another-required-field', - type: 'Input', - dataModelBindings: { - // Same binding as 'sendersName' on the 'hide' page - simpleBinding: 'Endringsmelding-grp-9786.Avgiver-grp-9787.OppgavegiverNavn-datadef-68.value', - }, - textResourceBindings: { - title: 'Yet another required field', - }, - required: true, - } as CompInputExternal); - - layoutSet.prefill.data.hidden = [ - 'or', - ['equals', ['component', 'sendersName'], 'hidePrefill'], - [ - 'equals', - [ - 'dataModel', - 'Endringsmelding-grp-9786.OversiktOverEndringene-grp-9788[1].nested-grp-1234[0].SkattemeldingEndringEtterFristKommentar-datadef-37133.value', - ], - 'hidePrefill', - ], - ]; - layoutSet.repeating.data.hidden = [ - 'and', - ['equals', ['component', 'sendersName'], 'hideRepeating'], - [ - 'or', - ['equals', ['component', 'choose-group-prefills'], ''], - ['equals', ['component', 'choose-group-prefills'], null], - ], - ]; - }, - ); - cy.intercept('POST', '**/pages/order*').as('getPageOrder'); - - cy.goto('group'); - cy.get(appFrontend.nextButton).click(); - cy.get(appFrontend.group.showGroupToContinue).find('input').dsCheck(); - - cy.get(appFrontend.navMenuButtons).should('have.length', 4); - cy.gotoNavPage('summary'); - cy.get(appFrontend.sendinButton).click(); - cy.get(appFrontend.errorReport).findAllByRole('listitem').should('have.length', 2); - cy.get(appFrontend.errorReport).should('contain.text', 'Du må fylle ut oppgave giver navn'); - cy.get(appFrontend.errorReport).should('contain.text', 'Du må fylle ut yet another required field'); - - cy.gotoNavPage('repeating'); - cy.addItemToGroup(1, 11, 'automation'); - cy.get(appFrontend.nextButton).click(); - cy.wait('@getPageOrder'); - - cy.get(appFrontend.navMenuButtons).should('have.length', 3); - cy.get(appFrontend.group.sendersName).should('not.exist'); - cy.get(appFrontend.group.summaryText).should('be.visible'); - cy.get(appFrontend.navMenuCurrent).should('have.text', '3. summary'); - - // At this point the input field on the 'hide' page is gone, so we shouldn't see it in the error report either - cy.get(appFrontend.sendinButton).click(); - cy.get(appFrontend.errorReport).findAllByRole('listitem').should('have.length', 1); - cy.get(appFrontend.errorReport).should('contain.text', 'Du må fylle ut yet another required field'); - - cy.gotoNavPage('repeating'); - cy.get(appFrontend.group.row(0).editBtn).click(); - cy.get(appFrontend.group.newValue).clear(); - cy.get(appFrontend.group.newValue).type('2'); - - cy.get(appFrontend.nextButton).click(); - cy.wait('@getPageOrder'); - cy.get(appFrontend.navMenuButtons).should('have.length', 4); - cy.get(appFrontend.group.sendersName).should('exist'); - cy.get(appFrontend.navMenuCurrent).should('have.text', '3. hide'); - - cy.get(appFrontend.navMenuButtons).should('contain.text', '1. prefill'); - cy.get(appFrontend.group.sendersName).type('hidePrefill'); - cy.get(appFrontend.prevButton).click(); - cy.get(appFrontend.navMenuButtons).should('have.length', 3); - cy.get(appFrontend.navMenuCurrent).should('have.text', '1. repeating'); - - cy.gotoNavPage('hide'); - cy.get(appFrontend.group.sendersName).clear(); - cy.gotoNavPage('repeating'); - cy.get(appFrontend.group.saveMainGroup).click(); - cy.addItemToGroup(1, 11, 'hidePrefill'); - cy.get(appFrontend.navMenuButtons).should('have.length', 3); - - // Testing to make sure our required field hidden by an expression does not show up in the error report - cy.gotoNavPage('summary'); - cy.get(appFrontend.sendinButton).click(); - cy.get(appFrontend.errorReport).findAllByRole('listitem').should('have.length', 1); - cy.get(appFrontend.errorReport).should('contain.text', 'Du må fylle ut oppgave giver navn'); - - cy.gotoNavPage('repeating'); - cy.get(appFrontend.group.row(1).deleteBtn).click(); - - cy.gotoNavPage('hide'); - - // Clicking previous here is expected to not have any effect, because the triggered action is rejected when - // the 'repeating' page is supposed to be hidden by the change. Clicking too fast leads to a failure... - cy.get(appFrontend.group.sendersName).type('hideRepeating'); - cy.get(appFrontend.prevButton).click(); - - // ...but clicking 'previous' after this point will have updated the components to know that the previous page - // now is the 'prefill' page - cy.get(appFrontend.navMenuButtons).should('have.length', 3); - cy.get(appFrontend.prevButton).click(); - - cy.get(appFrontend.navMenuCurrent).should('have.text', '1. prefill'); - cy.get(appFrontend.navMenuButtons).should('contain.text', '2. hide'); - - const reproduceBug = JSON.parse('false'); - if (reproduceBug) { - cy.get(appFrontend.group.prefill.liten).click(); - cy.get(appFrontend.nextButton).click(); - - // And this is, in essence, a bug. Navigating to the next page should consider what the next page is, even if - // just-made-changes affects which page is the next one. Right now the component re-render loop needs to run - // for NavigationButtons to know what the next layout is in order to navigate to the correct one. - // TODO: Fix this by triggering a 'navigate to the next page' action instead of 'navigate to this exact page' - cy.get(appFrontend.navMenuCurrent).should('have.text', '3. hide'); - cy.get(appFrontend.navMenuButtons).should('contain.text', '2. repeating'); - - // TODO: Comment this in and delete the lines above when the bug is fixed: - // cy.get(appFrontend.navMenuCurrent).should('have.text', '2. repeating'); - // cy.get(appFrontend.navMenuButtons).should('contain.text', '3. hide'); - } - }); - it('Testing pageOrder with hidden next page via dynamics', () => { cy.interceptLayout( 'group', - (component) => { - if (component.type === 'NavigationButtons') { - if (!component.triggers) { - component.triggers = [Triggers.CalculatePageOrder]; - } else if (!component.triggers?.includes(Triggers.CalculatePageOrder)) { - component.triggers.push(Triggers.CalculatePageOrder); - } - } - }, + () => {}, (layoutSet) => { layoutSet.hide.data.hidden = ['equals', ['component', 'choose-group-prefills'], 'stor']; layoutSet.repeating.data.hidden = ['equals', ['component', 'choose-group-prefills'], 'stor']; From 4c7e769fe44f61c7ed853e350f6bc0474dd5a09f Mon Sep 17 00:00:00 2001 From: Mikael Solstad Date: Mon, 13 Nov 2023 12:28:13 +0100 Subject: [PATCH 4/4] fix: summary e2e-tests --- src/features/layout/formLayoutSlice.ts | 10 ++++++++++ src/features/layout/formLayoutTypes.ts | 1 - .../layout/update/updateFormLayoutSagas.test.ts | 12 ------------ src/features/layout/update/updateFormLayoutSagas.ts | 6 +----- src/layout/NavigationBar/NavigationBarComponent.tsx | 2 +- src/types/index.ts | 2 +- ...ate-page-order.ts => page-order-with-dynamics.ts} | 0 test/e2e/integration/frontend-test/summary.ts | 2 +- 8 files changed, 14 insertions(+), 21 deletions(-) rename test/e2e/integration/frontend-test/{calculate-page-order.ts => page-order-with-dynamics.ts} (100%) diff --git a/src/features/layout/formLayoutSlice.ts b/src/features/layout/formLayoutSlice.ts index d10a17a9fb..e49869c079 100644 --- a/src/features/layout/formLayoutSlice.ts +++ b/src/features/layout/formLayoutSlice.ts @@ -240,6 +240,16 @@ export const formLayoutSlice = () => { moveToNextPage: mkAction({ takeEvery: moveToNextPageSaga, }), + /** + * This action (setPageOrder) is used by the e2e-tests + * in summary.ts. It is not used in the application. + */ + setPageOrder: mkAction<{ order: string[] }>({ + reducer: (state, action) => { + const { order } = action.payload; + state.uiConfig.pageOrderConfig.order = order; + }, + }), moveToNextPageRejected: genericReject, updateHiddenLayouts: mkAction({ takeEvery: findAndMoveToNextVisibleLayout, diff --git a/src/features/layout/formLayoutTypes.ts b/src/features/layout/formLayoutTypes.ts index c77220df43..80637511b5 100644 --- a/src/features/layout/formLayoutTypes.ts +++ b/src/features/layout/formLayoutTypes.ts @@ -81,7 +81,6 @@ export interface IKeepComponentScrollPos { export interface IMoveToNextPage { runValidations?: TriggersPageValidation; - skipMoveToNext?: boolean; keepScrollPos?: IKeepComponentScrollPos; } diff --git a/src/features/layout/update/updateFormLayoutSagas.test.ts b/src/features/layout/update/updateFormLayoutSagas.test.ts index c032b75017..9b1ab05d3c 100644 --- a/src/features/layout/update/updateFormLayoutSagas.test.ts +++ b/src/features/layout/update/updateFormLayoutSagas.test.ts @@ -55,18 +55,6 @@ describe('updateLayoutSagas', () => { .run(); }); - it('should not update current view if skipMoveToNext is true', () => { - const action: PayloadAction = { - type: 'test', - payload: { - skipMoveToNext: true, - }, - }; - return expectSaga(moveToNextPageSaga, action) - .provide([[select(), state]]) - .run(); - }); - it('stateless: should fetch pageOrder and update state accordingly', () => { const action: PayloadAction = { type: 'test', diff --git a/src/features/layout/update/updateFormLayoutSagas.ts b/src/features/layout/update/updateFormLayoutSagas.ts index bb67ea9aa8..6fad7bfe6b 100644 --- a/src/features/layout/update/updateFormLayoutSagas.ts +++ b/src/features/layout/update/updateFormLayoutSagas.ts @@ -173,7 +173,7 @@ export function* updateCurrentViewSaga({ } export function* moveToNextPageSaga({ - payload: { runValidations, skipMoveToNext, keepScrollPos }, + payload: { runValidations, keepScrollPos }, }: PayloadAction): SagaIterator { try { const state: IRuntimeState = yield select(); @@ -184,10 +184,6 @@ export function* moveToNextPageSaga({ return; } - if (skipMoveToNext) { - return; - } - const returnToView = state.formLayout.uiConfig.returnToView; const layoutOrder = getLayoutOrderFromPageOrderConfig(state.formLayout.uiConfig.pageOrderConfig) || []; const newView = returnToView || layoutOrder[layoutOrder.indexOf(currentView) + 1]; diff --git a/src/layout/NavigationBar/NavigationBarComponent.tsx b/src/layout/NavigationBar/NavigationBarComponent.tsx index 31a5d23f16..3a04b421a0 100644 --- a/src/layout/NavigationBar/NavigationBarComponent.tsx +++ b/src/layout/NavigationBar/NavigationBarComponent.tsx @@ -190,7 +190,7 @@ export const NavigationBarComponent = ({ node }: INavigationBar) => { {pageIds.map((pageId, index) => (