Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/codegen/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const common = {
.setTitle('Expanded width')
.setDescription('Sets expanded width for pages'),
),
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
),
),
)
Expand Down Expand Up @@ -762,6 +763,7 @@ const common = {
.setTitle('Task navigation settings')
.setDescription('Shows the listed tasks in the sidebar navigation menu'),
),
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
),
IPagesBaseSettings: () =>
new CG.obj(
Expand All @@ -781,6 +783,7 @@ const common = {
'Name of a custom layout file to use for PDF creation instead of the automatically generated PDF.',
),
),
new CG.prop('validationOnNavigation', CG.common('PageValidation').optional()),
),
INavigationBasePageGroup: () =>
new CG.obj(
Expand All @@ -792,6 +795,12 @@ const common = {
.optional({ default: false })
.setDescription('Whether this group should mark pages as completed when the user finishes'),
),
new CG.prop(
'expandedByDefault',
new CG.bool()
.optional({ default: false })
.setDescription('Whether the sidebar group should be expanded by default'),
),
),
IPagesSettingsWithGroups: () =>
new CG.obj(
Expand Down Expand Up @@ -836,7 +845,6 @@ const common = {
.setTitle('Layout settings')
.setDescription('Settings regarding layout pages and components'),

// Layout sets:
ILayoutSets: () =>
new CG.obj(
new CG.prop('$schema', new CG.str().optional()),
Expand Down
13 changes: 12 additions & 1 deletion src/features/form/layout/LayoutsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import { makeLikertChildId } from 'src/layout/Likert/Generator/makeLikertChildId
import { fetchLayoutsForInstance } from 'src/queries/queries';
import type { QueryDefinition } from 'src/core/queries/usePrefetchQuery';
import type { CompExternal, ILayoutCollection, ILayouts } from 'src/layout/layout';
import type { IExpandedWidthLayouts, IHiddenLayoutsExternal } from 'src/types';
import type { IExpandedWidthLayouts, IHiddenLayoutsExternal, IPreventNavigationLayouts } from 'src/types';

export interface LayoutContextValue {
layouts: ILayouts;
hiddenLayoutsExpressions: IHiddenLayoutsExternal;
expandedWidthLayouts: IExpandedWidthLayouts;
layoutCollection: ILayoutCollection;
preventNavigationLayouts: IPreventNavigationLayouts;
}

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

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

export const useLayoutCollection = () => useCtx().layoutCollection;

export const usePreventNavigationLayouts = () => useCtx().preventNavigationLayouts;

function processLayouts(input: ILayoutCollection, layoutSetId: string, dataModelType: string): LayoutContextValue {
const layouts: ILayouts = {};
const hiddenLayoutsExpressions: IHiddenLayoutsExternal = {};
const expandedWidthLayouts: IExpandedWidthLayouts = {};
const preventNavigationLayouts: IPreventNavigationLayouts = {};

for (const key of Object.keys(input)) {
const file = input[key];
layouts[key] = cleanLayout(file.data.layout, dataModelType);
hiddenLayoutsExpressions[key] = file.data.hidden;
expandedWidthLayouts[key] = file.data.expandedWidth;
preventNavigationLayouts[key] = !!file.data.validationOnNavigation;
}

const withQuirksFixed = applyLayoutQuirks(layouts, layoutSetId);
Expand All @@ -132,6 +141,8 @@ function processLayouts(input: ILayoutCollection, layoutSetId: string, dataModel
layouts: withQuirksFixed,
hiddenLayoutsExpressions,
expandedWidthLayouts,
layoutCollection: input,
preventNavigationLayouts,
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/features/form/layoutSettings/LayoutSettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function processData(settings: ILayoutSettings | null): ProcessedLayoutSettings
showLanguageSelector: settings.pages.showLanguageSelector,
showProgress: settings.pages.showProgress,
taskNavigation: settings.pages.taskNavigation?.map((g) => ({ ...g, id: uuidv4() })),
validationOnNavigation: settings.pages.validationOnNavigation,
}),
pdfLayoutName: settings.pages.pdfLayoutName,
};
Expand Down Expand Up @@ -121,7 +122,7 @@ export const usePageGroups = () => {

const emptyArray = [];

const defaults: Required<GlobalPageSettings> = {
const defaults: Omit<Required<GlobalPageSettings>, 'validationOnNavigation'> = {
hideCloseButton: false,
showLanguageSelector: false,
showProgress: false,
Expand All @@ -131,7 +132,8 @@ const defaults: Required<GlobalPageSettings> = {
taskNavigation: [],
};

export const usePageSettings = (): Required<GlobalPageSettings> => {
export const usePageSettings = (): Required<Omit<GlobalPageSettings, 'validationOnNavigation'>> &
Pick<GlobalPageSettings, 'validationOnNavigation'> => {
const globalUISettings = useLaxGlobalUISettings();
const layoutSettings = useLaxCtx();

Expand Down
27 changes: 15 additions & 12 deletions src/features/navigation/components/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,40 @@ import { Lang } from 'src/features/language/Lang';
import { useLanguage } from 'src/features/language/useLanguage';
import classes from 'src/features/navigation/components/Page.module.css';
import { SubformsForPage } from 'src/features/navigation/components/SubformsForPage';
import { useNavigateToPageWithValidation } from 'src/features/navigation/useNavigateToPageWithValidation';
import { useGetNavigationIsPrevented } from 'src/features/navigation/utils';
import { useNavigationParam } from 'src/hooks/navigation';
import { useNavigatePage } from 'src/hooks/useNavigatePage';

export function Page({
page,
onNavigate,
hasErrors,
isComplete,
expandedByDefault,
}: {
page: string;
onNavigate?: () => void;
hasErrors: boolean;
isComplete: boolean;
expandedByDefault?: boolean;
}) {
const currentPageId = useNavigationParam('pageKey');
const isCurrentPage = page === currentPageId;

const { navigateToPage } = useNavigatePage();
const { performProcess, isAnyProcessing, isThisProcessing: isNavigating } = useIsProcessing();
const navigateToPageWithValidation = useNavigateToPageWithValidation();

const navigationIsPrevented = useGetNavigationIsPrevented()(page);

const handleNavigationClick = () => performProcess(() => navigateToPageWithValidation(page, onNavigate));

return (
<li className={classes.pageListItem}>
<button
disabled={isAnyProcessing}
disabled={isAnyProcessing || navigationIsPrevented}
aria-current={isCurrentPage ? 'page' : undefined}
className={cn(classes.pageButton, 'fds-focus')}
onClick={() =>
performProcess(async () => {
if (!isCurrentPage) {
await navigateToPage(page);
onNavigate?.();
}
})
}
onClick={handleNavigationClick}
>
<PageSymbol
error={hasErrors}
Expand All @@ -64,7 +64,10 @@ export function Page({
)}
</span>
</button>
<SubformsForPage pageKey={page} />
<SubformsForPage
pageKey={page}
expandedByDefault={expandedByDefault}
/>
</li>
);
}
Expand Down
36 changes: 21 additions & 15 deletions src/features/navigation/components/PageGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import { useLanguage } from 'src/features/language/useLanguage';
import { Page } from 'src/features/navigation/components/Page';
import classes from 'src/features/navigation/components/PageGroup.module.css';
import { SubformsForPage } from 'src/features/navigation/components/SubformsForPage';
import { getTaskIcon, useValidationsForPages, useVisiblePages } from 'src/features/navigation/utils';
import { useNavigateToPageWithValidation } from 'src/features/navigation/useNavigateToPageWithValidation';
import {
getTaskIcon,
useGetNavigationIsPrevented,
useValidationsForPages,
useVisiblePages,
} from 'src/features/navigation/utils';
import { useNavigationParam } from 'src/hooks/navigation';
import { useNavigatePage } from 'src/hooks/useNavigatePage';
import type {
NavigationPageGroup,
NavigationPageGroupMultiple,
Expand Down Expand Up @@ -73,27 +78,21 @@ function PageGroupSingle({
validations,
onNavigate,
}: PageGroupProps<NavigationPageGroupSingle>) {
const { navigateToPage } = useNavigatePage();
const { performProcess, isAnyProcessing, isThisProcessing: isNavigating } = useIsProcessing();
const navigateToPageWithValidation = useNavigateToPageWithValidation();
const page = group.order[0];
const navigationIsPrevented = useGetNavigationIsPrevented()(page);

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

return (
<li>
<button
disabled={isAnyProcessing}
disabled={isAnyProcessing || navigationIsPrevented}
aria-current={isCurrentPage ? 'page' : undefined}
className={cn(classes.groupButton, classes.groupButtonSingle, 'fds-focus')}
onClick={() =>
performProcess(async () => {
if (!isCurrentPage) {
await navigateToPage(page);
onNavigate?.();
}
})
}
onClick={() => performProcess(() => navigateToPageWithValidation(page, onNavigate))}
>
<PageGroupSymbol
single
Expand All @@ -117,7 +116,10 @@ function PageGroupSingle({
)}
</span>
</button>
<SubformsForPage pageKey={page} />
<SubformsForPage
pageKey={page}
expandedByDefault={group.expandedByDefault}
/>
</li>
);
}
Expand All @@ -132,8 +134,11 @@ function PageGroupMultiple({
const buttonId = `navigation-button-${group.id}`;
const listId = `navigation-page-list-${group.id}`;

const [isOpen, setIsOpen] = useState(containsCurrentPage);
useLayoutEffect(() => setIsOpen(containsCurrentPage), [containsCurrentPage]);
const [isOpen, setIsOpen] = useState(containsCurrentPage || !!group.expandedByDefault);
useLayoutEffect(
() => setIsOpen(containsCurrentPage || !!group.expandedByDefault),
[containsCurrentPage, group.expandedByDefault],
);

const pageGroupHasErrors = validations !== ContextNotProvided && validations.hasErrors.group;
const pageGroupIsComplete = validations !== ContextNotProvided && validations.isCompleted.group;
Expand Down Expand Up @@ -186,6 +191,7 @@ function PageGroupMultiple({
onNavigate={onNavigate}
hasErrors={validations !== ContextNotProvided && validations.hasErrors.pages[page]}
isComplete={validations !== ContextNotProvided && validations.isCompleted.pages[page]}
expandedByDefault={group.expandedByDefault}
/>
))}
</ul>
Expand Down
14 changes: 10 additions & 4 deletions src/features/navigation/components/SubformsForPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useLayoutEffect, useState } from 'react';

import { ChevronDownIcon } from '@navikt/aksel-icons';
import cn from 'classnames';
Expand All @@ -11,6 +11,7 @@
import classes from 'src/features/navigation/components/SubformsForPage.module.css';
import { isSubformValidation } from 'src/features/validation';
import { useComponentValidationsFor } from 'src/features/validation/selectors/componentValidationsForNode';
import { useNavigationParam } from 'src/hooks/navigation';
import { useNavigatePage } from 'src/hooks/useNavigatePage';
import {
getSubformEntryDisplayName,
Expand All @@ -22,7 +23,7 @@
import type { ExprValToActualOrExpr } from 'src/features/expressions/types';
import type { IData } from 'src/types/shared';

export function SubformsForPage({ pageKey }: { pageKey: string }) {
export function SubformsForPage({ pageKey, expandedByDefault }: { pageKey: string; expandedByDefault?: boolean }) {
const lookups = useLayoutLookups();
const subformIds = lookups.topLevelComponents[pageKey]?.filter((id) => lookups.allComponents[id]?.type === 'Subform');
if (!subformIds?.length) {
Expand All @@ -33,17 +34,22 @@
<SubformGroup
key={baseId}
baseId={baseId}
expandedByDefault={expandedByDefault}
/>
));
}

function SubformGroup({ baseId }: { baseId: string }) {
const [isOpen, setIsOpen] = useState(false);
function SubformGroup({ baseId, expandedByDefault }: { baseId: string; expandedByDefault?: boolean }) {

Check warning on line 42 in src/features/navigation/components/SubformsForPage.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-frontend-react&issues=AZzSnQfdNQvIVGdB2CvI&open=AZzSnQfdNQvIVGdB2CvI&pullRequest=4016
const currentPageId = useNavigationParam('pageKey');
const pageKey = useLayoutLookups().componentToPage[baseId];
if (!pageKey) {
throw new Error(`Unable to find page for subform with id ${baseId}`);
}

const isCurrentPage = pageKey === currentPageId;
const [isOpen, setIsOpen] = useState(isCurrentPage || !!expandedByDefault);
useLayoutEffect(() => setIsOpen(isCurrentPage || !!expandedByDefault), [isCurrentPage, expandedByDefault]);

const subformIdsWithError = useComponentValidationsFor(baseId).find(isSubformValidation)?.subformDataElementIds;
const { layoutSet, textResourceBindings, entryDisplayName } = useExternalItem(baseId, 'Subform');
const title = useEvalExpression(textResourceBindings?.title, {
Expand Down
38 changes: 38 additions & 0 deletions src/features/navigation/useNavigateToPageWithValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useOnPageNavigationValidation } from 'src/features/validation/callbacks/onPageNavigationValidation';
import { useNavigationParam } from 'src/hooks/navigation';
import { useNavigatePage } from 'src/hooks/useNavigatePage';
import { useEffectivePageValidation } from 'src/hooks/usePageValidation';

export function useNavigateToPageWithValidation() {
const currentPageId = useNavigationParam('pageKey');
const { navigateToPage, order, maybeSaveOnPageChange } = useNavigatePage();
const onPageNavigationValidation = useOnPageNavigationValidation();
const { getPageValidation } = useEffectivePageValidation(currentPageId ?? '');

return async (targetPage: string, onNavigate?: () => void) => {
if (!currentPageId || targetPage === currentPageId) {
return;
}

const currentIndex = order.indexOf(currentPageId);
const targetIndex = order.indexOf(targetPage);
if (currentIndex === -1 || targetIndex === -1) {
return;
}

const isForward = targetIndex > currentIndex;
const validationOnNavigation = getPageValidation();

await maybeSaveOnPageChange();

if (isForward && validationOnNavigation) {
const hasValidationErrors = await onPageNavigationValidation(currentPageId, validationOnNavigation);
if (hasValidationErrors) {
return;
}
}

await navigateToPage(targetPage);
onNavigate?.();
};
}
Loading
Loading