From 8d2314f8f18cea25401eb2015ecfdd139cbc1cfb Mon Sep 17 00:00:00 2001 From: sebo <sebastian.bergandy@external.mgm-cp.com> Date: Sun, 16 Mar 2025 16:29:17 +0100 Subject: [PATCH] OZG-7473 add validation Sub task: OZG-7889 --- .../statistik-fields-form.component.html | 9 ++-- .../statistik-fields-form.component.ts | 2 +- ...tatistik-field-mapping-form.component.html | 6 ++- .../statistik-fields.formservice.ts | 2 +- .../resource/list-resource.service.spec.ts | 23 +++++++++-- .../src/lib/resource/list-resource.service.ts | 26 ++++++------ .../validation/tech.validation.util.spec.ts | 27 +++++------- .../lib/validation/tech.validation.util.ts | 41 +++++++------------ 8 files changed, 70 insertions(+), 66 deletions(-) diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.html b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.html index 84f59729b8..b9f078d75e 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.html +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.html @@ -5,23 +5,26 @@ <form class="form flex-col" [formGroup]="formService.form" class="flex flex-col gap-2"> <ods-text-editor [formControlName]="StatistikFieldsFormService.FIELD_NAME" - label="Name *" + label="Name" placeholder="" + isRequired="true" data-test-id="statistik-name-text-editor" dataTestId="statistik-name" ></ods-text-editor> <div [formGroupName]="StatistikFieldsFormService.FIELD_FORM_IDENTIFIER" class="flex flex-col gap-4"> <ods-text-editor [formControlName]="StatistikFieldsFormService.FIELD_FORM_ENGINE_NAME" - label="Formengine *" + label="Formengine" placeholder="Tragen Sie hier die Formengine des Formulars ein." + isRequired="true" data-test-id="form-engine-name" dataTestId="form-engine-name" ></ods-text-editor> <ods-text-editor [formControlName]="StatistikFieldsFormService.FIELD_FORM_ID" - label="FormID *" + label="FormID" placeholder="Tragen Sie hier die FormID des Formulars ein." + isRequired="true" data-test-id="form-id" dataTestId="form-id" ></ods-text-editor> diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.ts index 9709c01258..b5f78af0ec 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.ts +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form.component.ts @@ -18,12 +18,12 @@ import { StatistikFieldsFormService } from './statistik-fields.formservice'; ButtonComponent, PlusIconComponent, ReactiveFormsModule, - TextEditorComponent, AdminSaveButtonComponent, AdminCancelButtonComponent, StatistikFieldsMappingsFormComponent, SpinnerComponent, AsyncPipe, + TextEditorComponent, ], providers: [{ provide: ADMIN_FORMSERVICE, useClass: StatistikFieldsFormService }], }) diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-mappings-form/statistik-field-mapping-form/statistik-field-mapping-form.component.html b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-mappings-form/statistik-field-mapping-form/statistik-field-mapping-form.component.html index b7af910dbf..05dd38f408 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-mappings-form/statistik-field-mapping-form/statistik-field-mapping-form.component.html +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-mappings-form/statistik-field-mapping-form/statistik-field-mapping-form.component.html @@ -19,15 +19,17 @@ <ods-text-editor class="flex-1" formControlName="sourcePath" - label="Pfad *" + label="Pfad" placeholder="Tragen Sie hier den gesamten Pfad des Datenfeldes ein, das Sie auswerten möchten." + isRequired="true" [dataTestId]="'source-mapping-field-' + index" [attr.data-test-id]="'source-mapping-field-' + index" ></ods-text-editor> <ods-text-editor class="flex-1" formControlName="targetPath" - label="Zielfeld *" + label="Zielfeld" + isRequired="true" placeholder="Tragen Sie hier den gesamten Pfad des Datenfeldes ein, das Sie auswerten möchten." [dataTestId]="'target-mapping-field-' + index" [attr.data-test-id]="'target-mapping-field-' + index" diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts index 75d717dbc1..10ba7d8039 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts @@ -35,7 +35,7 @@ export class StatistikFieldsFormService extends AbstractFormService<AggregationM } protected getPathPrefix(): string { - return 'settingBody'; + return EMPTY_STRING; } public addMapping(): void { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts index aa2dee9b69..648099dffa 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts @@ -29,13 +29,16 @@ import { cold } from 'jest-marbles'; import { DummyLinkRel, DummyListLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyListResource, createDummyResource, createFilledDummyListResource } from 'libs/tech-shared/test/resource'; import { BehaviorSubject, Observable, of } from 'rxjs'; -import { multipleCold, singleCold, singleHot } from '../../../test/marbles'; +import { multipleCold, singleCold, singleColdCompleted, singleHot } from '../../../test/marbles'; import { ResourceListService } from './list-resource.service'; import { CreateResourceData, LinkRelationName, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; -import { ListResource, StateResource, createEmptyStateResource, createStateResource } from './resource.util'; - import * as ResourceUtil from './resource.util'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, ListResource, StateResource } from './resource.util'; + +import { ProblemDetail } from '@alfa-client/tech-shared'; +import { expect } from '@jest/globals'; +import { createProblemDetail } from '../../../test/error'; describe('ListResourceService', () => { let service: ResourceListService<Resource, ListResource, ListItemResource>; @@ -350,6 +353,20 @@ describe('ListResourceService', () => { }); }); + describe('handle error', () => { + it('should return error state resource on unprocessable entity', () => { + const error: ProblemDetail = createProblemDetail(); + + expect(service._handleError(error)).toBeObservable(singleColdCompleted(createErrorStateResource(error))); + }); + + it('should throw error', () => { + const error: ProblemDetail = { ...createProblemDetail(), status: 500 }; + + expect(service._handleError(error)).toBeObservable(cold('#', null, error)); + }); + }); + describe('select', () => { const selfHref: ResourceUri = 'dummySelfHref'; const dummyResource: Resource = createResourceWithUri(selfHref); diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts index 852a8299c5..ae32eafd97 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts @@ -21,24 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; +import { getUrl, hasLink, Resource, ResourceUri } from '@ngxp/rest'; import { isEqual, isNil, isNull } from 'lodash-es'; -import { BehaviorSubject, Observable, combineLatest, debounceTime, filter, first, map, startWith, tap } from 'rxjs'; +import { BehaviorSubject, catchError, combineLatest, debounceTime, filter, first, map, Observable, of, startWith, tap, throwError, } from 'rxjs'; +import { isUnprocessableEntity } from '../http.util'; +import { ProblemDetail } from '../tech.model'; import { isNotNull, isNotUndefined } from '../tech.util'; import { CreateResourceData, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { mapToFirst, mapToResource } from './resource.rxjs.operator'; -import { - ListResource, - StateResource, - createEmptyStateResource, - createStateResource, - doIfLoadingRequired, - getEmbeddedResources, - isInvalidResourceCombination, - isLoadingRequired, - isStateResoureStable, -} from './resource.util'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, doIfLoadingRequired, getEmbeddedResources, isInvalidResourceCombination, isLoadingRequired, isStateResoureStable, ListResource, StateResource, } from './resource.util'; /** * B = Type of baseresource @@ -111,6 +103,7 @@ export class ResourceListService<B extends Resource, T extends ListResource, I e return this.repository.createResource(this.buildCreateResourceData(toCreate, this.config.createLinkRel)).pipe( map((listItemResource: I) => createStateResource(listItemResource)), startWith(createEmptyStateResource<I>(true)), + catchError((error: ProblemDetail) => this._handleError(error)), ); } @@ -131,6 +124,13 @@ export class ResourceListService<B extends Resource, T extends ListResource, I e return this.hasLinkRel(this.config.createLinkRel); } + _handleError(error: ProblemDetail): Observable<StateResource<I>> { + if (isUnprocessableEntity(error.status)) { + return of(createErrorStateResource(error)); + } + return throwError(() => error); + } + public select(uri: ResourceUri): void { this.setSelectedResourceLoading(); this.repository diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts index 32ce38f3c4..f5a6a56fb0 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts @@ -26,7 +26,7 @@ import { faker } from '@faker-js/faker'; import { createInvalidParam, createIssue, createProblemDetail } from '../../../test/error'; import { InvalidParam, Issue } from '../tech.model'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; -import { getControlForInvalidParam, getControlForIssue, getFieldPath, getMessageForInvalidParam, getMessageForIssue, getMessageReason, setInvalidParamValidationError, setIssueValidationError } from './tech.validation.util'; +import { getControlForInvalidParam, getControlForIssue, getFieldPath, getMessageForInvalidParam, getMessageForIssue, getMessageReason, setInvalidParamValidationError, setIssueValidationError, } from './tech.validation.util'; describe('ValidationUtils', () => { const baseField1Control: FormControl = new UntypedFormControl(); @@ -44,7 +44,7 @@ describe('ValidationUtils', () => { describe('set issue validation error', () => { describe('get control for issue', () => { it('should return base field control', () => { - const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'baseField1' }; const control: AbstractControl = getControlForIssue(form, issue); @@ -69,7 +69,7 @@ describe('ValidationUtils', () => { }); describe('in base field', () => { - const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'baseField1' }; it('should set error in control', () => { setIssueValidationError(form, issue); @@ -144,7 +144,7 @@ describe('ValidationUtils', () => { it('should return base field control', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'baseField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam); @@ -155,7 +155,7 @@ describe('ValidationUtils', () => { it('should return sub group field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.subGroup.subGroupField1', + name: 'resource.subGroup.subGroupField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); @@ -166,7 +166,7 @@ describe('ValidationUtils', () => { it('should ignore path prefix', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'resource.baseField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); @@ -178,7 +178,7 @@ describe('ValidationUtils', () => { describe('in base field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'baseField1', }; it('should set error in control', () => { @@ -209,7 +209,7 @@ describe('ValidationUtils', () => { describe('in subGroup Field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.subGroup.subGroupField1', + name: 'resource.subGroup.subGroupField1', }; it('should set error in control', () => { @@ -243,12 +243,9 @@ describe('ValidationUtils', () => { }); it('should return field from full path when resource is undefined', () => { - const fieldPath: string = 'field1'; - const fullPath: string = `${backendClassName}.${resource}.${fieldPath}`; - - const result: string = getFieldPath(fullPath, undefined); + const result: string = getFieldPath('field1', undefined); - expect(result).toBe(fieldPath); + expect(result).toBe('field1'); }); it('should return field from field when resource is undefined', () => { @@ -309,9 +306,7 @@ describe('ValidationUtils', () => { ...invalidParam, reason: ValidationMessageCode.FIELD_INVALID, }); - expect(message).toEqual( - VALIDATION_MESSAGES[ValidationMessageCode.FIELD_INVALID].replace('{field}', label), - ); + expect(message).toEqual(VALIDATION_MESSAGES[ValidationMessageCode.FIELD_INVALID].replace('{field}', label)); }); it('should return message with placeholders', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts index b0cb78f8a1..8ea81cca8d 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts @@ -31,28 +31,18 @@ export function isValidationError(issue: Issue): boolean { return issue.messageCode.includes('javax.validation.constraints'); } -export function setIssueValidationError( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): void { +export function setIssueValidationError(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): void { const control: AbstractControl = getControlForIssue(form, issue, pathPrefix); control.setErrors({ [issue.messageCode]: issue }); control.markAsTouched(); } -export function getControlForIssue( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): AbstractControl { +export function getControlForIssue(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): AbstractControl { const fieldPath: string = getFieldPath(issue.field, pathPrefix); let curControl: AbstractControl = form; - fieldPath - .split('.') - .forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); + fieldPath.split('.').forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); return curControl; } @@ -66,9 +56,7 @@ export function getMessageForIssue(label: string, issue: Issue): string { } msg = replacePlaceholder(msg, 'field', label); - issue.parameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + issue.parameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } @@ -84,11 +72,7 @@ export function getMessageCode(apiError: ApiError): string { return apiError.issues[0].messageCode; } -export function setInvalidParamValidationError( - form: UntypedFormGroup, - invalidParam: InvalidParam, - pathPrefix?: string, -): void { +export function setInvalidParamValidationError(form: UntypedFormGroup, invalidParam: InvalidParam, pathPrefix?: string): void { const control: AbstractControl = getControlForInvalidParam(form, invalidParam, pathPrefix); control.setErrors({ [invalidParam.reason]: invalidParam }); @@ -112,17 +96,20 @@ export function getMessageForInvalidParam(label: string, invalidParam: InvalidPa } msg = replacePlaceholder(msg, 'field', label); - invalidParam.constraintParameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + invalidParam.constraintParameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } export function getFieldPath(name: string, pathPrefix: string): string { + const path: string = _mapFormArrayElementNameToPath(name); if (isEmpty(pathPrefix)) { - return name.split('.').pop(); + return path; } - const indexOfField = name.lastIndexOf(pathPrefix) + pathPrefix.length + 1; - return name.slice(indexOfField); + const indexOfField = path.lastIndexOf(pathPrefix) + pathPrefix.length + 1; + return path.slice(indexOfField); +} + +export function _mapFormArrayElementNameToPath(name: string): string { + return name.replace(/\[(\d+?)]\./g, '.$1.'); } -- GitLab