diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.html index d324f8d83fe91defe9b5066f4f24d230bc33eb4f..3e7af75f493ada03112994766ea980b90033e282 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.html +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.html @@ -1,24 +1,27 @@ -<h1 class="heading-1 pb-4">Organisationseinheiten</h1> -<p id="absender-desc" class="p-1">Hinterlegen Sie Name und ID der Organisationseinheiten.</p> +<ng-container *ngIf="organisationseinheitItems$ | async as organisationseinheitItems"> + <h1 class="heading-1 pb-4">Organisationseinheiten</h1> + <p id="absender-desc" class="p-1">Hinterlegen Sie Name und ID der Organisationseinheiten.</p> -<admin-organisationseinheit-form - data-test-id="organisationseinheit-form" -></admin-organisationseinheit-form> + <admin-organisationseinheit-form + data-test-id="organisationseinheit-form" + [organisationseinheitItems]="organisationseinheitItems" + ></admin-organisationseinheit-form> -<admin-secondary-button - (clickEmitter)="openDialogForNewGroup()" - data-test-id="organisationseinheit-open-dialog-button" - label="Neue Organisationseinheit anlegen" -> -</admin-secondary-button> -<admin-spinner - data-test-id="organisationseinheit-spinner" - *ngIf="deleteInProgress$ | async" -></admin-spinner> + <admin-secondary-button + (clickEmitter)="openDialogForNewGroup()" + data-test-id="organisationseinheit-open-dialog-button" + label="Neue Organisationseinheit anlegen" + > + </admin-secondary-button> + <admin-spinner + data-test-id="organisationseinheit-spinner" + *ngIf="deleteInProgress$ | async" + ></admin-spinner> -<admin-organisationseinheit-list - [organisationseinheitItems]="organisationseinheitItems$ | async" - (editOrganisationseinheit)="edit($event)" - (deleteOrganisationseinheit)="delete($event)" - data-test-id="organisationseinheit-list" -></admin-organisationseinheit-list> + <admin-organisationseinheit-list + [organisationseinheitItems]="organisationseinheitItems" + (editOrganisationseinheit)="edit($event)" + (deleteOrganisationseinheit)="delete($event)" + data-test-id="organisationseinheit-list" + ></admin-organisationseinheit-list> +</ng-container> diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.spec.ts index 50aed3b14f080f41d6dd560101a3c1a358bc5b8f..126c1e2dbcc1bf491d15a23b982b8481996c078e 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.spec.ts @@ -1,3 +1,4 @@ +import { createStateResource } from '@alfa-client/tech-shared'; import { Mock, dispatchEventFromFixture, @@ -11,14 +12,11 @@ import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { singleCold } from 'libs/tech-shared/test/marbles'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; -import { - createOrganisationseinheit, - createOrganisationseinheitState, -} from '../../../../test/user/user'; +import { createOrganisationseinheit } from '../../../../test/user/user'; import { SecondaryButtonComponent } from '../../shared/secondary-button/secondary-button.component'; import { SpinnerComponent } from '../../shared/spinner/spinner.component'; import { Organisationseinheit } from '../../user/user.model'; -import { UserService } from '../../user/user.service'; +import { OrganisationseinheitService } from '../organisationseinheitService'; import { OrganisationseinheitContainerComponent } from './organisationseinheit-container.component'; import { OrganisationseinheitFormComponent } from './organisationseinheit-form/organisationseinheit-form.component'; import { OrganisationseinheitListComponent } from './organisationseinheit-list/organisationseinheit-list.component'; @@ -27,7 +25,9 @@ describe('OrganisationseinheitContainerComponent', () => { let component: OrganisationseinheitContainerComponent; let fixture: ComponentFixture<OrganisationseinheitContainerComponent>; - const userService: Mock<UserService> = mock(UserService); + const organisationseinheitService: Mock<OrganisationseinheitService> = mock( + OrganisationseinheitService, + ); const dialogOpenButtonSelector: string = getDataTestIdOf( 'organisationseinheit-open-dialog-button', @@ -48,15 +48,15 @@ describe('OrganisationseinheitContainerComponent', () => { MockComponent(OrganisationseinheitListComponent), MockComponent(SpinnerComponent), ], - providers: [{ provide: UserService, useValue: userService }], + providers: [{ provide: OrganisationseinheitService, useValue: organisationseinheitService }], }).compileComponents(); fixture = TestBed.createComponent(OrganisationseinheitContainerComponent); component = fixture.componentInstance; - userService.getOrganisationseinheitState = jest + organisationseinheitService.get = jest .fn() - .mockReturnValue(of(createOrganisationseinheitState(organisationseinheitItems))); + .mockReturnValue(of(createStateResource(organisationseinheitItems))); fixture.detectChanges(); formComponent = getElementFromFixtureByType(fixture, OrganisationseinheitFormComponent); @@ -99,13 +99,13 @@ describe('OrganisationseinheitContainerComponent', () => { const organisationseinheit: Organisationseinheit = organisationseinheitItems[0]; beforeEach(() => { - userService.deleteOrganisationseinheit = jest.fn().mockReturnValue(singleCold(true)); + organisationseinheitService.delete = jest.fn().mockReturnValue(singleCold(true)); }); it('should call service method', () => { component.delete(organisationseinheit); - expect(userService.deleteOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit.id); + expect(organisationseinheitService.delete).toHaveBeenCalledWith(organisationseinheit.id); }); it('should assign delete progress observable', () => { diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.ts index f2bc2061e143c013c4d7f9f3096c2020c8852d3a..b85e0c8447ec080e397018c686a0f44703e58840 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component.ts @@ -1,7 +1,8 @@ +import { StateResource } from '@alfa-client/tech-shared'; import { Component, OnInit, ViewChild } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Organisationseinheit } from '../../user/user.model'; -import { UserService } from '../../user/user.service'; +import { OrganisationseinheitService } from '../organisationseinheitService'; import { OrganisationseinheitFormComponent } from './organisationseinheit-form/organisationseinheit-form.component'; @Component({ @@ -9,16 +10,16 @@ import { OrganisationseinheitFormComponent } from './organisationseinheit-form/o templateUrl: './organisationseinheit-container.component.html', }) export class OrganisationseinheitContainerComponent implements OnInit { - organisationseinheitItems$: Observable<Organisationseinheit[]>; + organisationseinheitItems$: Observable<StateResource<Organisationseinheit[]>>; deleteInProgress$: Observable<boolean> = of(false); @ViewChild(OrganisationseinheitFormComponent) private form!: OrganisationseinheitFormComponent; - constructor(private userService: UserService) {} + constructor(private organisationseinheitService: OrganisationseinheitService) {} ngOnInit(): void { - this.organisationseinheitItems$ = this.userService.getOrganisationseinheitItems(); + this.organisationseinheitItems$ = this.organisationseinheitService.get(); } public openDialogForNewGroup(): void { @@ -30,6 +31,6 @@ export class OrganisationseinheitContainerComponent implements OnInit { } public delete(organisationseinheit: Organisationseinheit): void { - this.deleteInProgress$ = this.userService.deleteOrganisationseinheit(organisationseinheit.id); + this.deleteInProgress$ = this.organisationseinheitService.delete(organisationseinheit.id); } } diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.html index 8d00b44b3737090091e15a34e731f4168b12b601..3bd257438d0545a41b7b791b9574b1cf9184b009 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.html +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.html @@ -1,3 +1,4 @@ +<ng-container *ngIf="submitInProgress$ | async"></ng-container> <dialog #OrganisationseinheitDialog data-test-id="organisationseinheit-dialog" class="bg-gray-50"> <button (click)="OrganisationseinheitDialog.close()" @@ -21,13 +22,13 @@ [formControlName]="OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD" ></text-field> - <admin-primary-button + <ods-button-with-spinner data-test-id="organisationseinheit-save-button" class="justify-self-end" (clickEmitter)="submit()" - [submitInProgress]="submitInProgress$ | async" - label="Speichern" + [stateResource]="organisationseinheitItems" + text="Speichern" > - </admin-primary-button> + </ods-button-with-spinner> </form> </dialog> diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.ts index 5224f6db0c0c1abd27fba74b2adc3067fe00a460..9796922d5e713c9ff2bcc2964d0c70d94f9c4572 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.ts @@ -1,4 +1,5 @@ -import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; +import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { Observable, of, tap } from 'rxjs'; import { Organisationseinheit } from '../../../user/user.model'; import { OrganisationseinheitFormservice } from './organisationseinheit.formservice'; @@ -9,6 +10,9 @@ import { OrganisationseinheitFormservice } from './organisationseinheit.formserv providers: [OrganisationseinheitFormservice], }) export class OrganisationseinheitFormComponent implements AfterViewInit { + @Input() + organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([]); + static CREATE_LABEL: string = 'Neue Organisationseinheit anlegen'; static EDIT_LABEL: string = 'Organisationseinheit bearbeiten'; diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.ts index 850b5f775f723f0b27073ac5de0b99d7a2c877e8..3309b9a813a1158fc08e7ca007e35d9f9f90475d 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.ts @@ -1,17 +1,20 @@ -import { isNotNil } from '@alfa-client/tech-shared'; -import { Injectable } from '@angular/core'; +import { createStateResource, isNotNil, StateResource } from '@alfa-client/tech-shared'; +import { Injectable, Input } from '@angular/core'; import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { Observable, catchError, of } from 'rxjs'; +import { catchError, Observable, of } from 'rxjs'; import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType, } from '../../../user/user.model'; -import { UserService } from '../../../user/user.service'; import { getOrganisationseinheitErrorMessage } from '../../../user/user.util'; +import { OrganisationseinheitService } from '../../organisationseinheitService'; @Injectable() export class OrganisationseinheitFormservice { + @Input() + organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([]); + public static readonly ORGANISATIONSEINHEIT_NAME_FIELD: string = 'name'; public static readonly ORGANISATIONSEINHEIT_IDS_FIELD: string = 'organisationseinheit'; @@ -21,7 +24,7 @@ export class OrganisationseinheitFormservice { constructor( private formBuilder: UntypedFormBuilder, - private userService: UserService, + private organisationsEinheitService: OrganisationseinheitService, ) { this.form = this.formBuilder.group({ [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: new FormControl(''), @@ -47,14 +50,14 @@ export class OrganisationseinheitFormservice { } create(): Observable<boolean> { - return this.userService.createOrganisationseinheit( - this.getName(), - this.getOrganisationseinheitIds(), - ); + return this.organisationsEinheitService.create({ + name: this.getName(), + organisationseinheitIds: this.getOrganisationseinheitIds(), + }); } save(): Observable<boolean> { - return this.userService.saveOrganisationseinheit({ + return this.organisationsEinheitService.save({ ...this.source, name: this.getName(), organisationseinheitIds: this.getOrganisationseinheitIds(), diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.html index f26e6e0c0d028d7d8bf3b224279cf662d80e5603..35f04a2f2e65d9cd6354ccc65e4ebe980c7ad8bb 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.html +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.html @@ -1,52 +1,54 @@ -<table - *ngIf="organisationseinheitItems.length; else emptyMessage" - aria-label="Keycloak-Gruppen mit OrganisationseinheitIDs" - class="mb-2 mt-2 table-fixed" - data-test-id="organisationseinheit-table" -> - <tr class="invisible"> - <th scope="col">Name</th> - <th scope="col">Attribute</th> - </tr> - <tr *ngFor="let organisationseinheit of organisationseinheitItems" [id]="organisationseinheit.id"> - <td - [attr.data-test-id]=" - 'organisationseinheit-name-' + organisationseinheit.id | convertForDataTest - " - class="w-96 border border-slate-500 p-2 font-bold" - > - {{ organisationseinheit.name }} - </td> - <td - [attr.data-test-id]=" - 'organisationseinheit-attr-' + organisationseinheit.id | convertForDataTest - " - class="w-96 border border-slate-500 p-2" - > - OrganisationseinheitID: {{ organisationseinheit.organisationseinheitIds.join(', ') }} - <admin-more-menu class="float-right"> - <admin-more-item-button - (clickEmitter)="editOrganisationseinheit.emit(organisationseinheit)" - more-menu-item - [attr.data-test-id]=" - 'organisationseinheit-edit-' + organisationseinheit.id | convertForDataTest - " - label="Bearbeiten" - ></admin-more-item-button> - <admin-more-item-button - (clickEmitter)="deleteOrganisationseinheit.emit(organisationseinheit)" - more-menu-item - [attr.data-test-id]=" - 'organisationseinheit-delete-' + organisationseinheit.id | convertForDataTest - " - label="Löschen" - ></admin-more-item-button> - </admin-more-menu> - </td> - </tr> -</table> -<ng-template #emptyMessage - ><span data-test-id="organisationseinheit-empty-message" class="mb-2 mt-2 block italic" - >Keine Organisationseinheiten vorhanden.</span +<ng-container *ngIf="organisationseinheitItems.resource"> + <table + *ngIf="organisationseinheitItems.resource.length; else emptyMessage" + aria-label="Keycloak-Gruppen mit OrganisationseinheitIDs" + class="mb-2 mt-2 table-fixed" + data-test-id="organisationseinheit-table" > -</ng-template> + <tr class="invisible"> + <th scope="col">Name</th> + <th scope="col">Attribute</th> + </tr> + <tr *ngFor="let organisationseinheit of organisationseinheitItems.resource" [id]="organisationseinheit.id"> + <td + [attr.data-test-id]=" + 'organisationseinheit-name-' + organisationseinheit.id | convertForDataTest + " + class="w-96 border border-slate-500 p-2 font-bold" + > + {{ organisationseinheit.name }} + </td> + <td + [attr.data-test-id]=" + 'organisationseinheit-attr-' + organisationseinheit.id | convertForDataTest + " + class="w-96 border border-slate-500 p-2" + > + OrganisationseinheitID: {{ organisationseinheit.organisationseinheitIds.join(', ') }} + <admin-more-menu class="float-right"> + <admin-more-item-button + (clickEmitter)="editOrganisationseinheit.emit(organisationseinheit)" + more-menu-item + [attr.data-test-id]=" + 'organisationseinheit-edit-' + organisationseinheit.id | convertForDataTest + " + label="Bearbeiten" + ></admin-more-item-button> + <admin-more-item-button + (clickEmitter)="deleteOrganisationseinheit.emit(organisationseinheit)" + more-menu-item + [attr.data-test-id]=" + 'organisationseinheit-delete-' + organisationseinheit.id | convertForDataTest + " + label="Löschen" + ></admin-more-item-button> + </admin-more-menu> + </td> + </tr> + </table> + <ng-template #emptyMessage + ><span data-test-id="organisationseinheit-empty-message" class="mb-2 mt-2 block italic" + >Keine Organisationseinheiten vorhanden.</span + > + </ng-template> +</ng-container> diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.spec.ts index 912a82f8a93e2865f011ec5a8f0c40905a910d85..53a1347003495d3b5501794c11b04af3f60008c7 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.spec.ts @@ -1,4 +1,9 @@ -import { ConvertForDataTestPipe, convertForDataTest } from '@alfa-client/tech-shared'; +import { + ConvertForDataTestPipe, + StateResource, + convertForDataTest, + createStateResource, +} from '@alfa-client/tech-shared'; import { dispatchEventFromFixture, existsAsHtmlElement, @@ -57,10 +62,10 @@ describe('OrganisationseinheitListComponent', () => { }); describe('with organisationseinheit items', () => { - const organisationseinheitItems: Organisationseinheit[] = [ + const organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([ createOrganisationseinheit(), createOrganisationseinheit(), - ]; + ]); beforeEach(() => { component.organisationseinheitItems = organisationseinheitItems; fixture.detectChanges(); @@ -79,13 +84,13 @@ describe('OrganisationseinheitListComponent', () => { const rowIds: string[] = rows.map((row) => row.id); expect(rowIds).toEqual( - organisationseinheitItems.map( + organisationseinheitItems.resource.map( (organisationseinheit: Organisationseinheit) => organisationseinheit.id, ), ); }); - it.each(organisationseinheitItems)( + it.each(organisationseinheitItems.resource)( 'should show name of organisationseinheit %#', (organisationseinheit: Organisationseinheit) => { const nameTableCell = getElementFromFixture( @@ -97,7 +102,7 @@ describe('OrganisationseinheitListComponent', () => { }, ); - it.each(organisationseinheitItems)( + it.each(organisationseinheitItems.resource)( 'should show organisationseinheitId of organisationseinheit %#', (organisationseinheit: Organisationseinheit) => { const attrTableCell = getElementFromFixture( @@ -111,7 +116,7 @@ describe('OrganisationseinheitListComponent', () => { }, ); - it.each(organisationseinheitItems)( + it.each(organisationseinheitItems.resource)( 'should emit editOrganisationseinheit %# on edit button click ', (organisationseinheit: Organisationseinheit) => { component.editOrganisationseinheit.emit = jest.fn(); @@ -128,7 +133,7 @@ describe('OrganisationseinheitListComponent', () => { }, ); - it.each(organisationseinheitItems)( + it.each(organisationseinheitItems.resource)( 'should emit deleteOrganisationseinheit %# on delete button click ', (organisationseinheit: Organisationseinheit) => { component.deleteOrganisationseinheit.emit = jest.fn(); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.ts index dadd2f0401682461a67e76f73e79dfc09afb54be..39b105cb05759aad5be906fb7b78e7b0b167dcb6 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component.ts @@ -1,3 +1,4 @@ +import { createStateResource, StateResource } from '@alfa-client/tech-shared'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Organisationseinheit } from '../../../user/user.model'; @@ -7,7 +8,7 @@ import { Organisationseinheit } from '../../../user/user.model'; }) export class OrganisationseinheitListComponent { @Input() - organisationseinheitItems: Organisationseinheit[] = []; + organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([]); @Output() editOrganisationseinheit: EventEmitter<Organisationseinheit> = new EventEmitter(); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..97557340dccaf5851d41a6b9bcbf55916a161ff0 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.spec.ts @@ -0,0 +1,48 @@ +import { mock, Mock, useFromMock } from '@alfa-client/test-utils'; +import { createOrganisationseinheit } from '../../../test/user/user'; +import { UserRepository } from '../user/user.repository.service'; +import { OrganisationseinheitService } from './organisationseinheitService'; + +describe('OrganisationseinheitService', () => { + let service: OrganisationseinheitService; + let repository: Mock<UserRepository>; + + const organisationseinheit = createOrganisationseinheit(); + + beforeEach(() => { + repository = mock(UserRepository); + service = new OrganisationseinheitService(useFromMock(repository)); + }); + + describe('getItemsFromKeycloak', () => { + it('should call findOrganisationseinheitItems from userRepository', () => { + service.getItemsFromKeycloak(); + + expect(repository.findOrganisationseinheitItems).toHaveBeenCalled(); + }); + }); + + describe('saveInKeycloak', () => { + it('should call saveOrganisationseinheit from userRepository', () => { + service.saveInKeycloak(organisationseinheit); + + expect(repository.saveOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit); + }); + }); + + describe('createInKeycloak', () => { + it('should call createOrganisationseinheit from userRepository', () => { + service.createInKeycloak(organisationseinheit); + + expect(repository.createOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit); + }); + }); + + describe('deleteInKeycloak', () => { + it('should call deleteOrganisationseinheit from userRepository', () => { + service.deleteInKeycloak(organisationseinheit.id); + + expect(repository.deleteOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit.id); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.ts new file mode 100644 index 0000000000000000000000000000000000000000..4bd882e2f5a1240c58b135c0136989a725c78fba --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheitService.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { KeycloakResourceService } from '../user/keycloak.resource.service'; +import { Organisationseinheit } from '../user/user.model'; +import { UserRepository } from '../user/user.repository.service'; + +@Injectable({ + providedIn: 'root', +}) +export class OrganisationseinheitService extends KeycloakResourceService<Organisationseinheit> { + constructor(private userRepository: UserRepository) { + super(); + } + + getItemsFromKeycloak(): Observable<Organisationseinheit[]> { + return this.userRepository.findOrganisationseinheitItems(); + } + saveInKeycloak(organisationseinheit: Organisationseinheit): Observable<void> { + return this.userRepository.saveOrganisationseinheit(organisationseinheit); + } + + createInKeycloak(organisationseinheit: { + name: string; + organisationseinheitIds: string[]; + }): Observable<Organisationseinheit> { + return this.userRepository.createOrganisationseinheit(organisationseinheit); + } + + deleteInKeycloak(id: string): Observable<void> { + return this.userRepository.deleteOrganisationseinheit(id); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f9b19d82c3bd9ca6b91cf8e723026f42de8d9d52 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.spec.ts @@ -0,0 +1,242 @@ +import { fakeAsync, tick } from '@angular/core/testing'; +import faker from '@faker-js/faker'; +import { cold } from 'jest-marbles'; +import { + StateResource, + createEmptyStateResource, +} from 'libs/tech-shared/src/lib/resource/resource.util'; +import { createDummy } from 'libs/tech-shared/test/dummy'; +import { singleCold } from 'libs/tech-shared/test/marbles'; +import { Observable, of } from 'rxjs'; +import * as resourceUtil from '../../../../../tech-shared/src/lib/resource/resource.util'; +import { KeycloakResourceService } from './keycloak.resource.service'; + +describe('KeycloakResourceService', () => { + let service: KeycloakResourceService<unknown>; + + const dummyObject = createDummy(); + const id = faker.random.word(); + const dummyAction = of(dummyObject); + + beforeEach(() => { + service = new TestResourceService(); + }); + + describe('get', () => { + beforeEach(() => { + service.handleChanges = jest.fn(); + }); + + it('should return stateResource as observable', (done) => { + service.get().subscribe((stateResource) => { + expect(stateResource).toBe(service.stateResource.value); + done(); + }); + }); + + it('should call handleChanges ', fakeAsync(() => { + service.get().subscribe(); + + expect(service.handleChanges).toHaveBeenCalled(); + })); + }); + + describe('handleChanges', () => { + beforeEach(() => { + service.loadResource = jest.fn(); + }); + + it('should call loadResource when loading is required', () => { + jest.spyOn(resourceUtil, 'isLoadingRequired').mockReturnValue(true); + + service.handleChanges(createEmptyStateResource()); + + expect(service.loadResource).toHaveBeenCalled(); + }); + + it('should not call loadResource when loading is required', () => { + jest.spyOn(resourceUtil, 'isLoadingRequired').mockReturnValue(false); + + service.handleChanges(createEmptyStateResource()); + + expect(service.loadResource).not.toHaveBeenCalled(); + }); + }); + + describe('loadResource', () => { + it('should call set loading', () => { + service.setLoading = jest.fn(); + + service.loadResource(); + + expect(service.setLoading).toHaveBeenCalled(); + }); + + it('should update Resource', fakeAsync(() => { + const dummyItems = [createDummy(), createDummy()]; + service.getItemsFromKeycloak = jest.fn().mockReturnValue(of(dummyItems)); + + service.loadResource(); + tick(); + + expect(service.stateResource.value.resource).toEqual(dummyItems); + })); + }); + + describe('create', () => { + const saveObject = createDummy(); + + it('should call handleLoading', () => { + service.handleLoading = jest.fn(); + + service.create(saveObject); + + expect(service.handleLoading).toHaveBeenCalled(); + }); + + it('should call createInKeycloak', () => { + service.createInKeycloak = jest.fn().mockReturnValue(of({})); + + service.create(saveObject); + + expect(service.createInKeycloak).toHaveBeenCalled(); + }); + }); + + describe('save', () => { + it('should call handleLoading', () => { + service.handleLoading = jest.fn(); + + service.save(dummyObject); + + expect(service.handleLoading).toHaveBeenCalled(); + }); + + it('should call createInKeycloak', () => { + service.saveInKeycloak = jest.fn().mockReturnValue(of({})); + + service.save(dummyObject); + + expect(service.saveInKeycloak).toHaveBeenCalled(); + }); + }); + + describe('delete', () => { + it('should call handleLoading', () => { + service.handleLoading = jest.fn(); + + service.delete(id); + + expect(service.handleLoading).toHaveBeenCalled(); + }); + + it('should call createInKeycloak', () => { + service.saveInKeycloak = jest.fn().mockReturnValue(of({})); + + service.save(id); + + expect(service.saveInKeycloak).toHaveBeenCalled(); + }); + }); + + describe('handleLoading', () => { + it('should set loading', () => { + service.handleLoading(dummyAction); + + expect(service.stateResource.value.loading).toBe(true); + }); + + it('should call refreshAfterFirstEmit', () => { + service.refreshAfterFirstEmit = jest.fn().mockReturnValue(dummyAction); + + service.handleLoading(dummyAction); + + expect(service.refreshAfterFirstEmit).toHaveBeenCalled(); + }); + + it('should call progress', () => { + service.progress = jest.fn().mockReturnValue(dummyAction); + + service.handleLoading(dummyAction); + + expect(service.progress).toHaveBeenCalled(); + }); + }); + + // warum funktioniert hier fakeAsync nicht? + describe('refreshAfterFirstEmit', () => { + it('should call refresh after first emit', (done) => { + service.refresh = jest.fn(); + + service.refreshAfterFirstEmit(dummyAction).subscribe(() => { + expect(service.refresh).toHaveBeenCalled(); + done(); + }); + }); + }); + + describe('progress', () => { + it('should emit true at the start and false after first parameter emit', () => { + const result = service.progress(cold('--x', { x: dummyObject })); + + expect(result).toBeObservable(cold('a-b', { a: true, b: false })); + }); + }); + + describe('setLoading', () => { + it('should set loading in state to true without parameter', () => { + service.setLoading(); + + expect(service.stateResource.value.loading).toEqual(true); + }); + + it('should set loading in state to false for parameter false', () => { + service.setLoading(false); + + expect(service.stateResource.value.loading).toEqual(false); + }); + }); + + describe('refresh', () => { + it('should set reload in state to true', () => { + service.refresh(); + + expect(service.stateResource.value.reload).toBe(true); + }); + + it('should clear resource in state', () => { + service.refresh(); + + expect(service.stateResource.value.resource).toBe(null); + }); + }); + + describe('select resource', () => { + it('should return state resource', () => { + const stateResource = createEmptyStateResource<unknown[]>(); + service.stateResource.next(stateResource); + + const resource$: Observable<StateResource<unknown[]>> = service.selectResource(); + + expect(resource$).toBeObservable(singleCold(stateResource)); + }); + }); +}); + +class TestResourceService extends KeycloakResourceService<unknown> { + getItemsFromKeycloak(): Observable<unknown[]> { + return of([null]); + } + + saveInKeycloak(): Observable<void> { + return of(null); + } + + createInKeycloak(): Observable<void> { + return of(null); + } + + deleteInKeycloak(): Observable<void> { + return of(null); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.ts b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ed82d400b566d68ba97b19d902bd446b1d9e12d --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.ts @@ -0,0 +1,94 @@ +import { + createEmptyStateResource, + createStateResource, + isLoadingRequired, + StateResource, +} from '@alfa-client/tech-shared'; +import { BehaviorSubject, first, map, Observable, startWith, tap } from 'rxjs'; + +// KeycloakListResourceService? +export abstract class KeycloakResourceService<T> { + readonly stateResource: BehaviorSubject<StateResource<T[]>> = new BehaviorSubject( + createEmptyStateResource(), + ); + + public get() { + return this.stateResource + .asObservable() + .pipe(tap((stateResource) => this.handleChanges(stateResource))); + } + + handleChanges(stateResource: StateResource<T[]>) { + if (isLoadingRequired(stateResource)) { + this.loadResource(); + } + } + + loadResource(): void { + this.setLoading(); + this.getItemsFromKeycloak() + .pipe(first()) + .subscribe((items) => this.updateResource(items)); + } + + abstract getItemsFromKeycloak(): Observable<T[]>; + + private updateResource(items: T[]) { + this.stateResource.next(createStateResource(items)); + } + + public create(item: Partial<T>): Observable<boolean> { + return this.handleLoading(this.createInKeycloak(item)); + } + + abstract createInKeycloak(item: Partial<T>): Observable<T>; + + public save(item: T): Observable<boolean> { + return this.handleLoading(this.saveInKeycloak(item)); + } + + abstract saveInKeycloak(item: T): Observable<void>; + + public delete(id: string): Observable<boolean> { + return this.handleLoading(this.deleteInKeycloak(id)); + } + + abstract deleteInKeycloak(id: string): Observable<void>; + + handleLoading(action: Observable<unknown>): Observable<boolean> { + this.setLoading(); + return this.progress(this.refreshAfterFirstEmit(action)); + } + + refreshAfterFirstEmit(action: Observable<unknown>): Observable<unknown> { + return action.pipe( + first(), + tap(() => this.refresh()), + ); + } + + progress(action: Observable<unknown>): Observable<boolean> { + return action.pipe( + map(() => false), + startWith(true), + ); + } + + setLoading(loading: boolean = true): void { + this.stateResource.next({ + ...this.stateResource.value, + loading, + }); + } + + refresh(): void { + this.stateResource.next({ + ...createEmptyStateResource(), + reload: true, + }); + } + + public selectResource(): Observable<StateResource<T[]>> { + return this.stateResource.asObservable(); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.repository.service.ts b/alfa-client/libs/admin/settings/src/lib/user/user.repository.service.ts index 72aec105655334902e5b61799f314c4dcc479eb7..bca73c460b000f7480db5bba114929d034c7d67b 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.repository.service.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.repository.service.ts @@ -72,21 +72,20 @@ export class UserRepository { ).pipe(this.rethrowMappedGroupsError()); } - public createOrganisationseinheit( - name: string, - organisationseinheitIds: string[], - ): Observable<Organisationseinheit> { + public createOrganisationseinheit(organisationseinheit: { + name: string; + organisationseinheitIds: string[]; + }): Observable<Organisationseinheit> { return from( this.kcAdminClient.groups.create({ - name, - attributes: { organisationseinheitId: organisationseinheitIds }, + name: organisationseinheit.name, + attributes: { organisationseinheitId: organisationseinheit.organisationseinheitIds }, }), ).pipe( map( ({ id }): Organisationseinheit => ({ + ...organisationseinheit, id, - name, - organisationseinheitIds: organisationseinheitIds, }), ), this.rethrowMappedGroupsError(), diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.service.ts b/alfa-client/libs/admin/settings/src/lib/user/user.service.ts index e8ace5eaa78c6121639bccb6f16ad79311bc3f77..4dd8938c040e2f5dcc53b1f3cf8cc41e609dd61f 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.service.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.service.ts @@ -42,7 +42,7 @@ export class UserService { organisationseinheitIds: string[], ): Observable<boolean> { return this.setLoadingUntilFirst( - this.userRepository.createOrganisationseinheit(name, organisationseinheitIds), + this.userRepository.createOrganisationseinheit({ name, organisationseinheitIds }), (organisationseinheit: Organisationseinheit) => { this.addOrganisationseinheit(organisationseinheit); }, diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.spec.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8335ce2e8d416270dccf93a3c3fa0e655e2251dc --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.spec.ts @@ -0,0 +1,21 @@ +import { mock, Mock, useFromMock } from '@alfa-client/test-utils'; +import { UserRepository } from '../user/user.repository.service'; +import { UserAndRolesService } from './userAndRolesService'; + +describe('OrganisationseinheitService', () => { + let service: UserAndRolesService; + let repository: Mock<UserRepository>; + + beforeEach(() => { + repository = mock(UserRepository); + service = new UserAndRolesService(useFromMock(repository)); + }); + + describe('getItemsFromKeycloak', () => { + it('should call findOrganisationseinheitItems from userRepository', () => { + service.getItemsFromKeycloak(); + + expect(repository.getUsers).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.ts new file mode 100644 index 0000000000000000000000000000000000000000..33cea0dc83abbd108ebc19e6d19c41772c99de14 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/userAndRolesService.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { KeycloakResourceService } from '../user/keycloak.resource.service'; +import { User } from '../user/user.model'; +import { UserRepository } from '../user/user.repository.service'; + +@Injectable({ + providedIn: 'root', +}) +export class UserAndRolesService extends KeycloakResourceService<User> { + constructor(private userRepository: UserRepository) { + super(); + } + + getItemsFromKeycloak(): Observable<User[]> { + return this.userRepository.getUsers(); + } + + createInKeycloak(item: Partial<User>): Observable<User> { + throw new Error('Method not implemented.'); + } + + saveInKeycloak(item: User): Observable<void> { + throw new Error('Method not implemented.'); + } + + deleteInKeycloak(id: string): Observable<void> { + throw new Error('Method not implemented.'); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html index a62acb25d8136a1bdb913f30b00b2b6347334f5e..96bab232f6ade64a1f96eff377da82bc77922195 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html @@ -2,7 +2,7 @@ <ods-button-with-spinner text="Benutzer hinzufügen" class="py-8" dataTestId="add-user-button" /> <ng-container *ngIf="users$ | async as users"> <ul> - <li *ngFor="let user of users"> + <li *ngFor="let user of users.resource"> {{ user.firstName }} </li> </ul> diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts index 05f641c948593ea3fefa0771d8eee46d41e070fe..f7adf7487c55c682370c0146fc52ff3daeb65be6 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts @@ -1,7 +1,8 @@ import { Component } from '@angular/core'; +import { StateResource } from '@alfa-client/tech-shared'; import { Observable } from 'rxjs'; import { User } from '../user/user.model'; -import { UserService } from '../user/user.service'; +import { UserAndRolesService } from './userAndRolesService'; @Component({ selector: 'admin-users-roles', @@ -9,9 +10,9 @@ import { UserService } from '../user/user.service'; styleUrl: './users-roles.component.scss', }) export class UsersRolesComponent { - users$: Observable<User[]>; + users$: Observable<StateResource<User[]>>; - constructor(private userService: UserService) { - this.users$ = this.userService.getUsers(); + constructor(private userAndRolesService: UserAndRolesService) { + this.users$ = this.userAndRolesService.get(); } } 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 2489bb3d94ad7f490abf17231a075dc0050700c2..cc180a4ec9069d0946eb9dcaaa3be7d270eb82ab 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 @@ -2,7 +2,6 @@ import { CommandResource, hasCommandError } from '@alfa-client/command-shared'; import { StateResource, createEmptyStateResource, isLoaded } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { Resource } from '@ngxp/rest'; import { ButtonComponent, ErrorMessageComponent, buttonVariants } from '@ods/system'; import { VariantProps } from 'class-variance-authority'; import { isNil } from 'lodash-es'; @@ -33,7 +32,7 @@ type ButtonVariants = VariantProps<typeof buttonVariants>; export class ButtonWithSpinnerComponent implements OnInit { @Input() text: string = ''; @Input() dataTestId: string = ''; - @Input() stateResource: StateResource<Resource>; + @Input() stateResource: StateResource<unknown>; @Input() variant: ButtonVariants['variant'] = 'primary'; @Input() size: ButtonVariants['size'] = 'medium'; @@ -43,8 +42,8 @@ export class ButtonWithSpinnerComponent implements OnInit { this.stateResource = this.getStateResource(); } - getStateResource(): StateResource<Resource> { - return isNil(this.stateResource) ? createEmptyStateResource<Resource>() : this.stateResource; + getStateResource(): StateResource<unknown> { + return isNil(this.stateResource) ? createEmptyStateResource<unknown>() : this.stateResource; } get isLoading(): boolean {