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