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 84f59729b87800db58a80cbd887a46bba3970278..b9f078d75ec0fbb962ef8f589c2c60b58196519c 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 9709c01258eddc3fe1cbd5d40854511868e759b4..b5f78af0ec4a9516994e98de8ec56dc2fd9b31d0 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 b7af910dbf298393457e930fed2c318a1a6f4514..05dd38f4089152f5d1e233632b09b1e3f9db9b94 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 75d717dbc1c6290e86d95a5193a9ea99cf6d5384..10ba7d8039cbce66558408a405f3c6ec385c1b99 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 aa2dee9b6966288e6326463b82a4568abef68ecd..648099dffa599d2eb7d5a2234bb056eb59e3fa90 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 852a8299c56d256d93bf778e9600ab0717edc00f..ae32eafd971d4baf014b3476f557f07bdaf5636a 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 32ce38f3c400ca02773522f97c30b6e7fab1b246..f5a6a56fb0b3e4e99a732b2b0406f2a3b5e379d0 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 b0cb78f8a1d59efdddeb25e26c4deda6a746d7fa..8ea81cca8dd3c0d1be3cc69c44afbf9ee9ee9770 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.'); }