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 865f904f83dbfd0ad22873ec8578531815b1c8dd..6fe0a332ba918b071d6bddfb712e7b4e177a4891 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 @@ -26,6 +26,7 @@ import { Injectable } from '@angular/core'; import { TestBed } 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'; import { singleCold, singleHot } from 'libs/tech-shared/test/marbles'; import { createSpy, mock, Mock } from 'libs/test-utils/src/lib/mocking'; @@ -261,6 +262,19 @@ describe('KeycloakFormService', () => { expect(isInvalid).toBeTruthy(); }); }); + + describe('get id', () => { + const patchConfigId: string = faker.string.alphanumeric(); + const patchConfig: PatchConfig = { id: patchConfigId, doPatch: false }; + + it('should return id from patch config', () => { + service._patchConfig = patchConfig; + + const id: string = service.getId(); + + expect(id).toBe(patchConfigId); + }); + }); }); @Injectable() @@ -268,6 +282,7 @@ export class TestKeycloakFormService extends KeycloakFormService<Dummy> { public static readonly FIELD: string = 'attribute'; public static SUBMIT_OBSERVABLE = () => of(createEmptyStateResource()); + public static DELETE_OBSERVABLE = () => of(createEmptyStateResource()); public static LOAD_OBSERVABLE = () => of(createEmptyStateResource()); _initForm(): FormGroup { @@ -287,4 +302,8 @@ export class TestKeycloakFormService extends KeycloakFormService<Dummy> { _doSubmit(): Observable<StateResource<Dummy>> { return TestKeycloakFormService.SUBMIT_OBSERVABLE(); } + + _doDelete(): Observable<StateResource<Dummy>> { + return TestKeycloakFormService.DELETE_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 ecd71702a2a6d3e6176dc5890bdc9cd61dc7fe7c..49a2e6c84e3fdbf69b22ca324ee0100a34ae6763 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 @@ -94,6 +94,10 @@ export abstract class KeycloakFormService<T> { public isInvalid(): boolean { return this.form.invalid; } + + public getId(): string { + return this._patchConfig.id; + } } export interface PatchConfig { diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts index ae9c56bedd57177e3bac765ec968d7d00660794d..d0806e198efa5090d375832bbd313ca99482149b 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts @@ -415,6 +415,29 @@ describe('UserRepository', () => { }); }); + describe('deleteUser', () => { + const userId: string = faker.string.uuid(); + + beforeEach(() => { + kcAdminClient.users = <any>{ + del: jest.fn().mockReturnValue(Promise.resolve(undefined)), + }; + }); + + it('should call kcAdminClient users del', () => { + repository.deleteUser(userId); + + expect(kcAdminClient.users['del']).toHaveBeenCalledWith({ id: userId }); + }); + + it('should return void', (done) => { + repository.deleteUser(userId).subscribe((result: void) => { + expect(result).toBeUndefined(); + done(); + }); + }); + }); + describe('getUsers', () => { const user: User = createUser(); const userArray: User[] = [user, user, user]; diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index 130176623c25c1cf1f848643b0a5687906fbae34..a5df128a7de4ceb8bd71c4d2edc337078b57bcca 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -151,6 +151,10 @@ export class UserRepository { return throwError(() => new Error('An error occurred while saving the user.')); } + public deleteUser(userId: string): Observable<void> { + return from(this.kcAdminClient.users.del({ id: userId })); + } + public getUsers(): Observable<User[]> { return from(this.kcAdminClient.users.find()).pipe( map((userReps: UserRepresentation[]): User[] => diff --git a/alfa-client/libs/admin/shared/src/index.ts b/alfa-client/libs/admin/shared/src/index.ts index 8f0992f8376dfb02cbd0096e33f516ac819f3d97..4257af281863dad5164a007d329fa6efea4f2c5a 100644 --- a/alfa-client/libs/admin/shared/src/index.ts +++ b/alfa-client/libs/admin/shared/src/index.ts @@ -1,4 +1,5 @@ export * from './lib/admin-cancel-button/admin-cancel-button.component'; +export * from './lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component'; export * from './lib/admin-save-button/admin-save-button.component'; export * from './lib/routes'; export * from './lib/token'; diff --git a/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.html b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3577b2352dcfb4450a36e0726ae4d47b34d80ed4 --- /dev/null +++ b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.html @@ -0,0 +1,3 @@ +<ods-open-dialog-button variant='outline_error' label="Löschen" dataTestId="delete-button" > + <ods-delete-icon icon /> +</ods-open-dialog-button> \ No newline at end of file diff --git a/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.spec.ts b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3250b9c18a432b0f5e8fe523fdb25d79e82f10be --- /dev/null +++ b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OpenDialogButtonComponent } from '@ods/component'; +import { DeleteIconComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { AdminDeleteOpenDialogButtonComponent } from './admin-delete-open-dialog-button.component'; + +describe('AdminDeleteOpenDialogButtonComponent', () => { + let component: AdminDeleteOpenDialogButtonComponent; + let fixture: ComponentFixture<AdminDeleteOpenDialogButtonComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AdminDeleteOpenDialogButtonComponent], + declarations: [MockComponent(OpenDialogButtonComponent), MockComponent(DeleteIconComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(AdminDeleteOpenDialogButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.ts b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9bcd3472723a4cfe91fbc91f897fedd16b76db9 --- /dev/null +++ b/alfa-client/libs/admin/shared/src/lib/admin-delete-open-dialog-button/admin-delete-open-dialog-button.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { OpenDialogButtonComponent } from '@ods/component'; +import { DeleteIconComponent } from '@ods/system'; + +@Component({ + selector: 'admin-delete-open-dialog-button', + standalone: true, + imports: [DeleteIconComponent, OpenDialogButtonComponent], + templateUrl: './admin-delete-open-dialog-button.component.html', +}) +export class AdminDeleteOpenDialogButtonComponent {} diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts b/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts index 28e7e8cce9ce062979c43175587834aa818724ea..fce4d7d2cc427160ab70357168b5fbc347e7df8e 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts @@ -101,4 +101,21 @@ describe('UserService', () => { expect(user).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: userStateResource })); }); }); + + describe('deleteInKeycloak', () => { + it('should call userRepository delete', () => { + service._deleteInKeycloak(user.id); + + expect(repository.deleteUser).toHaveBeenCalledWith(user.id); + }); + + it('should return void', (done) => { + repository.deleteUser.mockReturnValue(of(undefined)); + + service._deleteInKeycloak(user.id).subscribe((result: void) => { + expect(result).toBeUndefined(); + done(); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.service.ts b/alfa-client/libs/admin/user-shared/src/lib/user.service.ts index e927702221df953f2c5882fa6e8791ca98e0929e..d65103108f19ee160c78a8897f447ce7621b2bb0 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.service.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.service.ts @@ -47,7 +47,7 @@ export class UserService extends KeycloakResourceService<User> { } _deleteInKeycloak(id: string): Observable<void> { - throw new Error('Method not implemented.'); + return this.userRepository.deleteUser(id); } public getUserById(userId: string): Observable<StateResource<User>> { diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b49a93cae8a97f0cf12a2e10c50abacc140ab8b6 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.html @@ -0,0 +1 @@ +<admin-delete-open-dialog-button /> \ No newline at end of file diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec691fc665e848da93f5c649141ec8d34c180e61 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserFormDeleteButtonContainerComponent } from './user-form-delete-button-container.component'; + +describe('UserFormDeleteButtonComponent', () => { + let component: UserFormDeleteButtonContainerComponent; + let fixture: ComponentFixture<UserFormDeleteButtonContainerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserFormDeleteButtonContainerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UserFormDeleteButtonContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c66a5500b2e177a0f0a39aedad3f1ccd5e7e3e9 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-button/user-form-delete-button-container.component.ts @@ -0,0 +1,13 @@ +import { AdminDeleteOpenDialogButtonComponent } from '@admin-client/shared'; +import { DIALOG_COMPONENT } from '@alfa-client/ui'; +import { Component } from '@angular/core'; +import { UserDeleteDialogContainerComponent } from '../user-form-delete-dialog-container/user-delete-dialog-container.component'; + +@Component({ + selector: 'admin-user-form-delete-container-button', + standalone: true, + imports: [AdminDeleteOpenDialogButtonComponent], + providers: [{ provide: DIALOG_COMPONENT, useValue: UserDeleteDialogContainerComponent }], + templateUrl: './user-form-delete-button-container.component.html', +}) +export class UserFormDeleteButtonContainerComponent {} diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..367d0a4abbbd1f76c76468fe03e27403935a7577 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.html @@ -0,0 +1,6 @@ +<admin-user-delete-dialog + [username]="formService.getUserName()" + [deleteUserStateResource]="deleteUserStateResource$ | async" + (delete)="deleteUser()" + data-test-id="delete-dialog" +/> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..474c396b29e6f618186fafc524113a0a19c18097 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.spec.ts @@ -0,0 +1,124 @@ +import { ROUTES } from '@admin-client/shared'; +import { UserService } from '@admin-client/user-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; +import { dispatchEventFromFixture, getMockComponent, Mock, mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { faker } from '@faker-js/faker/locale/de'; +import { Observable, of } from 'rxjs'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { singleColdCompleted } from '../../../../../../tech-shared/test/marbles'; +import { createUserFormGroup } from '../../../../test/form'; +import { UserFormService } from '../user.formservice'; +import { UserDeleteDialogContainerComponent } from './user-delete-dialog-container.component'; +import { UserDeleteDialogComponent } from './user-delete-dialog/user-delete-dialog.component'; + +describe('UserDeleteDialogContainerComponent', () => { + let component: UserDeleteDialogContainerComponent; + let fixture: ComponentFixture<UserDeleteDialogContainerComponent>; + + const deletDialogLocator: string = getDataTestIdOf('delete-dialog'); + + let formService: Mock<UserFormService>; + let userService: Mock<UserService>; + let navigationService: Mock<NavigationService>; + + beforeEach(async () => { + formService = mock(UserFormService); + userService = mock(UserService); + navigationService = mock(NavigationService); + formService = { + ...mock(UserFormService), + form: createUserFormGroup(), + delete: jest.fn(), + } as any; + + await TestBed.configureTestingModule({ + imports: [UserDeleteDialogContainerComponent, UserDeleteDialogComponent], + providers: [ + { provide: UserFormService, useValue: formService }, + { provide: UserService, useValue: userService }, + { provide: NavigationService, useValue: navigationService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(UserDeleteDialogContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('user form delete dialog', () => { + it('should be called', () => { + const username: string = faker.word.sample(); + const stateResource: StateResource<unknown> = createEmptyStateResource(); + formService.getUserName = jest.fn().mockReturnValue(username); + component.deleteUserStateResource$ = of(stateResource); + + fixture.detectChanges(); + + const deleteDialog: UserDeleteDialogComponent = getMockComponent(fixture, UserDeleteDialogComponent); + expect(deleteDialog.username).toBe(username); + expect(deleteDialog.deleteUserStateResource).toBe(stateResource); + }); + + it('should call deleteUser on emit delete', () => { + component.deleteUser = jest.fn(); + + dispatchEventFromFixture(fixture, deletDialogLocator, 'delete'); + + expect(component.deleteUser).toHaveBeenCalled(); + }); + }); + + describe('component', () => { + describe('deleteUser', () => { + const userId: string = faker.string.uuid(); + const loadingStateResource: StateResource<unknown> = createEmptyStateResource(true); + const loadingStateResource$: Observable<StateResource<unknown>> = of(loadingStateResource); + const loadingDoneStateResource$: Observable<StateResource<unknown>> = of(createEmptyStateResource()); + + beforeEach(() => { + userService.delete = jest.fn().mockReturnValue(loadingStateResource$); + formService.getId = jest.fn().mockReturnValue(userId); + }); + + it('should call formService getId', () => { + component.deleteUser(); + + expect(formService.getId).toHaveBeenCalled(); + }); + + it('should call userService delete', () => { + component.deleteUser(); + + expect(userService.delete).toHaveBeenCalledWith(userId); + }); + + it('should set deleteUserStateResource$', () => { + component.deleteUser(); + + expect(component.deleteUserStateResource$).toBeObservable(singleColdCompleted(loadingStateResource)); + }); + + it('should not navigate on loading', () => { + component.deleteUser(); + component.deleteUserStateResource$.subscribe(); + + expect(navigationService.navigate).not.toHaveBeenCalled(); + }); + + it('should navigate on delete success', () => { + userService.delete.mockReturnValue(loadingDoneStateResource$); + + component.deleteUser(); + component.deleteUserStateResource$.subscribe(); + + expect(navigationService.navigate).toHaveBeenCalledWith(ROUTES.BENUTZER); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb3f0e0a9ae2e286b4ab6e49d4f4e79965b65dc5 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog-container.component.ts @@ -0,0 +1,35 @@ +import { ROUTES } from '@admin-client/shared'; +import { UserService } from '@admin-client/user-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { createEmptyStateResource, isNotLoading, StateResource } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { Observable, of, tap } from 'rxjs'; +import { UserFormService } from '../user.formservice'; +import { UserDeleteDialogComponent } from './user-delete-dialog/user-delete-dialog.component'; + +@Component({ + selector: 'admin-delete-dialog-container', + standalone: true, + imports: [UserDeleteDialogComponent, AsyncPipe], + templateUrl: './user-delete-dialog-container.component.html', +}) +export class UserDeleteDialogContainerComponent { + public readonly formService = inject(UserFormService); + public readonly userService = inject(UserService); + public readonly navigationService = inject(NavigationService); + + public deleteUserStateResource$: Observable<StateResource<unknown>> = of(createEmptyStateResource()); + + public deleteUser(): void { + this.deleteUserStateResource$ = this.userService + .delete(this.formService.getId()) + .pipe(tap((state: StateResource<unknown>) => this.navigateOnDeleteSuccess(state))); + } + + private navigateOnDeleteSuccess(state: StateResource<any>): void { + if (isNotLoading(state)) { + this.navigationService.navigate(ROUTES.BENUTZER); + } + } +} diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.html new file mode 100644 index 0000000000000000000000000000000000000000..83b4694f02ef4fb61dbb8b65d0e83e114b69840d --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.html @@ -0,0 +1,16 @@ +<div class="block bg-background-100 flex flex-col gap-4 p-8"> + <p>Sind Sie sicher, dass sie <span class="font-bold">{{ username }}</span> löschen möchten?</p> + <p>Hinweis: Die zugewiesenen Vorgänge bleiben bestehen.</p> + + <div class="flex justify-between"> + <ods-cancel-dialog-button data-test-id="dialog-cancel-button-host" /> + <ods-button-with-spinner + [stateResource]="deleteUserStateResource" + (clickEmitter)="delete.emit()" + variant="primary" + text="Löschen" + dataTestId="dialog-delete" + data-test-id="dialog-delete-button-host" + /> + </div> +</div> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2cfff1676a8048f0c8cc896450e25437533e39d --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.spec.ts @@ -0,0 +1,57 @@ +import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; +import { dispatchEventFromFixture, existsAsHtmlElement, getMockComponent, MockEvent } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonWithSpinnerComponent, CancelDialogButtonComponent } from '@ods/component'; +import { MockComponent } from 'ng-mocks'; +import { getDataTestIdOf } from '../../../../../../../tech-shared/test/data-test'; +import { UserDeleteDialogComponent } from './user-delete-dialog.component'; + +describe('UserFormDeleteDialogComponent', () => { + let component: UserDeleteDialogComponent; + let fixture: ComponentFixture<UserDeleteDialogComponent>; + + const deleteButton: string = getDataTestIdOf('dialog-delete-button-host'); + const cancelButton: string = getDataTestIdOf('dialog-cancel-button-host'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserDeleteDialogComponent], + declarations: [MockComponent(CancelDialogButtonComponent), MockComponent(ButtonWithSpinnerComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(UserDeleteDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('cancel button', () => { + it('should exist', () => { + existsAsHtmlElement(fixture, cancelButton); + }); + }); + + describe('delete button', () => { + it('should exist', () => { + const stateResource: StateResource<unknown> = createEmptyStateResource(); + component.deleteUserStateResource = stateResource; + + fixture.detectChanges(); + + expect(getMockComponent(fixture, ButtonWithSpinnerComponent).stateResource).toBe(stateResource); + }); + + it('should emit delete on click', () => { + component.delete.emit = jest.fn(); + + dispatchEventFromFixture(fixture, deleteButton, MockEvent.CLICK); + + expect(component.delete.emit).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ced40bf330256dd8bd455248717390d1fdc4b62 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-delete-dialog-container/user-delete-dialog/user-delete-dialog.component.ts @@ -0,0 +1,16 @@ +import { StateResource } from '@alfa-client/tech-shared'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ButtonWithSpinnerComponent, CancelDialogButtonComponent } from '@ods/component'; + +@Component({ + selector: 'admin-user-delete-dialog', + standalone: true, + imports: [ButtonWithSpinnerComponent, CancelDialogButtonComponent], + templateUrl: './user-delete-dialog.component.html', +}) +export class UserDeleteDialogComponent { + @Input() username: string; + @Input() deleteUserStateResource: StateResource<unknown>; + + @Output() delete: EventEmitter<void> = new EventEmitter<void>(); +} 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 dcab3c25b5a05434e0eedd7f56e889914253b5d7..5ce59646fde16f7fd3299848b2615f318685e536 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 @@ -32,6 +32,11 @@ [formGroupParent]="formService.form" [formGroupOrganisationsEinheiten]="formService.getOrganisationsEinheitenGroup()" /> - <admin-user-form-save-button /> + <div class="flex justify-between"> + <admin-user-form-save-button /> + @if (formService.isPatch()) { + <admin-user-form-delete-container-button data-test-id="delete-button-container"/> + } + </div> </div> </ods-spinner> 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 085f2fe196fdb6d881240f4485527e72123b51ba..160ab43ceea91f4c2fd18bc37ca506e99edfa033 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 @@ -23,7 +23,8 @@ */ import { User } from '@admin-client/user-shared'; import { createEmptyStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; -import { getMockComponent, mock, Mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { existsAsHtmlElement, getMockComponent, mock, Mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +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'; @@ -47,6 +48,7 @@ describe('UserFormComponent', () => { let formService: Mock<UserFormService>; const userContent: string = getDataTestIdOf('user-content'); + const deleteButtonContainer: string = getDataTestIdOf('delete-button-container'); beforeEach(async () => { formService = <any>{ @@ -67,7 +69,7 @@ describe('UserFormComponent', () => { MockComponent(UserFormRolesComponent), MockComponent(UserFormHeadlineComponent), ], - providers: [{ provide: UserFormService, useValue: formService }], + providers: [{ provide: DIALOG_COMPONENT, useValue: {} }], }) .overrideComponent(UserFormComponent, { set: { @@ -130,7 +132,6 @@ describe('UserFormComponent', () => { it('should exist with input', () => { const formDataComponent: UserFormDataComponent = getMockComponent(fixture, UserFormDataComponent); - expect(formDataComponent).toBeTruthy(); expect(formDataComponent.formGroupParent).toBe(component.formService.form); }); }); @@ -151,7 +152,6 @@ describe('UserFormComponent', () => { UserFormOrganisationsEinheitListComponent, ); - expect(organisationsEinheitListComponent).toBeTruthy(); expect(organisationsEinheitListComponent.formGroupParent).toBe(component.formService.form); expect(organisationsEinheitListComponent.formGroupOrganisationsEinheiten).toBe( component.formService.getOrganisationsEinheitenGroup(), @@ -167,4 +167,22 @@ describe('UserFormComponent', () => { notExistsAsHtmlElement(fixture, userContent); }); }); + + describe('admin delete button container', () => { + it('should exist', () => { + formService.isPatch.mockReturnValue(true); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, deleteButtonContainer); + }); + + it('should not exist', () => { + formService.isPatch.mockReturnValue(false); + + 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 03199641945f4841fcd0360b025262c2136d9ef3..a533ff55e37698883943902df16183dc776b22c2 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 @@ -29,6 +29,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SpinnerComponent } from '@ods/component'; import { Observable } from 'rxjs'; import { UserFormDataComponent } from './user-form-data/user-form-data.component'; +import { UserFormDeleteButtonContainerComponent } from './user-form-delete-button/user-form-delete-button-container.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'; @@ -44,12 +45,13 @@ import { UserFormService } from './user.formservice'; FormsModule, ReactiveFormsModule, AsyncPipe, + SpinnerComponent, UserFormDataComponent, UserFormRolesComponent, UserFormOrganisationsEinheitListComponent, UserFormHeadlineComponent, UserFormSaveButtonComponent, - SpinnerComponent, + UserFormDeleteButtonContainerComponent, ], }) export class UserFormComponent implements OnInit { 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 85dd58354b2597416aad10898d5c437edb445cfe..3be33cbcdfd5b941af8ce9ef4a51c63160419bb5 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 @@ -30,7 +30,7 @@ import { createEmptyStateResource, createStateResource, StateResource } from '@a import { Mock, mock } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, FormControl, FormGroup, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/locale/de'; import { cold } from 'jest-marbles'; @@ -45,6 +45,7 @@ import SpyInstance = jest.SpyInstance; describe('UserFormService', () => { let formService: UserFormService; + let roleGroup: UntypedFormGroup; let alfaGroup: UntypedFormGroup; let organisationsEinheitenGroup: UntypedFormGroup; @@ -62,7 +63,14 @@ describe('UserFormService', () => { ]); beforeEach(() => { - service = { ...mock(UserService), refresh: jest.fn(), create: jest.fn(), save: jest.fn(), getUserById: jest.fn() }; + service = { + ...mock(UserService), + refresh: jest.fn(), + create: jest.fn(), + save: jest.fn(), + delete: jest.fn(), + getUserById: jest.fn(), + }; adminOrganisationsEinheitService = { ...mock(AdminOrganisationsEinheitService), getAll: jest.fn().mockReturnValue(of(adminOrganisationsEinheitList)), @@ -456,4 +464,14 @@ describe('UserFormService', () => { expect(formService._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); }); }); + + describe('get userName', () => { + it('should return form control value of userName', () => { + formService.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') }); + + const userName: string = formService.getUserName(); + + expect(userName).toBe('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 e7e537b362cf5938842d80e3ededcf7297665b78..a456872659a6b1ebd15a0bb570080b5834069fdd 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 @@ -263,4 +263,8 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest ngOnDestroy(): void { this._initOrganisationsEinheiten$.unsubscribe(); } + + public getUserName(): string { + return this.form.get(UserFormService.USERNAME).value; + } } diff --git a/alfa-client/libs/design-component/src/index.ts b/alfa-client/libs/design-component/src/index.ts index 745720be72b8665fb259372141e6ab5537efbfbe..9eec827c3a019115cc42c751521816fdde5c87a7 100644 --- a/alfa-client/libs/design-component/src/index.ts +++ b/alfa-client/libs/design-component/src/index.ts @@ -22,6 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ export * from './lib/button-with-spinner/button-with-spinner.component'; +export * from './lib/cancel-dialog-button/cancel-dialog-button.component'; export * from './lib/download-button/download-button.component'; export * from './lib/form/button-toggle-group/button-toggle-group.component'; export * from './lib/form/checkbox-editor/checkbox-editor.component'; @@ -30,5 +31,6 @@ export * from './lib/form/formcontrol-editor.abstract.component'; export * from './lib/form/single-file-upload-editor/single-file-upload-editor.component'; export * from './lib/form/text-editor/text-editor.component'; export * from './lib/form/textarea-editor/textarea-editor.component'; +export * from './lib/open-dialog-button/open-dialog-button.component'; export * from './lib/routing-button/routing-button.component'; export * from './lib/spinner/spinner.component'; diff --git a/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.spec.ts b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..110c81e4d4072ef0417676ea9966a7fa3394f144 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.spec.ts @@ -0,0 +1,47 @@ +import { dispatchEventFromFixture, mock, Mock, MockEvent } from '@alfa-client/test-utils'; +import { OzgcloudDialogService } from '@alfa-client/ui'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonComponent } from '@ods/system'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { CancelDialogButtonComponent } from './cancel-dialog-button.component'; + +describe('CancelDialogButtonComponent', () => { + let component: CancelDialogButtonComponent; + let fixture: ComponentFixture<CancelDialogButtonComponent>; + + let dialogService: Mock<OzgcloudDialogService>; + + const cancelDialog: string = getDataTestIdOf('cancel-dialog'); + + beforeEach(async () => { + dialogService = mock(OzgcloudDialogService); + + await TestBed.configureTestingModule({ + imports: [CancelDialogButtonComponent], + declarations: [MockComponent(ButtonComponent)], + providers: [ + { + provide: OzgcloudDialogService, + useValue: dialogService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CancelDialogButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('on button click', () => { + it('should call dialog service to close all', () => { + dispatchEventFromFixture(fixture, cancelDialog, MockEvent.CLICK); + + expect(dialogService.closeAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5d80db0f4e7755c1a2bd9ae9eb7b921c3cf58349 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts @@ -0,0 +1,23 @@ +import { OzgcloudDialogService } from '@alfa-client/ui'; +import { Component, inject } from '@angular/core'; +import { ButtonComponent } from '@ods/system'; + +@Component({ + selector: 'ods-cancel-dialog-button', + standalone: true, + imports: [ButtonComponent], + template: `<ods-button + (clickEmitter)="cancel()" + variant="outline" + text="Abbrechen" + dataTestId="cancel-dialog" + data-test-id="cancel-dialog" + />`, +}) +export class CancelDialogButtonComponent { + public readonly dialogService = inject(OzgcloudDialogService); + + public cancel(): void { + this.dialogService.closeAll(); + } +} diff --git a/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.spec.ts b/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8cbe8c1ae06eabaea31340ddd8eb31f710cab503 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.spec.ts @@ -0,0 +1,104 @@ +import { OzgCloudComponentFactory } from '@alfa-client/tech-shared'; +import { dispatchEventFromFixture, Mock, mock, MockEvent, mockGetValue } from '@alfa-client/test-utils'; +import { DIALOG_COMPONENT, OzgcloudDialogService } from '@alfa-client/ui'; +import { ComponentType } from '@angular/cdk/portal'; +import { ComponentRef, Injector, ViewContainerRef } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonComponent } from '@ods/system'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { OpenDialogButtonComponent } from './open-dialog-button.component'; + +describe('OpenDialogButtonComponent', () => { + let component: OpenDialogButtonComponent; + let fixture: ComponentFixture<OpenDialogButtonComponent>; + + let dialogComponent: Mock<ComponentType<any>>; + let componentFactory: Mock<OzgCloudComponentFactory>; + let dialogService: Mock<OzgcloudDialogService>; + let viewContainerRef: Mock<ViewContainerRef>; + let injector: Mock<Injector>; + + const openDialog: string = getDataTestIdOf('open-dialog'); + + const componentRef: ComponentRef<any> = <any>{ instance: { constructor: null } }; + const dialogResponse: any = {}; + + beforeEach(async () => { + dialogComponent = {}; + componentFactory = mock(OzgCloudComponentFactory); + dialogService = { ...mock(OzgcloudDialogService), openInContext: jest.fn().mockReturnValue({ closed: of(dialogResponse) }) }; + viewContainerRef = mock(ViewContainerRef as any); + injector = mock(Injector as any); + + await TestBed.configureTestingModule({ + imports: [OpenDialogButtonComponent], + declarations: [MockComponent(ButtonComponent)], + providers: [ + { + provide: DIALOG_COMPONENT, + useValue: dialogComponent, + }, + { + provide: OzgCloudComponentFactory, + useValue: componentFactory, + }, + { + provide: OzgcloudDialogService, + useValue: dialogService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(OpenDialogButtonComponent); + component = fixture.componentInstance; + mockGetValue(component, 'viewContainerRef', viewContainerRef); + mockGetValue(component, 'injector', injector); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('on button click', () => { + beforeEach(() => { + component._createComponent = jest.fn().mockReturnValue(componentRef); + }); + + it('should call create component factory to create component', () => { + dispatchEventFromFixture(fixture, openDialog, MockEvent.CLICK); + + expect(component._createComponent).toHaveBeenCalled(); + }); + + it('should call dialog service to open dialog', () => { + dispatchEventFromFixture(fixture, openDialog, MockEvent.CLICK); + + expect(dialogService.openInContext).toHaveBeenCalledWith(componentRef.instance.constructor, viewContainerRef); + }); + }); + + describe('open', () => { + beforeEach(() => { + component._createComponent = jest.fn().mockReturnValue(componentRef); + }); + + it('should emit close emitter on dialog close', () => { + component.close.emit = jest.fn(); + + component.open(); + + expect(component.close.emit).toHaveBeenCalled(); + }); + }); + + describe('create component', () => { + it('should call component factory to create component', () => { + component._createComponent(); + + expect(componentFactory.createComponent).toHaveBeenCalledWith(dialogComponent, injector); + }); + }); +}); diff --git a/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.ts b/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..2815aab6fd3ed8601930bfe061fecc23fc0e4704 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/open-dialog-button/open-dialog-button.component.ts @@ -0,0 +1,49 @@ +import { OzgCloudComponentFactory } from '@alfa-client/tech-shared'; +import { DIALOG_COMPONENT, OzgcloudDialogService } from '@alfa-client/ui'; +import { ComponentType } from '@angular/cdk/portal'; +import { Component, ComponentRef, EventEmitter, inject, Injector, Input, Output, ViewContainerRef } from '@angular/core'; +import { ButtonComponent, ButtonVariants } from '@ods/system'; +import { first } from 'rxjs'; + +@Component({ + selector: 'ods-open-dialog-button', + standalone: true, + imports: [ButtonComponent], + template: `<ods-button + (clickEmitter)="open()" + [variant]="variant" + [text]="label" + [dataTestId]="dataTestId" + data-test-id="open-dialog" + > + <ng-container icon> + <ng-content select="[icon]" /> + </ng-container> + </ods-button>`, +}) +export class OpenDialogButtonComponent { + private readonly component: ComponentType<any> = inject(DIALOG_COMPONENT); + private readonly componentFactory = inject(OzgCloudComponentFactory); + + private readonly dialogService = inject(OzgcloudDialogService); + + readonly viewContainerRef = inject(ViewContainerRef); + private readonly injector = inject(Injector); + + @Input() label: string; + @Input() dataTestId: string; + @Input() variant: ButtonVariants['variant'] = 'primary'; + + @Output() close: EventEmitter<void> = new EventEmitter(); + + public open(): void { + this.dialogService + .openInContext(this._createComponent().instance.constructor, this.viewContainerRef) + .closed.pipe(first()) + .subscribe(this.close.emit); + } + + _createComponent(): ComponentRef<any> { + return this.componentFactory.createComponent<any>(this.component, this.injector); + } +} 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 f7805648f827ff51ca7fde46354aec02345a0fa3..c0f7892c3c2c5184571b175dbff2326591415495 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 @@ -39,6 +39,8 @@ export const buttonVariants = cva( primary: 'bg-primary text-whitetext shadow-md hover:enabled:bg-primary-hover focus-visible:bg-primary-hover', outline: 'border border-primary bg-background-50 text-primary shadow-md hover:enabled:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', + outline_error: + 'border border-error bg-background-50 text-error shadow-md hover:enabled:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', ghost: 'border border-transparent hover:enabled:bg-ghost-hover text-primary focus-visible:border-background-200 focus-visible:bg-ghost-hover font-semibold [&]:focus-visible:outline-offset-1', }, diff --git a/alfa-client/libs/tech-shared/src/index.ts b/alfa-client/libs/tech-shared/src/index.ts index 6fbc4e5294b1490dd9924a225715b0007c0ba80f..be9ebae3ab1b5b0b13a375f8bd097acbfa8c11e8 100644 --- a/alfa-client/libs/tech-shared/src/index.ts +++ b/alfa-client/libs/tech-shared/src/index.ts @@ -57,6 +57,7 @@ export * from './lib/resource/resource.repository'; export * from './lib/resource/resource.rxjs.operator'; export * from './lib/resource/resource.service'; export * from './lib/resource/resource.util'; +export * from './lib/service/component.factory'; export * from './lib/service/formservice.abstract'; export * from './lib/tech.model'; export * from './lib/tech.util'; diff --git a/alfa-client/libs/tech-shared/src/lib/service/component.factory.spec.ts b/alfa-client/libs/tech-shared/src/lib/service/component.factory.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..32fb2f6d84f6be4f3699b21359aa61395a0d50f8 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/service/component.factory.spec.ts @@ -0,0 +1,101 @@ +import { Mock, mock, mockGetValue } from '@alfa-client/test-utils'; +import { ApplicationRef, ComponentRef, EnvironmentInjector, Injector, ViewRef } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { OzgCloudComponentFactory } from './component.factory'; + +import { ComponentType } from '@angular/cdk/portal'; + +jest.mock('@angular/core', () => ({ + ...jest.requireActual('@angular/core'), + createComponent: jest.fn(), +})); + +describe('OzgCloudComponentFactory', () => { + let factory: OzgCloudComponentFactory; + + let appRef: Mock<ApplicationRef>; + let envInjector: Mock<EnvironmentInjector>; + + beforeEach(() => { + appRef = mock(ApplicationRef); + envInjector = mock(EnvironmentInjector as any); + + factory = TestBed.inject(OzgCloudComponentFactory); + + mockGetValue(factory, 'appRef', appRef); + mockGetValue(factory, 'envInjector', envInjector); + }); + + it('should be created', () => { + expect(factory).toBeTruthy(); + }); + + describe('create component', () => { + let createComponentSpy: jest.SpyInstance; + const injectorMock: Mock<Injector> = mock(Injector as any); + const componentRefMock: Mock<ComponentRef<any>> = mock(ComponentRef as any); + + const componentType: ComponentType<any> = <any>{}; + const injector: Injector = <any>{}; + + beforeEach(() => { + createComponentSpy = jest.spyOn(require('@angular/core'), 'createComponent').mockReturnValue(componentRefMock); + factory._createElementInjector = jest.fn().mockReturnValue(injectorMock); + factory._registerComponentToChangeDetection = jest.fn(); + }); + + it('should call createComponent', () => { + factory.createComponent(componentType, injector); + + expect(createComponentSpy).toHaveBeenCalledWith(componentType, { + environmentInjector: envInjector, + elementInjector: injectorMock, + }); + }); + + it('should register component to angulars change detection', () => { + factory.createComponent(componentType, injector); + + expect(factory._registerComponentToChangeDetection).toHaveBeenCalledWith(componentRefMock); + }); + + it('should return created component', () => { + const componentRef: ComponentRef<any> = factory.createComponent(componentType, injector); + + expect(componentRef).toBe(componentRefMock); + }); + }); + + describe('create element injector', () => { + const injectorMock: Mock<Injector> = mock(Injector as any); + + let createSpy: jest.SpyInstance; + + beforeEach(() => { + createSpy = jest.spyOn(Injector, 'create').mockReturnValue(injectorMock as Injector); + }); + + it('should create an injector by given parent selector', () => { + factory._createElementInjector(injectorMock); + + expect(createSpy).toHaveBeenCalled(); + }); + + it('should return created injector', () => { + const injector: Injector = factory._createElementInjector(injectorMock); + + expect(injector).toBe(injectorMock); + }); + }); + + describe('register component to angulars change detection', () => { + const hostView: ViewRef = <any>{}; + const componentRef: ComponentRef<any> = <any>{ hostView }; + + it('should set hostview to appRef', () => { + factory._registerComponentToChangeDetection(componentRef); + + expect(appRef.attachView).toHaveBeenCalledWith(hostView); + }); + }); +}); diff --git a/alfa-client/libs/tech-shared/src/lib/service/component.factory.ts b/alfa-client/libs/tech-shared/src/lib/service/component.factory.ts new file mode 100644 index 0000000000000000000000000000000000000000..e54497e9fc92bbb198891d487ddcf7565ffab139 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/service/component.factory.ts @@ -0,0 +1,25 @@ +import { ComponentType } from '@angular/cdk/portal'; +import { ApplicationRef, ComponentRef, createComponent, EnvironmentInjector, inject, Injectable, Injector } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class OzgCloudComponentFactory { + private readonly appRef = inject(ApplicationRef); + private readonly envInjector = inject(EnvironmentInjector); + + public createComponent<T>(componentType: ComponentType<any>, parentInjector: Injector): ComponentRef<T> { + const component: ComponentRef<any> = <ComponentRef<any>>createComponent(componentType, { + environmentInjector: this.envInjector, + elementInjector: this._createElementInjector(parentInjector), + }); + this._registerComponentToChangeDetection(component); + return component; + } + + _createElementInjector(parentInjector: Injector): Injector { + return Injector.create({ providers: [], parent: parentInjector }); + } + + _registerComponentToChangeDetection(component: ComponentRef<any>): void { + this.appRef.attachView(component.hostView); + } +} diff --git a/alfa-client/libs/test-utils/src/lib/model.ts b/alfa-client/libs/test-utils/src/lib/model.ts index 1094ac96376b5a8d2d81cf5a7ebf13c69aa2a550..b2564a9ddf9869f6511f01c27ac61a0de6943879 100644 --- a/alfa-client/libs/test-utils/src/lib/model.ts +++ b/alfa-client/libs/test-utils/src/lib/model.ts @@ -30,6 +30,6 @@ export interface EventData<T> { data?: unknown; } -export const MockEvent = { - CLICK: 'clickEmitter', -}; +export enum MockEvent { + CLICK = 'clickEmitter', +} diff --git a/alfa-client/libs/ui/src/index.ts b/alfa-client/libs/ui/src/index.ts index 2db371a18e23b0b2d01eb93bcf1738bd8ef43f61..76832535931b3342403ac5674f6f866c4c7eb58f 100644 --- a/alfa-client/libs/ui/src/index.ts +++ b/alfa-client/libs/ui/src/index.ts @@ -47,6 +47,7 @@ export * from './lib/ui/open-url-button/open-url-button.component'; export * from './lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component'; export * from './lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component'; export * from './lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component'; +export * from './lib/ui/ozgcloud-dialog/ozgcloud-dialog.model'; export * from './lib/ui/ozgcloud-dialog/ozgcloud-dialog.result'; export * from './lib/ui/ozgcloud-dialog/ozgcloud-dialog.service'; export * from './lib/ui/ozgcloud-icon/ozgcloud-icon.component'; diff --git a/alfa-client/libs/ui/src/lib/ui/dialog/dialog.service.ts b/alfa-client/libs/ui/src/lib/ui/dialog/dialog.service.ts index cd72feef7dac8d6194e6fb83ab08e9b76e38ffa7..48e0bf00d2f803bbe0d4cdcd95c0ff0e84479aa1 100644 --- a/alfa-client/libs/ui/src/lib/ui/dialog/dialog.service.ts +++ b/alfa-client/libs/ui/src/lib/ui/dialog/dialog.service.ts @@ -30,6 +30,10 @@ import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dial import { InternalServerErrorDialogComponent } from '../notification/internal-server-error-dialog/internal-server-error-dialog.component'; import { RetryInTimeDialog } from './retry-in-time.dialog'; +/** + * @deprecated use {@link OzgcloudDialogService} instead + * @see OzgcloudDialogService + */ @Injectable({ providedIn: 'root' }) export class DialogService { private dialog = inject(MatDialog); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.model.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..441d097a057ac2121ffd01610740f149e6d6d5b1 --- /dev/null +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.model.ts @@ -0,0 +1,4 @@ +import { ComponentType } from '@angular/cdk/portal'; +import { InjectionToken } from '@angular/core'; + +export const DIALOG_COMPONENT: InjectionToken<ComponentType<any>> = new InjectionToken<ComponentType<any>>('DialogComponent'); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.spec.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.spec.ts index 91585f64a670993693cd3eb2202a181dc9d805ae..b95396349c8cb79f0e97601877a7fcd108529e56 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.spec.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { Mock, mock } from '@alfa-client/test-utils'; -import { Dialog, DialogConfig } from '@angular/cdk/dialog'; +import { Dialog, DialogConfig, DialogRef } from '@angular/cdk/dialog'; import { TestBed } from '@angular/core/testing'; import { OzgcloudDialogService } from './ozgcloud-dialog.service'; @@ -82,6 +82,26 @@ describe('OzgcloudDialogService', () => { }); }); + describe('open in context', () => { + const dialogRefMock: Mock<DialogRef> = mock(DialogRef); + + beforeEach(() => { + service.openInCallingComponentContext = jest.fn().mockReturnValue(dialogRefMock); + }); + + it('should call open in calling component context', () => { + service.openInContext(component, viewContainerRef, dialogData); + + expect(service.openInCallingComponentContext).toHaveBeenCalledWith(component, viewContainerRef, dialogData); + }); + + it('should return dialog ref', () => { + const dialogRef: DialogRef = service.openInContext(component, viewContainerRef, dialogData); + + expect(dialogRef).toBe(dialogRefMock); + }); + }); + describe('openInCallingComponentContext', () => { it('should open dialog with data', () => { service.openInCallingComponentContext(component, viewContainerRef, dialogData); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.ts index 077780a3e1d01c0e198efc1de9f12244315b1194..aebed88efe5a54fafce9ec09283f0314c744fc5e 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-dialog/ozgcloud-dialog.service.ts @@ -30,7 +30,7 @@ import { isNil } from 'lodash-es'; providedIn: 'root', }) export class OzgcloudDialogService { - private dialog = inject(Dialog); + private readonly dialog = inject(Dialog); readonly WIZARD_DIALOG_CONFIG: DialogConfig = { width: '1000px', @@ -47,10 +47,21 @@ export class OzgcloudDialogService { return this.openDialog<C, R>(component, this.buildDialogConfigWithData<D>(data, this.WIZARD_DIALOG_CONFIG)); } + /** + * @deprecated use openInContext instead + */ public open<C, D = unknown, R = unknown>(component: ComponentType<C>, data?: D): DialogRef<R> { return this.openDialog(component, this.buildDialogConfigWithData(data)); } + public openInContext<C, D = unknown, R = unknown>( + component: ComponentType<C>, + viewContainerRef: ViewContainerRef, + data?: D, + ): DialogRef<R> { + return this.openInCallingComponentContext<C, D, R>(component, viewContainerRef, data); + } + public openFullScreenDialog<C, D = unknown, R = unknown>( component: ComponentType<C>, viewContainerRef: ViewContainerRef, @@ -59,6 +70,7 @@ export class OzgcloudDialogService { return this.openInCallingComponentContext<C, D, R>(component, viewContainerRef, data, this.GREY_BLUR_CONFIG); } + //TODO private machen und openInContext bei den jeweiligen Stellen nutzen public openInCallingComponentContext<C, D = unknown, R = unknown>( component: ComponentType<C>, viewContainerRef: ViewContainerRef,