From 53acf65431105a42e1a55f2108fac134fa7ba85a Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Wed, 22 Jun 2022 20:52:50 +0200 Subject: [PATCH] Better error messages for required fields (#230) * better error messages for required fields * Update src/altinn-app-frontend/src/utils/formComponentUtils.test.ts Co-authored-by: Steffen Lorang Ekeberg * update error message test in cypress test * remove .only on describe statement * Remove . at end of validation message * update tests * a few more .s Co-authored-by: Steffen Lorang Ekeberg --- .../form/data/submit/submitFormDataSagas.ts | 1 + .../layout/update/updateFormLayoutSagas.ts | 1 + .../src/utils/databindings.test.ts | 28 +++ .../src/utils/databindings.ts | 19 +- .../src/utils/formComponentUtils.test.ts | 56 ++++++ .../src/utils/formComponentUtils.ts | 23 +++ .../src/utils/validation.test.ts | 190 ++++++++++++------ .../src/utils/validation.ts | 65 ++++-- src/shared/src/language/texts/en.ts | 40 ++-- src/shared/src/language/texts/nb.ts | 20 +- src/shared/src/language/texts/nn.ts | 20 +- test/cypress/e2e/fixtures/texts.json | 1 + .../e2e/integration/app-frontend/summary.js | 4 +- 13 files changed, 362 insertions(+), 106 deletions(-) diff --git a/src/altinn-app-frontend/src/features/form/data/submit/submitFormDataSagas.ts b/src/altinn-app-frontend/src/features/form/data/submit/submitFormDataSagas.ts index 7cf861af30..3acd52a7bb 100644 --- a/src/altinn-app-frontend/src/features/form/data/submit/submitFormDataSagas.ts +++ b/src/altinn-app-frontend/src/features/form/data/submit/submitFormDataSagas.ts @@ -57,6 +57,7 @@ function* submitFormSaga({ payload: { apiMode, stopWithWarnings } }: PayloadActi state.language.language, state.formLayout.uiConfig.hiddenFields, state.formLayout.uiConfig.repeatingGroups, + state.textResources.resources, ); validations = mergeValidationObjects(validations, componentSpecificValidations); diff --git a/src/altinn-app-frontend/src/features/form/layout/update/updateFormLayoutSagas.ts b/src/altinn-app-frontend/src/features/form/layout/update/updateFormLayoutSagas.ts index 7d922db88b..dfba02901f 100644 --- a/src/altinn-app-frontend/src/features/form/layout/update/updateFormLayoutSagas.ts +++ b/src/altinn-app-frontend/src/features/form/layout/update/updateFormLayoutSagas.ts @@ -160,6 +160,7 @@ export function* updateCurrentViewSaga({ payload: { state.language.language, state.formLayout.uiConfig.hiddenFields, state.formLayout.uiConfig.repeatingGroups, + state.textResources.resources, ); let validations = mergeValidationObjects(validationResult.validations, componentSpecificValidations, emptyFieldsValidations); const instanceId = state.instanceData.instance.id; diff --git a/src/altinn-app-frontend/src/utils/databindings.test.ts b/src/altinn-app-frontend/src/utils/databindings.test.ts index 5d8f617adf..a672b10c1e 100644 --- a/src/altinn-app-frontend/src/utils/databindings.test.ts +++ b/src/altinn-app-frontend/src/utils/databindings.test.ts @@ -4,6 +4,7 @@ import type { IMapping } from 'src/types'; import { flattenObject, + getFormDataFromFieldKey, getKeyWithoutIndex, mapFormData, removeGroupData, @@ -271,4 +272,31 @@ describe('utils/databindings.ts', () => { }, ); }); + + describe('getFormDataFromFieldKey', () => { + const formData = { + 'field1': 'value1', + 'group[0].field': 'someValue', + 'group[1].field': 'another value', + } + it('should return correct form data for a field not in a group', () => { + const result = getFormDataFromFieldKey( + 'simpleBinding', + { simpleBinding: 'field1' }, + formData, + ); + expect(result).toEqual('value1'); + }); + + it('should return correct form data for a field in a group', () => { + const result = getFormDataFromFieldKey( + 'simpleBinding', + { simpleBinding: 'group.field' }, + formData, + 'group', + 1, + ); + expect(result).toEqual('another value'); + }); + }); }); diff --git a/src/altinn-app-frontend/src/utils/databindings.ts b/src/altinn-app-frontend/src/utils/databindings.ts index 25f5e9d764..d14aa80160 100644 --- a/src/altinn-app-frontend/src/utils/databindings.ts +++ b/src/altinn-app-frontend/src/utils/databindings.ts @@ -1,7 +1,7 @@ /* eslint-disable max-len */ import { object } from 'dot-object'; import { ILayout, ILayoutGroup } from 'src/features/form/layout'; -import { IMapping, IRepeatingGroup } from 'src/types'; +import { IDataModelBindings, IMapping, IRepeatingGroup } from 'src/types'; import { getParentGroup } from './validation'; import { IFormData } from 'src/features/form/data/formDataReducer'; @@ -157,3 +157,20 @@ export function mapFormData(formData: IFormData, mapping: IMapping) { }); return mappedFormData; } + +export function getFormDataFromFieldKey( + fieldKey: string, + dataModelBindings: IDataModelBindings, + formData: any, + groupDataBinding?: string, + index?: number, +) { + let dataModelBindingKey = dataModelBindings[fieldKey]; + if (groupDataBinding) { + dataModelBindingKey = dataModelBindingKey.replace( + groupDataBinding, + `${groupDataBinding}[${index}]`, + ); + } + return formData[dataModelBindingKey]; +} diff --git a/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts b/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts index fe88b6805f..cf5164470b 100644 --- a/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts +++ b/src/altinn-app-frontend/src/utils/formComponentUtils.test.ts @@ -20,6 +20,7 @@ import { isComponentValid, componentValidationsHandledByGenericComponent, componentHasValidationMessages, + getFieldName, } from './formComponentUtils'; describe('formComponentUtils', () => { @@ -451,4 +452,59 @@ describe('formComponentUtils', () => { expect(result).toEqual(true); }); }); + + describe('getFieldName', () => { + const mockTextResources = [ + { id: 'title', value: 'Component name'}, + { id: 'short', value: 'name'}, + ]; + const mockLanguage = { + form_filler: { + error_required: 'Du må fylle ut {0}', + address: 'Gateadresse', + postPlace: 'Poststed', + zipCode: 'Postnummer', + }, + validation: { + generic_field: 'dette feltet', + }, + }; + + it('should return field text from languages when fieldKey is present', () => { + const result = getFieldName( + { title: 'title' }, + mockTextResources, + mockLanguage, + 'address' + ); + expect(result).toEqual('Gateadresse'); + }); + + it('should return component shortName (textResourceBindings) when no fieldKey is present', () => { + const result = getFieldName( + { title: 'title', shortName: 'short' }, + mockTextResources, + mockLanguage, + ); + expect(result).toEqual('name'); + }); + + it('should return component title (textResourceBindings) when no shortName (textResourceBindings) and no fieldKey is present', () => { + const result = getFieldName( + { title: 'title' }, + mockTextResources, + mockLanguage, + ); + expect(result).toEqual('Component name'); + }); + + it('should return generic field name when fieldKey, shortName and title are all not available', () => { + const result = getFieldName( + { something: 'someTextKey' }, + mockTextResources, + mockLanguage, + ); + expect(result).toEqual('dette feltet'); + }); + }); }); diff --git a/src/altinn-app-frontend/src/utils/formComponentUtils.ts b/src/altinn-app-frontend/src/utils/formComponentUtils.ts index 688c0c861a..250930a4eb 100644 --- a/src/altinn-app-frontend/src/utils/formComponentUtils.ts +++ b/src/altinn-app-frontend/src/utils/formComponentUtils.ts @@ -23,6 +23,7 @@ import { } from 'src/types'; import { AsciiUnitSeparator } from './attachment'; import { getOptionLookupKey } from './options'; +import { getTextFromAppOrDefault } from './textResource'; export const componentValidationsHandledByGenericComponent = ( dataModelBindings: any, @@ -412,3 +413,25 @@ export const atleastOneTagExists = (attachments: IAttachment[]): boolean => { return totalTagCount !== undefined && totalTagCount >= 1; }; + +export function getFieldName( + textResourceBindings: ITextResourceBindings, + textResources: ITextResource[], + language: ILanguage, + fieldKey?: string, +): string { + if (fieldKey) + { + return getTextFromAppOrDefault(`form_filler.${fieldKey}`, textResources, language, null, true); + } + + if (textResourceBindings.shortName) { + return getTextResourceByKey(textResourceBindings.shortName, textResources); + } + + if (textResourceBindings.title) { + return getTextResourceByKey(textResourceBindings.title, textResources); + } + + return getLanguageFromKey('validation.generic_field', language); +} diff --git a/src/altinn-app-frontend/src/utils/validation.test.ts b/src/altinn-app-frontend/src/utils/validation.test.ts index 3730498359..cc2ac57d64 100644 --- a/src/altinn-app-frontend/src/utils/validation.test.ts +++ b/src/altinn-app-frontend/src/utils/validation.test.ts @@ -13,6 +13,7 @@ import type { IComponentBindingValidation, IComponentValidations, ILayoutValidations, + ITextResource, } from 'src/types'; import type { ILayoutComponent, ILayoutGroup } from 'src/features/form/layout'; @@ -40,15 +41,22 @@ describe('utils > validation', () => { let mockLanguage: any; let mockFormAttachments: any; let mockDataElementValidations: IValidationIssue[]; + let mockTextResources: ITextResource[]; beforeEach(() => { mockLanguage = { language: { form_filler: { - error_required: 'Feltet er påkrevd', + error_required: 'Du må fylle ut {0}', file_uploader_validation_error_file_number_1: 'For å fortsette må du laste opp', file_uploader_validation_error_file_number_2: 'vedlegg', + address: 'Gateadresse', + postPlace: 'Poststed', + zipCode: 'Postnummer', + }, + validation: { + generic_field: 'dette feltet', }, validation_errors: { minLength: 'length must be bigger than {0}', @@ -58,6 +66,33 @@ describe('utils > validation', () => { }, }; + mockTextResources = [ + { + id: 'c1Title', + value: 'component_1' + }, + { + id: 'c2Title', + value: 'component_2' + }, + { + id: 'c3Title', + value: 'component_3' + }, + { + id: 'c4Title', + value: 'component_4' + }, + { + id: 'c5Title', + value: 'component_5' + }, + { + id: 'c6Title', + value: 'component_6' + }, + ]; + mockComponent4 = { type: 'Input', id: 'componentId_4', @@ -66,7 +101,9 @@ describe('utils > validation', () => { }, required: true, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c4Title', + }, }; mockComponent5 = { @@ -77,7 +114,9 @@ describe('utils > validation', () => { }, required: false, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c5Title', + }, }; mockGroup2 = { @@ -125,7 +164,9 @@ describe('utils > validation', () => { }, required: true, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c1Title', + }, }, { type: 'Input', @@ -135,7 +176,9 @@ describe('utils > validation', () => { }, required: true, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c2Title', + }, }, { type: 'TextArea', @@ -145,7 +188,9 @@ describe('utils > validation', () => { }, required: true, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c3Title', + }, }, mockGroup1, mockGroup2, @@ -168,7 +213,9 @@ describe('utils > validation', () => { }, required: true, readOnly: false, - textResourceBindings: {}, + textResourceBindings: { + title: 'c6Title', + }, }, { type: 'Input', @@ -644,23 +691,24 @@ describe('utils > validation', () => { mockLanguage.language, [], repeatingGroups, + mockTextResources, ); const mockResult = { FormLayout: { componentId_3: { - simpleBinding: { errors: ['Feltet er påkrevd'], warnings: [] }, + simpleBinding: { errors: ['Du må fylle ut component_3'], warnings: [] }, }, 'componentId_4-0': { - simpleBinding: { errors: ['Feltet er påkrevd'], warnings: [] }, + simpleBinding: { errors: ['Du må fylle ut component_4'], warnings: [] }, }, componentId_6: { - address: { errors: ['Feltet er påkrevd'], warnings: [] }, - postPlace: { errors: ['Feltet er påkrevd'], warnings: [] }, - zipCode: { errors: ['Feltet er påkrevd'], warnings: [] }, + address: { errors: ['Du må fylle ut Gateadresse'], warnings: [] }, + postPlace: { errors: ['Du må fylle ut Poststed'], warnings: [] }, + zipCode: { errors: ['Du må fylle ut Postnummer'], warnings: [] }, }, required_in_group_simple: { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }}, }, @@ -683,20 +731,21 @@ describe('utils > validation', () => { mockLanguage.language, ['componentId_4-0'], repeatingGroups, + mockTextResources, ); const mockResult = { FormLayout: { componentId_3: { - simpleBinding: { errors: ['Feltet er påkrevd'], warnings: [] }, + simpleBinding: { errors: ['Du må fylle ut component_3'], warnings: [] }, }, componentId_6: { - address: { errors: ['Feltet er påkrevd'], warnings: [] }, - postPlace: { errors: ['Feltet er påkrevd'], warnings: [] }, - zipCode: { errors: ['Feltet er påkrevd'], warnings: [] }, + address: { errors: ['Du må fylle ut Gateadresse'], warnings: [] }, + postPlace: { errors: ['Du må fylle ut Poststed'], warnings: [] }, + zipCode: { errors: ['Du må fylle ut Postnummer'], warnings: [] }, }, required_in_group_simple: { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }} }, @@ -719,6 +768,7 @@ describe('utils > validation', () => { mockLanguage.language, [], repeatingGroups, + mockTextResources, ); expect(componentSpesificValidations).toEqual({}); @@ -732,12 +782,14 @@ describe('utils > validation', () => { validations[component.id] = validation.validateEmptyField( mockFormData, component.dataModelBindings, + component.textResourceBindings, + mockTextResources, mockLanguage.language, ); const mockResult = { componentId_3: { - simpleBinding: { errors: ['Feltet er påkrevd'], warnings: [] }, + simpleBinding: { errors: ['Du må fylle ut component_3'], warnings: [] }, }, }; @@ -752,14 +804,16 @@ describe('utils > validation', () => { validations[component.id] = validation.validateEmptyField( mockFormData, component.dataModelBindings, + component.textResourceBindings, + mockTextResources, mockLanguage.language, ); const mockResult = { componentId_6: { - address: { errors: ['Feltet er påkrevd'], warnings: [] }, - postPlace: { errors: ['Feltet er påkrevd'], warnings: [] }, - zipCode: { errors: ['Feltet er påkrevd'], warnings: [] }, + address: { errors: ['Du må fylle ut Gateadresse'], warnings: [] }, + postPlace: { errors: ['Du må fylle ut Poststed'], warnings: [] }, + zipCode: { errors: ['Du må fylle ut Postnummer'], warnings: [] }, }, }; @@ -779,12 +833,19 @@ describe('utils > validation', () => { mockLanguage.language, hiddenFields, repeatingGroups, + mockTextResources, ); const requiredFieldInSimpleGroup = 'required_in_group_simple'; - const requiredError = { - simpleBinding: { errors: ['Feltet er påkrevd'], warnings: [] }, - }; + const requiredError = (name?: string) => { + const fieldName = name || 'dette feltet'; + return { + simpleBinding: { + errors: [`Du må fylle ut ${fieldName}`], + warnings: [], + }, + }; + } it('should pass validation on required field in hidden group', () => { expect(_with({hiddenFields: ['group_simple']})[requiredFieldInSimpleGroup]).toBeUndefined(); @@ -793,7 +854,7 @@ describe('utils > validation', () => { expect(_with({hiddenFields: [requiredFieldInSimpleGroup]})[requiredFieldInSimpleGroup]).toBeUndefined(); }); it('should mark as required with required field in visible group', () => { - expect(_with({hiddenFields: []})[requiredFieldInSimpleGroup]).toEqual(requiredError); + expect(_with({hiddenFields: []})[requiredFieldInSimpleGroup]).toEqual(requiredError()); }); it('should validate successfully with no instances of repeating groups', () => { @@ -823,12 +884,12 @@ describe('utils > validation', () => { // Group2 has no instances inside the third instance of group1 }, })).toEqual({ - 'componentId_4-0': requiredError, - 'componentId_4-1': requiredError, - 'componentId_4-2': requiredError, - 'componentId_5-0-0': requiredError, - 'componentId_5-0-1': requiredError, - 'componentId_5-1-0': requiredError, + 'componentId_4-0': requiredError('component_4'), + 'componentId_4-1': requiredError('component_4'), + 'componentId_4-2': requiredError('component_4'), + 'componentId_5-0-0': requiredError('component_5'), + 'componentId_5-0-1': requiredError('component_5'), + 'componentId_5-1-0': requiredError('component_5'), }); }); @@ -839,8 +900,8 @@ describe('utils > validation', () => { group1: { index: 1 }, // Group1 has 2 instances }, })).toEqual({ - 'componentId_4-0': requiredError, - 'componentId_4-1': requiredError, + 'componentId_4-0': requiredError('component_4'), + 'componentId_4-1': requiredError('component_4'), }); }); @@ -855,8 +916,8 @@ describe('utils > validation', () => { group3: { index: 1 }, }, })).toEqual({ - 'componentId_4-0': requiredError, - 'componentId_4-1': requiredError, + 'componentId_4-0': requiredError('component_4'), + 'componentId_4-1': requiredError('component_4'), }); }); @@ -875,9 +936,9 @@ describe('utils > validation', () => { 'group2-0': { index: 0 }, }, })).toEqual({ - 'componentId_4-0': requiredError, - 'componentId_4-1': requiredError, - 'componentId_5-0-0': requiredError, + 'componentId_4-0': requiredError('component_4'), + 'componentId_4-1': requiredError('component_4'), + 'componentId_5-0-0': requiredError('component_5'), }); }); }); @@ -1533,6 +1594,11 @@ describe('utils > validation', () => { formData: { formData: mockFormData, } as any, + textResources: { + error: null, + language: 'nb', + resources: mockTextResources, + } }); const result: IValidations = validation.validateGroup('group1', state); expect(result).toEqual({ @@ -1540,7 +1606,7 @@ describe('utils > validation', () => { 'componentId_4-0': { simpleBinding: { errors: [ - 'Feltet er påkrevd', + 'Du må fylle ut component_4', getParsedLanguageFromKey( `validation_errors.pattern`, state.language.language, @@ -1646,19 +1712,19 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'group1-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'componentId_4-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut component_4'], warnings: [], }, }, @@ -1681,7 +1747,7 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -1695,13 +1761,13 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'group1-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -1736,7 +1802,7 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -1762,13 +1828,13 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'group2-0-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -1807,7 +1873,7 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -1833,25 +1899,25 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'group2-0-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'componentId_5-0-0': { simpleBinding: { - errors: ['Feltet er påkrevd 1'], + errors: ['Du må fylle ut 1'], warnings: [], }, }, 'componentId_5-0-1': { simpleBinding: { - errors: ['Feltet er påkrevd 2'], + errors: ['Du må fylle ut 2'], warnings: [], }, }, @@ -1885,19 +1951,19 @@ describe('utils > validation', () => { FormLayout: { 'group1-0': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'group2-0-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, 'componentId_5-0-1': { simpleBinding: { - errors: ['Feltet er påkrevd'], + errors: ['Du må fylle ut dette feltet'], warnings: [], }, }, @@ -2193,7 +2259,7 @@ describe('utils > validation', () => { const validations: ILayoutValidations = { field: { 'simple_binding': { - errors: ['Some random error', 'Feltet er påkrevd'], + errors: ['Some random error', 'Du må fylle ut dette feltet'], warnings: [], } } @@ -2202,7 +2268,7 @@ describe('utils > validation', () => { expect(result).toBeTruthy(); }); it('should return true when validations contain messages (react element) for missing fields', () => { - const node = createElement('span', {}, 'Feltet er påkrevd'); + const node = createElement('span', {}, 'Du må fylle ut '); const validations: ILayoutValidations = { field: { 'simple_binding': { @@ -2223,9 +2289,9 @@ describe('utils > validation', () => { } } }); - const shallow = ['Første linje', "\n", 'Feltet er påkrevd']; - const deep = ['Dette er feil:', ['Første linje', "\n", 'Feltet er påkrevd']]; - const withNode = ['Dette er feil:', ['Første linje', "\n", createElement('span', {}, 'Feltet er påkrevd')]]; + const shallow = ['Første linje', "\n", 'Du må fylle ut ']; + const deep = ['Dette er feil:', ['Første linje', "\n", 'Du må fylle ut ']]; + const withNode = ['Dette er feil:', ['Første linje', "\n", createElement('span', {}, 'Du må fylle ut ')]]; expect(validation.missingFieldsInLayoutValidations(validations(shallow), mockLanguage.language)).toBeTruthy(); expect(validation.missingFieldsInLayoutValidations(validations(deep), mockLanguage.language)).toBeTruthy(); expect(validation.missingFieldsInLayoutValidations(validations(withNode), mockLanguage.language)).toBeTruthy(); diff --git a/src/altinn-app-frontend/src/utils/validation.ts b/src/altinn-app-frontend/src/utils/validation.ts index b72e3fcced..d82744af64 100644 --- a/src/altinn-app-frontend/src/utils/validation.ts +++ b/src/altinn-app-frontend/src/utils/validation.ts @@ -21,6 +21,7 @@ import { ILayoutValidations, IDataModelBindings, IRuntimeState, + ITextResourceBindings, } from 'src/types'; import { ILayouts, @@ -29,9 +30,9 @@ import { ILayout, } from '../features/form/layout'; import { IValidationIssue, Severity, DateFlags } from '../types'; -import { getFormDataForComponent } from './formComponentUtils'; +import { getFieldName, getFormDataForComponent } from './formComponentUtils'; import { getParsedTextResourceByKey } from './textResource'; -import { convertDataBindingToModel, getKeyWithoutIndex } from './databindings'; +import { convertDataBindingToModel, getFormDataFromFieldKey, getKeyWithoutIndex } from './databindings'; // eslint-disable-next-line import/no-cycle import { matchLayoutComponent, setupGroupComponents } from './layout'; import { @@ -176,6 +177,7 @@ export function validateEmptyFields( language: ILanguage, hiddenFields: string[], repeatingGroups: IRepeatingGroups, + textResources: ITextResource[], ) { const validations = {}; Object.keys(layouts).forEach((id) => { @@ -186,6 +188,7 @@ export function validateEmptyFields( language, hiddenFields, repeatingGroups, + textResources, ); validations[id] = result; } @@ -202,6 +205,7 @@ export function validateEmptyFieldsForLayout( language: ILanguage, hiddenFields: string[], repeatingGroups: IRepeatingGroups, + textResources: ITextResource[], ): ILayoutValidations { const validations: any = {}; const allGroups = formLayout.filter( @@ -227,6 +231,8 @@ export function validateEmptyFieldsForLayout( const result = validateEmptyField( formData, component.dataModelBindings, + component.textResourceBindings, + textResources, language, ); if (result !== null) { @@ -282,6 +288,8 @@ export function validateEmptyFieldsForLayout( const result = validateEmptyField( formData, componentToCheck.dataModelBindings, + componentToCheck.textResourceBindings, + textResources, language, indexedGroupDataBinding, i, @@ -308,6 +316,8 @@ export function validateEmptyFieldsForLayout( const result = validateEmptyField( formData, componentToCheck.dataModelBindings, + componentToCheck.textResourceBindings, + textResources, language, groupDataModelBinding, i, @@ -322,6 +332,8 @@ export function validateEmptyFieldsForLayout( const result = validateEmptyField( formData, component.dataModelBindings, + component.textResourceBindings, + textResources, language, ); if (result !== null) { @@ -367,6 +379,8 @@ export function getGroupChildren( export function validateEmptyField( formData: any, dataModelBindings: IDataModelBindings, + textResourceBindings: ITextResourceBindings, + textResources: ITextResource[], language: ILanguage, groupDataBinding?: string, index?: number, @@ -380,21 +394,32 @@ export function validateEmptyField( }); const componentValidations: IComponentValidations = {}; fieldKeys.forEach((fieldKey) => { - let dataModelBindingKey = dataModelBindings[fieldKey]; - if (groupDataBinding) { - dataModelBindingKey = dataModelBindingKey.replace( - groupDataBinding, - `${groupDataBinding}[${index}]`, - ); - } - const value = formData[dataModelBindingKey]; + const value = getFormDataFromFieldKey( + fieldKey, + dataModelBindings, + formData, + groupDataBinding, + index, + ); if (!value && fieldKey) { componentValidations[fieldKey] = { errors: [], warnings: [], }; + + const fieldName = getFieldName( + textResourceBindings, + textResources, + language, + fieldKey !== 'simpleBinding' ? fieldKey : undefined, + ); componentValidations[fieldKey].errors.push( - getLanguageFromKey('form_filler.error_required', language), + getParsedLanguageFromKey( + 'form_filler.error_required', + language, + [fieldName], + true, + ), ); } }); @@ -674,10 +699,21 @@ export function validateComponentFormData( } if (component.required) { if (!formData || formData === '') { + const fieldName = getFieldName( + component.textResourceBindings, + textResources, + language, + fieldKey !== 'simpleBinding' ? fieldKey : undefined, + ); validationResult.validations[layoutId][ componentIdWithIndex || component.id ][fieldKey].errors.push( - getLanguageFromKey('form_filler.error_required', language), + getParsedLanguageFromKey( + 'form_filler.error_required', + language, + [fieldName], + true, + ), ); } } @@ -1556,6 +1592,7 @@ export function validateGroup( language, hiddenFields, repeatingGroups, + textResources, ); const componentValidations: ILayoutValidations = validateFormComponentsForLayout( @@ -1740,10 +1777,12 @@ export function missingFieldsInLayoutValidations( language: ILanguage, ): boolean { let result = false; - const requiredMessage = getLanguageFromKey( + let requiredMessage: string = getLanguageFromKey( 'form_filler.error_required', language, ); + // Strip away parametrized part of error message, as this will vary with each component. + requiredMessage = requiredMessage.substring(0, requiredMessage.indexOf('{0}')); const lookForRequiredMsg = (e: any) => { if (typeof e === 'string') { return e.includes(requiredMessage); diff --git a/src/shared/src/language/texts/en.ts b/src/shared/src/language/texts/en.ts index 13b0f0dae5..43efd36b89 100644 --- a/src/shared/src/language/texts/en.ts +++ b/src/shared/src/language/texts/en.ts @@ -41,7 +41,7 @@ export function en() { back_to_summary: 'Return to summary', error_report_header: 'There is a problem', error_report_description: 'The form contains errors that prevent it from being submitted. Try submitting again once the errors are corrected.', - error_required: 'Field is required', + error_required: 'You have to fill out {0}', file_upload_valid_file_format_all: 'all', file_uploader_add_attachment: 'Add more attachments', file_uploader_drag: 'Drag and drop or', @@ -75,6 +75,11 @@ export function en() { required_label: '*', summary_item_change: 'Change', summary_go_to_correct_page: 'Go to the correct page in the form', + address: 'Street Address', + careOf: 'C/O or other additional address', + houseNumber: 'House Number', + postPlace: 'Post Place', + zipCode: 'Zip Code', }, navigation: { main: 'App navigation', @@ -159,6 +164,16 @@ export function en() { authorization_error_instantiate_validation_info_customer_service: 'If you need help, contact customer service at {0}.', starting: 'Seatbelts on, it\'s time to launch!', }, + language: { + full_name: { + nb: 'Norwegian bokmål', + en: "English", + nn: "Norwegian nynorsk" + }, + selector: { + label: 'Language' + } + }, party_selection: { caption_prefix: 'Feil', invalid_selection_first_part: 'You started this app as', @@ -216,6 +231,14 @@ export function en() { log_out: 'Log out', profile_icon_aria_label: 'Profile icon button', }, + soft_validation: { + info_title: 'Information', + warning_title: 'Note', + success_title: 'How great!' + }, + validation: { + generic_field: 'this field', + }, validation_errors: { min: 'Minimum valid value is {0}', max: 'Maximum valid value is {0}', @@ -226,20 +249,5 @@ export function en() { required: 'Field is required', enum: 'Only the values {0} are permitted', }, - language: { - full_name: { - nb: 'Norwegian bokmål', - en: "English", - nn: "Norwegian nynorsk" - }, - selector: { - label: 'Language' - } - }, - soft_validation: { - info_title: 'Information', - warning_title: 'Note', - success_title: 'How great!' - } }; } diff --git a/src/shared/src/language/texts/nb.ts b/src/shared/src/language/texts/nb.ts index 07b66dbcb6..da33f0062f 100644 --- a/src/shared/src/language/texts/nb.ts +++ b/src/shared/src/language/texts/nb.ts @@ -41,7 +41,7 @@ export function nb() { back_to_summary: 'Tilbake til oppsummering', error_report_header: 'Det er feil i skjema', error_report_description: 'Skjemaet inneholder feil eller mangler som hindrer oss fra å sende det inn. Når du har rettet feilene, kan du sende inn skjemaet på nytt.', - error_required: 'Feltet er påkrevd', + error_required: 'Du må fylle ut {0}', file_upload_valid_file_format_all: 'alle', file_uploader_add_attachment: 'Legg til flere vedlegg', file_uploader_drag: 'Dra og slipp eller', @@ -75,6 +75,11 @@ export function nb() { required_label: '*', summary_item_change: 'Endre', summary_go_to_correct_page: 'Gå til riktig side i skjema', + address: 'Gateadresse', + careOf: 'C/O eller annen tilleggsadresse', + houseNumber: 'Bolignummer', + postPlace: 'Poststed', + zipCode: 'Postnr', }, navigation: { main: 'Appnavigasjon', @@ -226,6 +231,14 @@ export function nb() { log_out: 'Logg ut', profile_icon_aria_label: 'Profil ikon knapp', }, + soft_validation: { + info_title: 'Lurt å tenke på', + warning_title: 'OBS', + success_title: 'Så flott!' + }, + validation: { + generic_field: 'dette feltet', + }, validation_errors: { min: 'Minste gyldig verdi er {0}', max: 'Største gyldig verdi er {0}', @@ -236,10 +249,5 @@ export function nb() { required: 'Feltet er påkrevd', enum: 'Kun verdiene {0} er tillatt', }, - soft_validation: { - info_title: 'Lurt å tenke på', - warning_title: 'OBS', - success_title: 'Så flott!' - } }; } diff --git a/src/shared/src/language/texts/nn.ts b/src/shared/src/language/texts/nn.ts index 98eea4b407..043271df2b 100644 --- a/src/shared/src/language/texts/nn.ts +++ b/src/shared/src/language/texts/nn.ts @@ -40,7 +40,7 @@ export function nn() { back_to_summary: 'Attende til samandrag', error_report_header: 'Det er feil i skjema', error_report_description: 'Skjemaet inneheld feil eller manglar som hindrar oss frå å sende det inn. Når du har retta feila, kan du sende inn skjemaet på nytt.', - error_required: 'Feltet er påkravd', + error_required: 'Du må fylle ut {0}', file_upload_valid_file_format_all: 'alle', file_uploader_add_attachment: 'Legg til fleire vedlegg', file_uploader_drag: 'Dra og slepp eller', @@ -74,6 +74,11 @@ export function nn() { required_label: '*', summary_item_change: 'Endre', summary_go_to_correct_page: 'Gå til riktig side i skjema', + address: 'Gateadresse', + careOf: 'C/O eller annan tilleggsadresse', + houseNumber: 'Bustadnummer', + postPlace: 'Poststed', + zipCode: 'Postnr', }, navigation: { main: 'Appnavigasjon', @@ -223,6 +228,14 @@ export function nn() { log_out: 'Logg ut', profile_icon_aria_label: 'Profil ikon knapp', }, + soft_validation: { + info_title: 'Lurt å tenke på', + warning_title: 'OBS', + success_title: 'Så flott!' + }, + validation: { + generic_field: 'dette feltet', + }, validation_errors: { min: 'Minste gyldige verdi er {0}', max: 'Største gyldige verdi er {0}', @@ -233,10 +246,5 @@ export function nn() { required: 'Feltet er påkravd', enum: 'Kun verdiane {0} er tillatne', }, - soft_validation: { - info_title: 'Lurt å tenke på', - warning_title: 'OBS', - success_title: 'Så flott!' - } }; } diff --git a/test/cypress/e2e/fixtures/texts.json b/test/cypress/e2e/fixtures/texts.json index 2069453fc0..1f894a94ae 100644 --- a/test/cypress/e2e/fixtures/texts.json +++ b/test/cypress/e2e/fixtures/texts.json @@ -15,6 +15,7 @@ "missingRights": "Du mangler rettigheter for å se denne tjenesten", "next": "Neste", "requiredField": "Feltet er påkrevd", + "requiredFieldDateFrom": "Du må fylle ut Dato for navneendring", "securityReasons": "Av sikkerhetshensyn vil verken innholdet i tjenesten eller denne meldingen være synlig i Altinn etter at du har forlatt denne siden", "selectNewReportee": "Velg ny aktør under", "startingSoon": "Hold deg fast, nå starter vi!", diff --git a/test/cypress/e2e/integration/app-frontend/summary.js b/test/cypress/e2e/integration/app-frontend/summary.js index 0642420864..b4e6b49a93 100644 --- a/test/cypress/e2e/integration/app-frontend/summary.js +++ b/test/cypress/e2e/integration/app-frontend/summary.js @@ -68,7 +68,7 @@ describe('Summary', () => { .contains(mui.gridContainer, texts.dateOfEffect) .then((summaryDate) => { cy.get(summaryDate).contains(texts.dateOfEffect).should('have.css', 'color', 'rgb(226, 59, 83)'); - cy.get(summaryDate).contains(mui.gridContainer, texts.requiredField).should('be.visible'); + cy.get(summaryDate).contains(mui.gridContainer, texts.requiredFieldDateFrom).should('be.visible'); cy.get(summaryDate).contains('button', texts.goToRightPage).should('be.visible').click(); cy.get(appFrontend.changeOfName.dateOfEffect) .siblings() @@ -87,7 +87,7 @@ describe('Summary', () => { .contains(mui.gridContainer, texts.dateOfEffect) .then((summaryDate) => { cy.get(summaryDate).contains(texts.dateOfEffect).should('not.have.css', 'color', 'rgb(226, 59, 83)'); - cy.get(summaryDate).contains(mui.gridContainer, texts.requiredField).should('not.exist'); + cy.get(summaryDate).contains(mui.gridContainer, texts.requiredFieldDateFrom).should('not.exist'); }); });