diff --git a/src/altinn-app-frontend/src/components/hooks/index.ts b/src/altinn-app-frontend/src/components/hooks/index.ts index b65c473cd3..51d3740708 100644 --- a/src/altinn-app-frontend/src/components/hooks/index.ts +++ b/src/altinn-app-frontend/src/components/hooks/index.ts @@ -1,11 +1,10 @@ import type { IInstanceContext, IDataSources } from "altinn-shared/types"; -import { replaceTextResourceParams } from "altinn-shared/utils"; import { buildInstanceContext } from "altinn-shared/utils/instanceContext"; import { useState, useEffect } from "react"; import { shallowEqual } from "react-redux"; import { useAppSelector } from "src/common/hooks"; import type { IMapping, IOptionSource, IOption } from "src/types"; -import { getOptionLookupKey, getRelevantFormDataForOptionSource, replaceOptionDataField } from "src/utils/options"; +import { getOptionLookupKey, getRelevantFormDataForOptionSource, setupSourceOptions } from "src/utils/options"; interface IUseGetOptionsParams { optionsId: string; @@ -39,24 +38,13 @@ export const useGetOptions = ({ optionsId, mapping, source }: IUseGetOptionsPara instanceContext: instanceContext }; - const replacedOptionLabels = - replaceTextResourceParams([relevantTextResource], dataSources, repeatingGroups); - - const repGroup = Object.values(repeatingGroups).find((group) => { - return group.dataModelBinding === source.group; - }); - - - const newOptions: IOption[] = []; - for (let i = 0; i <= repGroup.index; i++) { - const option: IOption = { - label: replacedOptionLabels[i + 1].value, - value: replaceOptionDataField(relevantFormData, source.value, i), - }; - newOptions.push(option); - } - - setOptions(newOptions); + setOptions(setupSourceOptions({ + source, + relevantTextResource, + relevantFormData, + repeatingGroups, + dataSources + })); }, [applicationSettings, relevantFormData, instance, mapping, optionState, optionsId, repeatingGroups, source, relevantTextResource]); diff --git a/src/altinn-app-frontend/src/components/summary/SummaryComponent.tsx b/src/altinn-app-frontend/src/components/summary/SummaryComponent.tsx index af1c21d558..e34946b281 100644 --- a/src/altinn-app-frontend/src/components/summary/SummaryComponent.tsx +++ b/src/altinn-app-frontend/src/components/summary/SummaryComponent.tsx @@ -110,6 +110,7 @@ export function SummaryComponent({ id, grid, ...summaryProps }: ISummaryComponen formComponent as ILayoutComponent, state.textResources.resources, state.optionState.options, + state.formLayout.uiConfig.repeatingGroups, true, ) ); diff --git a/src/altinn-app-frontend/src/components/summary/SummaryGroupComponent.tsx b/src/altinn-app-frontend/src/components/summary/SummaryGroupComponent.tsx index 9a4c89ba9c..d9706f1643 100644 --- a/src/altinn-app-frontend/src/components/summary/SummaryGroupComponent.tsx +++ b/src/altinn-app-frontend/src/components/summary/SummaryGroupComponent.tsx @@ -245,6 +245,7 @@ function SummaryGroupComponent({ componentDeepCopy, textResources, options, + repeatingGroups, ); if (hiddenFields.find((field) => field === `${componentId}-${i}`)) { @@ -315,6 +316,7 @@ function SummaryGroupComponent({ groupComponent.dataModelBindings.group, textResources, options, + repeatingGroups, ); } groupContainer.children.push(summaryId); diff --git a/src/altinn-app-frontend/src/features/form/containers/RepeatingGroupTable.tsx b/src/altinn-app-frontend/src/features/form/containers/RepeatingGroupTable.tsx index c991f7356a..b4cddc31d0 100644 --- a/src/altinn-app-frontend/src/features/form/containers/RepeatingGroupTable.tsx +++ b/src/altinn-app-frontend/src/features/form/containers/RepeatingGroupTable.tsx @@ -159,6 +159,7 @@ export function RepeatingGroupTable({ container.dataModelBindings.group, textResources, options, + repeatingGroups, ); }; diff --git a/src/altinn-app-frontend/src/features/form/layout/index.ts b/src/altinn-app-frontend/src/features/form/layout/index.ts index 5b3e8f8699..aac6235917 100644 --- a/src/altinn-app-frontend/src/features/form/layout/index.ts +++ b/src/altinn-app-frontend/src/features/form/layout/index.ts @@ -1,5 +1,5 @@ import { GridSize } from '@material-ui/core'; -import { IMapping, IOption, Triggers } from '../../../types'; +import { IMapping, IOption, IOptionSource, Triggers } from '../../../types'; export interface ILayouts { [id: string]: ILayout; @@ -98,6 +98,7 @@ export interface ISelectionComponentProps extends ILayoutComponent { optionsId?: string; mapping?: IMapping; secure?: boolean; + source?: IOptionSource; } export interface IGrid extends IGridStyling { diff --git a/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts b/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts index 8aaddbaf94..ec8296d52f 100644 --- a/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts +++ b/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts @@ -1,6 +1,6 @@ import parseHtmlToReact from 'html-react-parser'; -import type { IComponentBindingValidation, IComponentValidations, IOptions, ITextResource } from 'src/types'; +import type { IComponentBindingValidation, IComponentValidations, IOptions, ITextResource, IRepeatingGroups } from 'src/types'; import type { IFormData } from 'src/features/form/data/formDataReducer'; import type { ILayoutComponent, @@ -36,6 +36,9 @@ describe('formComponentUtils', () => { mockBindingRadioButtonsWithMapping: 'mockOptionsWithMapping1', mockBindingLikert: 'optionValue1', mockBindingLikertWithMapping: 'mockOptionsWithMapping1', + mockBindingDropdownWithReduxOptions: 'mockReduxOptionValue', + 'someGroup[0].fieldUsedAsValue': 'mockReduxOptionValue', + 'someGroup[0].fieldUsedAsLabel': 'mockReduxOptionLabel', mockBindingAttachmentSingle: '12345', 'mockBindingAttachmentMulti[0]': '123457', 'mockBindingAttachmentMulti[1]': '123456', @@ -61,6 +64,17 @@ describe('formComponentUtils', () => { id: 'repTextKey3', value: 'RepValue3', }, + { + id: 'dropdown.label', + value: 'Label value: {0}', + unparsedValue: 'Label value: {0}', + variables: [ + { + key: 'someGroup[{0}].fieldUsedAsLabel', + dataSource: 'dataModel.default' + }, + ] + } ]; const mockOptions: IOptions = { mockOption: { @@ -149,6 +163,8 @@ describe('formComponentUtils', () => { }, ]; + const mockRepeatingGroups: IRepeatingGroups = {}; + describe('getDisplayFormData', () => { it('should return form data for a component', () => { const inputComponent = { @@ -162,6 +178,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('test'); }); @@ -179,6 +196,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('Value1, Value2'); }); @@ -197,6 +215,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('Value Mapping 1, Value Mapping 2'); }); @@ -214,6 +233,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, true, ); const expected = { @@ -236,6 +256,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('Value1'); }) @@ -254,10 +275,82 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('Value Mapping 1'); }) + it('should return text resource for radio button component', () => { + const radioButtonComponent = { + type: 'RadioButtons', + optionsId: 'mockOption', + id: 'some-id' + } as ISelectionComponentProps; + const result = getDisplayFormData( + 'mockBindingRadioButtons', + radioButtonComponent, + radioButtonComponent.id, + mockAttachments, + mockFormData, + mockOptions, + mockTextResources, + mockRepeatingGroups, + ); + expect(result).toEqual('Value1'); + }); + + it('should return text resource for radio button component with mapping', () => { + const radioButtonComponentWithMapping = { + type: 'RadioButtons', + optionsId: 'mockOptionsWithMapping', + mapping: { someDataField: 'someUrlParam' }, + id: 'some-id' + } as unknown as ISelectionComponentProps; + const result = getDisplayFormData( + 'mockBindingRadioButtonsWithMapping', + radioButtonComponentWithMapping, + radioButtonComponentWithMapping.id, + mockAttachments, + mockFormData, + mockOptions, + mockTextResources, + mockRepeatingGroups, + ); + expect(result).toEqual('Value Mapping 1'); + }); + + it('should return correct label for dropdown setup with options from redux', () => { + const dropdownComponentWithReduxOptions = { + type: 'RadioButtons', + id: 'some-id', + source: { + group: 'someGroup', + label: 'dropdown.label', + value: 'someGroup[{0}].fieldUsedAsValue' + } + } as unknown as ISelectionComponentProps; + + const repGroups: IRepeatingGroups = { + group1: { + index: 0, + dataModelBinding: 'someGroup' + }, + }; + + const result = getDisplayFormData( + 'mockBindingDropdownWithReduxOptions', + dropdownComponentWithReduxOptions, + dropdownComponentWithReduxOptions.id, + mockAttachments, + mockFormData, + mockOptions, + mockTextResources, + repGroups, + ); + + expect(result).toEqual('Label value: mockReduxOptionLabel'); + }); + it('should return a single attachment name for a FileUpload component', () => { const component = { id: 'upload', @@ -274,6 +367,7 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('mockNameAttachment1'); }); @@ -294,13 +388,14 @@ describe('formComponentUtils', () => { mockFormData, mockOptions, mockTextResources, + mockRepeatingGroups, ); expect(result).toEqual('mockNameAttachment3, mockNameAttachment2'); }); }); describe('getFormDataForComponentInRepeatingGroup', () => { - it('should return comma separated string of text resources for checkboxes with mulitiple values', () => { + it('should return comma separated string of text resources for checkboxes with multiple values', () => { const checkboxComponent = { type: 'Checkboxes', optionsId: 'mockRepOption', @@ -316,6 +411,7 @@ describe('formComponentUtils', () => { 'group', mockTextResources, mockOptions, + mockRepeatingGroups, ); expect(result).toEqual('RepValue1, RepValue2, RepValue3'); }); diff --git a/src/altinn-app-frontend/src/utils/formComponentUtils.ts b/src/altinn-app-frontend/src/utils/formComponentUtils.ts index 41a9edba35..3e39227eef 100644 --- a/src/altinn-app-frontend/src/utils/formComponentUtils.ts +++ b/src/altinn-app-frontend/src/utils/formComponentUtils.ts @@ -20,9 +20,10 @@ import type { IOption, IOptions, IValidations, + IRepeatingGroups, } from 'src/types'; import { AsciiUnitSeparator } from './attachment'; -import { getOptionLookupKey } from './options'; +import { getOptionLookupKey, getRelevantFormDataForOptionSource, setupSourceOptions } from './options'; import { getTextFromAppOrDefault } from './textResource'; import { isFileUploadComponent, isFileUploadWithTagComponent } from "src/utils/formLayout"; @@ -92,6 +93,7 @@ export const getDisplayFormDataForComponent = ( component: ILayoutComponent, textResources: ITextResource[], options: IOptions, + repeatingGroups: IRepeatingGroups, multiChoice?: boolean, ) => { if (component.dataModelBindings?.simpleBinding || component.dataModelBindings?.list) { @@ -103,6 +105,7 @@ export const getDisplayFormDataForComponent = ( formData, options, textResources, + repeatingGroups, multiChoice, ); } @@ -118,6 +121,7 @@ export const getDisplayFormDataForComponent = ( formData, options, textResources, + repeatingGroups ); }); return formDataObj; @@ -131,6 +135,7 @@ export const getDisplayFormData = ( formData: any, options: IOptions, textResources: ITextResource[], + repeatingGroups: IRepeatingGroups, asObject?: boolean, ) => { let formDataValue = formData[dataModelBinding] || ''; @@ -157,8 +162,20 @@ export const getDisplayFormData = ( label = selectionComponent.options.find( (option: IOption) => option.value === formDataValue, )?.label; + } else if (selectionComponent.source) { + const reduxOptions = setupSourceOptions({ + source: selectionComponent.source, + relevantTextResource: textResources.find((e) => e.id === selectionComponent.source.label), + relevantFormData: getRelevantFormDataForOptionSource(formData, selectionComponent.source), + repeatingGroups, + dataSources: { + dataModel: formData, + }, + }); + label = reduxOptions.find(option => option.value === formDataValue)?.label; } - return getTextResourceByKey(label, textResources) || ''; + + return getTextResourceByKey(label, textResources) || formDataValue; } if (component.type === 'Checkboxes') { const selectionComponent = component as ISelectionComponentProps; @@ -170,18 +187,18 @@ export const getDisplayFormData = ( split?.forEach((value: string) => { const optionsForComponent = selectionComponent?.optionsId ? options[ - getOptionLookupKey( - selectionComponent.optionsId, - selectionComponent.mapping, - ) - ].options + getOptionLookupKey( + selectionComponent.optionsId, + selectionComponent.mapping, + ) + ].options : selectionComponent.options; const textKey = optionsForComponent?.find( (option: IOption) => option.value === value, )?.label || ''; displayFormData[value] = - getTextResourceByKey(textKey, textResources) || ''; + getTextResourceByKey(textKey, textResources) || formDataValue; }); return displayFormData; @@ -245,6 +262,7 @@ export const getFormDataForComponentInRepeatingGroup = ( groupDataModelBinding: string, textResources: ITextResource[], options: IOptions, + repeatingGroups: IRepeatingGroups, ) => { if ( !component.dataModelBindings || @@ -281,6 +299,7 @@ export const getFormDataForComponentInRepeatingGroup = ( formData, options, textResources, + repeatingGroups ); }; @@ -369,11 +388,11 @@ export function getFileUploadComponentValidations( componentValidations.simpleBinding.errors.push( // If validation has attachmentId, add to start of message and seperate using ASCII Universal Seperator attachmentId + - AsciiUnitSeparator + - getLanguageFromKey( - 'form_filler.file_uploader_validation_error_update', - language, - ), + AsciiUnitSeparator + + getLanguageFromKey( + 'form_filler.file_uploader_validation_error_update', + language, + ), ); } } else if (validationError === 'delete') { diff --git a/src/altinn-app-frontend/src/utils/options.test.ts b/src/altinn-app-frontend/src/utils/options.test.ts index 5c5bb8a380..7cb48956f5 100644 --- a/src/altinn-app-frontend/src/utils/options.test.ts +++ b/src/altinn-app-frontend/src/utils/options.test.ts @@ -1,4 +1,7 @@ -import { getOptionLookupKey } from "./options"; +import { IDataSources, ITextResource } from "altinn-shared/types"; +import { IFormData } from "src/features/form/data/formDataReducer"; +import { IOptionSource, IRepeatingGroups } from "src/types"; +import { getOptionLookupKey, setupSourceOptions } from "./options"; describe('utils > options', () => { describe('getOptionLookupKey', () => { @@ -14,4 +17,62 @@ describe('utils > options', () => { expect(result).toEqual(expected); }); }); + + describe('setupSourceOptions', () => { + it('should setup correct set of options', () => { + const source: IOptionSource = { + group: 'someGroup', + label: 'dropdown.label', + value: 'someGroup[{0}].fieldUsedAsValue' + }; + const relevantTextResource: ITextResource = { + id: 'dropdown.label', + value: '{0}', + unparsedValue: '{0}', + variables: [ + { + key: 'someGroup[{0}].fieldUsedAsLabel', + dataSource: 'dataModel.default' + }, + ] + }; + const relevantFormData: IFormData = { + 'someGroup[0].fieldUsedAsValue': 'Value 1', + 'someGroup[0].fieldUsedAsLabel': 'Label 1', + 'someGroup[1].fieldUsedAsValue': 'Value 2', + 'someGroup[1].fieldUsedAsLabel': 'Label 2', + 'someGroup[2].fieldUsedAsValue': 'Value 3', + 'someGroup[2].fieldUsedAsLabel': 'Label 3', + }; + const repeatingGroups: IRepeatingGroups = { + someGroup: { + index: 2, + dataModelBinding: 'someGroup' + } + }; + + const dataSources: IDataSources = { + dataModel: relevantFormData, + }; + + const options = setupSourceOptions({ + source, + relevantTextResource, + relevantFormData, + repeatingGroups, + dataSources, + }); + + expect(options.length).toBe(3); + + expect(options[0].label).toBe('Label 1'); + expect(options[0].value).toBe('Value 1'); + + expect(options[1].label).toBe('Label 2'); + expect(options[1].value).toBe('Value 2'); + + expect(options[2].label).toBe('Label 3'); + expect(options[2].value).toBe('Value 3'); + }); + }); }) diff --git a/src/altinn-app-frontend/src/utils/options.ts b/src/altinn-app-frontend/src/utils/options.ts index ed05cb7033..652681b80e 100644 --- a/src/altinn-app-frontend/src/utils/options.ts +++ b/src/altinn-app-frontend/src/utils/options.ts @@ -1,5 +1,7 @@ +import type { IDataSources } from "altinn-shared/types"; +import { replaceTextResourceParams } from "altinn-shared/utils"; import type { IFormData } from "src/features/form/data/formDataReducer"; -import type { IMapping, IOptionSource } from "src/types"; +import type { IMapping, IOption, IOptionSource, IRepeatingGroups, ITextResource } from "src/types"; export function getOptionLookupKey(id: string, mapping?: IMapping) { if (!mapping) { @@ -29,3 +31,36 @@ export function getRelevantFormDataForOptionSource(formData: IFormData, source: return relevantFormData; } + +interface ISetupSourceOptionsParams { + source: IOptionSource; + relevantTextResource: ITextResource; + relevantFormData: IFormData; + repeatingGroups: IRepeatingGroups; + dataSources: IDataSources; +} + +export function setupSourceOptions({ + source, + relevantTextResource, + relevantFormData, + repeatingGroups, + dataSources +} : ISetupSourceOptionsParams) { + const replacedOptionLabels = + replaceTextResourceParams([relevantTextResource], dataSources, repeatingGroups); + + const repGroup = Object.values(repeatingGroups).find((group) => { + return group.dataModelBinding === source.group; + }); + + const options: IOption[] = []; + for (let i = 0; i <= repGroup.index; i++) { + const option: IOption = { + label: replacedOptionLabels[i + 1].value, + value: replaceOptionDataField(relevantFormData, source.value, i), + }; + options.push(option); + } + return options; +}