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