From 929c3c75668a83bc018cd5893b955ddf6931b78e Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 12:33:23 +0200 Subject: [PATCH 01/23] OZG-7974 button type submit --- .../libs/design-system/src/lib/button/button.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alfa-client/libs/design-system/src/lib/button/button.component.ts b/alfa-client/libs/design-system/src/lib/button/button.component.ts index 3f88a32abc..78bc43acf3 100644 --- a/alfa-client/libs/design-system/src/lib/button/button.component.ts +++ b/alfa-client/libs/design-system/src/lib/button/button.component.ts @@ -104,8 +104,8 @@ export type ButtonVariants = VariantProps<typeof buttonVariants>; selector: 'ods-button', standalone: true, imports: [CommonModule, SpinnerIconComponent], - template: `<button - type="button" + template: ` <button + type="submit" [ngClass]="buttonVariants({ size, variant, disabled: isDisabled, destructive })" [attr.aria-disabled]="isDisabled" [attr.aria-label]="text" -- GitLab From e828a9e0b4558657ac754c49484a7cfa6196efda Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 15:20:21 +0200 Subject: [PATCH 02/23] OZG-7974 formControl editor abstract --- ...mcontrol-editor.abstract.component.spec.ts | 159 ++++++++++++++++++ .../formcontrol-editor.abstract.component.ts | 48 +++--- .../test/form/MockNgControl.ts | 10 +- 3 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts new file mode 100644 index 0000000000..210d995986 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -0,0 +1,159 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FormGroup, + FormGroupDirective, + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors, +} from '@angular/forms'; +import { FormControlEditorAbstractComponent } from '@ods/component'; +import { MockNgControl } from '../../../test/form/MockNgControl'; + +describe('FormControlEditorAbstractComponent', () => { + let component: TestComponent; + let fixture: ComponentFixture<TestComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, TestComponent], + providers: [FormGroupDirective], + }); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + component.control = new MockNgControl(); + fixture.detectChanges(); + }); + + describe('constructor', () => { + it('should set control value accessor', () => { + expect(component.control.valueAccessor).toBe(component); + }); + }); + + describe('ng on init', () => { + it('should set valueChange subscription', () => { + component.ngOnInit(); + + expect(component._changesSubscr).toBeDefined(); + }); + + it('should call field control on change handler when fieldControl value changes ', () => { + component._fieldControlOnChangeHandler = jest.fn(); + component.ngOnInit(); + + component.fieldControl.setValue('testValue'); + + expect(component._fieldControlOnChangeHandler).toHaveBeenCalledWith('testValue'); + }); + + it('should set statusChange subscription', () => { + component.ngOnInit(); + + expect(component._statusSubscr).toBeDefined(); + }); + + it('should call setErrors on statusChange', () => { + component.setErrors = jest.fn(); + component.ngOnInit(); + + component.control.control.setErrors({ required: true }); + + expect(component.setErrors).toHaveBeenCalled(); + }); + }); + + describe('writeValue', () => { + it('should set value to fieldControl', () => { + const value = 'testValue'; + + component.writeValue(value); + + expect(component.fieldControl.value).toBe(value); + }); + }); + + describe('setErrors', () => { + it('should set fieldControl errors', () => { + const errors: ValidationErrors = { required: true }; + + component.control.control.setErrors(errors); + + expect(component.fieldControl.errors).toEqual(errors); + }); + + it('should call update invalid params', () => { + component._updateInvalidParams = jest.fn(); + + component.setErrors(); + + expect(component._updateInvalidParams).toHaveBeenCalled(); + }); + }); + + describe('remove errors', () => { + it('should remove fieldControl errors', () => { + component.fieldControl.setErrors({ fehler: 'this is an validation error' }); + + component.removeErrors(); + + expect(component.fieldControl.errors).toBeNull(); + }); + + it('should call update invalid params', () => { + component._updateInvalidParams = jest.fn(); + + component.removeErrors(); + + expect(component._updateInvalidParams).toHaveBeenCalled(); + }); + + it('should call clear all parent controls', () => { + component._clearAllParentErrors = jest.fn(); + + component.removeErrors(); + + expect(component._clearAllParentErrors).toHaveBeenCalledWith(component.control.control); + }); + }); + + describe('clear all parent errors', () => { + const parentControl: UntypedFormGroup = new FormGroup({ child: new UntypedFormControl() }); + const childControl: UntypedFormControl = <UntypedFormControl>parentControl.get('child'); + + it('should set errors to null on parent control if parent exists', () => { + const setErrorsSpy: jest.SpyInstance = jest.spyOn(parentControl, 'setErrors'); + + component._clearAllParentErrors(childControl); + + expect(setErrorsSpy).toHaveBeenCalledWith(null); + }); + + it('should call clear all parent errors if parent exists', () => { + const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); + + component._clearAllParentErrors(childControl); + + expect(clearAllParentsSpy).toHaveBeenCalledWith(parentControl); + }); + + it('should not call clear all parent errors again if parent does not exist', () => { + const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); + + component._clearAllParentErrors(parentControl); + + expect(clearAllParentsSpy).toHaveBeenCalledTimes(1); + }); + }); +}); + +@Component({ + standalone: true, + template: '', + imports: [CommonModule, ReactiveFormsModule], + providers: [FormGroupDirective], +}) +class TestComponent extends FormControlEditorAbstractComponent {} diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 89c0cd5fb1..d84fcd376b 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -23,7 +23,7 @@ */ import { InvalidParam } from '@alfa-client/tech-shared'; import { Component, OnDestroy, OnInit, Optional, Self } from '@angular/core'; -import { ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; +import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; @Component({ @@ -31,29 +31,29 @@ import { Subscription } from 'rxjs'; }) export abstract class FormControlEditorAbstractComponent implements ControlValueAccessor, OnInit, OnDestroy { readonly fieldControl: UntypedFormControl = new UntypedFormControl(); - public onChange = (text: string | Date) => undefined; + public onChange = (value: unknown) => undefined; public onTouched = () => undefined; public invalidParams: InvalidParam[] = []; - private changesSubscr: Subscription; - private statusSubscr: Subscription; + _changesSubscr: Subscription; + _statusSubscr: Subscription; disabled: boolean = false; constructor(@Self() @Optional() public control: NgControl | null) { if (this.control) this.control.valueAccessor = this; - - this.changesSubscr = this.fieldControl.valueChanges.subscribe((val) => { - this.onChange(val); - this.setErrors(); - }); } ngOnInit(): void { - if (!this.statusSubscr && this.control) - this.statusSubscr = this.control.statusChanges.subscribe(() => { - this.setErrors(); - }); + this._changesSubscr = this.fieldControl.valueChanges.subscribe(this._fieldControlOnChangeHandler); + if (this.control) { + this._statusSubscr = this.control.statusChanges.subscribe(this.setErrors); + } + } + + _fieldControlOnChangeHandler(value: unknown): void { + this.onChange(value); + this.removeErrors(); } touch(): void { @@ -62,7 +62,6 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue writeValue(text: string): void { this.fieldControl.setValue(text); - this.setErrors(); } registerOnChange(fn: (text: string | Date) => {}): void { @@ -78,20 +77,31 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnDestroy(): void { - if (this.changesSubscr) this.changesSubscr.unsubscribe(); - if (this.statusSubscr) this.statusSubscr.unsubscribe(); + if (this._changesSubscr) this._changesSubscr.unsubscribe(); + if (this._statusSubscr) this._statusSubscr.unsubscribe(); } setErrors(): void { if (this.control) { this.fieldControl.setErrors(this.control.errors); - if (this.control.invalid) { - this.fieldControl.markAsTouched(); - } this._updateInvalidParams(); } } + removeErrors(): void { + this.fieldControl.setErrors(null); + this._updateInvalidParams(); + this._clearAllParentErrors(this.control.control); + } + + _clearAllParentErrors(control: AbstractControl): void { + const parent: AbstractControl = control.parent; + if (parent) { + parent.setErrors(null); + this._clearAllParentErrors(parent); + } + } + _updateInvalidParams(): void { this.invalidParams = this.fieldControl.errors ? diff --git a/alfa-client/libs/design-component/test/form/MockNgControl.ts b/alfa-client/libs/design-component/test/form/MockNgControl.ts index 4d41bb1b1e..caad15ac47 100644 --- a/alfa-client/libs/design-component/test/form/MockNgControl.ts +++ b/alfa-client/libs/design-component/test/form/MockNgControl.ts @@ -28,9 +28,17 @@ import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } export class MockNgControl extends NgControl { valueAccessor: ControlValueAccessor | null = null; + private _control: AbstractControl = new UntypedFormControl(null); + get control(): AbstractControl { - return new UntypedFormControl(null); + return this._control; + } + + set control(ctrl: AbstractControl) { + this._control = ctrl; } viewToModelUpdate(newValue: any): void {} + + setErrors(errors: any): void {} } -- GitLab From 1ec9e7056407e1d957e83a14b7d43bd2bc37f32d Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 16:02:13 +0200 Subject: [PATCH 03/23] OZG-7974 form on ngSubmit --- .../user-form-save-button.component.html | 1 - .../user-form-save-button.component.spec.ts | 35 +---------- .../user-form-save-button.component.ts | 11 +--- .../lib/user-form/user-form.component.html | 34 ++++++----- .../lib/user-form/user-form.component.spec.ts | 52 ++++++++++++---- .../src/lib/user-form/user-form.component.ts | 5 ++ .../src/lib/user-form/user.formservice.ts | 59 ++++++++++--------- 7 files changed, 98 insertions(+), 99 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html index 064289aa47..c38b3871bc 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html @@ -1,6 +1,5 @@ <ods-button-with-spinner [stateResource]="submitStateResource$ | async" - (clickEmitter)="submit()" text="Speichern" dataTestId="save-button" /> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.spec.ts index dcd4e3712e..1ab28ef608 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.spec.ts @@ -2,9 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { User } from '@admin-client/user-shared'; import { createStateResource, StateResource } from '@alfa-client/tech-shared'; -import { dispatchEventFromFixture, getDebugElementFromFixtureByCss, mock, Mock } from '@alfa-client/test-utils'; +import { getDebugElementFromFixtureByCss, mock, Mock } from '@alfa-client/test-utils'; import { ButtonWithSpinnerComponent } from '@ods/component'; -import { cold } from 'jest-marbles'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { getDataTestIdAttributeOf } from '../../../../../../tech-shared/test/data-test'; @@ -40,28 +39,6 @@ describe('UserFormSaveButtonComponent', () => { expect(component).toBeTruthy(); }); - describe('component', () => { - describe('submit', () => { - const userStateResource: StateResource<User> = createStateResource(createUser()); - - beforeEach(() => { - formService.submit.mockReturnValue(of(userStateResource)); - }); - - it('should call formService submit', () => { - component.submit(); - - expect(formService.submit).toHaveBeenCalled(); - }); - - it('should set submitState$', () => { - component.submit(); - - expect(component.submitStateResource$).toBeObservable(cold('(a|)', { a: userStateResource })); - }); - }); - }); - describe('template', () => { describe('button save', () => { describe('input', () => { @@ -74,16 +51,6 @@ describe('UserFormSaveButtonComponent', () => { expect(getDebugElementFromFixtureByCss(fixture, saveButton).componentInstance.stateResource).toEqual(stateResource); }); }); - - describe('output', () => { - it('should call submit', () => { - component.submit = jest.fn(); - - dispatchEventFromFixture(fixture, saveButton, 'clickEmitter'); - - expect(component.submit).toHaveBeenCalled(); - }); - }); }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts index 941904eb8d..a575d872a1 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.ts @@ -1,10 +1,9 @@ import { User } from '@admin-client/user-shared'; import { StateResource } from '@alfa-client/tech-shared'; import { AsyncPipe } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { ButtonWithSpinnerComponent } from '@ods/component'; import { Observable } from 'rxjs'; -import { UserFormService } from '../user.formservice'; @Component({ selector: 'admin-user-form-save-button', @@ -13,11 +12,5 @@ import { UserFormService } from '../user.formservice'; templateUrl: './user-form-save-button.component.html', }) export class UserFormSaveButtonComponent { - public readonly formService = inject(UserFormService); - - public submitStateResource$: Observable<StateResource<User>>; - - public submit(): void { - this.submitStateResource$ = this.formService.submit(); - } + @Input() submitStateResource$: Observable<StateResource<User>>; } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html index 132dc6ba2c..e147b63cf7 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.html @@ -23,20 +23,22 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<ods-spinner [stateResource]="userStateResource$ | async"> - <div class="max-w-[960px]" data-test-id="user-content"> - <admin-user-form-headline [isPatch]="isPatch" /> - <admin-user-form-data [formGroupParent]="formService.form" [isPatch]="isPatch" [userName]="userName" /> - <admin-user-form-roles [formGroupParent]="formService.form" /> - <admin-user-form-organisations-einheit-list - [formGroupParent]="formService.form" - [formGroupOrganisationsEinheiten]="formService.getOrganisationsEinheitenGroup()" - /> - <div class="mb-6 flex justify-between"> - <admin-user-form-save-button /> - @if (isPatch) { - <admin-delete-open-dialog-button data-test-id="delete-button-container" /> - } +<form [formGroup]="formService.form" (ngSubmit)="submit()"> + <ods-spinner [stateResource]="userStateResource$ | async"> + <div class="max-w-[960px]" data-test-id="user-content"> + <admin-user-form-headline [isPatch]="isPatch" /> + <admin-user-form-data [formGroupParent]="formService.form" [isPatch]="isPatch" [userName]="userName" /> + <admin-user-form-roles [formGroupParent]="formService.form" /> + <admin-user-form-organisations-einheit-list + [formGroupParent]="formService.form" + [formGroupOrganisationsEinheiten]="formService.getOrganisationsEinheitenGroup()" + /> + <div class="mb-6 flex justify-between"> + <admin-user-form-save-button [submitStateResource$]="submitStateResource$"/> + @if (isPatch) { + <admin-delete-open-dialog-button data-test-id="delete-button-container" /> + } + </div> </div> - </div> -</ods-spinner> + </ods-spinner> +</form> \ No newline at end of file diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.spec.ts index bbaa75605a..632bab5c79 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.spec.ts @@ -29,16 +29,18 @@ import { DIALOG_COMPONENT } from '@alfa-client/ui'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { expect } from '@jest/globals'; import { ButtonWithSpinnerComponent, SpinnerComponent } from '@ods/component'; import { cold } from 'jest-marbles'; import { createUser } from 'libs/admin/user-shared/test/user'; import { MockComponent } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { getDataTestIdOf } from '../../../../../tech-shared/test/data-test'; import { UserFormDataComponent } from './user-form-data/user-form-data.component'; import { UserFormHeadlineComponent } from './user-form-headline/user-form-headline.component'; import { UserFormOrganisationsEinheitListComponent } from './user-form-organisations-einheit-list/user-form-organisations-einheit-list.component'; import { UserFormRolesComponent } from './user-form-roles/user-form-roles.component'; +import { UserFormSaveButtonComponent } from './user-form-save-button/user-form-save-button.component'; import { UserFormComponent } from './user-form.component'; import { UserFormService } from './user.formservice'; @@ -60,6 +62,7 @@ describe('UserFormComponent', () => { isInvalid: jest.fn(), form: new FormGroup({}), isPatch: jest.fn(), + submit: jest.fn(), }; await TestBed.configureTestingModule({ @@ -145,6 +148,22 @@ describe('UserFormComponent', () => { expect(component.userName).toBe(userName); }); }); + describe('submit', () => { + it('should call formservice submit', () => { + component.submit(); + + expect(formService.submit).toHaveBeenCalled(); + }); + + it('should set submit state resource', () => { + const stateResource$: Observable<StateResource<User>> = of(createStateResource(createUser())); + formService.submit.mockReturnValue(stateResource$); + + component.submit(); + + expect(component.submitStateResource$).toBe(stateResource$); + }); + }); }); describe('template', () => { @@ -204,23 +223,34 @@ describe('UserFormComponent', () => { notExistsAsHtmlElement(fixture, userContent); }); - }); - describe('admin delete button container', () => { - it('should exist', () => { - component.isPatch = true; + describe('admin save button', () => { + it('should exist with input', () => { + component.submitStateResource$ = of(createStateResource<User>(createUser())); - fixture.detectChanges(); + fixture.detectChanges(); - existsAsHtmlElement(fixture, deleteButtonContainer); + const saveButtonComponent: UserFormSaveButtonComponent = getMockComponent(fixture, UserFormSaveButtonComponent); + expect(saveButtonComponent.submitStateResource$).toBe(component.submitStateResource$); + }); }); - it('should not exist', () => { - component.isPatch = false; + describe('admin delete button container', () => { + it('should exist', () => { + component.isPatch = true; - fixture.detectChanges(); + fixture.detectChanges(); + + existsAsHtmlElement(fixture, deleteButtonContainer); + }); + + it('should not exist', () => { + component.isPatch = false; - notExistsAsHtmlElement(fixture, deleteButtonContainer); + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, deleteButtonContainer); + }); }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts index f60e03692e..9874532168 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form.component.ts @@ -62,6 +62,7 @@ export class UserFormComponent implements OnInit { public userStateResource$: Observable<StateResource<User>>; public isPatch: boolean; public userName: string; + public submitStateResource$: Observable<StateResource<User>>; ngOnInit(): void { this.userStateResource$ = this.formService.get().pipe( @@ -71,4 +72,8 @@ export class UserFormComponent implements OnInit { }), ); } + + public submit(): void { + this.submitStateResource$ = this.formService.submit(); + } } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index b0cb7867c9..d97dee1e2b 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -100,34 +100,37 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } _initForm(): UntypedFormGroup { - return this.formBuilder.group({ - [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.FIRST_NAME)), - [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.LAST_NAME)), - [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, [fieldLengthValidator(UserFormService.USERNAME, 3, 255)]), - [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [ - fieldInvalidValidator(UserFormService.EMAIL, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/), - ]), - [UserFormService.CLIENT_ROLES]: this.formBuilder.group( - { - [UserFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({ - [UserFormService.ADMIN]: new FormControl(false), - [UserFormService.DATENBEAUFTRAGUNG]: new FormControl(false), - }), - [UserFormService.ALFA_GROUP]: this.formBuilder.group({ - [UserFormService.LOESCHEN]: new FormControl(false), - [UserFormService.USER]: new FormControl(false), - [UserFormService.POSTSTELLE]: new FormControl(false), - }), - }, - { - validators: checkBoxGroupsEmptyValidator(UserFormService.CLIENT_ROLES, [ - UserFormService.ADMINISTRATION_GROUP, - UserFormService.ALFA_GROUP, - ]), - }, - ), - [UserFormService.GROUPS]: this.formBuilder.group({}), - }); + return this.formBuilder.group( + { + [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.FIRST_NAME)), + [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.LAST_NAME)), + [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, [fieldLengthValidator(UserFormService.USERNAME, 3, 255)]), + [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [ + fieldInvalidValidator(UserFormService.EMAIL, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/), + ]), + [UserFormService.CLIENT_ROLES]: this.formBuilder.group( + { + [UserFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({ + [UserFormService.ADMIN]: new FormControl(false), + [UserFormService.DATENBEAUFTRAGUNG]: new FormControl(false), + }), + [UserFormService.ALFA_GROUP]: this.formBuilder.group({ + [UserFormService.LOESCHEN]: new FormControl(false), + [UserFormService.USER]: new FormControl(false), + [UserFormService.POSTSTELLE]: new FormControl(false), + }), + }, + { + validators: checkBoxGroupsEmptyValidator(UserFormService.CLIENT_ROLES, [ + UserFormService.ADMINISTRATION_GROUP, + UserFormService.ALFA_GROUP, + ]), + }, + ), + [UserFormService.GROUPS]: this.formBuilder.group({}), + }, + { updateOn: 'submit' }, + ); } _initOrganisationsEinheiten(): Observable<AdminOrganisationsEinheit[]> { -- GitLab From b9b9ad1342da8a7658d63e18aae43eb08458b20a Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 16:12:30 +0200 Subject: [PATCH 04/23] OZG-7974 fix --- .../formcontrol-editor.abstract.component.ts | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index d84fcd376b..29b75f0e5b 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -42,12 +42,16 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue constructor(@Self() @Optional() public control: NgControl | null) { if (this.control) this.control.valueAccessor = this; + this._changesSubscr = this.fieldControl.valueChanges.subscribe((value: unknown) => { + this._fieldControlOnChangeHandler(value); + }); } ngOnInit(): void { - this._changesSubscr = this.fieldControl.valueChanges.subscribe(this._fieldControlOnChangeHandler); if (this.control) { - this._statusSubscr = this.control.statusChanges.subscribe(this.setErrors); + this._statusSubscr = this.control.statusChanges.subscribe(() => { + this.setErrors(); + }); } } @@ -82,24 +86,27 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } setErrors(): void { - if (this.control) { - this.fieldControl.setErrors(this.control.errors); - this._updateInvalidParams(); - } + if (!this.control) return; + + this.fieldControl.setErrors(this.control.errors); + this._updateInvalidParams(); } removeErrors(): void { + if (!this.control) return; + this.fieldControl.setErrors(null); this._updateInvalidParams(); this._clearAllParentErrors(this.control.control); } _clearAllParentErrors(control: AbstractControl): void { - const parent: AbstractControl = control.parent; - if (parent) { - parent.setErrors(null); - this._clearAllParentErrors(parent); - } + const parent: AbstractControl = control?.parent; + + if (!parent) return; + + parent.setErrors(null); + this._clearAllParentErrors(parent); } _updateInvalidParams(): void { -- GitLab From 84483f6939033b0f340e2a340e7da5b414393bfb Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 19:17:09 +0200 Subject: [PATCH 05/23] OZG-7974 form service --- .../lib/user-form/user.formservice.spec.ts | 107 +++++++++++------- .../src/lib/user-form/user.formservice.ts | 51 +++++---- 2 files changed, 98 insertions(+), 60 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index caabe30992..71f4fffde3 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -40,7 +40,7 @@ import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/locale/de'; import { expect } from '@jest/globals'; import { createUser } from 'libs/admin/user-shared/test/user'; -import { Observable, of } from 'rxjs'; +import { Observable, of, Subscription } from 'rxjs'; import { createUrlSegment } from '../../../../../navigation-shared/test/navigation-test-factory'; import { singleCold, singleColdCompleted, singleHot } from '../../../../../tech-shared/test/marbles'; import { createKeycloakHttpErrorResponse } from '../../../../keycloak-shared/src/test/keycloak'; @@ -107,6 +107,32 @@ describe('UserFormService', () => { expect(service).toBeTruthy(); }); + describe('ngOnInit', () => { + beforeEach(() => { + service._initOrganisationsEinheiten = jest.fn().mockReturnValue(of()); + }); + + it('should call initOrganisationsEinheiten', () => { + service.ngOnInit(); + + expect(service._initOrganisationsEinheiten).toHaveBeenCalled(); + }); + + it('should set initOrganisationsEinheiten$', () => { + service.ngOnInit(); + + expect(service._initOrganisationsEinheiten$).toBeDefined(); + }); + + it('should call _disableAlfaCheckboxes', () => { + service._disableAlfaCheckboxes = jest.fn(); + + service.ngOnInit(); + + expect(service._disableAlfaCheckboxes).toHaveBeenCalled(); + }); + }); + describe('build patch config', () => { describe('on matching route', () => { it('should contains id and value for patch indication', () => { @@ -151,26 +177,6 @@ describe('UserFormService', () => { }); }); - describe('listenToAlfaGroupChanges', () => { - it('should call handleAlfaGroupChange on initial change', () => { - service._handleAlfaGroupChange = jest.fn(); - - service.listenToAlfaGroupChanges(); - - expect(service._handleAlfaGroupChange).toHaveBeenCalled(); - }); - - it('should call handleAlfaGroupChange when value of form element changes', fakeAsync(() => { - service._handleAlfaGroupChange = jest.fn(); - - alfaGroup.get(UserFormService.LOESCHEN).setValue(true); - - tick(); - - expect(service._handleAlfaGroupChange).toHaveBeenCalled(); - })); - }); - describe('initOrganisationsEinheiten', () => { beforeEach(() => { service._addOrganisationsEinheitenToForm = jest.fn(); @@ -201,10 +207,6 @@ describe('UserFormService', () => { expect(service._organisationsEinheitToGroupIdMap.get(adminOrganisationsEinheit.name)).toEqual(adminOrganisationsEinheit.id); }); - it('should set initOrganisationsEinheiten$', () => { - expect(service['_initOrganisationsEinheiten$']).toBeDefined(); - }); - it('should not throw any exception on loading state resource', () => { adminOrganisationsEinheitService.getAll.mockReturnValue(of(createLoadingStateResource())); const errorMock: any = mockWindowError(); @@ -230,19 +232,10 @@ describe('UserFormService', () => { service._isAnyChecked = jest.fn().mockReturnValue(true); service._disableUncheckedCheckboxes = jest.fn(); - service._handleAlfaGroupChange(alfaGroup); + service._disableAlfaCheckboxes(); expect(service._disableUncheckedCheckboxes).toHaveBeenCalled(); }); - - it('should call enableAllCheckboxes if not any checkbox is checked', () => { - service._isAnyChecked = jest.fn().mockReturnValue(false); - service._enableAllCheckboxes = jest.fn(); - - service._handleAlfaGroupChange(alfaGroup); - - expect(service._enableAllCheckboxes).toHaveBeenCalled(); - }); }); describe('isAnyChecked', () => { @@ -292,12 +285,40 @@ describe('UserFormService', () => { }); }); - describe('enableAllCheckboxes', () => { + describe('updateAlfaCheckboxes', () => { + it('should set control value', () => { + const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); + + service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + + expect(control.value).toBe(true); + }); + + it('should call disableUncheckedCheckboxes', () => { + service._disableUncheckedCheckboxes = jest.fn(); + + service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + + expect(service._disableUncheckedCheckboxes).toHaveBeenCalledWith( + service.form.get(UserFormService.CLIENT_ROLES).get(UserFormService.ALFA_GROUP), + ); + }); + + it('should call enableAllAlfaCheckboxes if value is false', () => { + service._enableAllAlfaCheckboxes = jest.fn(); + + service.updateAlfaCheckboxes(UserFormService.LOESCHEN, false); + + expect(service._enableAllAlfaCheckboxes).toHaveBeenCalled(); + }); + }); + + describe('enableAllAlfaCheckboxes', () => { it('if control value is true then control should be enabled', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); - const enableSpy = jest.spyOn(control, 'enable'); + const enableSpy: jest.SpyInstance = jest.spyOn(control, 'enable'); - service._enableAllCheckboxes(alfaGroup); + service._enableAllAlfaCheckboxes(); expect(enableSpy).toHaveBeenCalled(); }); @@ -438,17 +459,21 @@ describe('UserFormService', () => { }); describe('ngOnDestroy', () => { - it('should unsubscribe from initOrganisationsEinheiten$', () => { + beforeEach(() => { + service._initOrganisationsEinheiten$ = new Subscription(); service._initOrganisationsEinheiten$.unsubscribe = jest.fn(); + service._alfaGroupChanges = new Subscription(); + service._alfaGroupChanges.unsubscribe = jest.fn(); + }); + + it('should unsubscribe from initOrganisationsEinheiten$', () => { service.ngOnDestroy(); expect(service._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); }); it('should unsubscribe from initOrganisationsEinheiten$', () => { - service._alfaGroupChanges.unsubscribe = jest.fn(); - service.ngOnDestroy(); expect(service._alfaGroupChanges.unsubscribe).toHaveBeenCalled(); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index d97dee1e2b..0e89e82cec 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -43,13 +43,13 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UrlSegment } from '@angular/router'; import { filter, Observable, Subscription, tap } from 'rxjs'; @Injectable() -export class UserFormService extends KeycloakFormService<User> implements OnDestroy { +export class UserFormService extends KeycloakFormService<User> implements OnDestroy, OnInit { public static readonly FIRST_NAME: string = 'firstName'; public static readonly LAST_NAME: string = 'lastName'; public static readonly USERNAME: string = 'username'; @@ -78,9 +78,11 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest private snackBarService: SnackBarService, ) { super(); + } + ngOnInit() { this._initOrganisationsEinheiten$ = this._initOrganisationsEinheiten().subscribe(); - this.listenToAlfaGroupChanges(); + this._disableAlfaCheckboxes(); } _buildPatchConfig(url: UrlSegment[]): PatchConfig { @@ -160,20 +162,11 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest }); } - listenToAlfaGroupChanges(): void { + _disableAlfaCheckboxes(): void { const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); - this._handleAlfaGroupChange(alfaGroup); - this._alfaGroupChanges = alfaGroup.valueChanges.subscribe(() => { - this._handleAlfaGroupChange(alfaGroup); - }); - } - - _handleAlfaGroupChange(group: UntypedFormGroup): void { - const anyChecked: boolean = this._isAnyChecked(group); + const anyChecked: boolean = this._isAnyChecked(alfaGroup); if (anyChecked) { - this._disableUncheckedCheckboxes(group); - } else { - this._enableAllCheckboxes(group); + this._disableUncheckedCheckboxes(alfaGroup); } } @@ -183,16 +176,36 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest _disableUncheckedCheckboxes(alfaGroup: UntypedFormGroup): void { for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { - if (!control.value) control.disable({ emitEvent: false }); + if (!control.value) this.disableCheckbox(control); } } - _enableAllCheckboxes(group: UntypedFormGroup): void { - for (const control of Object.values<AbstractControl>(group.controls)) { - control.enable({ emitEvent: false }); + updateAlfaCheckboxes(formControlName: string, value: boolean) { + const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); + this.setControlValueInGroup(alfaGroup, formControlName, value); + this._disableUncheckedCheckboxes(alfaGroup); + if (!value) this._enableAllAlfaCheckboxes(); + } + + private setControlValueInGroup(group: UntypedFormGroup, formControlName: string, value: boolean): void { + group.get(formControlName).setValue(value, { emitEvent: false }); + } + + _enableAllAlfaCheckboxes() { + const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); + for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { + this.enableCheckbox(control); } } + private enableCheckbox(control: AbstractControl): void { + control.enable({ emitEvent: false }); + } + + private disableCheckbox(control: AbstractControl): void { + if (!control.value) control.disable({ emitEvent: false }); + } + _doSubmit(): Observable<StateResource<User>> { const user: User = this._createUser(); return this._createOrSave(user).pipe( -- GitLab From 1ff51c20b1825da17be161d15417d6995a8d4227 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 3 Apr 2025 19:21:56 +0200 Subject: [PATCH 06/23] OZG-7974 small fix --- .../lib/form/formcontrol-editor.abstract.component.spec.ts | 6 ------ .../src/lib/form/formcontrol-editor.abstract.component.ts | 5 +++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts index 210d995986..cee669970f 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -28,12 +28,6 @@ describe('FormControlEditorAbstractComponent', () => { fixture.detectChanges(); }); - describe('constructor', () => { - it('should set control value accessor', () => { - expect(component.control.valueAccessor).toBe(component); - }); - }); - describe('ng on init', () => { it('should set valueChange subscription', () => { component.ngOnInit(); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 29b75f0e5b..6e07be87f8 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -42,12 +42,13 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue constructor(@Self() @Optional() public control: NgControl | null) { if (this.control) this.control.valueAccessor = this; + } + + ngOnInit(): void { this._changesSubscr = this.fieldControl.valueChanges.subscribe((value: unknown) => { this._fieldControlOnChangeHandler(value); }); - } - ngOnInit(): void { if (this.control) { this._statusSubscr = this.control.statusChanges.subscribe(() => { this.setErrors(); -- GitLab From ef6786bfe175b3a50f99faf42edb2a9cdc929ed0 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Fri, 4 Apr 2025 09:26:09 +0200 Subject: [PATCH 07/23] OZG-7974 checkboxes inputchange --- .../user-form-roles.component.html | 24 ++++++- .../user-form-roles.component.spec.ts | 63 ++++++++++++++++++- .../user-form-roles.component.ts | 8 ++- .../checkbox-editor.component.html | 1 + .../checkbox-editor.component.spec.ts | 17 ++++- .../checkbox-editor.component.ts | 4 +- .../form/checkbox/checkbox.component.spec.ts | 14 +++++ .../lib/form/checkbox/checkbox.component.ts | 9 ++- 8 files changed, 132 insertions(+), 8 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index d3d39a71ce..f5d4c15916 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -35,7 +35,13 @@ <div [formGroupName]="UserFormService.ALFA_GROUP" class="flex flex-1 flex-col gap-2"> <h3 class="text-md block font-medium text-text">Alfa</h3> <div class="flex items-center gap-2"> - <ods-checkbox-editor [formControlName]="UserFormService.LOESCHEN" label="Löschen" inputId="delete" /> + <ods-checkbox-editor + [formControlName]="UserFormService.LOESCHEN" + (inputChange)="updateOtherAlfaCheckboxes(UserFormService.LOESCHEN, $event)" + label="Löschen" + inputId="delete" + data-test-id="checkbox-loeschen" + /> <button data-test-id="loschen-role-info-button" tooltip='Diese Rolle hat dieselben Rechte wie die Rolle "User". Zusätzlich kann "Löschen" Löschanträge aus Alfa bestätigen. ' @@ -44,7 +50,13 @@ </button> </div> <div class="flex items-center gap-2"> - <ods-checkbox-editor [formControlName]="UserFormService.USER" label="User" inputId="user" /> + <ods-checkbox-editor + [formControlName]="UserFormService.USER" + (inputChange)="updateOtherAlfaCheckboxes(UserFormService.USER, $event)" + label="User" + inputId="user" + data-test-id="checkbox-user" + /> <button data-test-id="user-role-info-button" tooltip="Diese Rolle kann alle Vorgänge sehen und bearbeiten, wenn diese seiner Organisationseinheit zugewiesen sind." @@ -53,7 +65,13 @@ </button> </div> <div class="flex items-center gap-2"> - <ods-checkbox-editor [formControlName]="UserFormService.POSTSTELLE" label="Poststelle" inputId="post_office" /> + <ods-checkbox-editor + [formControlName]="UserFormService.POSTSTELLE" + (inputChange)="updateOtherAlfaCheckboxes(UserFormService.POSTSTELLE, $event)" + label="Poststelle" + inputId="post_office" + data-test-id="checkbox-poststelle" + /> <button data-test-id="poststelle-role-info-button" tooltip="Diese Rolle kann alle neu eingegangenen Vorgänge sehen."> <ods-info-icon /> </button> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index 5f21edb7a5..dcb15e84da 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -1,5 +1,5 @@ import { InvalidParam } from '@alfa-client/tech-shared'; -import { existsAsHtmlElement, getElementComponentFromFixtureByCss } from '@alfa-client/test-utils'; +import { existsAsHtmlElement, getElementComponentFromFixtureByCss, mock, Mock, triggerEvent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AbstractControl, FormControl, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { expect } from '@jest/globals'; @@ -19,9 +19,14 @@ describe('UserFormRolesComponent', () => { const validationErrorTestId: string = getDataTestIdOf('rollen-error'); + let formService: Mock<UserFormService>; + beforeEach(async () => { + formService = mock(UserFormService); + await TestBed.configureTestingModule({ imports: [UserFormRolesComponent, ReactiveFormsModule, MockComponent(InfoIconComponent), MockDirective(TooltipDirective)], + providers: [{ provide: UserFormService, useValue: formService }], }).compileComponents(); fixture = TestBed.createComponent(UserFormRolesComponent); @@ -91,6 +96,17 @@ describe('UserFormRolesComponent', () => { control.setErrors(null); }); }); + + describe('update all other alfa checkboxes', () => { + it('should call form service updateAlfaCheckboxes', () => { + const formControlName: string = 'dummy'; + const value: boolean = true; + + component.updateOtherAlfaCheckboxes(formControlName, value); + + expect(formService.updateAlfaCheckboxes).toHaveBeenCalledWith(formControlName, value); + }); + }); }); describe('template', () => { @@ -112,5 +128,50 @@ describe('UserFormRolesComponent', () => { expect(validationErrorComponent.invalidParams).toEqual([invalidParam]); }); }); + + describe('checkbox loeschen', () => { + it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { + component.updateOtherAlfaCheckboxes = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: getDataTestIdOf('checkbox-loeschen'), + name: 'inputChange', + data: true, + }); + + expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.LOESCHEN, true); + }); + }); + + describe('checkbox user', () => { + it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { + component.updateOtherAlfaCheckboxes = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: getDataTestIdOf('checkbox-user'), + name: 'inputChange', + data: true, + }); + + expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.USER, true); + }); + }); + + describe('checkbox poststelle', () => { + it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { + component.updateOtherAlfaCheckboxes = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: getDataTestIdOf('checkbox-poststelle'), + name: 'inputChange', + data: true, + }); + + expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.POSTSTELLE, true); + }); + }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 7a8c901dab..9de8f65744 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -1,6 +1,6 @@ import { generateValidationErrorId, InvalidParam } from '@alfa-client/tech-shared'; import { AsyncPipe } from '@angular/common'; -import { Component, Input, OnInit } from '@angular/core'; +import { Component, inject, Input, OnInit } from '@angular/core'; import { AbstractControl, FormControlStatus, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; import { CheckboxEditorComponent, ValidationErrorComponent } from '@ods/component'; import { InfoIconComponent, TooltipDirective } from '@ods/system'; @@ -22,6 +22,8 @@ import { UserFormService } from '../user.formservice'; templateUrl: './user-form-roles.component.html', }) export class UserFormRolesComponent implements OnInit { + public readonly formService = inject(UserFormService); + @Input() formGroupParent: UntypedFormGroup; public invalidParams$: Observable<InvalidParam[]> = of([]); @@ -37,4 +39,8 @@ export class UserFormRolesComponent implements OnInit { tap((invalidParams: InvalidParam[]) => (this.isValid = isEmpty(invalidParams))), ); } + + updateOtherAlfaCheckboxes(formControlName: string, value: boolean) { + this.formService.updateAlfaCheckboxes(formControlName, value); + } } diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html index 1cc8b07470..cf21692584 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html @@ -30,6 +30,7 @@ [disabled]="control.disabled" [hasError]="hasError" [ariaDescribedBy]="validationErrorId" + (inputChange)="inputChange.emit($event)" > <ods-validation-error error diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.spec.ts index cada7efb7a..1b06d47d14 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.spec.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { getElementFromFixture } from '@alfa-client/test-utils'; +import { getElementFromFixture, triggerEvent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; @@ -91,4 +91,19 @@ describe('CheckboxEditorComponent', () => { }); }); }); + + describe('input change', () => { + it('should emit input change', () => { + component.inputChange.emit = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: 'ods-checkbox', + name: 'inputChange', + data: true, + }); + + expect(component.inputChange.emit).toHaveBeenCalledWith(true); + }); + }); }); diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts index bbb9f693fc..0a098c993e 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { CheckboxComponent } from '@ods/system'; import { FormControlEditorAbstractComponent } from '../formcontrol-editor.abstract.component'; @@ -38,6 +38,8 @@ export class CheckboxEditorComponent extends FormControlEditorAbstractComponent @Input() inputId: string; @Input() label: string; + @Output() inputChange = new EventEmitter<boolean>(); + public readonly validationErrorId: string = generateValidationErrorId(); get hasError(): boolean { diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.spec.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.spec.ts index 508b08e5d8..f37e2213f3 100644 --- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.spec.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { triggerEvent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CheckboxComponent } from './checkbox.component'; @@ -41,4 +42,17 @@ describe('CheckboxComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should emit input change', () => { + component.inputChange.emit = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: 'input', + name: 'change', + data: { target: { checked: true } }, + }); + + expect(component.inputChange.emit).toHaveBeenCalledWith(true); + }); }); diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts index 3b3750bd31..a90820605b 100644 --- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts @@ -23,7 +23,7 @@ */ import { ConvertForDataTestPipe, EMPTY_STRING } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @Component({ @@ -46,6 +46,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; [attr.disabled]="disabled ? true : null" [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor'" [attr.aria-describedby]="ariaDescribedBy" + (change)="inputChangeHandler($event)" /> <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label> <svg @@ -71,4 +72,10 @@ export class CheckboxComponent { @Input() disabled: boolean = false; @Input() hasError: boolean = false; @Input() ariaDescribedBy: string = EMPTY_STRING; + + @Output() inputChange = new EventEmitter<boolean>(); + + inputChangeHandler(event: Event) { + this.inputChange.emit((event.target as HTMLInputElement).checked); + } } -- GitLab From 1630f557b5543a2cf171306123cce6cfd20cf0a1 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Fri, 4 Apr 2025 09:27:26 +0200 Subject: [PATCH 08/23] OZG-7974 small fix --- .../lib/form/formcontrol-editor.abstract.component.spec.ts | 6 +++--- .../src/lib/form/formcontrol-editor.abstract.component.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts index cee669970f..e14da6e4b8 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -92,7 +92,7 @@ describe('FormControlEditorAbstractComponent', () => { it('should remove fieldControl errors', () => { component.fieldControl.setErrors({ fehler: 'this is an validation error' }); - component.removeErrors(); + component._removeErrors(); expect(component.fieldControl.errors).toBeNull(); }); @@ -100,7 +100,7 @@ describe('FormControlEditorAbstractComponent', () => { it('should call update invalid params', () => { component._updateInvalidParams = jest.fn(); - component.removeErrors(); + component._removeErrors(); expect(component._updateInvalidParams).toHaveBeenCalled(); }); @@ -108,7 +108,7 @@ describe('FormControlEditorAbstractComponent', () => { it('should call clear all parent controls', () => { component._clearAllParentErrors = jest.fn(); - component.removeErrors(); + component._removeErrors(); expect(component._clearAllParentErrors).toHaveBeenCalledWith(component.control.control); }); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 6e07be87f8..2a11953641 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -58,7 +58,7 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue _fieldControlOnChangeHandler(value: unknown): void { this.onChange(value); - this.removeErrors(); + this._removeErrors(); } touch(): void { @@ -93,7 +93,7 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue this._updateInvalidParams(); } - removeErrors(): void { + _removeErrors(): void { if (!this.control) return; this.fieldControl.setErrors(null); -- GitLab From 8411739637ef8476f73921a46869ef3df4f7c175 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Fri, 4 Apr 2025 10:40:35 +0200 Subject: [PATCH 09/23] OZG-7974 delayed button --- .../src/lib/keycloak-formservice.spec.ts | 110 ++++-------------- .../src/lib/keycloak-formservice.ts | 26 ++--- .../formcontrol-editor.abstract.component.ts | 4 +- 3 files changed, 36 insertions(+), 104 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index 038187decc..f6dfce08e0 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -21,10 +21,17 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { createEmptyStateResource, createStateResource, EMPTY_STRING, InvalidParam, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared'; +import { + createEmptyStateResource, + createStateResource, + EMPTY_STRING, + InvalidParam, + setInvalidParamValidationError, + StateResource, +} from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms'; +import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/.'; import { createDummy, Dummy } from 'libs/tech-shared/test/dummy'; @@ -213,25 +220,30 @@ describe('KeycloakFormService', () => { beforeEach(() => { service._doSubmit = jest.fn().mockReturnValue(singleHot(dummyStateResource)); - service._processInvalidForm = jest.fn().mockReturnValue(of(createEmptyStateResource())); service._processResponseValidationErrors = jest.fn().mockReturnValue(of(createEmptyStateResource())); service.form.setErrors(null); }); describe('on client validation error', () => { - it('should process invalid form', () => { + it('should return empty state resource with loading first', () => { service.form.setErrors({ dummy: 'dummy error' }); - service.submit().subscribe(); - - expect(service._processInvalidForm).toHaveBeenCalled(); + service.submit().subscribe((stateResource: StateResource<Dummy>) => { + expect(stateResource).toEqual(createEmptyStateResource(true)); + }); }); - it('should return invalid form processing result on invalid form', () => { + it('should return empty state resource withouth loading after delay', fakeAsync(() => { service.form.setErrors({ dummy: 'dummy error' }); - expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource())); - }); + tick(200); + + service.submit().subscribe((stateResource: StateResource<Dummy>) => { + expect(stateResource).toEqual(createEmptyStateResource()); + }); + + flush(); + })); }); it('should call do submit', () => { @@ -265,22 +277,6 @@ describe('KeycloakFormService', () => { }); }); - describe('process invalid form', () => { - beforeEach(() => { - service._showValidationErrorForAllInvalidControls = jest.fn(); - }); - - it('should show validation errors on all invalid controls', () => { - service._processInvalidForm(); - - expect(service._showValidationErrorForAllInvalidControls).toHaveBeenCalledWith(service.form); - }); - - it('should return emit state resource', () => { - expect(service._processInvalidForm()).toBeObservable(singleColdCompleted(createEmptyStateResource())); - }); - }); - describe('process response validation errors', () => { const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse(); @@ -343,66 +339,6 @@ describe('KeycloakFormService', () => { }); }); - describe('show validation errors on all invalid controls', () => { - it('should update value and validity on invalid control', () => { - const control: AbstractControl = new FormControl(); - control.setErrors({ dummy: 'error' }); - const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); - - service._showValidationErrorForAllInvalidControls(control); - - expect(spy).toHaveBeenCalled(); - }); - - it('should update value and validity form invalid control from group', () => { - const control: AbstractControl = new FormControl(); - control.setErrors({ dummy: 'error' }); - const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); - const group: UntypedFormGroup = new FormGroup({ - someControl: control, - }); - - service._showValidationErrorForAllInvalidControls(group); - - expect(spy).toHaveBeenCalledWith(control); - }); - - it('should update value and validity on invalid control from group', () => { - const control: AbstractControl = new FormControl(); - control.setErrors({ dummy: 'error' }); - const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); - const group: UntypedFormGroup = new FormGroup({ - someControl: control, - }); - - service._showValidationErrorForAllInvalidControls(group); - - expect(spy).toHaveBeenCalled(); - }); - - it('should update value and validity form invalid control from array', () => { - const control: AbstractControl = new FormControl(); - control.setErrors({ dummy: 'error' }); - const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); - const array: FormArray = new FormArray([control]); - - service._showValidationErrorForAllInvalidControls(array); - - expect(spy).toHaveBeenCalledWith(control); - }); - - it('should update value and validity on invalid control from group', () => { - const control: AbstractControl = new FormControl(); - control.setErrors({ dummy: 'error' }); - const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); - const array: FormArray = new FormArray([control]); - - service._showValidationErrorForAllInvalidControls(array); - - expect(spy).toHaveBeenCalled(); - }); - }); - describe('set validation errors on controls', () => { it('should set invalid param validation error', () => { const invalidParam: InvalidParam = createInvalidParam(); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index e1856df17c..28fd9f9cf9 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -21,11 +21,17 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { createEmptyStateResource, InvalidParam, isLoaded, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared'; +import { + createEmptyStateResource, + InvalidParam, + isLoaded, + setInvalidParamValidationError, + StateResource, +} from '@alfa-client/tech-shared'; import { inject, Injectable } from '@angular/core'; -import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; -import { catchError, first, Observable, of, tap } from 'rxjs'; +import { catchError, delay, first, Observable, of, startWith, tap } from 'rxjs'; import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; import * as FormUtil from './form.util'; import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model'; @@ -73,16 +79,15 @@ export abstract class KeycloakFormService<T> { public submit(): Observable<StateResource<T>> { if (this.form.invalid) { - return this._processInvalidForm(); + return this.creatDelayedEmptyStateResource(); } return this._doSubmit().pipe( catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)), ); } - _processInvalidForm(): Observable<StateResource<T>> { - this._showValidationErrorForAllInvalidControls(this.form); - return of(createEmptyStateResource<T>()); + private creatDelayedEmptyStateResource(): Observable<StateResource<T>> { + return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); } _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { @@ -97,13 +102,6 @@ export abstract class KeycloakFormService<T> { return of(createEmptyStateResource<T>()); } - _showValidationErrorForAllInvalidControls(control: AbstractControl): void { - if (control.invalid) control.updateValueAndValidity(); - if (control instanceof FormGroup || control instanceof FormArray) { - Object.values(control.controls).forEach((control) => this._showValidationErrorForAllInvalidControls(control)); - } - } - _setValidationErrorsOnControls(invalidParams: InvalidParam[]): void { invalidParams.forEach((invalidParam: InvalidParam) => { setInvalidParamValidationError(this.form, invalidParam); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 2a11953641..3e91b04d68 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -45,9 +45,7 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnInit(): void { - this._changesSubscr = this.fieldControl.valueChanges.subscribe((value: unknown) => { - this._fieldControlOnChangeHandler(value); - }); + this._changesSubscr = this.fieldControl.valueChanges.subscribe(this._fieldControlOnChangeHandler); if (this.control) { this._statusSubscr = this.control.statusChanges.subscribe(() => { -- GitLab From 80214bc0064df3d5d4f84b45298d8f68a1d6c017 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Fri, 4 Apr 2025 10:58:13 +0200 Subject: [PATCH 10/23] OZG-7974 naming --- .../user-form-roles.component.html | 6 +++--- .../user-form-roles.component.spec.ts | 16 ++++++++-------- .../user-form-roles/user-form-roles.component.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index f5d4c15916..ab7ea88705 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -37,7 +37,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.LOESCHEN" - (inputChange)="updateOtherAlfaCheckboxes(UserFormService.LOESCHEN, $event)" + (inputChange)="updateAlfaCheckboxes(UserFormService.LOESCHEN, $event)" label="Löschen" inputId="delete" data-test-id="checkbox-loeschen" @@ -52,7 +52,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.USER" - (inputChange)="updateOtherAlfaCheckboxes(UserFormService.USER, $event)" + (inputChange)="updateAlfaCheckboxes(UserFormService.USER, $event)" label="User" inputId="user" data-test-id="checkbox-user" @@ -67,7 +67,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.POSTSTELLE" - (inputChange)="updateOtherAlfaCheckboxes(UserFormService.POSTSTELLE, $event)" + (inputChange)="updateAlfaCheckboxes(UserFormService.POSTSTELLE, $event)" label="Poststelle" inputId="post_office" data-test-id="checkbox-poststelle" diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index dcb15e84da..a394794c4d 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -97,12 +97,12 @@ describe('UserFormRolesComponent', () => { }); }); - describe('update all other alfa checkboxes', () => { + describe('update alfa checkboxes', () => { it('should call form service updateAlfaCheckboxes', () => { const formControlName: string = 'dummy'; const value: boolean = true; - component.updateOtherAlfaCheckboxes(formControlName, value); + component.updateAlfaCheckboxes(formControlName, value); expect(formService.updateAlfaCheckboxes).toHaveBeenCalledWith(formControlName, value); }); @@ -131,7 +131,7 @@ describe('UserFormRolesComponent', () => { describe('checkbox loeschen', () => { it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateOtherAlfaCheckboxes = jest.fn(); + component.updateAlfaCheckboxes = jest.fn(); triggerEvent({ fixture, @@ -140,13 +140,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.LOESCHEN, true); + expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.LOESCHEN, true); }); }); describe('checkbox user', () => { it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateOtherAlfaCheckboxes = jest.fn(); + component.updateAlfaCheckboxes = jest.fn(); triggerEvent({ fixture, @@ -155,13 +155,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.USER, true); + expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.USER, true); }); }); describe('checkbox poststelle', () => { it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateOtherAlfaCheckboxes = jest.fn(); + component.updateAlfaCheckboxes = jest.fn(); triggerEvent({ fixture, @@ -170,7 +170,7 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateOtherAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.POSTSTELLE, true); + expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.POSTSTELLE, true); }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 9de8f65744..0a0ff53228 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -40,7 +40,7 @@ export class UserFormRolesComponent implements OnInit { ); } - updateOtherAlfaCheckboxes(formControlName: string, value: boolean) { + updateAlfaCheckboxes(formControlName: string, value: boolean) { this.formService.updateAlfaCheckboxes(formControlName, value); } } -- GitLab From e8de98eb4c650b20405dd55b4d5a2a97569fa012 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sun, 6 Apr 2025 16:30:22 +0200 Subject: [PATCH 11/23] OZG-7974 setErrors Checkbox --- .../user-form-roles.component.html | 10 +++- .../user-form-roles.component.spec.ts | 38 +++++++++++++++ .../user-form-roles.component.ts | 4 ++ .../lib/user-form/user.formservice.spec.ts | 48 +++++++++++-------- .../src/lib/user-form/user.formservice.ts | 38 ++++++++------- ...mcontrol-editor.abstract.component.spec.ts | 46 +----------------- .../formcontrol-editor.abstract.component.ts | 14 +----- 7 files changed, 103 insertions(+), 95 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index ab7ea88705..f71da37b73 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -10,7 +10,13 @@ <div [formGroupName]="UserFormService.ADMINISTRATION_GROUP" class="flex flex-1 flex-col gap-2"> <h3 class="text-md block font-medium text-text">Administration</h3> <div class="flex items-center gap-2"> - <ods-checkbox-editor [formControlName]="UserFormService.ADMIN" label="Admin" inputId="admin" /> + <ods-checkbox-editor + [formControlName]="UserFormService.ADMIN" + (inputChange)="removeCheckboxError()" + data-test-id="checkbox-admin" + label="Admin" + inputId="admin" + /> <button data-test-id="admin-role-info-button" tooltip="Diese Rolle kann Funktionen der OZG-Cloud konfigurieren, z.B. Benutzer anlegen, Organisationseinheiten hinzufügen und Rollen zuweisen." @@ -21,6 +27,8 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.DATENBEAUFTRAGUNG" + (inputChange)="removeCheckboxError()" + data-test-id="checkbox-datenbeauftragung" label="Datenbeauftragung" inputId="datenbeauftragung" /> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index a394794c4d..8bd40289a8 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -107,6 +107,14 @@ describe('UserFormRolesComponent', () => { expect(formService.updateAlfaCheckboxes).toHaveBeenCalledWith(formControlName, value); }); }); + + describe('remove checkbox error', () => { + it('should call form service removeCheckboxError', () => { + component.removeCheckboxError(); + + expect(formService.removeCheckboxError).toHaveBeenCalled(); + }); + }); }); describe('template', () => { @@ -129,6 +137,36 @@ describe('UserFormRolesComponent', () => { }); }); + describe('checkbox admin', () => { + it('should call removeCheckboxError on inputChange emit', () => { + component.removeCheckboxError = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: getDataTestIdOf('checkbox-admin'), + name: 'inputChange', + data: true, + }); + + expect(component.removeCheckboxError).toHaveBeenCalled(); + }); + }); + + describe('checkbox datenbeauftragung', () => { + it('should call removeCheckboxError on inputChange emit', () => { + component.removeCheckboxError = jest.fn(); + + triggerEvent({ + fixture, + elementSelector: getDataTestIdOf('checkbox-datenbeauftragung'), + name: 'inputChange', + data: true, + }); + + expect(component.removeCheckboxError).toHaveBeenCalled(); + }); + }); + describe('checkbox loeschen', () => { it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { component.updateAlfaCheckboxes = jest.fn(); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 0a0ff53228..79e41b9b0e 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -43,4 +43,8 @@ export class UserFormRolesComponent implements OnInit { updateAlfaCheckboxes(formControlName: string, value: boolean) { this.formService.updateAlfaCheckboxes(formControlName, value); } + + removeCheckboxError() { + this.formService.removeCheckboxError(); + } } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 71f4fffde3..73e57e188c 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -229,18 +229,18 @@ describe('UserFormService', () => { describe('handleAlfaGroupChange', () => { it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { - service._isAnyChecked = jest.fn().mockReturnValue(true); - service._disableUncheckedCheckboxes = jest.fn(); + service._isAnyAlfaCheckboxChecked = jest.fn().mockReturnValue(true); + service._disableUncheckedAlfaCheckboxes = jest.fn(); service._disableAlfaCheckboxes(); - expect(service._disableUncheckedCheckboxes).toHaveBeenCalled(); + expect(service._disableUncheckedAlfaCheckboxes).toHaveBeenCalled(); }); }); - describe('isAnyChecked', () => { + describe('isAnyAlfaCheckboxChecked', () => { it('should return false if no checkbox is checked', () => { - const result = service._isAnyChecked(alfaGroup); + const result = service._isAnyAlfaCheckboxChecked(); expect(result).toBe(false); }); @@ -248,18 +248,18 @@ describe('UserFormService', () => { it('should return true if any checkbox is checked', () => { alfaGroup.get(UserFormService.LOESCHEN).setValue(true); - const result = service._isAnyChecked(alfaGroup); + const result = service._isAnyAlfaCheckboxChecked(); expect(result).toBe(true); }); }); - describe('disableUncheckedCheckboxes', () => { + describe('disableUncheckedAlfaCheckboxes', () => { it('if control value is false then control should be disabled', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(false); - service._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedAlfaCheckboxes(); expect(control.disabled).toBe(true); }); @@ -268,7 +268,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(true); - service._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedAlfaCheckboxes(); expect(control.disabled).toBe(false); }); @@ -279,7 +279,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(false); - service._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedAlfaCheckboxes(); expect(control.disabled).toBe(true); }); @@ -294,22 +294,30 @@ describe('UserFormService', () => { expect(control.value).toBe(true); }); - it('should call disableUncheckedCheckboxes', () => { - service._disableUncheckedCheckboxes = jest.fn(); + it('should call removeCheckboxError', () => { + service.removeCheckboxError = jest.fn(); service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); - expect(service._disableUncheckedCheckboxes).toHaveBeenCalledWith( - service.form.get(UserFormService.CLIENT_ROLES).get(UserFormService.ALFA_GROUP), - ); + expect(service.removeCheckboxError).toHaveBeenCalled(); + }); + + it('should call disableAlfaCheckboxes', () => { + service._disableAlfaCheckboxes = jest.fn(); + + service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + + expect(service._disableAlfaCheckboxes).toHaveBeenCalled(); }); + }); - it('should call enableAllAlfaCheckboxes if value is false', () => { - service._enableAllAlfaCheckboxes = jest.fn(); + describe('removeCheckboxError', () => { + it('should remove error on clientRoles group', () => { + roleGroup.setErrors({ error: 'Client Roles Error' }); - service.updateAlfaCheckboxes(UserFormService.LOESCHEN, false); + service.removeCheckboxError(); - expect(service._enableAllAlfaCheckboxes).toHaveBeenCalled(); + expect(roleGroup.errors).toBeNull(); }); }); @@ -361,7 +369,7 @@ describe('UserFormService', () => { userService.create.mockReturnValue(of(createEmptyStateResource(true))); const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(service, 'handleOnCreateUserSuccess'); - service.submit().subscribe(); + service._doSubmit().subscribe(); tick(); expect(handleOnCreateUserSuccessSpy).not.toHaveBeenCalled(); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index 0e89e82cec..e7795f858b 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -162,33 +162,37 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest }); } - _disableAlfaCheckboxes(): void { - const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); - const anyChecked: boolean = this._isAnyChecked(alfaGroup); - if (anyChecked) { - this._disableUncheckedCheckboxes(alfaGroup); - } + updateAlfaCheckboxes(formControlName: string, value: boolean) { + this.setControlValueInAlfa(formControlName, value); + this.removeCheckboxError(); + this._disableAlfaCheckboxes(); } - _isAnyChecked(group: UntypedFormGroup): boolean { - return Object.keys(group.controls).some((key) => group.controls[key].value); + private setControlValueInAlfa(formControlName: string, value: boolean): void { + this.getRoleGroup(UserFormService.ALFA_GROUP).get(formControlName).setValue(value, { emitEvent: false }); } - _disableUncheckedCheckboxes(alfaGroup: UntypedFormGroup): void { - for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { - if (!control.value) this.disableCheckbox(control); + removeCheckboxError() { + this.form.get(UserFormService.CLIENT_ROLES).setErrors(null); + } + + _disableAlfaCheckboxes(): void { + if (this._isAnyAlfaCheckboxChecked()) { + this._disableUncheckedAlfaCheckboxes(); + } else { + this._enableAllAlfaCheckboxes(); } } - updateAlfaCheckboxes(formControlName: string, value: boolean) { + _isAnyAlfaCheckboxChecked(): boolean { const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); - this.setControlValueInGroup(alfaGroup, formControlName, value); - this._disableUncheckedCheckboxes(alfaGroup); - if (!value) this._enableAllAlfaCheckboxes(); + return Object.keys(alfaGroup.controls).some((key) => alfaGroup.controls[key].value); } - private setControlValueInGroup(group: UntypedFormGroup, formControlName: string, value: boolean): void { - group.get(formControlName).setValue(value, { emitEvent: false }); + _disableUncheckedAlfaCheckboxes(): void { + for (const control of Object.values<AbstractControl>(this.getRoleGroup(UserFormService.ALFA_GROUP).controls)) { + if (!control.value) this.disableCheckbox(control); + } } _enableAllAlfaCheckboxes() { diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts index e14da6e4b8..c12275820f 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -1,14 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - FormGroup, - FormGroupDirective, - ReactiveFormsModule, - UntypedFormControl, - UntypedFormGroup, - ValidationErrors, -} from '@angular/forms'; +import { FormGroupDirective, ReactiveFormsModule, ValidationErrors } from '@angular/forms'; import { FormControlEditorAbstractComponent } from '@ods/component'; import { MockNgControl } from '../../../test/form/MockNgControl'; @@ -104,43 +97,6 @@ describe('FormControlEditorAbstractComponent', () => { expect(component._updateInvalidParams).toHaveBeenCalled(); }); - - it('should call clear all parent controls', () => { - component._clearAllParentErrors = jest.fn(); - - component._removeErrors(); - - expect(component._clearAllParentErrors).toHaveBeenCalledWith(component.control.control); - }); - }); - - describe('clear all parent errors', () => { - const parentControl: UntypedFormGroup = new FormGroup({ child: new UntypedFormControl() }); - const childControl: UntypedFormControl = <UntypedFormControl>parentControl.get('child'); - - it('should set errors to null on parent control if parent exists', () => { - const setErrorsSpy: jest.SpyInstance = jest.spyOn(parentControl, 'setErrors'); - - component._clearAllParentErrors(childControl); - - expect(setErrorsSpy).toHaveBeenCalledWith(null); - }); - - it('should call clear all parent errors if parent exists', () => { - const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); - - component._clearAllParentErrors(childControl); - - expect(clearAllParentsSpy).toHaveBeenCalledWith(parentControl); - }); - - it('should not call clear all parent errors again if parent does not exist', () => { - const clearAllParentsSpy: jest.SpyInstance = jest.spyOn(component, '_clearAllParentErrors'); - - component._clearAllParentErrors(parentControl); - - expect(clearAllParentsSpy).toHaveBeenCalledTimes(1); - }); }); }); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 3e91b04d68..17f8e71999 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -23,7 +23,7 @@ */ import { InvalidParam } from '@alfa-client/tech-shared'; import { Component, OnDestroy, OnInit, Optional, Self } from '@angular/core'; -import { AbstractControl, ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; +import { ControlValueAccessor, NgControl, UntypedFormControl } from '@angular/forms'; import { Subscription } from 'rxjs'; @Component({ @@ -45,7 +45,7 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnInit(): void { - this._changesSubscr = this.fieldControl.valueChanges.subscribe(this._fieldControlOnChangeHandler); + this._changesSubscr = this.fieldControl.valueChanges.subscribe((value: unknown) => this._fieldControlOnChangeHandler(value)); if (this.control) { this._statusSubscr = this.control.statusChanges.subscribe(() => { @@ -96,16 +96,6 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue this.fieldControl.setErrors(null); this._updateInvalidParams(); - this._clearAllParentErrors(this.control.control); - } - - _clearAllParentErrors(control: AbstractControl): void { - const parent: AbstractControl = control?.parent; - - if (!parent) return; - - parent.setErrors(null); - this._clearAllParentErrors(parent); } _updateInvalidParams(): void { -- GitLab From fa63f7aa6d28892c33e7a3ce34372098e615a2f2 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sun, 6 Apr 2025 17:29:35 +0200 Subject: [PATCH 12/23] OZG-7974 delayed button --- .../src/lib/keycloak-formservice.spec.ts | 20 ++++++------------- .../src/lib/keycloak-formservice.ts | 9 +++------ .../src/lib/resource/resource.util.ts | 5 +++++ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index f6dfce08e0..94f9f9636d 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -30,7 +30,7 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; -import { fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/.'; @@ -225,24 +225,16 @@ describe('KeycloakFormService', () => { }); describe('on client validation error', () => { - it('should return empty state resource with loading first', () => { - service.form.setErrors({ dummy: 'dummy error' }); - - service.submit().subscribe((stateResource: StateResource<Dummy>) => { - expect(stateResource).toEqual(createEmptyStateResource(true)); - }); - }); - it('should return empty state resource withouth loading after delay', fakeAsync(() => { service.form.setErrors({ dummy: 'dummy error' }); - tick(200); - - service.submit().subscribe((stateResource: StateResource<Dummy>) => { - expect(stateResource).toEqual(createEmptyStateResource()); + const results: StateResource<unknown>[] = []; + service.submit().subscribe((value: StateResource<unknown>) => { + results.push(value); }); + tick(200); - flush(); + expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); })); }); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index 28fd9f9cf9..c36e8fa65e 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -22,6 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { + creatDelayedEmptyStateResource, createEmptyStateResource, InvalidParam, isLoaded, @@ -31,7 +32,7 @@ import { import { inject, Injectable } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; -import { catchError, delay, first, Observable, of, startWith, tap } from 'rxjs'; +import { catchError, first, Observable, of, tap } from 'rxjs'; import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; import * as FormUtil from './form.util'; import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model'; @@ -79,17 +80,13 @@ export abstract class KeycloakFormService<T> { public submit(): Observable<StateResource<T>> { if (this.form.invalid) { - return this.creatDelayedEmptyStateResource(); + return creatDelayedEmptyStateResource(); } return this._doSubmit().pipe( catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)), ); } - private creatDelayedEmptyStateResource(): Observable<StateResource<T>> { - return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); - } - _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { try { this._setValidationErrorsOnControls( diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index 201e8100c1..40f4dabc86 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -23,6 +23,7 @@ */ import { getEmbeddedResource, getUrl, hasLink, Resource, ResourceUri } from '@ngxp/rest'; import { isEqual, isNil, isNull } from 'lodash-es'; +import { delay, Observable, of, startWith } from 'rxjs'; import { HttpError } from '../tech.model'; import { encodeUrlForEmbedding, isNotNull } from '../tech.util'; @@ -54,6 +55,10 @@ export function createErrorStateResource<T>(error: HttpError): StateResource<any return { ...createEmptyStateResource<T>(), error, loaded: true }; } +export function creatDelayedEmptyStateResource<T>(): Observable<StateResource<T>> { + return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); +} + export function doIfLoadingRequired(stateResource: StateResource<any>, runable: () => void): boolean { if (isLoadingRequired(stateResource)) { runable(); -- GitLab From beb889959f9b285108adb5c0a2fc9b4003563194 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sun, 6 Apr 2025 18:38:28 +0200 Subject: [PATCH 13/23] OZG-7974 disableChecbkoxes initially --- .../lib/user-form/user.formservice.spec.ts | 65 +++++++++++++++---- .../src/lib/user-form/user.formservice.ts | 22 +++++-- 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 73e57e188c..8629008efe 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -43,6 +43,7 @@ import { createUser } from 'libs/admin/user-shared/test/user'; import { Observable, of, Subscription } from 'rxjs'; import { createUrlSegment } from '../../../../../navigation-shared/test/navigation-test-factory'; import { singleCold, singleColdCompleted, singleHot } from '../../../../../tech-shared/test/marbles'; +import { createDummyResource } from '../../../../../tech-shared/test/resource'; import { createKeycloakHttpErrorResponse } from '../../../../keycloak-shared/src/test/keycloak'; import { createAdminOrganisationsEinheit } from '../../../../organisations-einheit-shared/src/test/organisations-einheit'; import { UserFormService } from './user.formservice'; @@ -107,29 +108,62 @@ describe('UserFormService', () => { expect(service).toBeTruthy(); }); - describe('ngOnInit', () => { + describe('init', () => { beforeEach(() => { service._initOrganisationsEinheiten = jest.fn().mockReturnValue(of()); }); it('should call initOrganisationsEinheiten', () => { - service.ngOnInit(); + service.init(); expect(service._initOrganisationsEinheiten).toHaveBeenCalled(); }); it('should set initOrganisationsEinheiten$', () => { - service.ngOnInit(); + service.init(); expect(service._initOrganisationsEinheiten$).toBeDefined(); }); - it('should call _disableAlfaCheckboxes', () => { - service._disableAlfaCheckboxes = jest.fn(); + it('should call updateAlfaCheckboxStatesOnPatch', () => { + service._updateAlfaCheckboxStatesOnPatch = jest.fn(); - service.ngOnInit(); + service.init(); - expect(service._disableAlfaCheckboxes).toHaveBeenCalled(); + expect(service._updateAlfaCheckboxStatesOnPatch).toHaveBeenCalled(); + }); + + it('should set updateAlfaCheckboxesOnPatch$', () => { + service.init(); + + expect(service._updateAlfaCheckboxesOnPatch$).toBeDefined(); + }); + }); + + describe('updateAlfaCheckboxStatesOnPatch', () => { + it('should call get', () => { + service.get = jest.fn().mockReturnValue(of(createEmptyStateResource())); + service._updateAlfaCheckboxStatesOnPatch(); + + expect(service.get).toHaveBeenCalled(); + }); + + it('should not call updateAlfaCheckboxStates before patch', () => { + service.get = jest.fn().mockReturnValue(of(createEmptyStateResource())); + service._updateAlfaCheckboxStates = jest.fn(); + + service._updateAlfaCheckboxStatesOnPatch().subscribe(); + + expect(service._updateAlfaCheckboxStates).not.toHaveBeenCalled(); + }); + + it('should call updateAlfaCheckboxStates after patch', () => { + service.get = jest.fn().mockReturnValue(of(createStateResource(createDummyResource()))); + service._updateAlfaCheckboxStates = jest.fn(); + + service._updateAlfaCheckboxStatesOnPatch().subscribe(); + + expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); }); }); @@ -227,15 +261,24 @@ describe('UserFormService', () => { }); }); - describe('handleAlfaGroupChange', () => { + describe('updateAlfaCheckboxStates', () => { it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { service._isAnyAlfaCheckboxChecked = jest.fn().mockReturnValue(true); service._disableUncheckedAlfaCheckboxes = jest.fn(); - service._disableAlfaCheckboxes(); + service._updateAlfaCheckboxStates(); expect(service._disableUncheckedAlfaCheckboxes).toHaveBeenCalled(); }); + + it('should call enableAllAlfaCheckboxes if any checkbox is checked', () => { + service._isAnyAlfaCheckboxChecked = jest.fn().mockReturnValue(false); + service._enableAllAlfaCheckboxes = jest.fn(); + + service._updateAlfaCheckboxStates(); + + expect(service._enableAllAlfaCheckboxes).toHaveBeenCalled(); + }); }); describe('isAnyAlfaCheckboxChecked', () => { @@ -303,11 +346,11 @@ describe('UserFormService', () => { }); it('should call disableAlfaCheckboxes', () => { - service._disableAlfaCheckboxes = jest.fn(); + service._updateAlfaCheckboxStates = jest.fn(); service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); - expect(service._disableAlfaCheckboxes).toHaveBeenCalled(); + expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index e7795f858b..b3fddbc337 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -43,13 +43,13 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; -import { Injectable, OnDestroy, OnInit } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UrlSegment } from '@angular/router'; import { filter, Observable, Subscription, tap } from 'rxjs'; @Injectable() -export class UserFormService extends KeycloakFormService<User> implements OnDestroy, OnInit { +export class UserFormService extends KeycloakFormService<User> implements OnDestroy { public static readonly FIRST_NAME: string = 'firstName'; public static readonly LAST_NAME: string = 'lastName'; public static readonly USERNAME: string = 'username'; @@ -66,6 +66,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest public static readonly POSTSTELLE: string = 'VERWALTUNG_POSTSTELLE'; _initOrganisationsEinheiten$: Subscription; + _updateAlfaCheckboxesOnPatch$: Subscription; _alfaGroupChanges: Subscription; _organisationsEinheitToGroupIdMap: Map<string, string> = new Map<string, string>(); @@ -78,11 +79,19 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest private snackBarService: SnackBarService, ) { super(); + this.init(); } - ngOnInit() { + init() { this._initOrganisationsEinheiten$ = this._initOrganisationsEinheiten().subscribe(); - this._disableAlfaCheckboxes(); + this._updateAlfaCheckboxesOnPatch$ = this._updateAlfaCheckboxStatesOnPatch().subscribe(); + } + + _updateAlfaCheckboxStatesOnPatch() { + return this.get().pipe( + filter(isLoaded), + tap(() => this._updateAlfaCheckboxStates()), + ); } _buildPatchConfig(url: UrlSegment[]): PatchConfig { @@ -165,7 +174,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest updateAlfaCheckboxes(formControlName: string, value: boolean) { this.setControlValueInAlfa(formControlName, value); this.removeCheckboxError(); - this._disableAlfaCheckboxes(); + this._updateAlfaCheckboxStates(); } private setControlValueInAlfa(formControlName: string, value: boolean): void { @@ -176,7 +185,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest this.form.get(UserFormService.CLIENT_ROLES).setErrors(null); } - _disableAlfaCheckboxes(): void { + _updateAlfaCheckboxStates(): void { if (this._isAnyAlfaCheckboxChecked()) { this._disableUncheckedAlfaCheckboxes(); } else { @@ -269,6 +278,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } ngOnDestroy(): void { + console.log('destroy'); this._initOrganisationsEinheiten$.unsubscribe(); this._alfaGroupChanges.unsubscribe(); } -- GitLab From fbd959ec1839877bb780d54179d282c32335d2cf Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sun, 6 Apr 2025 19:00:49 +0200 Subject: [PATCH 14/23] OZG-7974 small fix --- .../libs/admin/user/src/lib/user-form/user.formservice.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index b3fddbc337..812d64c1f6 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -67,7 +67,6 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest _initOrganisationsEinheiten$: Subscription; _updateAlfaCheckboxesOnPatch$: Subscription; - _alfaGroupChanges: Subscription; _organisationsEinheitToGroupIdMap: Map<string, string> = new Map<string, string>(); @@ -278,9 +277,8 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } ngOnDestroy(): void { - console.log('destroy'); this._initOrganisationsEinheiten$.unsubscribe(); - this._alfaGroupChanges.unsubscribe(); + this._updateAlfaCheckboxesOnPatch$.unsubscribe(); } public getUserName(): string { -- GitLab From b48f281904da658dbad9fdd6ea988b313a74efc9 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sun, 6 Apr 2025 19:11:27 +0200 Subject: [PATCH 15/23] OZG-7974 small fix --- .../user/src/lib/user-form/user.formservice.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 8629008efe..92c0176558 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -111,6 +111,7 @@ describe('UserFormService', () => { describe('init', () => { beforeEach(() => { service._initOrganisationsEinheiten = jest.fn().mockReturnValue(of()); + service._updateAlfaCheckboxStatesOnPatch = jest.fn().mockReturnValue(of()); }); it('should call initOrganisationsEinheiten', () => { @@ -126,8 +127,6 @@ describe('UserFormService', () => { }); it('should call updateAlfaCheckboxStatesOnPatch', () => { - service._updateAlfaCheckboxStatesOnPatch = jest.fn(); - service.init(); expect(service._updateAlfaCheckboxStatesOnPatch).toHaveBeenCalled(); @@ -514,8 +513,8 @@ describe('UserFormService', () => { service._initOrganisationsEinheiten$ = new Subscription(); service._initOrganisationsEinheiten$.unsubscribe = jest.fn(); - service._alfaGroupChanges = new Subscription(); - service._alfaGroupChanges.unsubscribe = jest.fn(); + service._updateAlfaCheckboxesOnPatch$ = new Subscription(); + service._updateAlfaCheckboxesOnPatch$.unsubscribe = jest.fn(); }); it('should unsubscribe from initOrganisationsEinheiten$', () => { @@ -524,10 +523,10 @@ describe('UserFormService', () => { expect(service._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); }); - it('should unsubscribe from initOrganisationsEinheiten$', () => { + it('should unsubscribe from updateAlfaCheckboxesOnPatch$', () => { service.ngOnDestroy(); - expect(service._alfaGroupChanges.unsubscribe).toHaveBeenCalled(); + expect(service._updateAlfaCheckboxesOnPatch$.unsubscribe).toHaveBeenCalled(); }); }); -- GitLab From 7f7cde279a41ce7ae52e316897e87a35b2a59551 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Mon, 7 Apr 2025 11:03:35 +0200 Subject: [PATCH 16/23] OZG-7974 small fix --- .../src/lib/keycloak-formservice.spec.ts | 104 ++++++++++++++++-- .../src/lib/keycloak-formservice.ts | 18 ++- .../src/lib/resource/resource.util.ts | 2 +- 3 files changed, 111 insertions(+), 13 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index 94f9f9636d..c2afe9895a 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -31,7 +31,7 @@ import { } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/.'; import { createDummy, Dummy } from 'libs/tech-shared/test/dummy'; @@ -220,22 +220,25 @@ describe('KeycloakFormService', () => { beforeEach(() => { service._doSubmit = jest.fn().mockReturnValue(singleHot(dummyStateResource)); + service._processInvalidForm = jest.fn().mockReturnValue(of(createEmptyStateResource())); service._processResponseValidationErrors = jest.fn().mockReturnValue(of(createEmptyStateResource())); service.form.setErrors(null); }); describe('on client validation error', () => { - it('should return empty state resource withouth loading after delay', fakeAsync(() => { + it('should process invalid form', () => { service.form.setErrors({ dummy: 'dummy error' }); - const results: StateResource<unknown>[] = []; - service.submit().subscribe((value: StateResource<unknown>) => { - results.push(value); - }); - tick(200); + service.submit().subscribe(); + + expect(service._processInvalidForm).toHaveBeenCalled(); + }); - expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); - })); + it('should return invalid form processing result on invalid form', () => { + service.form.setErrors({ dummy: 'dummy error' }); + + expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource())); + }); }); it('should call do submit', () => { @@ -269,6 +272,29 @@ describe('KeycloakFormService', () => { }); }); + describe('process invalid form', () => { + beforeEach(() => { + service._showValidationErrorForAllInvalidControls = jest.fn(); + }); + + it('should show validation errors on all invalid controls', () => { + service._processInvalidForm(); + + expect(service._showValidationErrorForAllInvalidControls).toHaveBeenCalledWith(service.form); + }); + + it('should return delayed empty state resource', fakeAsync(() => { + const results: StateResource<unknown>[] = []; + + service._processInvalidForm().subscribe((value: StateResource<unknown>) => { + results.push(value); + }); + tick(200); + + expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); + })); + }); + describe('process response validation errors', () => { const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse(); @@ -331,6 +357,66 @@ describe('KeycloakFormService', () => { }); }); + describe('show validation errors on all invalid controls', () => { + it('should update value and validity on invalid control', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + + service._showValidationErrorForAllInvalidControls(control); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from array', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalled(); + }); + }); + describe('set validation errors on controls', () => { it('should set invalid param validation error', () => { const invalidParam: InvalidParam = createInvalidParam(); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index c36e8fa65e..0b10e2acef 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { - creatDelayedEmptyStateResource, + creatDelayedEmptyStateResourceObservable, createEmptyStateResource, InvalidParam, isLoaded, @@ -30,7 +30,7 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { inject, Injectable } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { catchError, first, Observable, of, tap } from 'rxjs'; import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; @@ -80,13 +80,18 @@ export abstract class KeycloakFormService<T> { public submit(): Observable<StateResource<T>> { if (this.form.invalid) { - return creatDelayedEmptyStateResource(); + return this._processInvalidForm(); } return this._doSubmit().pipe( catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)), ); } + _processInvalidForm(): Observable<StateResource<T>> { + this._showValidationErrorForAllInvalidControls(this.form); + return creatDelayedEmptyStateResourceObservable<T>(); + } + _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { try { this._setValidationErrorsOnControls( @@ -99,6 +104,13 @@ export abstract class KeycloakFormService<T> { return of(createEmptyStateResource<T>()); } + _showValidationErrorForAllInvalidControls(control: AbstractControl): void { + if (control.invalid) control.updateValueAndValidity(); + if (control instanceof FormGroup || control instanceof FormArray) { + Object.values(control.controls).forEach((control) => this._showValidationErrorForAllInvalidControls(control)); + } + } + _setValidationErrorsOnControls(invalidParams: InvalidParam[]): void { invalidParams.forEach((invalidParam: InvalidParam) => { setInvalidParamValidationError(this.form, invalidParam); diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index 40f4dabc86..d2b4e27d56 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -55,7 +55,7 @@ export function createErrorStateResource<T>(error: HttpError): StateResource<any return { ...createEmptyStateResource<T>(), error, loaded: true }; } -export function creatDelayedEmptyStateResource<T>(): Observable<StateResource<T>> { +export function creatDelayedEmptyStateResourceObservable<T>(): Observable<StateResource<T>> { return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); } -- GitLab From 978739c14106087f344d8dc7aef414cb53393db3 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 10 Apr 2025 15:46:10 +0200 Subject: [PATCH 17/23] OZG-7974 cr anmerkungen --- .../src/lib/keycloak-formservice.spec.ts | 6 +- .../user-form-roles.component.html | 10 +- .../user-form-roles.component.spec.ts | 38 +++---- .../user-form-roles.component.ts | 4 +- .../lib/user-form/user.formservice.spec.ts | 104 ++++-------------- .../src/lib/user-form/user.formservice.ts | 29 ++--- 6 files changed, 57 insertions(+), 134 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index c2afe9895a..6e71337e1f 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -284,14 +284,14 @@ describe('KeycloakFormService', () => { }); it('should return delayed empty state resource', fakeAsync(() => { - const results: StateResource<unknown>[] = []; + const expectedEmits: StateResource<unknown>[] = []; service._processInvalidForm().subscribe((value: StateResource<unknown>) => { - results.push(value); + expectedEmits.push(value); }); tick(200); - expect(results).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); + expect(expectedEmits).toEqual([createEmptyStateResource(true), createEmptyStateResource()]); })); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index f71da37b73..29de0bc1bc 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -12,7 +12,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.ADMIN" - (inputChange)="removeCheckboxError()" + (inputChange)="handleAdminRoleChange()" data-test-id="checkbox-admin" label="Admin" inputId="admin" @@ -27,7 +27,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.DATENBEAUFTRAGUNG" - (inputChange)="removeCheckboxError()" + (inputChange)="handleAdminRoleChange()" data-test-id="checkbox-datenbeauftragung" label="Datenbeauftragung" inputId="datenbeauftragung" @@ -45,7 +45,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.LOESCHEN" - (inputChange)="updateAlfaCheckboxes(UserFormService.LOESCHEN, $event)" + (inputChange)="handleAlfaRoleChange(UserFormService.LOESCHEN, $event)" label="Löschen" inputId="delete" data-test-id="checkbox-loeschen" @@ -60,7 +60,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.USER" - (inputChange)="updateAlfaCheckboxes(UserFormService.USER, $event)" + (inputChange)="handleAlfaRoleChange(UserFormService.USER, $event)" label="User" inputId="user" data-test-id="checkbox-user" @@ -75,7 +75,7 @@ <div class="flex items-center gap-2"> <ods-checkbox-editor [formControlName]="UserFormService.POSTSTELLE" - (inputChange)="updateAlfaCheckboxes(UserFormService.POSTSTELLE, $event)" + (inputChange)="handleAlfaRoleChange(UserFormService.POSTSTELLE, $event)" label="Poststelle" inputId="post_office" data-test-id="checkbox-poststelle" diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index 8bd40289a8..d657690baa 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -97,20 +97,20 @@ describe('UserFormRolesComponent', () => { }); }); - describe('update alfa checkboxes', () => { + describe('handle alfa role change', () => { it('should call form service updateAlfaCheckboxes', () => { const formControlName: string = 'dummy'; const value: boolean = true; - component.updateAlfaCheckboxes(formControlName, value); + component.handleAlfaRoleChange(formControlName, value); expect(formService.updateAlfaCheckboxes).toHaveBeenCalledWith(formControlName, value); }); }); - describe('remove checkbox error', () => { + describe('handle admin role change', () => { it('should call form service removeCheckboxError', () => { - component.removeCheckboxError(); + component.handleAdminRoleChange(); expect(formService.removeCheckboxError).toHaveBeenCalled(); }); @@ -138,8 +138,8 @@ describe('UserFormRolesComponent', () => { }); describe('checkbox admin', () => { - it('should call removeCheckboxError on inputChange emit', () => { - component.removeCheckboxError = jest.fn(); + it('should call handleAdminRoleChange on inputChange emit', () => { + component.handleAdminRoleChange = jest.fn(); triggerEvent({ fixture, @@ -148,13 +148,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.removeCheckboxError).toHaveBeenCalled(); + expect(component.handleAdminRoleChange).toHaveBeenCalled(); }); }); describe('checkbox datenbeauftragung', () => { - it('should call removeCheckboxError on inputChange emit', () => { - component.removeCheckboxError = jest.fn(); + it('should call handleAdminRoleChange on inputChange emit', () => { + component.handleAdminRoleChange = jest.fn(); triggerEvent({ fixture, @@ -163,13 +163,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.removeCheckboxError).toHaveBeenCalled(); + expect(component.handleAdminRoleChange).toHaveBeenCalled(); }); }); describe('checkbox loeschen', () => { - it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateAlfaCheckboxes = jest.fn(); + it('should call handleAlfaRoleChange on inputChange emit', () => { + component.handleAlfaRoleChange = jest.fn(); triggerEvent({ fixture, @@ -178,13 +178,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.LOESCHEN, true); + expect(component.handleAlfaRoleChange).toHaveBeenCalledWith(UserFormService.LOESCHEN, true); }); }); describe('checkbox user', () => { - it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateAlfaCheckboxes = jest.fn(); + it('should call handleAlfaRoleChange on inputChange emit', () => { + component.handleAlfaRoleChange = jest.fn(); triggerEvent({ fixture, @@ -193,13 +193,13 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.USER, true); + expect(component.handleAlfaRoleChange).toHaveBeenCalledWith(UserFormService.USER, true); }); }); describe('checkbox poststelle', () => { - it('should call updateOtherAlfaCheckboxes on inputChange emit', () => { - component.updateAlfaCheckboxes = jest.fn(); + it('should call handleAlfaRoleChange on inputChange emit', () => { + component.handleAlfaRoleChange = jest.fn(); triggerEvent({ fixture, @@ -208,7 +208,7 @@ describe('UserFormRolesComponent', () => { data: true, }); - expect(component.updateAlfaCheckboxes).toHaveBeenCalledWith(UserFormService.POSTSTELLE, true); + expect(component.handleAlfaRoleChange).toHaveBeenCalledWith(UserFormService.POSTSTELLE, true); }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 79e41b9b0e..9d3288e4c0 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -40,11 +40,11 @@ export class UserFormRolesComponent implements OnInit { ); } - updateAlfaCheckboxes(formControlName: string, value: boolean) { + public handleAlfaRoleChange(formControlName: string, value: boolean) { this.formService.updateAlfaCheckboxes(formControlName, value); } - removeCheckboxError() { + public handleAdminRoleChange() { this.formService.removeCheckboxError(); } } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 92c0176558..f4eece3f17 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -40,10 +40,9 @@ import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/locale/de'; import { expect } from '@jest/globals'; import { createUser } from 'libs/admin/user-shared/test/user'; -import { Observable, of, Subscription } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { createUrlSegment } from '../../../../../navigation-shared/test/navigation-test-factory'; -import { singleCold, singleColdCompleted, singleHot } from '../../../../../tech-shared/test/marbles'; -import { createDummyResource } from '../../../../../tech-shared/test/resource'; +import { singleCold, singleColdCompleted } from '../../../../../tech-shared/test/marbles'; import { createKeycloakHttpErrorResponse } from '../../../../keycloak-shared/src/test/keycloak'; import { createAdminOrganisationsEinheit } from '../../../../organisations-einheit-shared/src/test/organisations-einheit'; import { UserFormService } from './user.formservice'; @@ -108,64 +107,6 @@ describe('UserFormService', () => { expect(service).toBeTruthy(); }); - describe('init', () => { - beforeEach(() => { - service._initOrganisationsEinheiten = jest.fn().mockReturnValue(of()); - service._updateAlfaCheckboxStatesOnPatch = jest.fn().mockReturnValue(of()); - }); - - it('should call initOrganisationsEinheiten', () => { - service.init(); - - expect(service._initOrganisationsEinheiten).toHaveBeenCalled(); - }); - - it('should set initOrganisationsEinheiten$', () => { - service.init(); - - expect(service._initOrganisationsEinheiten$).toBeDefined(); - }); - - it('should call updateAlfaCheckboxStatesOnPatch', () => { - service.init(); - - expect(service._updateAlfaCheckboxStatesOnPatch).toHaveBeenCalled(); - }); - - it('should set updateAlfaCheckboxesOnPatch$', () => { - service.init(); - - expect(service._updateAlfaCheckboxesOnPatch$).toBeDefined(); - }); - }); - - describe('updateAlfaCheckboxStatesOnPatch', () => { - it('should call get', () => { - service.get = jest.fn().mockReturnValue(of(createEmptyStateResource())); - service._updateAlfaCheckboxStatesOnPatch(); - - expect(service.get).toHaveBeenCalled(); - }); - - it('should not call updateAlfaCheckboxStates before patch', () => { - service.get = jest.fn().mockReturnValue(of(createEmptyStateResource())); - service._updateAlfaCheckboxStates = jest.fn(); - - service._updateAlfaCheckboxStatesOnPatch().subscribe(); - - expect(service._updateAlfaCheckboxStates).not.toHaveBeenCalled(); - }); - - it('should call updateAlfaCheckboxStates after patch', () => { - service.get = jest.fn().mockReturnValue(of(createStateResource(createDummyResource()))); - service._updateAlfaCheckboxStates = jest.fn(); - - service._updateAlfaCheckboxStatesOnPatch().subscribe(); - - expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); - }); - }); - describe('build patch config', () => { describe('on matching route', () => { it('should contains id and value for patch indication', () => { @@ -194,7 +135,8 @@ describe('UserFormService', () => { const loadedUser: StateResource<User> = createStateResource(createUser()); beforeEach(() => { - userService.getUserById.mockReturnValue(singleHot(loadedUser)); + userService.getUserById.mockReturnValue(singleCold(loadedUser)); + service._updateAlfaCheckboxStates = jest.fn(); }); it('should call service to get user by id', () => { @@ -208,6 +150,22 @@ describe('UserFormService', () => { expect(response).toBeObservable(singleCold(loadedUser)); }); + + it('should update alfa roles after user is loaded', () => { + userService.getUserById.mockReturnValue(of(loadedUser)); + + service._load(id).subscribe(); + + expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); + }); + + it('should not update alfa roles if user is not loaded', () => { + userService.getUserById.mockReturnValue(of(createEmptyStateResource())); + + service._load(id).subscribe(); + + expect(service._updateAlfaCheckboxStates).not.toHaveBeenCalled(); + }); }); describe('initOrganisationsEinheiten', () => { @@ -508,28 +466,6 @@ describe('UserFormService', () => { }); }); - describe('ngOnDestroy', () => { - beforeEach(() => { - service._initOrganisationsEinheiten$ = new Subscription(); - service._initOrganisationsEinheiten$.unsubscribe = jest.fn(); - - service._updateAlfaCheckboxesOnPatch$ = new Subscription(); - service._updateAlfaCheckboxesOnPatch$.unsubscribe = jest.fn(); - }); - - it('should unsubscribe from initOrganisationsEinheiten$', () => { - service.ngOnDestroy(); - - expect(service._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); - }); - - it('should unsubscribe from updateAlfaCheckboxesOnPatch$', () => { - service.ngOnDestroy(); - - expect(service._updateAlfaCheckboxesOnPatch$.unsubscribe).toHaveBeenCalled(); - }); - }); - describe('get userName', () => { it('should return form control value of userName', () => { service.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index 812d64c1f6..e083adaeac 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -43,13 +43,13 @@ import { StateResource, } from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; -import { Injectable, OnDestroy } from '@angular/core'; +import { Injectable } from '@angular/core'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UrlSegment } from '@angular/router'; -import { filter, Observable, Subscription, tap } from 'rxjs'; +import { filter, Observable, take, tap } from 'rxjs'; @Injectable() -export class UserFormService extends KeycloakFormService<User> implements OnDestroy { +export class UserFormService extends KeycloakFormService<User> { public static readonly FIRST_NAME: string = 'firstName'; public static readonly LAST_NAME: string = 'lastName'; public static readonly USERNAME: string = 'username'; @@ -65,9 +65,6 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest public static readonly USER: string = 'VERWALTUNG_USER'; public static readonly POSTSTELLE: string = 'VERWALTUNG_POSTSTELLE'; - _initOrganisationsEinheiten$: Subscription; - _updateAlfaCheckboxesOnPatch$: Subscription; - _organisationsEinheitToGroupIdMap: Map<string, string> = new Map<string, string>(); constructor( @@ -82,15 +79,7 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } init() { - this._initOrganisationsEinheiten$ = this._initOrganisationsEinheiten().subscribe(); - this._updateAlfaCheckboxesOnPatch$ = this._updateAlfaCheckboxStatesOnPatch().subscribe(); - } - - _updateAlfaCheckboxStatesOnPatch() { - return this.get().pipe( - filter(isLoaded), - tap(() => this._updateAlfaCheckboxStates()), - ); + this._initOrganisationsEinheiten().pipe(take(1)).subscribe(); } _buildPatchConfig(url: UrlSegment[]): PatchConfig { @@ -106,7 +95,10 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } _load(id: string): Observable<StateResource<User>> { - return this.userService.getUserById(id); + return this.userService.getUserById(id).pipe( + filter(isLoaded), + tap(() => this._updateAlfaCheckboxStates()), + ); } _initForm(): UntypedFormGroup { @@ -276,11 +268,6 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest return <UntypedFormGroup>this.form.get(UserFormService.CLIENT_ROLES).get(roleGroup); } - ngOnDestroy(): void { - this._initOrganisationsEinheiten$.unsubscribe(); - this._updateAlfaCheckboxesOnPatch$.unsubscribe(); - } - public getUserName(): string { return this.form.get(UserFormService.USERNAME).value; } -- GitLab From e31c6b27776cbf7c857695cae49bd8ca2b4a91d8 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 10 Apr 2025 16:40:51 +0200 Subject: [PATCH 18/23] OZG-7974 cr anmerkungen --- .../user-form-roles.component.spec.ts | 8 +- .../user-form-roles.component.ts | 4 +- .../user-form-save-button.component.html | 1 + .../lib/user-form/user.formservice.spec.ts | 91 ++++++++----------- .../src/lib/user-form/user.formservice.ts | 40 ++++---- .../button-with-spinner.component.ts | 2 + .../src/lib/button/button.component.ts | 3 +- 7 files changed, 71 insertions(+), 78 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index d657690baa..703a699d97 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -98,21 +98,21 @@ describe('UserFormRolesComponent', () => { }); describe('handle alfa role change', () => { - it('should call form service updateAlfaCheckboxes', () => { + it('should call form service changeAlfaRole', () => { const formControlName: string = 'dummy'; const value: boolean = true; component.handleAlfaRoleChange(formControlName, value); - expect(formService.updateAlfaCheckboxes).toHaveBeenCalledWith(formControlName, value); + expect(formService.changeAlfaRole).toHaveBeenCalledWith(formControlName, value); }); }); describe('handle admin role change', () => { - it('should call form service removeCheckboxError', () => { + it('should call form service removeClientRolesValidationErrors', () => { component.handleAdminRoleChange(); - expect(formService.removeCheckboxError).toHaveBeenCalled(); + expect(formService.removeClientRolesValidationErrors).toHaveBeenCalled(); }); }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index 9d3288e4c0..04bdc68434 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -41,10 +41,10 @@ export class UserFormRolesComponent implements OnInit { } public handleAlfaRoleChange(formControlName: string, value: boolean) { - this.formService.updateAlfaCheckboxes(formControlName, value); + this.formService.changeAlfaRole(formControlName, value); } public handleAdminRoleChange() { - this.formService.removeCheckboxError(); + this.formService.removeClientRolesValidationErrors(); } } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html index c38b3871bc..40841d6b9b 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-save-button/user-form-save-button.component.html @@ -2,4 +2,5 @@ [stateResource]="submitStateResource$ | async" text="Speichern" dataTestId="save-button" + type="submit" /> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index f4eece3f17..71ea6569bd 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -136,7 +136,7 @@ describe('UserFormService', () => { beforeEach(() => { userService.getUserById.mockReturnValue(singleCold(loadedUser)); - service._updateAlfaCheckboxStates = jest.fn(); + service._updateAlfaRoleStates = jest.fn(); }); it('should call service to get user by id', () => { @@ -151,20 +151,20 @@ describe('UserFormService', () => { expect(response).toBeObservable(singleCold(loadedUser)); }); - it('should update alfa roles after user is loaded', () => { + it('should update alfa role states after user is loaded', () => { userService.getUserById.mockReturnValue(of(loadedUser)); service._load(id).subscribe(); - expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); + expect(service._updateAlfaRoleStates).toHaveBeenCalled(); }); - it('should not update alfa roles if user is not loaded', () => { + it('should not update alfa role states if user is not loaded', () => { userService.getUserById.mockReturnValue(of(createEmptyStateResource())); service._load(id).subscribe(); - expect(service._updateAlfaCheckboxStates).not.toHaveBeenCalled(); + expect(service._updateAlfaRoleStates).not.toHaveBeenCalled(); }); }); @@ -218,115 +218,104 @@ describe('UserFormService', () => { }); }); - describe('updateAlfaCheckboxStates', () => { - it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { - service._isAnyAlfaCheckboxChecked = jest.fn().mockReturnValue(true); - service._disableUncheckedAlfaCheckboxes = jest.fn(); + describe('updateAlfaRoleStates', () => { + it('should disable alfa roles if no role is assigned', () => { + service._isAnyAlfaRoleAssigned = jest.fn().mockReturnValue(true); + service._disableAlfaRoles = jest.fn(); - service._updateAlfaCheckboxStates(); + service._updateAlfaRoleStates(); - expect(service._disableUncheckedAlfaCheckboxes).toHaveBeenCalled(); + expect(service._disableAlfaRoles).toHaveBeenCalled(); }); - it('should call enableAllAlfaCheckboxes if any checkbox is checked', () => { - service._isAnyAlfaCheckboxChecked = jest.fn().mockReturnValue(false); - service._enableAllAlfaCheckboxes = jest.fn(); + it('should enable alfa roles if any role is assigned', () => { + service._isAnyAlfaRoleAssigned = jest.fn().mockReturnValue(false); + service._enableAlfaRoles = jest.fn(); - service._updateAlfaCheckboxStates(); + service._updateAlfaRoleStates(); - expect(service._enableAllAlfaCheckboxes).toHaveBeenCalled(); + expect(service._enableAlfaRoles).toHaveBeenCalled(); }); }); - describe('isAnyAlfaCheckboxChecked', () => { - it('should return false if no checkbox is checked', () => { - const result = service._isAnyAlfaCheckboxChecked(); + describe('isAnyAlfaRoleAssigned', () => { + it('should return false if no role is assigned', () => { + const result = service._isAnyAlfaRoleAssigned(); expect(result).toBe(false); }); - it('should return true if any checkbox is checked', () => { + it('should return true if any role is assigned', () => { alfaGroup.get(UserFormService.LOESCHEN).setValue(true); - const result = service._isAnyAlfaCheckboxChecked(); + const result = service._isAnyAlfaRoleAssigned(); expect(result).toBe(true); }); }); - describe('disableUncheckedAlfaCheckboxes', () => { - it('if control value is false then control should be disabled', () => { + describe('disableAlfaRoles', () => { + it('if role is not assigned it should be disabled', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(false); - service._disableUncheckedAlfaCheckboxes(); + service._disableAlfaRoles(); expect(control.disabled).toBe(true); }); - it('if control value is true then control should NOT be disabled', () => { + it('if role is assigned then it should NOT be disabled', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(true); - service._disableUncheckedAlfaCheckboxes(); + service._disableAlfaRoles(); expect(control.disabled).toBe(false); }); }); - describe('updateCheckboxStates', () => { - it('if control value is false then control should be disabled', () => { - const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); - control.setValue(false); - - service._disableUncheckedAlfaCheckboxes(); - - expect(control.disabled).toBe(true); - }); - }); - - describe('updateAlfaCheckboxes', () => { + describe('changeAlfaRole', () => { it('should set control value', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); - service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + service.changeAlfaRole(UserFormService.LOESCHEN, true); expect(control.value).toBe(true); }); - it('should call removeCheckboxError', () => { - service.removeCheckboxError = jest.fn(); + it('should call removeClientRolesValidationErrors', () => { + service.removeClientRolesValidationErrors = jest.fn(); - service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + service.changeAlfaRole(UserFormService.LOESCHEN, true); - expect(service.removeCheckboxError).toHaveBeenCalled(); + expect(service.removeClientRolesValidationErrors).toHaveBeenCalled(); }); - it('should call disableAlfaCheckboxes', () => { - service._updateAlfaCheckboxStates = jest.fn(); + it('should call updateAlfaRoleStates', () => { + service._updateAlfaRoleStates = jest.fn(); - service.updateAlfaCheckboxes(UserFormService.LOESCHEN, true); + service.changeAlfaRole(UserFormService.LOESCHEN, true); - expect(service._updateAlfaCheckboxStates).toHaveBeenCalled(); + expect(service._updateAlfaRoleStates).toHaveBeenCalled(); }); }); - describe('removeCheckboxError', () => { + describe('removeClientRolesValidationErrors', () => { it('should remove error on clientRoles group', () => { roleGroup.setErrors({ error: 'Client Roles Error' }); - service.removeCheckboxError(); + service.removeClientRolesValidationErrors(); expect(roleGroup.errors).toBeNull(); }); }); - describe('enableAllAlfaCheckboxes', () => { + describe('enableAlfaRoles', () => { it('if control value is true then control should be enabled', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); const enableSpy: jest.SpyInstance = jest.spyOn(control, 'enable'); - service._enableAllAlfaCheckboxes(); + service._enableAlfaRoles(); expect(enableSpy).toHaveBeenCalled(); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index e083adaeac..2e717f6912 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -97,7 +97,7 @@ export class UserFormService extends KeycloakFormService<User> { _load(id: string): Observable<StateResource<User>> { return this.userService.getUserById(id).pipe( filter(isLoaded), - tap(() => this._updateAlfaCheckboxStates()), + tap(() => this._updateAlfaRoleStates()), ); } @@ -162,52 +162,52 @@ export class UserFormService extends KeycloakFormService<User> { }); } - updateAlfaCheckboxes(formControlName: string, value: boolean) { - this.setControlValueInAlfa(formControlName, value); - this.removeCheckboxError(); - this._updateAlfaCheckboxStates(); + public changeAlfaRole(formControlName: string, value: boolean) { + this.setAlfaRole(formControlName, value); + this.removeClientRolesValidationErrors(); + this._updateAlfaRoleStates(); } - private setControlValueInAlfa(formControlName: string, value: boolean): void { + private setAlfaRole(formControlName: string, value: boolean): void { this.getRoleGroup(UserFormService.ALFA_GROUP).get(formControlName).setValue(value, { emitEvent: false }); } - removeCheckboxError() { + public removeClientRolesValidationErrors() { this.form.get(UserFormService.CLIENT_ROLES).setErrors(null); } - _updateAlfaCheckboxStates(): void { - if (this._isAnyAlfaCheckboxChecked()) { - this._disableUncheckedAlfaCheckboxes(); + _updateAlfaRoleStates(): void { + if (this._isAnyAlfaRoleAssigned()) { + this._disableAlfaRoles(); } else { - this._enableAllAlfaCheckboxes(); + this._enableAlfaRoles(); } } - _isAnyAlfaCheckboxChecked(): boolean { + _isAnyAlfaRoleAssigned(): boolean { const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); return Object.keys(alfaGroup.controls).some((key) => alfaGroup.controls[key].value); } - _disableUncheckedAlfaCheckboxes(): void { + _disableAlfaRoles(): void { for (const control of Object.values<AbstractControl>(this.getRoleGroup(UserFormService.ALFA_GROUP).controls)) { - if (!control.value) this.disableCheckbox(control); + if (!control.value) this.disableControl(control); } } - _enableAllAlfaCheckboxes() { + _enableAlfaRoles() { const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserFormService.ALFA_GROUP); for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { - this.enableCheckbox(control); + this.enableControl(control); } } - private enableCheckbox(control: AbstractControl): void { - control.enable({ emitEvent: false }); + private enableControl(control: AbstractControl): void { + control.enable({ onlySelf: true }); } - private disableCheckbox(control: AbstractControl): void { - if (!control.value) control.disable({ emitEvent: false }); + private disableControl(control: AbstractControl): void { + if (!control.value) control.disable({ onlySelf: true }); } _doSubmit(): Observable<StateResource<User>> { diff --git a/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts b/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts index 79e617880c..f7a4b9f109 100644 --- a/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts +++ b/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts @@ -42,6 +42,7 @@ type ButtonVariants = VariantProps<typeof buttonVariants>; [text]="text" [variant]="variant" [size]="size" + [type]="type" [dataTestId]="dataTestId" [isLoading]="isLoading" [disabled]="disabled" @@ -59,6 +60,7 @@ export class ButtonWithSpinnerComponent { @Input() variant: ButtonVariants['variant'] = 'primary'; @Input() size: ButtonVariants['size'] = 'medium'; @Input() disabled: boolean = false; + @Input() type: 'button' | 'submit' = 'button'; @Output() public clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); diff --git a/alfa-client/libs/design-system/src/lib/button/button.component.ts b/alfa-client/libs/design-system/src/lib/button/button.component.ts index 78bc43acf3..68a0d753e5 100644 --- a/alfa-client/libs/design-system/src/lib/button/button.component.ts +++ b/alfa-client/libs/design-system/src/lib/button/button.component.ts @@ -105,7 +105,7 @@ export type ButtonVariants = VariantProps<typeof buttonVariants>; standalone: true, imports: [CommonModule, SpinnerIconComponent], template: ` <button - type="submit" + [type]="type" [ngClass]="buttonVariants({ size, variant, disabled: isDisabled, destructive })" [attr.aria-disabled]="isDisabled" [attr.aria-label]="text" @@ -133,6 +133,7 @@ export class ButtonComponent { @Input() variant: ButtonVariants['variant']; @Input() size: ButtonVariants['size']; @Input() spinnerSize: IconVariants['size'] = 'medium'; + @Input() type: 'button' | 'submit' = 'button'; @Output() public clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); -- GitLab From c24420e3a051115a45bc377b059ab46258278ffb Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Thu, 10 Apr 2025 16:48:28 +0200 Subject: [PATCH 19/23] OZG-7974 test improvements --- ...mcontrol-editor.abstract.component.spec.ts | 26 +++++++++---------- .../formcontrol-editor.abstract.component.ts | 16 +++++++----- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts index c12275820f..d27faa3632 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -21,29 +21,29 @@ describe('FormControlEditorAbstractComponent', () => { fixture.detectChanges(); }); - describe('ng on init', () => { - it('should set valueChange subscription', () => { + describe('ngOnInit', () => { + it('should set subscription', () => { component.ngOnInit(); - expect(component._changesSubscr).toBeDefined(); + expect(component._changesSubscription).toBeDefined(); }); - it('should call field control on change handler when fieldControl value changes ', () => { - component._fieldControlOnChangeHandler = jest.fn(); + it('should handle field control value changes ', () => { + component.handleFieldControlValueChange = jest.fn(); component.ngOnInit(); component.fieldControl.setValue('testValue'); - expect(component._fieldControlOnChangeHandler).toHaveBeenCalledWith('testValue'); + expect(component.handleFieldControlValueChange).toHaveBeenCalledWith('testValue'); }); - it('should set statusChange subscription', () => { + it('should set subscription', () => { component.ngOnInit(); - expect(component._statusSubscr).toBeDefined(); + expect(component._statusSubscription).toBeDefined(); }); - it('should call setErrors on statusChange', () => { + it('should set errors on statusChange', () => { component.setErrors = jest.fn(); component.ngOnInit(); @@ -54,7 +54,7 @@ describe('FormControlEditorAbstractComponent', () => { }); describe('writeValue', () => { - it('should set value to fieldControl', () => { + it('should set fieldControl value', () => { const value = 'testValue'; component.writeValue(value); @@ -72,7 +72,7 @@ describe('FormControlEditorAbstractComponent', () => { expect(component.fieldControl.errors).toEqual(errors); }); - it('should call update invalid params', () => { + it('should update invalid params', () => { component._updateInvalidParams = jest.fn(); component.setErrors(); @@ -81,7 +81,7 @@ describe('FormControlEditorAbstractComponent', () => { }); }); - describe('remove errors', () => { + describe('removeErrors', () => { it('should remove fieldControl errors', () => { component.fieldControl.setErrors({ fehler: 'this is an validation error' }); @@ -90,7 +90,7 @@ describe('FormControlEditorAbstractComponent', () => { expect(component.fieldControl.errors).toBeNull(); }); - it('should call update invalid params', () => { + it('should update invalid params', () => { component._updateInvalidParams = jest.fn(); component._removeErrors(); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 17f8e71999..02760de100 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -35,8 +35,8 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue public onTouched = () => undefined; public invalidParams: InvalidParam[] = []; - _changesSubscr: Subscription; - _statusSubscr: Subscription; + _changesSubscription: Subscription; + _statusSubscription: Subscription; disabled: boolean = false; @@ -45,16 +45,18 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnInit(): void { - this._changesSubscr = this.fieldControl.valueChanges.subscribe((value: unknown) => this._fieldControlOnChangeHandler(value)); + this._changesSubscription = this.fieldControl.valueChanges.subscribe((value: unknown) => + this.handleFieldControlValueChange(value), + ); if (this.control) { - this._statusSubscr = this.control.statusChanges.subscribe(() => { + this._statusSubscription = this.control.statusChanges.subscribe(() => { this.setErrors(); }); } } - _fieldControlOnChangeHandler(value: unknown): void { + handleFieldControlValueChange(value: unknown): void { this.onChange(value); this._removeErrors(); } @@ -80,8 +82,8 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue } ngOnDestroy(): void { - if (this._changesSubscr) this._changesSubscr.unsubscribe(); - if (this._statusSubscr) this._statusSubscr.unsubscribe(); + if (this._changesSubscription) this._changesSubscription.unsubscribe(); + if (this._statusSubscription) this._statusSubscription.unsubscribe(); } setErrors(): void { -- GitLab From 1791f680541a3d6f9a3b31c3932047647f07dc6a Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Fri, 11 Apr 2025 17:17:49 +0200 Subject: [PATCH 20/23] OZG-7974 umbenennung --- .../admin/keycloak-shared/src/lib/keycloak-formservice.ts | 4 ++-- .../libs/tech-shared/src/lib/resource/resource.util.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index 0b10e2acef..e7de668f01 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { - creatDelayedEmptyStateResourceObservable, + creatDelayedEmptyStateResource, createEmptyStateResource, InvalidParam, isLoaded, @@ -89,7 +89,7 @@ export abstract class KeycloakFormService<T> { _processInvalidForm(): Observable<StateResource<T>> { this._showValidationErrorForAllInvalidControls(this.form); - return creatDelayedEmptyStateResourceObservable<T>(); + return creatDelayedEmptyStateResource<T>(); } _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index d2b4e27d56..40f4dabc86 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -55,7 +55,7 @@ export function createErrorStateResource<T>(error: HttpError): StateResource<any return { ...createEmptyStateResource<T>(), error, loaded: true }; } -export function creatDelayedEmptyStateResourceObservable<T>(): Observable<StateResource<T>> { +export function creatDelayedEmptyStateResource<T>(): Observable<StateResource<T>> { return of(createEmptyStateResource<T>()).pipe(delay(200), startWith(createEmptyStateResource<T>(true))); } -- GitLab From 3099a4ba83b0c4b0bbe19611ef2485881bea16e4 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sat, 12 Apr 2025 00:26:32 +0200 Subject: [PATCH 21/23] OZG-7974 small fix --- .../src/lib/keycloak-formservice.spec.ts | 11 +++++++++++ .../keycloak-shared/src/lib/keycloak-formservice.ts | 7 ++++++- .../user/src/lib/user-form/user.formservice.spec.ts | 10 ++++++++++ .../admin/user/src/lib/user-form/user.formservice.ts | 9 +++++---- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index 6e71337e1f..16736b029c 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -180,6 +180,7 @@ describe('KeycloakFormService', () => { service._patchConfig = patchConfig; service._load = jest.fn().mockReturnValue(singleHot(dummyStateResource)); service._patchIfLoaded = jest.fn(); + service._doAfterPatch = jest.fn(); }); it('should call load', () => { @@ -196,6 +197,14 @@ describe('KeycloakFormService', () => { expect(service._patchIfLoaded).toHaveBeenCalledWith(dummyStateResource); }); + it('should call do after patch', () => { + service._load = jest.fn().mockReturnValue(of(dummyStateResource)); + + service._initLoading().subscribe(); + + expect(service._doAfterPatch).toHaveBeenCalledWith(dummyStateResource); + }); + it('should return loaded value', () => { const loadedDummyStateResource: Observable<StateResource<Dummy>> = service._initLoading(); @@ -777,6 +786,8 @@ export class TestKeycloakFormService extends KeycloakFormService<Dummy> { return TestKeycloakFormService.LOAD_OBSERVABLE(); } + _doAfterPatch(stateResource: StateResource<Dummy>) {} + _doSubmit(): Observable<StateResource<Dummy>> { return TestKeycloakFormService.SUBMIT_OBSERVABLE(); } diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index e7de668f01..c8287dbaba 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -69,11 +69,16 @@ export abstract class KeycloakFormService<T> { } _initLoading(): Observable<StateResource<T>> { - return this._load(this._patchConfig.id).pipe(tap((stateResource: StateResource<T>) => this._patchIfLoaded(stateResource))); + return this._load(this._patchConfig.id).pipe( + tap((stateResource: StateResource<T>) => this._patchIfLoaded(stateResource)), + tap((stateResource: StateResource<T>) => this._doAfterPatch(stateResource)), + ); } abstract _load(id: string): Observable<StateResource<T>>; + abstract _doAfterPatch(stateResource: StateResource<T>): void; + _patchIfLoaded(stateResource: StateResource<T>): void { if (isLoaded(stateResource)) this._patch(stateResource.resource); } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 71ea6569bd..452e72c34a 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -168,6 +168,16 @@ describe('UserFormService', () => { }); }); + describe('doAfterPatch', () => { + it('should call _updateAlfaRoleStates', () => { + service._updateAlfaRoleStates = jest.fn(); + + service._doAfterPatch(createStateResource(createUser())); + + expect(service._updateAlfaRoleStates).toHaveBeenCalled(); + }); + }); + describe('initOrganisationsEinheiten', () => { beforeEach(() => { service._addOrganisationsEinheitenToForm = jest.fn(); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index 2e717f6912..616a0e9553 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -95,10 +95,11 @@ export class UserFormService extends KeycloakFormService<User> { } _load(id: string): Observable<StateResource<User>> { - return this.userService.getUserById(id).pipe( - filter(isLoaded), - tap(() => this._updateAlfaRoleStates()), - ); + return this.userService.getUserById(id); + } + + _doAfterPatch(stateResource: StateResource<User>): void { + this._updateAlfaRoleStates(); } _initForm(): UntypedFormGroup { -- GitLab From eca302bcd7b55d74e4788f9562f948f6656ef5f6 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Sat, 12 Apr 2025 00:48:11 +0200 Subject: [PATCH 22/23] OZG-7974 small fix --- .../admin/user/src/lib/user-form/user.formservice.spec.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 452e72c34a..0073a7dbef 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -151,14 +151,6 @@ describe('UserFormService', () => { expect(response).toBeObservable(singleCold(loadedUser)); }); - it('should update alfa role states after user is loaded', () => { - userService.getUserById.mockReturnValue(of(loadedUser)); - - service._load(id).subscribe(); - - expect(service._updateAlfaRoleStates).toHaveBeenCalled(); - }); - it('should not update alfa role states if user is not loaded', () => { userService.getUserById.mockReturnValue(of(createEmptyStateResource())); -- GitLab From 8b7e94e7f45b67d71249ed5bf695da14cf6ac566 Mon Sep 17 00:00:00 2001 From: Albert <Albert.Bruns@mgm-tp.com> Date: Tue, 15 Apr 2025 14:31:02 +0200 Subject: [PATCH 23/23] OZG-7974 mark as touched --- .../form/formcontrol-editor.abstract.component.spec.ts | 8 ++++++++ .../src/lib/form/formcontrol-editor.abstract.component.ts | 2 ++ 2 files changed, 10 insertions(+) diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts index d27faa3632..c6788f44db 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.spec.ts @@ -72,6 +72,14 @@ describe('FormControlEditorAbstractComponent', () => { expect(component.fieldControl.errors).toEqual(errors); }); + it('should set fieldControl to touched', () => { + component.fieldControl.markAsPristine(); + + component.setErrors(); + + expect(component.fieldControl.touched).toBe(true); + }); + it('should update invalid params', () => { component._updateInvalidParams = jest.fn(); diff --git a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts index 02760de100..1565a3646a 100644 --- a/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/formcontrol-editor.abstract.component.ts @@ -90,6 +90,8 @@ export abstract class FormControlEditorAbstractComponent implements ControlValue if (!this.control) return; this.fieldControl.setErrors(this.control.errors); + this.fieldControl.markAsTouched(); + this._updateInvalidParams(); } -- GitLab