From 9a03ddf8e5d8f8dc4485e68095b35a323292eae5 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 29 Oct 2025 10:36:17 +0100 Subject: [PATCH 1/5] add feature flag --- src/features/applicationMetadata/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/features/applicationMetadata/types.ts b/src/features/applicationMetadata/types.ts index 58c8d84f5a..a3a6e80b12 100644 --- a/src/features/applicationMetadata/types.ts +++ b/src/features/applicationMetadata/types.ts @@ -52,4 +52,5 @@ export interface IPartyTypesAllowed { export interface IBackendFeaturesState { jsonObjectInDataResponse: boolean; // Extended attachment validation + addInstanceIdentifierToLayoutRequests: boolean; // Add instance identifier to layout requests } From 174c1c504b63ed5c03c669d965c1b06f21e6957f Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Thu, 30 Oct 2025 07:35:31 +0100 Subject: [PATCH 2/5] route instance requests to separate endpoint if feature flag is enabled --- src/features/form/layout/LayoutsContext.tsx | 11 +++++++++-- src/queries/queries.ts | 3 ++- src/utils/urls/appUrlHelper.ts | 7 ++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/features/form/layout/LayoutsContext.tsx b/src/features/form/layout/LayoutsContext.tsx index 5af44595dd..03f5271755 100644 --- a/src/features/form/layout/LayoutsContext.tsx +++ b/src/features/form/layout/LayoutsContext.tsx @@ -6,13 +6,14 @@ import { useAppQueries } from 'src/core/contexts/AppQueriesProvider'; import { ContextNotProvided } from 'src/core/contexts/context'; import { delayedContext } from 'src/core/contexts/delayedContext'; import { createQueryContext } from 'src/core/contexts/queryContext'; +import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider'; import { useCurrentDataModelName } from 'src/features/datamodel/useBindingSchema'; import { cleanLayout } from 'src/features/form/layout/cleanLayout'; import { makeLayoutLookups } from 'src/features/form/layout/makeLayoutLookups'; import { applyLayoutQuirks } from 'src/features/form/layout/quirks'; import { useLayoutSets } from 'src/features/form/layoutSets/LayoutSetsProvider'; import { useLayoutSetIdFromUrl } from 'src/features/form/layoutSets/useCurrentLayoutSet'; -import { useInstanceDataQuery } from 'src/features/instance/InstanceContext'; +import { useInstanceDataQuery, useLaxInstanceId } from 'src/features/instance/InstanceContext'; import { useProcessQuery } from 'src/features/instance/useProcessQuery'; import { makeLikertChildId } from 'src/layout/Likert/Generator/makeLikertChildId'; import type { QueryDefinition } from 'src/core/queries/usePrefetchQuery'; @@ -32,10 +33,16 @@ export function useLayoutQueryDef( layoutSetId?: string, ): QueryDefinition { const { fetchLayouts } = useAppQueries(); + const instanceId = useLaxInstanceId(); + const features = useApplicationMetadata().features ?? {}; + return { queryKey: ['formLayouts', layoutSetId, enabled], queryFn: layoutSetId - ? () => fetchLayouts(layoutSetId).then((layouts) => processLayouts(layouts, layoutSetId, defaultDataModelType)) + ? () => + fetchLayouts(layoutSetId, features.addInstanceIdentifierToLayoutRequests ? instanceId : undefined).then( + (layouts) => processLayouts(layouts, layoutSetId, defaultDataModelType), + ) : skipToken, enabled: enabled && !!layoutSetId, }; diff --git a/src/queries/queries.ts b/src/queries/queries.ts index d847a751ba..29edc1d7c3 100644 --- a/src/queries/queries.ts +++ b/src/queries/queries.ts @@ -290,7 +290,8 @@ export const fetchFooterLayout = (): Promise => httpGet(ge export const fetchLayoutSets = (): Promise => httpGet(getLayoutSetsUrl()); -export const fetchLayouts = (layoutSetId: string): Promise => httpGet(getLayoutsUrl(layoutSetId)); +export const fetchLayouts = (layoutSetId: string, instanceId?: string): Promise => + httpGet(getLayoutsUrl(layoutSetId, instanceId)); export const fetchLayoutSettings = (layoutSetId: string): Promise => httpGet(getLayoutSettingsUrl(layoutSetId)); diff --git a/src/utils/urls/appUrlHelper.ts b/src/utils/urls/appUrlHelper.ts index 279d749ff5..d2ec81f820 100644 --- a/src/utils/urls/appUrlHelper.ts +++ b/src/utils/urls/appUrlHelper.ts @@ -185,7 +185,12 @@ export const getLayoutSettingsUrl = (layoutSetId: string) => `${appPath}/api/lay export const getLayoutSetsUrl = () => `${appPath}/api/layoutsets`; export const getFooterLayoutUrl = () => `${appPath}/api/v1/footer`; export const getFetchFormDynamicsUrl = (layoutSetId: string) => `${appPath}/api/ruleconfiguration/${layoutSetId}`; -export const getLayoutsUrl = (layoutSetId: string) => `${appPath}/api/layouts/${layoutSetId}`; +export const getLayoutsUrl = (layoutSetId: string, instanceId?: string) => { + if (instanceId) { + return `${appPath}/instance/${instanceId}/layouts/${layoutSetId}`; + } + return `${appPath}/api/layouts/${layoutSetId}`; +}; export const getRulehandlerUrl = (layoutSet: string) => `${appPath}/api/rulehandler/${layoutSet}`; export const getActiveInstancesUrl = (partyId: number) => `${appPath}/instances/${partyId}/active`; export const getInstanceUiUrl = (instanceId: string) => `${appPath}#/instance/${instanceId}`; From 559868f541d00691dd979b93c23f655a2aff7c51 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Mon, 10 Nov 2025 13:58:43 +0100 Subject: [PATCH 3/5] update path --- src/utils/urls/appUrlHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/urls/appUrlHelper.ts b/src/utils/urls/appUrlHelper.ts index d2ec81f820..2c38baeda5 100644 --- a/src/utils/urls/appUrlHelper.ts +++ b/src/utils/urls/appUrlHelper.ts @@ -187,7 +187,7 @@ export const getFooterLayoutUrl = () => `${appPath}/api/v1/footer`; export const getFetchFormDynamicsUrl = (layoutSetId: string) => `${appPath}/api/ruleconfiguration/${layoutSetId}`; export const getLayoutsUrl = (layoutSetId: string, instanceId?: string) => { if (instanceId) { - return `${appPath}/instance/${instanceId}/layouts/${layoutSetId}`; + return `${appPath}/instances/${instanceId}/layouts/${layoutSetId}`; } return `${appPath}/api/layouts/${layoutSetId}`; }; From 9a750e7e1bbc59a84d3ece3c9048465ed99b83bc Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 12 Nov 2025 08:44:42 +0100 Subject: [PATCH 4/5] add test for new instance api url --- src/utils/urls/appUrlHelper.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/urls/appUrlHelper.test.ts b/src/utils/urls/appUrlHelper.test.ts index 14abe83565..2512cdff61 100644 --- a/src/utils/urls/appUrlHelper.test.ts +++ b/src/utils/urls/appUrlHelper.test.ts @@ -377,6 +377,11 @@ describe('Frontend urlHelper.ts', () => { expect(result).toBe('https://local.altinn.cloud/ttd/test/api/layouts/custom-layout.json'); }); + + it('should include instance ID in layout URL when provided', () => { + const result = getLayoutsUrl('custom-layout.json', 'instanceId-1234'); + expect(result).toBe('https://local.altinn.cloud/ttd/test/instances/instanceId-1234/layouts/custom-layout.json'); + }); }); describe('getLayoutSettingsUrl', () => { From 4aaddfc3ca35cfbffdf5889f80728f4201823da2 Mon Sep 17 00:00:00 2001 From: Jonas Dyrlie Date: Wed, 12 Nov 2025 10:22:13 +0100 Subject: [PATCH 5/5] refactored layouts url code --- src/features/form/layout/LayoutsContext.tsx | 13 +++++++++---- src/queries/queries.ts | 7 +++++-- src/test/renderWithProviders.tsx | 1 + src/utils/urls/appUrlHelper.test.ts | 5 ++++- src/utils/urls/appUrlHelper.ts | 9 +++------ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/features/form/layout/LayoutsContext.tsx b/src/features/form/layout/LayoutsContext.tsx index 03f5271755..45646e6945 100644 --- a/src/features/form/layout/LayoutsContext.tsx +++ b/src/features/form/layout/LayoutsContext.tsx @@ -16,6 +16,7 @@ import { useLayoutSetIdFromUrl } from 'src/features/form/layoutSets/useCurrentLa import { useInstanceDataQuery, useLaxInstanceId } from 'src/features/instance/InstanceContext'; import { useProcessQuery } from 'src/features/instance/useProcessQuery'; 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'; @@ -39,10 +40,14 @@ export function useLayoutQueryDef( return { queryKey: ['formLayouts', layoutSetId, enabled], queryFn: layoutSetId - ? () => - fetchLayouts(layoutSetId, features.addInstanceIdentifierToLayoutRequests ? instanceId : undefined).then( - (layouts) => processLayouts(layouts, layoutSetId, defaultDataModelType), - ) + ? async () => { + const shouldUseInstanceEndpoint = features.addInstanceIdentifierToLayoutRequests && instanceId; + const layouts = shouldUseInstanceEndpoint + ? await fetchLayoutsForInstance(layoutSetId, instanceId) + : await fetchLayouts(layoutSetId); + + return processLayouts(layouts, layoutSetId, defaultDataModelType); + } : skipToken, enabled: enabled && !!layoutSetId, }; diff --git a/src/queries/queries.ts b/src/queries/queries.ts index 29edc1d7c3..8198a10785 100644 --- a/src/queries/queries.ts +++ b/src/queries/queries.ts @@ -27,6 +27,7 @@ import { getFileUploadUrl, getFileUploadUrlOld, getFooterLayoutUrl, + getInstanceLayoutsUrl, getInstantiateUrl, getJsonSchemaUrl, getLayoutSetsUrl, @@ -290,8 +291,10 @@ export const fetchFooterLayout = (): Promise => httpGet(ge export const fetchLayoutSets = (): Promise => httpGet(getLayoutSetsUrl()); -export const fetchLayouts = (layoutSetId: string, instanceId?: string): Promise => - httpGet(getLayoutsUrl(layoutSetId, instanceId)); +export const fetchLayouts = (layoutSetId: string): Promise => httpGet(getLayoutsUrl(layoutSetId)); + +export const fetchLayoutsForInstance = (layoutSetId: string, instanceId: string): Promise => + httpGet(getInstanceLayoutsUrl(layoutSetId, instanceId)); export const fetchLayoutSettings = (layoutSetId: string): Promise => httpGet(getLayoutSettingsUrl(layoutSetId)); diff --git a/src/test/renderWithProviders.tsx b/src/test/renderWithProviders.tsx index 9ca88f1e28..39664bb8a1 100644 --- a/src/test/renderWithProviders.tsx +++ b/src/test/renderWithProviders.tsx @@ -152,6 +152,7 @@ const defaultQueryMocks: AppQueries = { fetchPostPlace: async () => ({ valid: true, result: 'OSLO' }), fetchLayoutSettings: async () => ({ pages: { order: [] } }), fetchLayouts: () => Promise.reject(new Error('fetchLayouts not mocked')), + fetchLayoutsForInstance: () => Promise.reject(new Error('fetchLayoutsForInstance not mocked')), fetchBackendValidations: async () => [], fetchBackendValidationsForDataElement: async () => [], fetchPaymentInformation: async () => paymentResponsePayload, diff --git a/src/utils/urls/appUrlHelper.test.ts b/src/utils/urls/appUrlHelper.test.ts index 2512cdff61..9d7f9de492 100644 --- a/src/utils/urls/appUrlHelper.test.ts +++ b/src/utils/urls/appUrlHelper.test.ts @@ -5,6 +5,7 @@ import { getEnvironmentLoginUrl, getFetchFormDynamicsUrl, getHostname, + getInstanceLayoutsUrl, getInstantiateUrl, getLayoutSettingsUrl, getLayoutsUrl, @@ -377,9 +378,11 @@ describe('Frontend urlHelper.ts', () => { expect(result).toBe('https://local.altinn.cloud/ttd/test/api/layouts/custom-layout.json'); }); + }); + describe('getInstanceLayoutsUrl', () => { it('should include instance ID in layout URL when provided', () => { - const result = getLayoutsUrl('custom-layout.json', 'instanceId-1234'); + const result = getInstanceLayoutsUrl('custom-layout.json', 'instanceId-1234'); expect(result).toBe('https://local.altinn.cloud/ttd/test/instances/instanceId-1234/layouts/custom-layout.json'); }); }); diff --git a/src/utils/urls/appUrlHelper.ts b/src/utils/urls/appUrlHelper.ts index 2c38baeda5..66d642d028 100644 --- a/src/utils/urls/appUrlHelper.ts +++ b/src/utils/urls/appUrlHelper.ts @@ -185,12 +185,9 @@ export const getLayoutSettingsUrl = (layoutSetId: string) => `${appPath}/api/lay export const getLayoutSetsUrl = () => `${appPath}/api/layoutsets`; export const getFooterLayoutUrl = () => `${appPath}/api/v1/footer`; export const getFetchFormDynamicsUrl = (layoutSetId: string) => `${appPath}/api/ruleconfiguration/${layoutSetId}`; -export const getLayoutsUrl = (layoutSetId: string, instanceId?: string) => { - if (instanceId) { - return `${appPath}/instances/${instanceId}/layouts/${layoutSetId}`; - } - return `${appPath}/api/layouts/${layoutSetId}`; -}; +export const getLayoutsUrl = (layoutSetId: string) => `${appPath}/api/layouts/${layoutSetId}`; +export const getInstanceLayoutsUrl = (layoutSetId: string, instanceId: string) => + `${appPath}/instances/${instanceId}/layouts/${layoutSetId}`; export const getRulehandlerUrl = (layoutSet: string) => `${appPath}/api/rulehandler/${layoutSet}`; export const getActiveInstancesUrl = (partyId: number) => `${appPath}/instances/${partyId}/active`; export const getInstanceUiUrl = (instanceId: string) => `${appPath}#/instance/${instanceId}`;