From 3f8c5f6e63a949e4f5f519436e67c22296e375b2 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 3 Nov 2026 10:39:11 +0100 Subject: [PATCH] VSB-TUO/Give required inputs of an optional complex field a required message Port of dtq-dev PR 1667 (commit 718f7260b7). Co-Authored-By: Claude Opus 5.5 --- .../parsers/complex-field-parser.spec.ts | 69 +++++++++++++++++++ .../builder/parsers/complex-field-parser.ts | 6 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/parsers/complex-field-parser.spec.ts b/src/app/shared/form/builder/parsers/complex-field-parser.spec.ts index 09f024bf87d..1c88bbc9a11 100644 --- a/src/app/shared/form/builder/parsers/complex-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/complex-field-parser.spec.ts @@ -1,5 +1,9 @@ +import { TestBed } from '@angular/core/testing'; +import { DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { FormFieldModel } from '../models/form-field.model'; import { ParserOptions } from './parser-options'; +import { DynamicComplexModel } from '../ds-dynamic-form-ui/models/ds-dynamic-complex.model'; +import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DynamicRowArrayModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import {ComplexFieldParser} from './complex-field-parser'; import { getMockTranslateService } from '../../../mocks/translate.service.mock'; @@ -22,6 +26,9 @@ describe('ComplexFieldParser test suite', () => { }; const separator = ';'; + const requiredInputs = (complexModel: DynamicComplexModel) => + (complexModel.group as DsDynamicInputModel[]).filter((input) => input.required); + beforeEach(() => { field = { input: { @@ -63,4 +70,66 @@ describe('ComplexFieldParser test suite', () => { expect(fieldModel instanceof DynamicRowArrayModel).toBe(true); expect(fieldModel.label).toBe(expectedValue); }); + + it('should keep the mandatory message of a mandatory field on its required inputs', () => { + field.mandatory = 'true'; + field.mandatoryMessage = 'Please fill all the fields for the contact person.'; + const parser = new ComplexFieldParser(submissionId, field, initFormValues, parserOptions, separator, [], + translateService); + + const inputs = requiredInputs(parser.modelFactory()); + + expect(inputs.length).toBe(3); + inputs.forEach((input) => + expect(input.errorMessages.required).toBe('Please fill all the fields for the contact person.')); + }); + + describe('an optional field with required inputs', () => { + let complexModel: DynamicComplexModel; + + beforeEach(() => { + const fundingField = { + input: { + type: 'complex', + }, + mandatory: false, + label: 'Funding', + repeatable: true, + selectableMetadata: [ + { + metadata: 'local.sponsor', + }, + ], + languageCodes: [], + complexDefinition: '[{"type":{"name":"type","value-pairs-name":"metashare_funding","input-type":"dropdown",' + + '"label":"Funding type","required":"true"}},{"code":{"name":"code","input-type":"autocomplete",' + + '"label":"Grant no. or funding project code","required":"true"}},{"orgname":{"name":"orgname",' + + '"input-type":"text","label":"Funding organization","required":"true"}},{"projname":{"name":"projname",' + + '"input-type":"autocomplete","label":"Funding project name","required":"true"}},{"openaire_id":' + + '{"name":"openaire_id","input-type":"text","label":"EU Project Identifier (OpenAIRE)","readonly":"true"}}]', + } as unknown as FormFieldModel; + const parser = new ComplexFieldParser(submissionId, fundingField, initFormValues, parserOptions, separator, [], + translateService); + complexModel = parser.modelFactory(); + }); + + it('should give the required inputs the default required message', () => { + const inputs = requiredInputs(complexModel); + + expect(inputs.length).toBe(4); + inputs.forEach((input) => expect(input.errorMessages.required).toBe('error.validation.required')); + }); + + it('should build the error messages of the empty required inputs', () => { + const formGroup = TestBed.inject(DynamicFormService).createFormGroup([complexModel]); + const validationService = TestBed.inject(DynamicFormValidationService); + + requiredInputs(complexModel).forEach((input) => { + const control = formGroup.get([complexModel.id, input.id]); + + expect(control.hasError('required')).toBe(true); + expect(validationService.createErrorMessages(control, input)).toEqual(['error.validation.required']); + }); + }); + }); }); diff --git a/src/app/shared/form/builder/parsers/complex-field-parser.ts b/src/app/shared/form/builder/parsers/complex-field-parser.ts index 33d14855ccc..773114ef28c 100644 --- a/src/app/shared/form/builder/parsers/complex-field-parser.ts +++ b/src/app/shared/form/builder/parsers/complex-field-parser.ts @@ -17,7 +17,7 @@ import { SPONSOR_METADATA_NAME, } from '../ds-dynamic-form-ui/models/ds-dynamic-complex.model'; -import { hasValue, isNotEmpty, isUndefined } from '../../../empty.util'; +import { hasValue, isEmpty, isNotEmpty, isUndefined } from '../../../empty.util'; import { ParserOptions } from './parser-options'; import { CONFIG_DATA, @@ -160,6 +160,10 @@ export class ComplexFieldParser extends FieldParser { // required. It should be marked as required in the complex group for every input field. if (inputConfig.required) { this.markAsRequired(inputModel); + // an optional complex field has no mandatory message of its own + if (isEmpty(inputModel.errorMessages.required)) { + inputModel.errorMessages.required = 'error.validation.required'; + } } concatGroup.group.push(inputModel); });