From 96af1f24952b7a6442c36f01f8ae1e8791254ad8 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Thu, 26 Oct 2023 16:42:32 +0200 Subject: [PATCH 1/9] add options metadata for checkboxes and radios --- src/codegen/Common.ts | 7 ++++++ src/codegen/ComponentConfig.ts | 4 +++- .../dataTypes/GenerateComponentLike.ts | 4 +++- .../options/fetch/fetchOptionsSagas.ts | 24 +++++++++++++++---- src/layout/Checkboxes/config.ts | 2 +- src/layout/RadioButtons/config.ts | 2 +- src/types/index.ts | 8 ++++++- src/utils/network/networking.ts | 9 +++++++ 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 29e5430b79..9e52ed2a92 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -144,6 +144,13 @@ const common = { 'Describes the location in the data model where the component should store its value(s). A simple ' + 'binding is used for components that only store a single value, usually a string.', ), + IDataModelBindingsOptionsSimple: () => + new CG.obj(new CG.prop('simpleBinding', new CG.str()), new CG.prop('metadata', new CG.str().optional())) + .setTitle('Data model binding') + .setDescription( + 'Describes the location in the data model where the component should store its value(s). A simple ' + + 'binding is used for components that only store a single value, usually a string.', + ), IDataModelBindingsList: () => new CG.obj(new CG.prop('list', new CG.str())) .setTitle('Data model binding') diff --git a/src/codegen/ComponentConfig.ts b/src/codegen/ComponentConfig.ts index 53d534dc1e..2adca5063f 100644 --- a/src/codegen/ComponentConfig.ts +++ b/src/codegen/ComponentConfig.ts @@ -100,7 +100,9 @@ export class ComponentConfig extends GenerateComponentLike { } addDataModelBinding( - type: GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList'> | GenerateObject, + type: + | GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList' | 'IDataModelBindingsOptionsSimple'> + | GenerateObject, ): this { this.ensureNotOverridden(); return super.addDataModelBinding(type); diff --git a/src/codegen/dataTypes/GenerateComponentLike.ts b/src/codegen/dataTypes/GenerateComponentLike.ts index 9cc349cfc0..557370aa8c 100644 --- a/src/codegen/dataTypes/GenerateComponentLike.ts +++ b/src/codegen/dataTypes/GenerateComponentLike.ts @@ -57,7 +57,9 @@ export class GenerateComponentLike { * Adding multiple data model bindings to the component makes it a union */ public addDataModelBinding( - type: GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList'> | GenerateObject, + type: + | GenerateCommonImport<'IDataModelBindingsSimple' | 'IDataModelBindingsList' | 'IDataModelBindingsOptionsSimple'> + | GenerateObject, ): this { const name = 'dataModelBindings'; const existing = this.inner.getProperty(name)?.type; diff --git a/src/features/options/fetch/fetchOptionsSagas.ts b/src/features/options/fetch/fetchOptionsSagas.ts index bbfcd0d89f..a2ae51aa0a 100644 --- a/src/features/options/fetch/fetchOptionsSagas.ts +++ b/src/features/options/fetch/fetchOptionsSagas.ts @@ -2,6 +2,7 @@ import { call, fork, put, race, select, take } from 'redux-saga/effects'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { SagaIterator } from 'redux-saga'; +import { FormDataActions } from 'src/features/formData/formDataSlice'; import { OptionsActions } from 'src/features/options/optionsSlice'; import { staticUseLanguageFromState } from 'src/hooks/useLanguage'; import { @@ -10,14 +11,14 @@ import { getKeyWithoutIndexIndicators, replaceIndexIndicatorsWithIndexes, } from 'src/utils/databindings'; -import { httpGet } from 'src/utils/network/sharedNetworking'; +import { httpGetWithHeaders } from 'src/utils/network/networking'; import { getOptionLookupKey, getOptionLookupKeys } from 'src/utils/options'; import { selectNotNull } from 'src/utils/sagas'; import { getOptionsUrl } from 'src/utils/urls/appUrlHelper'; import type { IFormData } from 'src/features/formData'; import type { IUpdateFormData } from 'src/features/formData/formDataTypes'; import type { IUseLanguage } from 'src/hooks/useLanguage'; -import type { IOption, ISelectionComponent } from 'src/layout/common.generated'; +import type { IDataModelBindingsOptionsSimple, IOption, ISelectionComponent } from 'src/layout/common.generated'; import type { ILayouts } from 'src/layout/layout'; import type { IFetchSpecificOptionSaga, IOptions, IOptionsMetaData, IRepeatingGroups, IRuntimeState } from 'src/types'; @@ -53,7 +54,9 @@ export function* fetchOptionsSaga(): SagaIterator { const optionsWithIndexIndicators: IOptionsMetaData[] = []; for (const layoutId of Object.keys(layouts)) { for (const element of layouts[layoutId] || []) { - const { optionsId, mapping, queryParameters, secure } = element as ISelectionComponent; + const { optionsId, mapping, queryParameters, secure, dataModelBindings } = element as ISelectionComponent & { + dataModelBindings?: IDataModelBindingsOptionsSimple; + }; // if we have index indicators we get up the lookup keys for existing indexes const { keys, keyWithIndexIndicator } = @@ -84,6 +87,7 @@ export function* fetchOptionsSaga(): SagaIterator { dataMapping: mapping, fixedQueryParameters: queryParameters, secure, + dataModelBindings, }); fetchedOptions.push(lookupKey); } @@ -105,6 +109,7 @@ export function* fetchSpecificOptionSaga({ dataMapping, fixedQueryParameters, secure, + dataModelBindings, }: IFetchSpecificOptionSaga): SagaIterator { const key = getOptionLookupKey({ id: optionsId, mapping: dataMapping, fixedQueryParameters }); const instanceId = yield select(instanceIdSelector); @@ -129,7 +134,18 @@ export function* fetchSpecificOptionSaga({ instanceId, }); - const options: IOption[] = yield call(httpGet, url); + const optionsResponse = yield call(httpGetWithHeaders, url); + const downstreamParameters: string = optionsResponse.headers['altinn-downstreamparameters']; + if (downstreamParameters && dataModelBindings?.metadata) { + yield put( + FormDataActions.update({ + field: dataModelBindings?.metadata, + data: downstreamParameters, + skipValidation: true, + }), + ); + } + const options = optionsResponse.data as IOption[]; yield put(OptionsActions.fetchFulfilled({ key, options })); } catch (error) { yield put(OptionsActions.fetchRejected({ key, error })); diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 64919c6afd..a7dbbe7a32 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -12,7 +12,7 @@ export const Config = new CG.component({ }, }) .makeSelectionComponent() - .addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal })) + .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal })) .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())); // We don't render the label in GenericComponent, but we still need the diff --git a/src/layout/RadioButtons/config.ts b/src/layout/RadioButtons/config.ts index 9c2a4dbac5..0fb7bc7b2b 100644 --- a/src/layout/RadioButtons/config.ts +++ b/src/layout/RadioButtons/config.ts @@ -11,7 +11,7 @@ export const Config = new CG.component({ renderInAccordionGroup: false, }, }) - .addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal })) + .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal })) .makeSelectionComponent() .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( diff --git a/src/types/index.ts b/src/types/index.ts index 64c3e5a63c..1ea6d83937 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,12 @@ 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, IOption } from 'src/layout/common.generated'; +import type { + IDataModelBindingsOptionsSimple, + ILayoutNavigation, + IMapping, + IOption, +} from 'src/layout/common.generated'; import type { RootState } from 'src/redux/store'; export interface IFormFileUploaderWithTag { @@ -216,6 +221,7 @@ export interface IFetchSpecificOptionSaga { fixedQueryParameters?: Record; secure?: boolean; instanceId?: string; + dataModelBindings?: IDataModelBindingsOptionsSimple; } /** diff --git a/src/utils/network/networking.ts b/src/utils/network/networking.ts index fe0c3eb7ef..77a82d7280 100644 --- a/src/utils/network/networking.ts +++ b/src/utils/network/networking.ts @@ -16,6 +16,15 @@ export async function httpGet(url: string, options?: AxiosRequestConfig): Promis return response.data ? response.data : null; } +export async function httpGetWithHeaders(url: string, options?: AxiosRequestConfig): Promise { + const headers = options?.headers as RawAxiosRequestHeaders | undefined; + const response: AxiosResponse = await axios.get(url, { + ...options, + headers: { ...headers, Pragma: 'no-cache' }, + }); + return response.data ? response : null; +} + export async function httpPost(url: string, options?: AxiosRequestConfig, data?: any): Promise { return await axios.post(url, data, options); } From 6f33a29d8c50f169b2b14b4d16fc6dce3536dd0c Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 27 Oct 2023 10:49:47 +0200 Subject: [PATCH 2/9] update tests to use httpGetWithHeaders --- .../options/fetch/fetchOptionsSagas.test.ts | 37 +++++++++++++------ .../options/fetch/fetchOptionsSagas.ts | 10 +++-- src/types/index.ts | 9 +---- src/utils/network/networking.ts | 2 +- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/features/options/fetch/fetchOptionsSagas.test.ts b/src/features/options/fetch/fetchOptionsSagas.test.ts index af568094ca..0a549caf04 100644 --- a/src/features/options/fetch/fetchOptionsSagas.test.ts +++ b/src/features/options/fetch/fetchOptionsSagas.test.ts @@ -13,7 +13,7 @@ import { repeatingGroupsSelector, } from 'src/features/options/fetch/fetchOptionsSagas'; import { staticUseLanguageForTests, staticUseLanguageFromState } from 'src/hooks/useLanguage'; -import * as networking from 'src/utils/network/sharedNetworking'; +import * as networking from 'src/utils/network/networking'; import { selectNotNull } from 'src/utils/sagas'; import type { ILayouts } from 'src/layout/layout'; import type { IOptions, IRuntimeState } from 'src/types'; @@ -35,7 +35,7 @@ describe('fetchOptionsSagas', () => { }; it('should refetch a given option when an updated field is in a option mapping', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const optionsWithField: IOptions = { someOption: { id: 'someOption', @@ -87,7 +87,7 @@ describe('fetchOptionsSagas', () => { describe('fetchOptionsSaga', () => { it('should spawn fetchSpecificOptionSaga for each unique optionsId', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formLayoutWithTwoSharedOptionIds: ILayouts = { formLayout: [ { @@ -143,6 +143,7 @@ describe('fetchOptionsSagas', () => { dataMapping: undefined, fixedQueryParameters: undefined, secure: undefined, + metadataBinding: undefined, }) .fork(fetchSpecificOptionSaga, { optionsId: 'kommune', @@ -151,12 +152,13 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: undefined, secure: undefined, + metadataBinding: undefined, }) .run(); }); it('should spawn multiple fetchSpecificOptionSaga if components have shared optionsId but different mapping', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formLayoutWithSameOptionIdButDifferentMapping: ILayouts = { formLayout: [ { @@ -205,6 +207,7 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: undefined, secure: undefined, + metadataBinding: undefined, }) .fork(fetchSpecificOptionSaga, { optionsId: 'kommune', @@ -213,6 +216,7 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: undefined, secure: undefined, + metadataBinding: undefined, }) .run(); }); @@ -220,7 +224,7 @@ describe('fetchOptionsSagas', () => { describe('Fixed query parameters', () => { it('should include static query parameters and mapping', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formLayout: ILayouts = { formLayout: [ { @@ -270,6 +274,7 @@ describe('fetchOptionsSagas', () => { dataMapping: undefined, fixedQueryParameters: { level: '1' }, secure: undefined, + metadataBinding: undefined, }) .fork(fetchSpecificOptionSaga, { optionsId: 'kommune', @@ -278,12 +283,13 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: { level: '2' }, secure: undefined, + metadataBinding: undefined, }) .run(); }); it('should include static query parameters in url', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -294,18 +300,22 @@ describe('fetchOptionsSagas', () => { dataMapping: undefined, fixedQueryParameters: { level: '1' }, secure: undefined, + metadataBinding: undefined, }) .provide([ [select(formDataSelector), formData], [select(staticUseLanguageFromState), { selectedLanguage: 'nb' }], [select(instanceIdSelector), 'someId'], ]) - .call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1') + .call( + networking.httpGetWithHeaders, + 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1', + ) .run(); }); it('should include mapping in url', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -318,18 +328,22 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: undefined, secure: undefined, + metadataBinding: undefined, }) .provide([ [select(formDataSelector), formData], [select(staticUseLanguageFromState), { selectedLanguage: 'nb' }], [select(instanceIdSelector), 'someId'], ]) - .call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo') + .call( + networking.httpGetWithHeaders, + 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo', + ) .run(); }); it('should include static query parameters and mapping in request url', () => { - jest.spyOn(networking, 'httpGet').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -342,6 +356,7 @@ describe('fetchOptionsSagas', () => { }, fixedQueryParameters: { level: '1' }, secure: undefined, + metadataBinding: undefined, }) .provide([ [select(formDataSelector), formData], @@ -349,7 +364,7 @@ describe('fetchOptionsSagas', () => { [select(instanceIdSelector), 'someId'], ]) .call( - networking.httpGet, + networking.httpGetWithHeaders, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1&fylke=Oslo', ) .run(); diff --git a/src/features/options/fetch/fetchOptionsSagas.ts b/src/features/options/fetch/fetchOptionsSagas.ts index a2ae51aa0a..d9c040ad86 100644 --- a/src/features/options/fetch/fetchOptionsSagas.ts +++ b/src/features/options/fetch/fetchOptionsSagas.ts @@ -58,6 +58,8 @@ export function* fetchOptionsSaga(): SagaIterator { dataModelBindings?: IDataModelBindingsOptionsSimple; }; + const metadataBinding = dataModelBindings?.metadata; + // if we have index indicators we get up the lookup keys for existing indexes const { keys, keyWithIndexIndicator } = (optionsId && @@ -87,7 +89,7 @@ export function* fetchOptionsSaga(): SagaIterator { dataMapping: mapping, fixedQueryParameters: queryParameters, secure, - dataModelBindings, + metadataBinding, }); fetchedOptions.push(lookupKey); } @@ -109,7 +111,7 @@ export function* fetchSpecificOptionSaga({ dataMapping, fixedQueryParameters, secure, - dataModelBindings, + metadataBinding, }: IFetchSpecificOptionSaga): SagaIterator { const key = getOptionLookupKey({ id: optionsId, mapping: dataMapping, fixedQueryParameters }); const instanceId = yield select(instanceIdSelector); @@ -136,10 +138,10 @@ export function* fetchSpecificOptionSaga({ const optionsResponse = yield call(httpGetWithHeaders, url); const downstreamParameters: string = optionsResponse.headers['altinn-downstreamparameters']; - if (downstreamParameters && dataModelBindings?.metadata) { + if (downstreamParameters && metadataBinding) { yield put( FormDataActions.update({ - field: dataModelBindings?.metadata, + field: metadataBinding, data: downstreamParameters, skipValidation: true, }), diff --git a/src/types/index.ts b/src/types/index.ts index 1ea6d83937..cc4f2b81cc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,12 +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 { - IDataModelBindingsOptionsSimple, - ILayoutNavigation, - IMapping, - IOption, -} from 'src/layout/common.generated'; +import type { ILayoutNavigation, IMapping, IOption } from 'src/layout/common.generated'; import type { RootState } from 'src/redux/store'; export interface IFormFileUploaderWithTag { @@ -221,7 +216,7 @@ export interface IFetchSpecificOptionSaga { fixedQueryParameters?: Record; secure?: boolean; instanceId?: string; - dataModelBindings?: IDataModelBindingsOptionsSimple; + metadataBinding?: string; } /** diff --git a/src/utils/network/networking.ts b/src/utils/network/networking.ts index 77a82d7280..0fe9e5e928 100644 --- a/src/utils/network/networking.ts +++ b/src/utils/network/networking.ts @@ -7,7 +7,7 @@ export enum HttpStatusCodes { Forbidden = 403, } -export async function httpGet(url: string, options?: AxiosRequestConfig): Promise { +export async function httpGetWithHeadershttpGet(url: string, options?: AxiosRequestConfig): Promise { const headers = options?.headers as RawAxiosRequestHeaders | undefined; const response: AxiosResponse = await axios.get(url, { ...options, From 4292d6a15fa544d36c02109a74be42d172cf9402 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 27 Oct 2023 10:59:31 +0200 Subject: [PATCH 3/9] resolve misclick --- src/utils/network/networking.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/network/networking.ts b/src/utils/network/networking.ts index 0fe9e5e928..77a82d7280 100644 --- a/src/utils/network/networking.ts +++ b/src/utils/network/networking.ts @@ -7,7 +7,7 @@ export enum HttpStatusCodes { Forbidden = 403, } -export async function httpGetWithHeadershttpGet(url: string, options?: AxiosRequestConfig): Promise { +export async function httpGet(url: string, options?: AxiosRequestConfig): Promise { const headers = options?.headers as RawAxiosRequestHeaders | undefined; const response: AxiosResponse = await axios.get(url, { ...options, From 740b052d5bc97b08f80536d48d0d9f633e5d0fd4 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 27 Oct 2023 11:04:57 +0200 Subject: [PATCH 4/9] add metadata binding to rest of selectioncomponents --- src/layout/Dropdown/config.ts | 2 +- src/layout/Likert/config.ts | 2 +- src/layout/MultipleSelect/config.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layout/Dropdown/config.ts b/src/layout/Dropdown/config.ts index e7be9745d0..58f9bc67e5 100644 --- a/src/layout/Dropdown/config.ts +++ b/src/layout/Dropdown/config.ts @@ -12,4 +12,4 @@ export const Config = new CG.component({ }, }) .makeSelectionComponent() - .addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal })); + .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal })); diff --git a/src/layout/Likert/config.ts b/src/layout/Likert/config.ts index 8cbb14ef44..18b64540b0 100644 --- a/src/layout/Likert/config.ts +++ b/src/layout/Likert/config.ts @@ -11,7 +11,7 @@ export const Config = new CG.component({ renderInAccordionGroup: false, }, }) - .addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal })) + .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal })) .addTextResource( new CG.trb({ name: 'title', diff --git a/src/layout/MultipleSelect/config.ts b/src/layout/MultipleSelect/config.ts index de58623d12..a71332c7ba 100644 --- a/src/layout/MultipleSelect/config.ts +++ b/src/layout/MultipleSelect/config.ts @@ -11,5 +11,5 @@ export const Config = new CG.component({ renderInAccordionGroup: false, }, }) - .addDataModelBinding(CG.common('IDataModelBindingsSimple').optional({ onlyIn: Variant.Internal })) + .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple').optional({ onlyIn: Variant.Internal })) .makeSelectionComponent(); From e404e7e7badf42ab375e55886a296071a38c0cda Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Wed, 8 Nov 2023 17:08:43 +0100 Subject: [PATCH 5/9] add cypress test --- test/e2e/integration/frontend-test/options.ts | 12 ++++++++++++ test/e2e/pageobjects/app-frontend.ts | 2 ++ 2 files changed, 14 insertions(+) diff --git a/test/e2e/integration/frontend-test/options.ts b/test/e2e/integration/frontend-test/options.ts index ef46b8399d..c10d121903 100644 --- a/test/e2e/integration/frontend-test/options.ts +++ b/test/e2e/integration/frontend-test/options.ts @@ -42,4 +42,16 @@ describe('Options', () => { cy.findByRole('option', { name: 'Endre fra: 1, Endre til: 2' }).click(); cy.get(appFrontend.group.options).should('have.value', 'Endre fra: 1, Endre til: 2'); }); + it('retrieves metadata from header when metadata is set in datamodelBindings', () => { + cy.intercept({ method: 'GET', url: '**/options/**' }).as('optionsMunicipality'); + + cy.goto('changename'); + cy.wait('@optionsMunicipality'); + + cy.get(appFrontend.changeOfName.municipality).dsSelect('Oslo'); + + cy.get(appFrontend.changeOfName.municipalityMetadata) + .should('have.prop', 'value') + .should('match', /language=nb,id=131,variant=,date=\d{1,2}\/\d{1,2}\/\d{4},level=,parentCode=/); + }); }); diff --git a/test/e2e/pageobjects/app-frontend.ts b/test/e2e/pageobjects/app-frontend.ts index 733f2e09bc..f1d7a2a0c6 100644 --- a/test/e2e/pageobjects/app-frontend.ts +++ b/test/e2e/pageobjects/app-frontend.ts @@ -144,6 +144,8 @@ export class AppFrontend { reference: '#reference', reference2: '#reference2', dateOfEffect: '#dateOfEffect', + municipalityMetadata: '#kommuner-metadata', + municipality: '#kommune', upload: '#fileUpload-changename', uploadWithTag: { uploadZone: '#fileUploadWithTags-changename', From b16a760d4b1d49c5985a6fe85b18dbb245f15f5d Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Thu, 9 Nov 2023 11:41:38 +0100 Subject: [PATCH 6/9] change cypress test --- test/e2e/integration/frontend-test/options.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/e2e/integration/frontend-test/options.ts b/test/e2e/integration/frontend-test/options.ts index c10d121903..8967eb5207 100644 --- a/test/e2e/integration/frontend-test/options.ts +++ b/test/e2e/integration/frontend-test/options.ts @@ -43,7 +43,13 @@ describe('Options', () => { cy.get(appFrontend.group.options).should('have.value', 'Endre fra: 1, Endre til: 2'); }); it('retrieves metadata from header when metadata is set in datamodelBindings', () => { - cy.intercept({ method: 'GET', url: '**/options/**' }).as('optionsMunicipality'); + cy.intercept({ method: 'GET', url: '**/options/test-kommuner**' }, (req) => { + req.reply((res) => { + const headers = res.headers; + headers['altinn-downstreamparameters'] = 'language=nb,id=131,variant=,date=01/01/2021,level=,parentCode='; + res.send(res.body, headers); + }); + }).as('optionsMunicipality'); cy.goto('changename'); cy.wait('@optionsMunicipality'); From e32fc6d58d68d474e3e4f26a8829980cc1fe4e14 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Mon, 13 Nov 2023 14:15:16 +0100 Subject: [PATCH 7/9] add description for options metadata --- src/codegen/Common.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 9e52ed2a92..e39d39b4b8 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -145,12 +145,17 @@ const common = { 'binding is used for components that only store a single value, usually a string.', ), IDataModelBindingsOptionsSimple: () => - new CG.obj(new CG.prop('simpleBinding', new CG.str()), new CG.prop('metadata', new CG.str().optional())) - .setTitle('Data model binding') - .setDescription( - 'Describes the location in the data model where the component should store its value(s). A simple ' + - 'binding is used for components that only store a single value, usually a string.', + new CG.obj( + new CG.prop('simpleBinding', new CG.str()), + new CG.prop( + 'metadata', + new CG.str() + .optional() + .setDescription( + 'Describes the location where metadata for the option based component should be stored in the datamodel.', + ), ), + ), IDataModelBindingsList: () => new CG.obj(new CG.prop('list', new CG.str())) .setTitle('Data model binding') From 0097c95424ddd84949bfc47006088c91eb0022d9 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Mon, 13 Nov 2023 14:16:48 +0100 Subject: [PATCH 8/9] rename httpGetWithHeaders to httpGetRaw --- src/features/options/fetch/fetchOptionsSagas.ts | 4 ++-- src/utils/network/networking.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/features/options/fetch/fetchOptionsSagas.ts b/src/features/options/fetch/fetchOptionsSagas.ts index d9c040ad86..1c4cf9e894 100644 --- a/src/features/options/fetch/fetchOptionsSagas.ts +++ b/src/features/options/fetch/fetchOptionsSagas.ts @@ -11,7 +11,7 @@ import { getKeyWithoutIndexIndicators, replaceIndexIndicatorsWithIndexes, } from 'src/utils/databindings'; -import { httpGetWithHeaders } from 'src/utils/network/networking'; +import { httpGetRaw } from 'src/utils/network/networking'; import { getOptionLookupKey, getOptionLookupKeys } from 'src/utils/options'; import { selectNotNull } from 'src/utils/sagas'; import { getOptionsUrl } from 'src/utils/urls/appUrlHelper'; @@ -136,7 +136,7 @@ export function* fetchSpecificOptionSaga({ instanceId, }); - const optionsResponse = yield call(httpGetWithHeaders, url); + const optionsResponse = yield call(httpGetRaw, url); const downstreamParameters: string = optionsResponse.headers['altinn-downstreamparameters']; if (downstreamParameters && metadataBinding) { yield put( diff --git a/src/utils/network/networking.ts b/src/utils/network/networking.ts index 77a82d7280..d8cb44eaa4 100644 --- a/src/utils/network/networking.ts +++ b/src/utils/network/networking.ts @@ -16,7 +16,7 @@ export async function httpGet(url: string, options?: AxiosRequestConfig): Promis return response.data ? response.data : null; } -export async function httpGetWithHeaders(url: string, options?: AxiosRequestConfig): Promise { +export async function httpGetRaw(url: string, options?: AxiosRequestConfig): Promise { const headers = options?.headers as RawAxiosRequestHeaders | undefined; const response: AxiosResponse = await axios.get(url, { ...options, From 09203d89460cd5555485f571a62e3483170b29e9 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Mon, 13 Nov 2023 17:23:41 +0100 Subject: [PATCH 9/9] fix cypress tests --- .../options/fetch/fetchOptionsSagas.test.ts | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/features/options/fetch/fetchOptionsSagas.test.ts b/src/features/options/fetch/fetchOptionsSagas.test.ts index 0a549caf04..c9d616279b 100644 --- a/src/features/options/fetch/fetchOptionsSagas.test.ts +++ b/src/features/options/fetch/fetchOptionsSagas.test.ts @@ -35,7 +35,7 @@ describe('fetchOptionsSagas', () => { }; it('should refetch a given option when an updated field is in a option mapping', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const optionsWithField: IOptions = { someOption: { id: 'someOption', @@ -87,7 +87,7 @@ describe('fetchOptionsSagas', () => { describe('fetchOptionsSaga', () => { it('should spawn fetchSpecificOptionSaga for each unique optionsId', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formLayoutWithTwoSharedOptionIds: ILayouts = { formLayout: [ { @@ -158,7 +158,7 @@ describe('fetchOptionsSagas', () => { }); it('should spawn multiple fetchSpecificOptionSaga if components have shared optionsId but different mapping', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formLayoutWithSameOptionIdButDifferentMapping: ILayouts = { formLayout: [ { @@ -224,7 +224,7 @@ describe('fetchOptionsSagas', () => { describe('Fixed query parameters', () => { it('should include static query parameters and mapping', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formLayout: ILayouts = { formLayout: [ { @@ -289,7 +289,7 @@ describe('fetchOptionsSagas', () => { }); it('should include static query parameters in url', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -307,15 +307,12 @@ describe('fetchOptionsSagas', () => { [select(staticUseLanguageFromState), { selectedLanguage: 'nb' }], [select(instanceIdSelector), 'someId'], ]) - .call( - networking.httpGetWithHeaders, - 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1', - ) + .call(networking.httpGetRaw, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1') .run(); }); it('should include mapping in url', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -335,15 +332,12 @@ describe('fetchOptionsSagas', () => { [select(staticUseLanguageFromState), { selectedLanguage: 'nb' }], [select(instanceIdSelector), 'someId'], ]) - .call( - networking.httpGetWithHeaders, - 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo', - ) + .call(networking.httpGetRaw, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo') .run(); }); it('should include static query parameters and mapping in request url', () => { - jest.spyOn(networking, 'httpGetWithHeaders').mockResolvedValue([]); + jest.spyOn(networking, 'httpGetRaw').mockResolvedValue([]); const formData = { 'FlytteTil.Fylke': 'Oslo', @@ -364,7 +358,7 @@ describe('fetchOptionsSagas', () => { [select(instanceIdSelector), 'someId'], ]) .call( - networking.httpGetWithHeaders, + networking.httpGetRaw, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1&fylke=Oslo', ) .run();