diff --git a/src/components/form/Form.tsx b/src/components/form/Form.tsx index 8c837da5ae..0a8f92bf4f 100644 --- a/src/components/form/Form.tsx +++ b/src/components/form/Form.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; -import { useLocation } from 'react-router-dom'; import Grid from '@material-ui/core/Grid'; import deepEqual from 'fast-deep-equal'; @@ -18,15 +17,17 @@ import { useUiConfigContext } from 'src/features/form/layout/UiConfigContext'; import { usePageSettings } from 'src/features/form/layoutSettings/LayoutSettingsContext'; import { useLanguage } from 'src/features/language/useLanguage'; import { + SearchParams, useNavigate, useNavigationParam, + useNavigationPath, useQueryKey, useQueryKeysAsString, useQueryKeysAsStringAsRef, } from 'src/features/routing/AppRoutingContext'; import { useOnFormSubmitValidation } from 'src/features/validation/callbacks/onFormSubmitValidation'; import { useTaskErrors } from 'src/features/validation/selectors/taskErrors'; -import { SearchParams, useCurrentView, useNavigatePage, useStartUrl } from 'src/hooks/useNavigatePage'; +import { useCurrentView, useNavigatePage, useStartUrl } from 'src/hooks/useNavigatePage'; import { GenericComponentById } from 'src/layout/GenericComponent'; import { extractBottomButtons } from 'src/utils/formLayout'; import { getPageTitle } from 'src/utils/getPageTitle'; @@ -159,7 +160,7 @@ export function FormFirstPage() { const navigate = useNavigate(); const startUrl = useStartUrl(); - const currentLocation = `${useLocation().pathname}${useQueryKeysAsString()}`; + const currentLocation = `${useNavigationPath()}${useQueryKeysAsString()}`; useEffect(() => { if (currentLocation !== startUrl) { @@ -181,7 +182,6 @@ function useRedirectToStoredPage() { const instanceGuid = useNavigationParam('instanceGuid'); const { isValidPageId, navigateToPage } = useNavigatePage(); const applicationMetadataId = useApplicationMetadata()?.id; - const location = useLocation().pathname; const instanceId = `${partyId}/${instanceGuid}`; const currentViewCacheKey = instanceId || applicationMetadataId; @@ -194,7 +194,7 @@ function useRedirectToStoredPage() { navigateToPage(lastVisitedPage, { replace: true }); } } - }, [pageKey, currentViewCacheKey, isValidPageId, location, navigateToPage]); + }, [pageKey, currentViewCacheKey, isValidPageId, navigateToPage]); } /** diff --git a/src/components/form/LinkToPotentialNode.tsx b/src/components/form/LinkToPotentialNode.tsx index 7c4e55328a..e8fbd08375 100644 --- a/src/components/form/LinkToPotentialNode.tsx +++ b/src/components/form/LinkToPotentialNode.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router-dom'; import type { LinkProps } from 'react-router-dom'; -import { SearchParams } from 'src/hooks/useNavigatePage'; +import { SearchParams } from 'src/features/routing/AppRoutingContext'; import { Hidden, useNode } from 'src/utils/layout/NodesContext'; type Props = LinkProps & { children?: React.ReactNode }; diff --git a/src/components/wrappers/ProcessWrapper.tsx b/src/components/wrappers/ProcessWrapper.tsx index ed60473d46..6477e292ad 100644 --- a/src/components/wrappers/ProcessWrapper.tsx +++ b/src/components/wrappers/ProcessWrapper.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { Helmet } from 'react-helmet-async'; -import { Route, Routes, useLocation } from 'react-router-dom'; +import { Route, Routes } from 'react-router-dom'; import { Button } from '@digdir/designsystemet-react'; import Grid from '@material-ui/core/Grid'; @@ -21,7 +21,12 @@ import { PDFWrapper } from 'src/features/pdf/PDFWrapper'; import { Confirm } from 'src/features/processEnd/confirm/containers/Confirm'; import { Feedback } from 'src/features/processEnd/feedback/Feedback'; import { ReceiptContainer } from 'src/features/receipt/ReceiptContainer'; -import { useNavigate, useNavigationParam, useQueryKeysAsString } from 'src/features/routing/AppRoutingContext'; +import { + useNavigate, + useNavigationParam, + useNavigationPath, + useQueryKeysAsString, +} from 'src/features/routing/AppRoutingContext'; import { TaskKeys, useIsCurrentTask, useNavigatePage, useStartUrl } from 'src/hooks/useNavigatePage'; import { implementsSubRouting } from 'src/layout'; import { RedirectBackToMainForm } from 'src/layout/Subform/SubformWrapper'; @@ -86,7 +91,7 @@ export function NavigateToStartUrl() { const currentTaskId = useLaxProcessData()?.currentTask?.elementId; const startUrl = useStartUrl(currentTaskId); - const currentLocation = `${useLocation().pathname}${useQueryKeysAsString()}`; + const currentLocation = `${useNavigationPath()}${useQueryKeysAsString()}`; useEffect(() => { if (currentLocation !== startUrl) { diff --git a/src/features/datamodel/DataModelsProvider.tsx b/src/features/datamodel/DataModelsProvider.tsx index cef6b30a3d..fb05a74f19 100644 --- a/src/features/datamodel/DataModelsProvider.tsx +++ b/src/features/datamodel/DataModelsProvider.tsx @@ -22,6 +22,7 @@ import { MissingDataTypeException, } from 'src/features/datamodel/utils'; import { useLayouts } from 'src/features/form/layout/LayoutsContext'; +import { useCurrentLayoutSetId } from 'src/features/form/layoutSets/useCurrentLayoutSet'; import { useFormDataQuery } from 'src/features/formData/useFormDataQuery'; import { useLaxInstanceAllDataElementsNow, useLaxInstanceDataElements } from 'src/features/instance/InstanceContext'; import { MissingRolesError } from 'src/features/instantiate/containers/MissingRolesError'; @@ -33,6 +34,7 @@ import type { IExpressionValidations } from 'src/features/validation'; import type { IDataModelReference } from 'src/layout/common.generated'; interface DataModelsState { + layoutSetId: string | undefined; defaultDataType: string | undefined; allDataTypes: string[] | null; writableDataTypes: string[] | null; @@ -45,7 +47,12 @@ interface DataModelsState { } interface DataModelsMethods { - setDataTypes: (allDataTypes: string[], writableDataTypes: string[], defaultDataType: string | undefined) => void; + setDataTypes: ( + allDataTypes: string[], + writableDataTypes: string[], + defaultDataType: string | undefined, + layoutSetId: string | undefined, + ) => void; setInitialData: (dataType: string, initialData: object, dataElementId: string | null) => void; setDataModelSchema: (dataType: string, schema: JSONSchema7, lookupTool: SchemaLookupTool) => void; setExpressionValidationConfig: (dataType: string, config: IExpressionValidations | null) => void; @@ -54,6 +61,7 @@ interface DataModelsMethods { function initialCreateStore() { return createStore()((set) => ({ + layoutSetId: undefined, defaultDataType: undefined, allDataTypes: null, writableDataTypes: null, @@ -65,8 +73,8 @@ function initialCreateStore() { expressionValidationConfigs: {}, error: null, - setDataTypes: (allDataTypes, writableDataTypes, defaultDataType) => { - set(() => ({ allDataTypes, writableDataTypes, defaultDataType })); + setDataTypes: (allDataTypes, writableDataTypes, defaultDataType, layoutSetId) => { + set(() => ({ allDataTypes, writableDataTypes, defaultDataType, layoutSetId })); }, setInitialData: (dataType, initialData, dataElementId) => { set((state) => ({ @@ -137,6 +145,7 @@ function DataModelsLoader() { const defaultDataType = useCurrentDataModelName(); const isStateless = useApplicationMetadata().isStatelessApp; const dataElements = useLaxInstanceAllDataElementsNow(); + const layoutSetId = useCurrentLayoutSetId(); // Subform const overriddenDataElement = useTaskStore((state) => state.overriddenDataModelUuid); @@ -175,8 +184,8 @@ function DataModelsLoader() { } } - setDataTypes(allValidDataTypes, writableDataTypes, defaultDataType); - }, [applicationMetadata, defaultDataType, isStateless, layouts, setDataTypes, dataElements]); + setDataTypes(allValidDataTypes, writableDataTypes, defaultDataType, layoutSetId); + }, [applicationMetadata, defaultDataType, isStateless, layouts, setDataTypes, dataElements, layoutSetId]); // We should load form data and schema for all referenced data models, schema is used for dataModelBinding validation which we want to do even if it is readonly // We only need to load expression validation config for data types that are not readonly. Additionally, backend will error if we try to validate a model we are not supposed to @@ -201,9 +210,9 @@ function DataModelsLoader() { } function BlockUntilLoaded({ children }: PropsWithChildren) { - const { allDataTypes, writableDataTypes, initialData, schemas, expressionValidationConfigs, error } = useSelector( - (state) => state, - ); + const { layoutSetId, allDataTypes, writableDataTypes, initialData, schemas, expressionValidationConfigs, error } = + useSelector((state) => state); + const actualCurrentTask = useCurrentLayoutSetId(); const isPDF = useIsPdf(); if (error) { @@ -219,6 +228,12 @@ function BlockUntilLoaded({ children }: PropsWithChildren) { return ; } + if (layoutSetId !== actualCurrentTask) { + // The layout-set has changed since the state was set, so we need to wait for the new layout-set to be loaded + // and the relevant data model bindings there to be parsed. + return ; + } + // in PDF mode, we do not load schema, or expression validation config. So we should not block loading in that case // Edit: Since #2244, layout and data model binding validations work differently, so enabling schema loading to make things work for now. diff --git a/src/features/expressions/expression-functions.ts b/src/features/expressions/expression-functions.ts index 599c1dedc8..04f9b6444b 100644 --- a/src/features/expressions/expression-functions.ts +++ b/src/features/expressions/expression-functions.ts @@ -4,7 +4,7 @@ import type { Mutable } from 'utility-types'; import { ExprRuntimeError, NodeNotFound, NodeNotFoundWithoutContext } from 'src/features/expressions/errors'; import { ExprVal } from 'src/features/expressions/types'; import { addError } from 'src/features/expressions/validation'; -import { SearchParams } from 'src/hooks/useNavigatePage'; +import { SearchParams } from 'src/features/routing/AppRoutingContext'; import { implementsDisplayData } from 'src/layout'; import { buildAuthContext } from 'src/utils/authContext'; import { isDate } from 'src/utils/dateHelpers'; diff --git a/src/features/instance/ProcessNavigationContext.tsx b/src/features/instance/ProcessNavigationContext.tsx index ee97d62c13..646c3519c4 100644 --- a/src/features/instance/ProcessNavigationContext.tsx +++ b/src/features/instance/ProcessNavigationContext.tsx @@ -12,7 +12,7 @@ import { useLaxInstanceId, useStrictInstanceRefetch } from 'src/features/instanc import { useReFetchProcessData } from 'src/features/instance/ProcessContext'; import { Lang } from 'src/features/language/Lang'; import { useCurrentLanguage } from 'src/features/language/LanguageProvider'; -import { useNavigationParam } from 'src/features/routing/AppRoutingContext'; +import { useIsSubformPage } from 'src/features/routing/AppRoutingContext'; import { useUpdateInitialValidations } from 'src/features/validation/backendValidation/backendValidationQuery'; import { appSupportsIncrementalValidationFeatures } from 'src/features/validation/backendValidation/backendValidationUtils'; import { useOnFormSubmitValidation } from 'src/features/validation/callbacks/onFormSubmitValidation'; @@ -174,7 +174,7 @@ export function ProcessNavigationProvider({ children }: React.PropsWithChildren) export const useProcessNavigation = () => { // const { isSubformPage } = useNavigationParams(); - const isSubformPage = useNavigationParam('isSubformPage'); + const isSubformPage = useIsSubformPage(); if (isSubformPage) { throw new Error('Cannot use process navigation in a subform'); } diff --git a/src/features/routing/AppRoutingContext.tsx b/src/features/routing/AppRoutingContext.tsx index 188c4da01a..dfdfe07b1c 100644 --- a/src/features/routing/AppRoutingContext.tsx +++ b/src/features/routing/AppRoutingContext.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { useLocation, useMatch, useNavigate as useNativeNavigate } from 'react-router-dom'; +import { matchPath, useLocation, useNavigate as useNativeNavigate } from 'react-router-dom'; import type { MutableRefObject, PropsWithChildren } from 'react'; import { createStore } from 'zustand'; @@ -8,7 +8,7 @@ import { createZustandContext } from 'src/core/contexts/zustandContext'; export type NavigationEffectCb = () => void; -interface ContextParams { +interface PathParams { partyId?: string; instanceGuid?: string; taskId?: string; @@ -16,26 +16,27 @@ interface ContextParams { componentId?: string; dataElementId?: string; mainPageKey?: string; - isSubformPage?: boolean; } + +export enum SearchParams { + FocusComponentId = 'focusComponentId', + ExitSubform = 'exitSubform', + Validate = 'validate', + Pdf = 'pdf', +} + interface Context { - params: ContextParams; - queryKeys: { - [key: string]: string | undefined; - }; - updateParams: (params: Context['params']) => void; - updateQueryKeys: (queryKeys: Context['queryKeys']) => void; + hash: string; + updateHash: (hash: string) => void; effectCallback: NavigationEffectCb | null; setEffectCallback: (cb: NavigationEffectCb | null) => void; navigateRef: MutableRefObject>; } -function newStore() { +function newStore({ initialLocation }: { initialLocation: string | undefined }) { return createStore((set) => ({ - params: {}, - queryKeys: {}, - updateParams: (params) => set({ params }), - updateQueryKeys: (queryKeys) => set({ queryKeys }), + hash: initialLocation ? initialLocation : `${window.location.hash}`, + updateHash: (hash: string) => set({ hash }), effectCallback: null, setEffectCallback: (effectCallback: NavigationEffectCb) => set({ effectCallback }), // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -43,90 +44,148 @@ function newStore() { })); } -const { Provider, useSelector, useSelectorAsRef } = createZustandContext>({ +const { Provider, useSelector, useStaticSelector, useMemoSelector, useStore } = createZustandContext< + ReturnType +>({ name: 'AppRouting', required: true, initialCreateStore: newStore, }); +/** + * This provider is responsible for keeping track of the URL (hash) and providing hooks to read it. It fixes a + * fundamental issue with the react-router-dom library, where every hook reading the URL will cause a re-render + * regardless of whether the part of the URL you were actually interested in has changed or not. That includes + * the useNavigate() hook, which re-renders your component every time you navigate to a new URL - just so that it + * can support relative navigation. + * + * This wrapper solves this by making sure both of these use-cases gives you the most up-to-date URL: + * 1. When rendering a new component for the first time, caused by a route change, the URL is read from the current + * window.location.hash (not from the zustand store). + * 2. When the URL changes after the component has been rendered, all selectors will re-run and only re-render the + * component if the part of the URL they are interested in has changed (based on the zustand equality check). + * + * In an earlier iteration this read the URL parts from the zustand store as well, and updated them in a useEffect, + * but that caused the store to be out of sync with the actual URL - which in turn lead to some components getting + * the wrong state (and thus rendered the wrong thing) at first, only to correct itself after the useEffect had run. + */ export function AppRoutingProvider({ children }: PropsWithChildren) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const location = window.inUnitTest ? useLocation() : undefined; + const initialLocation = location ? location.pathname + location.search : undefined; + return ( - - - + + {children} ); } -export const useAllNavigationParamsAsRef = () => useSelectorAsRef((ctx) => ctx.params); -export const useNavigationParam = (key: T) => useSelector((ctx) => ctx.params[key]); +function getPath(hashFromState: string): string { + const hash = window.inUnitTest ? `#${hashFromState}` : window.location.hash; + return hash.slice(1).split('?')[0]; +} + +function getSearch(hashFromState: string): string { + const hash = window.inUnitTest ? hashFromState : window.location.hash; + const search = hash.split('?')[1] ?? ''; + return search ? `?${search}` : ''; +} + +/** + * This pretends to be a ref, but it's actually a getter that returns the current value (executes the getter each + * time you access the `current` property). + */ +class OnDemandRef { + constructor(private readonly getter: () => T) {} + + get current() { + return this.getter(); + } +} + +function useStaticRef(getter: (state: Context) => T) { + const store = useStore(); + return new OnDemandRef(() => getter(store.getState())) as { current: T }; +} + +export const useQueryKeysAsStringAsRef = () => useStaticRef((s) => getSearch(s.hash)); +export const useAllNavigationParamsAsRef = () => useStaticRef((s) => matchParams(getPath(s.hash))); + +export const useNavigationParam = (key: T) => + useSelector((s) => { + const path = getPath(s.hash); + const matches = matchers.map((matcher) => matchPath(matcher, path)); + return paramFrom(matches, key) as PathParams[T]; + }); + +export const useNavigationPath = () => useSelector((s) => getPath(s.hash)); +export const useNavigationParams = () => useMemoSelector((s) => matchParams(getPath(s.hash))); export const useNavigationEffect = () => useSelector((ctx) => ctx.effectCallback); export const useSetNavigationEffect = () => useSelector((ctx) => ctx.setEffectCallback); -export const useQueryKeysAsString = () => useSelector((ctx) => queryKeysToString(ctx.queryKeys)); -export const useQueryKeysAsStringAsRef = () => useSelectorAsRef((ctx) => queryKeysToString(ctx.queryKeys)); -export const useQueryKey = (key: string) => useSelector((ctx) => ctx.queryKeys[key]); +export const useQueryKeysAsString = () => useSelector((s) => getSearch(s.hash)); +export const useQueryKey = (key: SearchParams) => useSelector((s) => new URLSearchParams(getSearch(s.hash)).get(key)); + +export const useIsSubformPage = () => + useSelector((s) => { + const path = getPath(s.hash); + const matches = matchers.map((matcher) => matchPath(matcher, path)); + return !!paramFrom(matches, 'mainPageKey'); + }); // Use this instead of the native one to avoid re-rendering whenever the route changes export const useNavigate = () => useSelector((ctx) => ctx.navigateRef).current; -export const useNavigationParams = (): Context['params'] => { - const matches = [ - useMatch('/instance/:partyId/:instanceGuid'), - useMatch('/instance/:partyId/:instanceGuid/:taskId'), - useMatch('/instance/:partyId/:instanceGuid/:taskId/:pageKey'), - useMatch('/:pageKey'), // Stateless - - // Subform - useMatch('/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId'), - useMatch('/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId/:dataElementId'), - useMatch('/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId/:dataElementId/:pageKey'), - ]; - - const partyId = matches.reduce((acc, match) => acc ?? match?.params['partyId'], undefined); - const instanceGuid = matches.reduce((acc, match) => acc ?? match?.params['instanceGuid'], undefined); - const taskId = matches.reduce((acc, match) => acc ?? match?.params['taskId'], undefined); - const componentId = matches.reduce((acc, match) => acc ?? match?.params['componentId'], undefined); - const dataElementId = matches.reduce((acc, match) => acc ?? match?.params['dataElementId'], undefined); - const _pageKey = matches.reduce((acc, match) => acc ?? match?.params['pageKey'], undefined); - const _mainPageKey = matches.reduce((acc, match) => acc ?? match?.params['mainPageKey'], undefined); - const pageKey = _pageKey === undefined ? undefined : decodeURIComponent(_pageKey); - const mainPageKey = _mainPageKey === undefined ? undefined : decodeURIComponent(_mainPageKey); - - const isSubformPage = !!mainPageKey; +const matchers: string[] = [ + '/instance/:partyId/:instanceGuid', + '/instance/:partyId/:instanceGuid/:taskId', + '/instance/:partyId/:instanceGuid/:taskId/:pageKey', + '/:pageKey', // Stateless - return { - partyId, - instanceGuid, - taskId, - pageKey, - componentId, - dataElementId, - mainPageKey, - isSubformPage, - }; -}; + // Subform + '/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId', + '/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId/:dataElementId', + '/instance/:partyId/:instanceGuid/:taskId/:mainPageKey/:componentId/:dataElementId/:pageKey', +]; -function UpdateParams() { - const updateParams = useSelector((ctx) => ctx.updateParams); - const params = useNavigationParams(); +type Matches = ReturnType[]; - useEffect(() => { - updateParams(params); - }, [params, updateParams]); +const requiresDecoding: Set = new Set(['pageKey', 'mainPageKey']); - return null; +function paramFrom(matches: Matches, key: keyof PathParams): string | undefined { + const param = matches.reduce((acc, match) => acc ?? match?.params[key], undefined); + const decode = requiresDecoding.has(key); + return decode && param ? decodeURIComponent(param) : param; +} + +function matchParams(path: string): PathParams { + const matches = matchers.map((matcher) => matchPath(matcher, path)); + return { + partyId: paramFrom(matches, 'partyId'), + instanceGuid: paramFrom(matches, 'instanceGuid'), + taskId: paramFrom(matches, 'taskId'), + pageKey: paramFrom(matches, 'pageKey'), + componentId: paramFrom(matches, 'componentId'), + dataElementId: paramFrom(matches, 'dataElementId'), + mainPageKey: paramFrom(matches, 'mainPageKey'), + }; } -function UpdateQueryKeys() { - const queryKeys = useLocation().search ?? ''; - const updateQueryKeys = useSelector((ctx) => ctx.updateQueryKeys); +/** + * The URL hash is saved into the zustand store, but it's never read from there. This just serves to trigger the + * selectors to re-run when the hash changes, thus making the hooks that depend on the hash re-run and figure out + * if components should re-render based on URL changes. + */ +function UpdateHash() { + const updateHash = useStaticSelector((ctx) => ctx.updateHash); + const location = useLocation(); + const hash = location.pathname + location.search; useEffect(() => { - const map = Object.fromEntries(new URLSearchParams(queryKeys).entries()); - updateQueryKeys(map); - }, [queryKeys, updateQueryKeys]); + updateHash(hash); + }, [hash, updateHash]); return null; } @@ -137,17 +196,3 @@ function UpdateNavigate() { return null; } - -function queryKeysToString(qc: Context['queryKeys']): string { - const qcFiltered = Object.fromEntries(Object.entries(qc).filter(filterUndefined)); - if (Object.keys(qcFiltered).length === 0) { - return ''; - } - - const searchParams = new URLSearchParams(qcFiltered); - return `?${searchParams.toString()}`; -} - -function filterUndefined(obj: [string, string | undefined]): obj is [string, string] { - return obj[1] !== undefined; -} diff --git a/src/hooks/useIsPdf.ts b/src/hooks/useIsPdf.ts index 719cb4ab87..71404fe25d 100644 --- a/src/hooks/useIsPdf.ts +++ b/src/hooks/useIsPdf.ts @@ -1,8 +1,8 @@ -import { useQueryKey } from 'src/features/routing/AppRoutingContext'; +import { SearchParams, useQueryKey } from 'src/features/routing/AppRoutingContext'; /** * Hook checking whether we are in PDF generation mode */ export function useIsPdf() { - return useQueryKey('pdf') === '1'; + return useQueryKey(SearchParams.Pdf) === '1'; } diff --git a/src/hooks/useNavigatePage.ts b/src/hooks/useNavigatePage.ts index 9bc1b2df28..b9e84a30b4 100644 --- a/src/hooks/useNavigatePage.ts +++ b/src/hooks/useNavigatePage.ts @@ -8,6 +8,7 @@ import { useLaxLayoutSettings, usePageSettings } from 'src/features/form/layoutS import { FD } from 'src/features/formData/FormDataWrite'; import { useGetTaskTypeById, useLaxProcessData } from 'src/features/instance/ProcessContext'; import { + SearchParams, useAllNavigationParamsAsRef, useNavigate as useCtxNavigate, useNavigationParam, @@ -36,12 +37,6 @@ export enum TaskKeys { CustomReceipt = 'CustomReceipt', } -export enum SearchParams { - FocusComponentId = 'focusComponentId', - ExitSubform = 'exitSubform', - Validate = 'validate', -} - const emptyArray: never[] = []; /** @@ -125,10 +120,10 @@ export const useStartUrl = (forcedTaskId?: string) => { const queryKeys = useQueryKeysAsString(); const order = usePageOrder(); // This needs up to date params, so using the native hook that re-renders often - // However, this hook is only used in cases where we immediatly navigate to a different path + // However, this hook is only used in cases where we immediately navigate to a different path // so it does not make a difference here. - const { partyId, instanceGuid, taskId, isSubformPage, mainPageKey, componentId, dataElementId } = - useNavigationParams(); + const { partyId, instanceGuid, taskId, mainPageKey, componentId, dataElementId } = useNavigationParams(); + const isSubformPage = !!mainPageKey; const taskType = useGetTaskTypeById()(taskId); const isStateless = useApplicationMetadata().isStatelessApp; @@ -353,7 +348,7 @@ export function useNavigatePage() { }, [getPreviousPage, navigateToPage]); const exitSubform = async () => { - if (!navParams.current.isSubformPage || !navParams.current.mainPageKey) { + if (!navParams.current.mainPageKey) { window.logWarn('Tried to close subform page while not in a subform.'); return; } diff --git a/src/layout/CustomButton/CustomButtonComponent.tsx b/src/layout/CustomButton/CustomButtonComponent.tsx index 595965d817..4d5c6c3713 100644 --- a/src/layout/CustomButton/CustomButtonComponent.tsx +++ b/src/layout/CustomButton/CustomButtonComponent.tsx @@ -9,7 +9,7 @@ import { useResetScrollPosition } from 'src/core/ui/useResetScrollPosition'; import { FD } from 'src/features/formData/FormDataWrite'; import { useLaxProcessData } from 'src/features/instance/ProcessContext'; import { Lang } from 'src/features/language/Lang'; -import { useNavigationParam } from 'src/features/routing/AppRoutingContext'; +import { useIsSubformPage, useNavigationParam } from 'src/features/routing/AppRoutingContext'; import { useOnPageNavigationValidation } from 'src/features/validation/callbacks/onPageNavigationValidation'; import { useNavigatePage } from 'src/hooks/useNavigatePage'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; @@ -67,7 +67,7 @@ const isServerAction = (action: CBTypes.CustomAction): action is CBTypes.ServerA function useHandleClientActions(): UseHandleClientActions { const { navigateToPage, navigateToNextPage, navigateToPreviousPage, exitSubform } = useNavigatePage(); const mainPageKey = useNavigationParam('mainPageKey'); - const isSubformPage = useNavigationParam('isSubformPage'); + const isSubformPage = useIsSubformPage(); const frontendActions: ClientActionHandlers = useMemo( () => ({ diff --git a/src/layout/FileUpload/FileUploadComponent.tsx b/src/layout/FileUpload/FileUploadComponent.tsx index 72e2f45565..66da4abf4a 100644 --- a/src/layout/FileUpload/FileUploadComponent.tsx +++ b/src/layout/FileUpload/FileUploadComponent.tsx @@ -8,7 +8,7 @@ import { useAttachmentsFor, useAttachmentsUploader } from 'src/features/attachme import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; -import { useNavigationParam } from 'src/features/routing/AppRoutingContext'; +import { useIsSubformPage } from 'src/features/routing/AppRoutingContext'; import { ComponentValidations } from 'src/features/validation/ComponentValidations'; import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/unifiedValidationsForNode'; import { hasValidationErrors } from 'src/features/validation/utils'; @@ -38,7 +38,7 @@ export function FileUploadComponent({ node }: IFileUploadWithTagProps): React.JS textResourceBindings, dataModelBindings, } = item; - const isSubformPage = useNavigationParam('isSubformPage'); + const isSubformPage = useIsSubformPage(); if (isSubformPage) { throw new Error('Cannot use a FileUpload components within a subform'); } diff --git a/src/layout/Subform/SubformComponent.tsx b/src/layout/Subform/SubformComponent.tsx index d70197830e..34264be840 100644 --- a/src/layout/Subform/SubformComponent.tsx +++ b/src/layout/Subform/SubformComponent.tsx @@ -13,7 +13,7 @@ import { useFormDataQuery } from 'src/features/formData/useFormDataQuery'; import { useStrictDataElements, useStrictInstanceId } from 'src/features/instance/InstanceContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; -import { useNavigationParam } from 'src/features/routing/AppRoutingContext'; +import { useIsSubformPage } from 'src/features/routing/AppRoutingContext'; import { useAddEntryMutation, useDeleteEntryMutation } from 'src/features/subformData/useSubformMutations'; import { isSubformValidation } from 'src/features/validation'; import { useComponentValidationsForNode } from 'src/features/validation/selectors/componentValidationsForNode'; @@ -35,7 +35,7 @@ export function SubformComponent({ node }: PropsFromGenericComponent<'Subform'>) showDeleteButton = true, } = useNodeItem(node); - const isSubformPage = useNavigationParam('isSubformPage'); + const isSubformPage = useIsSubformPage(); if (isSubformPage) { window.logErrorOnce('Cannot use a SubformComponent component within a subform'); throw new Error('Cannot use a SubformComponent component within a subform'); diff --git a/src/layout/Subform/Summary/SubformSummaryTable.tsx b/src/layout/Subform/Summary/SubformSummaryTable.tsx index 72b5d7ce0f..77fef506c2 100644 --- a/src/layout/Subform/Summary/SubformSummaryTable.tsx +++ b/src/layout/Subform/Summary/SubformSummaryTable.tsx @@ -13,7 +13,7 @@ import { useStrictDataElements, useStrictInstanceId } from 'src/features/instanc import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { usePdfModeActive } from 'src/features/pdf/PDFWrapper'; -import { useNavigationParam } from 'src/features/routing/AppRoutingContext'; +import { useIsSubformPage } from 'src/features/routing/AppRoutingContext'; import { isSubformValidation } from 'src/features/validation'; import { useComponentValidationsForNode } from 'src/features/validation/selectors/componentValidationsForNode'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; @@ -102,7 +102,7 @@ function SubformTableRow({ export function SubformSummaryTable({ targetNode }: ISubformSummaryComponent): React.JSX.Element | null { const { id, layoutSet, textResourceBindings, tableColumns = [] } = useNodeItem(targetNode); - const isSubformPage = useNavigationParam('isSubformPage'); + const isSubformPage = useIsSubformPage(); if (isSubformPage) { window.logErrorOnce('Cannot use a SubformComponent component within a subform'); throw new Error('Cannot use a SubformComponent component within a subform');