Skip to content

Commit 5d2fe5b

Browse files
Magnusrmwalldenfilippawalldenfilippa
authored
Feat/16446 navigation validation (#4016)
* #17511 + #17512 (#3977) Co-authored-by: walldenfilippa <filippa.walden@digdir.no> * Override validation on navigationbuttons * minor clean up * added override validation on the navigationBar component * added ovverride customButton + configured the validateOnNext/Forward and validateOnPrevious/Backward with preventNavigation prop for validationOnNavigation * Feat/prevent-navigation (#3990) * add basic logic for preventing navigation * change wording * added PageValidation on layoutSets and layoutSettings (#3998) * added PageValidation on layoutSets and layoutSettings --------- Co-authored-by: walldenfilippa <filippa.walden@digdir.no> * Feat/refactor pagevalidation navigation (#4005) * refactor: navigation validation backward and improve page validation hooks * refactor preventing navigation to use simplified config --------- Co-authored-by: walldenfilippa <filippa.walden@digdir.no> Co-authored-by: Magnus Revheim Martinsen <mrmartinsen.96@gmail.com> * added prop expandedByDefault to groups for sidenavigation (#4006) * added prop expandedByDefault to groups for sidenavigation * added prop expandedByDefault to subform sidenavigation --------- Co-authored-by: walldenfilippa <filippa.walden@digdir.no> * Updated the validation hierarchy for the navigation rules. * fix prevent navigation * feat: enhance navigation validation by integrating validation checks (#4027) * feat: enhance navigation validation by integrating validation checks in navigation components * removed one hook and added useGetNavigationIsPrevented on pageGroup aswell * temporary notes * remove temporary note * update tests --------- Co-authored-by: walldenfilippa <filippa.walden@digdir.no> * merge main into feat/16446-navigation-validation * changed ovverride level direction on validation on navigation and moved validationOnNavigation to GlobalPageSettings (#4064) Co-authored-by: walldenfilippa <filippa.walden@digdir.no> * fix comments in feat/16446-navigation-validation (#4094) * fix comments in feat/16446-navigation-validation * fixed tests in NavigationButtons tests * refactor: implement navigation with validation logic in Page and PageGroup components --------- Co-authored-by: walldenfilippa <filippa.walden@digdir.no> --------- Co-authored-by: Filippa Wallden <143729834+walldenfilippa@users.noreply.github.com> Co-authored-by: walldenfilippa <filippa.walden@digdir.no>
1 parent ec42c55 commit 5d2fe5b

15 files changed

Lines changed: 319 additions & 56 deletions

File tree

src/codegen/Common.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ const common = {
7777
.setTitle('Expanded width')
7878
.setDescription('Sets expanded width for pages'),
7979
),
80+
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
8081
),
8182
),
8283
)
@@ -762,6 +763,7 @@ const common = {
762763
.setTitle('Task navigation settings')
763764
.setDescription('Shows the listed tasks in the sidebar navigation menu'),
764765
),
766+
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
765767
),
766768
IPagesBaseSettings: () =>
767769
new CG.obj(
@@ -781,6 +783,7 @@ const common = {
781783
'Name of a custom layout file to use for PDF creation instead of the automatically generated PDF.',
782784
),
783785
),
786+
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
784787
),
785788
INavigationBasePageGroup: () =>
786789
new CG.obj(
@@ -792,6 +795,12 @@ const common = {
792795
.optional({ default: false })
793796
.setDescription('Whether this group should mark pages as completed when the user finishes'),
794797
),
798+
new CG.prop(
799+
'expandedByDefault',
800+
new CG.bool()
801+
.optional({ default: false })
802+
.setDescription('Whether the sidebar group should be expanded by default'),
803+
),
795804
),
796805
IPagesSettingsWithGroups: () =>
797806
new CG.obj(
@@ -836,7 +845,6 @@ const common = {
836845
.setTitle('Layout settings')
837846
.setDescription('Settings regarding layout pages and components'),
838847

839-
// Layout sets:
840848
ILayoutSets: () =>
841849
new CG.obj(
842850
new CG.prop('$schema', new CG.str().optional()),

src/features/form/layout/LayoutsContext.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import { makeLikertChildId } from 'src/layout/Likert/Generator/makeLikertChildId
1919
import { fetchLayoutsForInstance } from 'src/queries/queries';
2020
import type { QueryDefinition } from 'src/core/queries/usePrefetchQuery';
2121
import type { CompExternal, ILayoutCollection, ILayouts } from 'src/layout/layout';
22-
import type { IExpandedWidthLayouts, IHiddenLayoutsExternal } from 'src/types';
22+
import type { IExpandedWidthLayouts, IHiddenLayoutsExternal, IPreventNavigationLayouts } from 'src/types';
2323

2424
export interface LayoutContextValue {
2525
layouts: ILayouts;
2626
hiddenLayoutsExpressions: IHiddenLayoutsExternal;
2727
expandedWidthLayouts: IExpandedWidthLayouts;
28+
layoutCollection: ILayoutCollection;
29+
preventNavigationLayouts: IPreventNavigationLayouts;
2830
}
2931

3032
// Also used for prefetching @see formPrefetcher.ts
@@ -113,15 +115,22 @@ export const useHiddenLayoutsExpressions = () => {
113115

114116
export const useExpandedWidthLayouts = () => useCtx().expandedWidthLayouts;
115117

118+
export const useLayoutCollection = () => useCtx().layoutCollection;
119+
120+
export const usePreventNavigationLayouts = () => useCtx().preventNavigationLayouts;
121+
116122
function processLayouts(input: ILayoutCollection, layoutSetId: string, dataModelType: string): LayoutContextValue {
117123
const layouts: ILayouts = {};
118124
const hiddenLayoutsExpressions: IHiddenLayoutsExternal = {};
119125
const expandedWidthLayouts: IExpandedWidthLayouts = {};
126+
const preventNavigationLayouts: IPreventNavigationLayouts = {};
127+
120128
for (const key of Object.keys(input)) {
121129
const file = input[key];
122130
layouts[key] = cleanLayout(file.data.layout, dataModelType);
123131
hiddenLayoutsExpressions[key] = file.data.hidden;
124132
expandedWidthLayouts[key] = file.data.expandedWidth;
133+
preventNavigationLayouts[key] = !!file.data.validationOnNavigation;
125134
}
126135

127136
const withQuirksFixed = applyLayoutQuirks(layouts, layoutSetId);
@@ -132,6 +141,8 @@ function processLayouts(input: ILayoutCollection, layoutSetId: string, dataModel
132141
layouts: withQuirksFixed,
133142
hiddenLayoutsExpressions,
134143
expandedWidthLayouts,
144+
layoutCollection: input,
145+
preventNavigationLayouts,
135146
};
136147
}
137148

src/features/form/layoutSettings/LayoutSettingsContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ function processData(settings: ILayoutSettings | null): ProcessedLayoutSettings
7070
showLanguageSelector: settings.pages.showLanguageSelector,
7171
showProgress: settings.pages.showProgress,
7272
taskNavigation: settings.pages.taskNavigation?.map((g) => ({ ...g, id: uuidv4() })),
73+
validationOnNavigation: settings.pages.validationOnNavigation,
7374
}),
7475
pdfLayoutName: settings.pages.pdfLayoutName,
7576
};
@@ -121,7 +122,7 @@ export const usePageGroups = () => {
121122

122123
const emptyArray = [];
123124

124-
const defaults: Required<GlobalPageSettings> = {
125+
const defaults: Omit<Required<GlobalPageSettings>, 'validationOnNavigation'> = {
125126
hideCloseButton: false,
126127
showLanguageSelector: false,
127128
showProgress: false,
@@ -131,7 +132,8 @@ const defaults: Required<GlobalPageSettings> = {
131132
taskNavigation: [],
132133
};
133134

134-
export const usePageSettings = (): Required<GlobalPageSettings> => {
135+
export const usePageSettings = (): Required<Omit<GlobalPageSettings, 'validationOnNavigation'>> &
136+
Pick<GlobalPageSettings, 'validationOnNavigation'> => {
135137
const globalUISettings = useLaxGlobalUISettings();
136138
const layoutSettings = useLaxCtx();
137139

src/features/navigation/components/Page.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,40 @@ import { Lang } from 'src/features/language/Lang';
99
import { useLanguage } from 'src/features/language/useLanguage';
1010
import classes from 'src/features/navigation/components/Page.module.css';
1111
import { SubformsForPage } from 'src/features/navigation/components/SubformsForPage';
12+
import { useNavigateToPageWithValidation } from 'src/features/navigation/useNavigateToPageWithValidation';
13+
import { useGetNavigationIsPrevented } from 'src/features/navigation/utils';
1214
import { useNavigationParam } from 'src/hooks/navigation';
13-
import { useNavigatePage } from 'src/hooks/useNavigatePage';
1415

1516
export function Page({
1617
page,
1718
onNavigate,
1819
hasErrors,
1920
isComplete,
21+
expandedByDefault,
2022
}: {
2123
page: string;
2224
onNavigate?: () => void;
2325
hasErrors: boolean;
2426
isComplete: boolean;
27+
expandedByDefault?: boolean;
2528
}) {
2629
const currentPageId = useNavigationParam('pageKey');
2730
const isCurrentPage = page === currentPageId;
2831

29-
const { navigateToPage } = useNavigatePage();
3032
const { performProcess, isAnyProcessing, isThisProcessing: isNavigating } = useIsProcessing();
33+
const navigateToPageWithValidation = useNavigateToPageWithValidation();
34+
35+
const navigationIsPrevented = useGetNavigationIsPrevented()(page);
36+
37+
const handleNavigationClick = () => performProcess(() => navigateToPageWithValidation(page, onNavigate));
3138

3239
return (
3340
<li className={classes.pageListItem}>
3441
<button
35-
disabled={isAnyProcessing}
42+
disabled={isAnyProcessing || navigationIsPrevented}
3643
aria-current={isCurrentPage ? 'page' : undefined}
3744
className={cn(classes.pageButton, 'fds-focus')}
38-
onClick={() =>
39-
performProcess(async () => {
40-
if (!isCurrentPage) {
41-
await navigateToPage(page);
42-
onNavigate?.();
43-
}
44-
})
45-
}
45+
onClick={handleNavigationClick}
4646
>
4747
<PageSymbol
4848
error={hasErrors}
@@ -64,7 +64,10 @@ export function Page({
6464
)}
6565
</span>
6666
</button>
67-
<SubformsForPage pageKey={page} />
67+
<SubformsForPage
68+
pageKey={page}
69+
expandedByDefault={expandedByDefault}
70+
/>
6871
</li>
6972
);
7073
}

src/features/navigation/components/PageGroup.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ import { useLanguage } from 'src/features/language/useLanguage';
1313
import { Page } from 'src/features/navigation/components/Page';
1414
import classes from 'src/features/navigation/components/PageGroup.module.css';
1515
import { SubformsForPage } from 'src/features/navigation/components/SubformsForPage';
16-
import { getTaskIcon, useValidationsForPages, useVisiblePages } from 'src/features/navigation/utils';
16+
import { useNavigateToPageWithValidation } from 'src/features/navigation/useNavigateToPageWithValidation';
17+
import {
18+
getTaskIcon,
19+
useGetNavigationIsPrevented,
20+
useValidationsForPages,
21+
useVisiblePages,
22+
} from 'src/features/navigation/utils';
1723
import { useNavigationParam } from 'src/hooks/navigation';
18-
import { useNavigatePage } from 'src/hooks/useNavigatePage';
1924
import type {
2025
NavigationPageGroup,
2126
NavigationPageGroupMultiple,
@@ -73,27 +78,21 @@ function PageGroupSingle({
7378
validations,
7479
onNavigate,
7580
}: PageGroupProps<NavigationPageGroupSingle>) {
76-
const { navigateToPage } = useNavigatePage();
7781
const { performProcess, isAnyProcessing, isThisProcessing: isNavigating } = useIsProcessing();
82+
const navigateToPageWithValidation = useNavigateToPageWithValidation();
7883
const page = group.order[0];
84+
const navigationIsPrevented = useGetNavigationIsPrevented()(page);
7985

8086
const pageGroupHasErrors = validations !== ContextNotProvided && validations.hasErrors.group;
8187
const pageGroupIsComplete = validations !== ContextNotProvided && validations.isCompleted.group;
8288

8389
return (
8490
<li>
8591
<button
86-
disabled={isAnyProcessing}
92+
disabled={isAnyProcessing || navigationIsPrevented}
8793
aria-current={isCurrentPage ? 'page' : undefined}
8894
className={cn(classes.groupButton, classes.groupButtonSingle, 'fds-focus')}
89-
onClick={() =>
90-
performProcess(async () => {
91-
if (!isCurrentPage) {
92-
await navigateToPage(page);
93-
onNavigate?.();
94-
}
95-
})
96-
}
95+
onClick={() => performProcess(() => navigateToPageWithValidation(page, onNavigate))}
9796
>
9897
<PageGroupSymbol
9998
single
@@ -117,7 +116,10 @@ function PageGroupSingle({
117116
)}
118117
</span>
119118
</button>
120-
<SubformsForPage pageKey={page} />
119+
<SubformsForPage
120+
pageKey={page}
121+
expandedByDefault={group.expandedByDefault}
122+
/>
121123
</li>
122124
);
123125
}
@@ -132,8 +134,11 @@ function PageGroupMultiple({
132134
const buttonId = `navigation-button-${group.id}`;
133135
const listId = `navigation-page-list-${group.id}`;
134136

135-
const [isOpen, setIsOpen] = useState(containsCurrentPage);
136-
useLayoutEffect(() => setIsOpen(containsCurrentPage), [containsCurrentPage]);
137+
const [isOpen, setIsOpen] = useState(containsCurrentPage || !!group.expandedByDefault);
138+
useLayoutEffect(
139+
() => setIsOpen(containsCurrentPage || !!group.expandedByDefault),
140+
[containsCurrentPage, group.expandedByDefault],
141+
);
137142

138143
const pageGroupHasErrors = validations !== ContextNotProvided && validations.hasErrors.group;
139144
const pageGroupIsComplete = validations !== ContextNotProvided && validations.isCompleted.group;
@@ -186,6 +191,7 @@ function PageGroupMultiple({
186191
onNavigate={onNavigate}
187192
hasErrors={validations !== ContextNotProvided && validations.hasErrors.pages[page]}
188193
isComplete={validations !== ContextNotProvided && validations.isCompleted.pages[page]}
194+
expandedByDefault={group.expandedByDefault}
189195
/>
190196
))}
191197
</ul>

src/features/navigation/components/SubformsForPage.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useLayoutEffect, useState } from 'react';
22

33
import { ChevronDownIcon } from '@navikt/aksel-icons';
44
import cn from 'classnames';
@@ -11,6 +11,7 @@ import { Lang } from 'src/features/language/Lang';
1111
import classes from 'src/features/navigation/components/SubformsForPage.module.css';
1212
import { isSubformValidation } from 'src/features/validation';
1313
import { useComponentValidationsFor } from 'src/features/validation/selectors/componentValidationsForNode';
14+
import { useNavigationParam } from 'src/hooks/navigation';
1415
import { useNavigatePage } from 'src/hooks/useNavigatePage';
1516
import {
1617
getSubformEntryDisplayName,
@@ -22,7 +23,7 @@ import { useExternalItem } from 'src/utils/layout/hooks';
2223
import type { ExprValToActualOrExpr } from 'src/features/expressions/types';
2324
import type { IData } from 'src/types/shared';
2425

25-
export function SubformsForPage({ pageKey }: { pageKey: string }) {
26+
export function SubformsForPage({ pageKey, expandedByDefault }: { pageKey: string; expandedByDefault?: boolean }) {
2627
const lookups = useLayoutLookups();
2728
const subformIds = lookups.topLevelComponents[pageKey]?.filter((id) => lookups.allComponents[id]?.type === 'Subform');
2829
if (!subformIds?.length) {
@@ -33,17 +34,22 @@ export function SubformsForPage({ pageKey }: { pageKey: string }) {
3334
<SubformGroup
3435
key={baseId}
3536
baseId={baseId}
37+
expandedByDefault={expandedByDefault}
3638
/>
3739
));
3840
}
3941

40-
function SubformGroup({ baseId }: { baseId: string }) {
41-
const [isOpen, setIsOpen] = useState(false);
42+
function SubformGroup({ baseId, expandedByDefault }: { baseId: string; expandedByDefault?: boolean }) {
43+
const currentPageId = useNavigationParam('pageKey');
4244
const pageKey = useLayoutLookups().componentToPage[baseId];
4345
if (!pageKey) {
4446
throw new Error(`Unable to find page for subform with id ${baseId}`);
4547
}
4648

49+
const isCurrentPage = pageKey === currentPageId;
50+
const [isOpen, setIsOpen] = useState(isCurrentPage || !!expandedByDefault);
51+
useLayoutEffect(() => setIsOpen(isCurrentPage || !!expandedByDefault), [isCurrentPage, expandedByDefault]);
52+
4753
const subformIdsWithError = useComponentValidationsFor(baseId).find(isSubformValidation)?.subformDataElementIds;
4854
const { layoutSet, textResourceBindings, entryDisplayName } = useExternalItem(baseId, 'Subform');
4955
const title = useEvalExpression(textResourceBindings?.title, {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useOnPageNavigationValidation } from 'src/features/validation/callbacks/onPageNavigationValidation';
2+
import { useNavigationParam } from 'src/hooks/navigation';
3+
import { useNavigatePage } from 'src/hooks/useNavigatePage';
4+
import { useEffectivePageValidation } from 'src/hooks/usePageValidation';
5+
6+
export function useNavigateToPageWithValidation() {
7+
const currentPageId = useNavigationParam('pageKey');
8+
const { navigateToPage, order, maybeSaveOnPageChange } = useNavigatePage();
9+
const onPageNavigationValidation = useOnPageNavigationValidation();
10+
const { getPageValidation } = useEffectivePageValidation(currentPageId ?? '');
11+
12+
return async (targetPage: string, onNavigate?: () => void) => {
13+
if (!currentPageId || targetPage === currentPageId) {
14+
return;
15+
}
16+
17+
const currentIndex = order.indexOf(currentPageId);
18+
const targetIndex = order.indexOf(targetPage);
19+
if (currentIndex === -1 || targetIndex === -1) {
20+
return;
21+
}
22+
23+
const isForward = targetIndex > currentIndex;
24+
const validationOnNavigation = getPageValidation();
25+
26+
await maybeSaveOnPageChange();
27+
28+
if (isForward && validationOnNavigation) {
29+
const hasValidationErrors = await onPageNavigationValidation(currentPageId, validationOnNavigation);
30+
if (hasValidationErrors) {
31+
return;
32+
}
33+
}
34+
35+
await navigateToPage(targetPage);
36+
onNavigate?.();
37+
};
38+
}

0 commit comments

Comments
 (0)