diff --git a/alfa-client/apps/admin/src/app/app.component.html b/alfa-client/apps/admin/src/app/app.component.html index aec8d53fbb5c0848c05817014a197e6fbec5ccdf..69e4b50b55c887010dbdea890b1f62c3b275478f 100644 --- a/alfa-client/apps/admin/src/app/app.component.html +++ b/alfa-client/apps/admin/src/app/app.component.html @@ -21,6 +21,10 @@ <!-- <ods-nav-item caption="Organisationseinheiten" to="/organisationseinheiten">--> <!-- <ods-orga-unit-icon icon />--> <!-- </ods-nav-item>--> + <ods-nav-item caption="Benutzer & Rollen" to="/benutzer_und_rollen"> + <ods-users-icon class="stroke-text" icon /> + </ods-nav-item> + <hr /> <ods-nav-item caption="Postfach" to="/postfach"> <ods-mailbox-icon icon /> </ods-nav-item> diff --git a/alfa-client/apps/admin/src/app/app.component.spec.ts b/alfa-client/apps/admin/src/app/app.component.spec.ts index d10cfd74e6f7e55e727bf6779c494f0f5311f8b7..fbf5f0baf5ba16a4c33e3140833521efb6e7a367 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -20,9 +20,9 @@ import { NavItemComponent, NavbarComponent, OrgaUnitIconComponent, + UsersIconComponent, } from '@ods/system'; import { AuthenticationService } from 'authentication'; -import { NavigationComponent } from 'libs/admin/settings/src/lib/navigation/navigation.component'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent, MockDirective } from 'ng-mocks'; @@ -54,9 +54,9 @@ describe('AppComponent', () => { await TestBed.configureTestingModule({ declarations: [ AppComponent, - MockComponent(NavigationComponent), MockComponent(AdminLogoIconComponent), MockComponent(OrgaUnitIconComponent), + MockComponent(UsersIconComponent), MockComponent(MailboxIconComponent), MockComponent(UserProfileButtonContainerComponent), MockComponent(UnavailablePageComponent), diff --git a/alfa-client/apps/admin/src/app/app.module.ts b/alfa-client/apps/admin/src/app/app.module.ts index 350fef2058a41e30e107d9cc3d691f98e8a5269f..7c9c63cf24edf02b34858e9a0f9f1ce98b2ffdcd 100644 --- a/alfa-client/apps/admin/src/app/app.module.ts +++ b/alfa-client/apps/admin/src/app/app.module.ts @@ -22,6 +22,7 @@ import { NavItemComponent, NavbarComponent, OrgaUnitIconComponent, + UsersIconComponent, } from '@ods/system'; import { OAuthModule } from 'angular-oauth2-oidc'; import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor'; @@ -30,6 +31,7 @@ import { environment } from '../environments/environment'; import { OrganisationseinheitPageComponent } from '../pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; +import { UserRolesPageComponent } from '../pages/users-roles/user-roles-page/user-roles-page.component'; import { AppComponent } from './app.component'; import { appRoutes } from './app.routes'; @@ -37,6 +39,7 @@ import { appRoutes } from './app.routes'; declarations: [ AppComponent, PostfachPageComponent, + UserRolesPageComponent, OrganisationseinheitPageComponent, UserProfileButtonContainerComponent, UnavailablePageComponent, @@ -50,6 +53,7 @@ import { appRoutes } from './app.routes'; NavbarComponent, OrgaUnitIconComponent, LogoutIconComponent, + UsersIconComponent, MailboxIconComponent, RouterModule.forRoot(appRoutes), BrowserModule, diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index fdff512b76c4e15d4026c8c250cce90357cf98be..9594ce3ee2736d7a990774c0b57db4f75e3a7582 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -1,5 +1,6 @@ import { Route } from '@angular/router'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; +import { UserRolesPageComponent } from '../pages/users-roles/user-roles-page/user-roles-page.component'; export const appRoutes: Route[] = [ { @@ -12,6 +13,11 @@ export const appRoutes: Route[] = [ component: PostfachPageComponent, title: 'Admin | Postfach', }, + { + path: 'benutzer_und_rollen', + component: UserRolesPageComponent, + title: 'Admin | Benutzer & Rollen', + }, // { // path: 'organisationseinheiten', // component: OrganisationseinheitPageComponent, diff --git a/alfa-client/apps/admin/src/index.html b/alfa-client/apps/admin/src/index.html index b1d42def4c92709d7794e04aaa3484164a21b4a8..e9b77e77d1493dd0f7e67f051941b5c48fff1b33 100644 --- a/alfa-client/apps/admin/src/index.html +++ b/alfa-client/apps/admin/src/index.html @@ -1,5 +1,5 @@ <!doctype html> -<html lang="en" class="h-full bg-white antialiased"> +<html lang="de" class="h-full bg-white antialiased"> <head> <meta charset="utf-8" /> <title>admin</title> @@ -7,7 +7,9 @@ <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" type="image/x-icon" href="favicon.ico" /> </head> - <body class="flex max-h-full min-h-full bg-white text-black dark:bg-slate-100 dark:bg-slate-900"> + <body + class="flex max-h-full min-h-full overflow-hidden bg-white text-black dark:bg-slate-900 dark:text-slate-100" + > <app-root class="flex w-full flex-col"></app-root> </body> </html> diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3000e0ffbac09b63c3d12d715638ab67f610bc39 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html @@ -0,0 +1 @@ +<admin-users-roles /> \ No newline at end of file diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1285eda7400123a525353d217969ab1d0d681074 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts @@ -0,0 +1,24 @@ +import { UsersRolesComponent } from '@admin-client/admin-settings'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { UserRolesPageComponent } from './user-roles-page.component'; + +describe('UserRolesPageComponent', () => { + let component: UserRolesPageComponent; + let fixture: ComponentFixture<UserRolesPageComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserRolesPageComponent], + imports: [MockComponent(UsersRolesComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(UserRolesPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b3576a76e34d01f319b4a8b2c4f44c82c62f44f --- /dev/null +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-user-roles-page', + templateUrl: './user-roles-page.component.html', +}) +export class UserRolesPageComponent {} diff --git a/alfa-client/libs/admin/settings/src/index.ts b/alfa-client/libs/admin/settings/src/index.ts index 8d33b086734e167bd7ba0f3b40e56033948b02a1..c4fb8070ef7b2b2df0fcfbe70f30c84a06235327 100644 --- a/alfa-client/libs/admin/settings/src/index.ts +++ b/alfa-client/libs/admin/settings/src/index.ts @@ -2,3 +2,4 @@ export * from './lib/admin-settings.module'; export * from './lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component'; export * from './lib/postfach/postfach-container/postfach-container.component'; export * from './lib/shared/navigation-item/navigation-item.component'; +export * from './lib/users-roles/users-roles.component'; diff --git a/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts b/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts index a6865423b56b2537999ce25764a3357a883fe0da..321e7c04c5850b85027d827f2bc43e971cdef1b1 100644 --- a/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts +++ b/alfa-client/libs/admin/settings/src/lib/admin-settings.module.ts @@ -7,30 +7,18 @@ import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import KcAdminClient from '@keycloak/keycloak-admin-client'; import { ButtonWithSpinnerComponent, TextareaEditorComponent } from '@ods/component'; -import { TextInputComponent } from '@ods/system'; -import { - createSettingListResourceService, - SettingListResourceService, -} from './admin-settings-resource.service'; +import { MailboxIconComponent, PersonIconComponent, TextInputComponent } from '@ods/system'; +import { createSettingListResourceService, SettingListResourceService } from './admin-settings-resource.service'; import { SettingsService } from './admin-settings.service'; -import { - ConfigurationResourceService, - createConfigurationResourceService, -} from './configuration/configuration-resource.service'; +import { ConfigurationResourceService, createConfigurationResourceService } from './configuration/configuration-resource.service'; import { ConfigurationService } from './configuration/configuration.service'; -import { NavigationComponent } from './navigation/navigation.component'; import { OrganisationseinheitContainerComponent } from './organisationseinheit/organisationseinheit-container/organisationseinheit-container.component'; import { OrganisationseinheitFormComponent } from './organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component'; import { OrganisationseinheitListComponent } from './organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component'; -import { OrganisationseinheitNavigationItemComponent } from './organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component'; import { PostfachContainerComponent } from './postfach/postfach-container/postfach-container.component'; import { PostfachFormComponent } from './postfach/postfach-container/postfach-form/postfach-form.component'; import { PostfachSignaturComponent } from './postfach/postfach-container/postfach-form/postfach-signatur/postfach-signatur.component'; -import { PostfachNavigationItemComponent } from './postfach/postfach-navigation-item/postfach-navigation-item.component'; -import { - createPostfachResourceService, - PostfachResourceService, -} from './postfach/postfach-resource.service'; +import { createPostfachResourceService, PostfachResourceService } from './postfach/postfach-resource.service'; import { PostfachService } from './postfach/postfach.service'; import { MoreItemButtonComponent } from './shared/more-menu/more-item-button/more-item-button.component'; import { MoreMenuComponent } from './shared/more-menu/more-menu.component'; @@ -39,6 +27,8 @@ import { PrimaryButtonComponent } from './shared/primary-button/primary-button.c import { SecondaryButtonComponent } from './shared/secondary-button/secondary-button.component'; import { SpinnerComponent } from './shared/spinner/spinner.component'; import { TextFieldComponent } from './shared/text-field/text-field.component'; +import { ToUserNamePipe } from './user/to-user-name.pipe'; +import { UsersRolesComponent } from './users-roles/users-roles.component'; @NgModule({ declarations: [ @@ -47,17 +37,15 @@ import { TextFieldComponent } from './shared/text-field/text-field.component'; PostfachSignaturComponent, NavigationItemComponent, TextFieldComponent, - PostfachNavigationItemComponent, OrganisationseinheitContainerComponent, OrganisationseinheitFormComponent, PrimaryButtonComponent, - NavigationComponent, SecondaryButtonComponent, - OrganisationseinheitNavigationItemComponent, OrganisationseinheitListComponent, MoreMenuComponent, MoreItemButtonComponent, SpinnerComponent, + UsersRolesComponent, ], imports: [ CommonModule, @@ -67,13 +55,11 @@ import { TextFieldComponent } from './shared/text-field/text-field.component'; TextInputComponent, ButtonWithSpinnerComponent, TextareaEditorComponent, + MailboxIconComponent, + PersonIconComponent, + ToUserNamePipe, ], - exports: [ - PostfachContainerComponent, - OrganisationseinheitContainerComponent, - NavigationComponent, - NavigationItemComponent, - ], + exports: [PostfachContainerComponent, OrganisationseinheitContainerComponent, NavigationItemComponent, UsersRolesComponent], providers: [ ConfigurationService, SettingsService, diff --git a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.html b/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.html deleted file mode 100644 index aa69f82b45426328c850c413c0653d4a074c7ac5..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.html +++ /dev/null @@ -1,2 +0,0 @@ -<!--<admin-organisationseinheit-navigation-item></admin-organisationseinheit-navigation-item>--> -<admin-postfach-navigation-item></admin-postfach-navigation-item> diff --git a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.scss b/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.spec.ts deleted file mode 100644 index 2598e618c4eb36e2762d7f34ac87260719706a99..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent } from 'ng-mocks'; -import { OrganisationseinheitNavigationItemComponent } from '../organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component'; -import { PostfachNavigationItemComponent } from '../postfach/postfach-navigation-item/postfach-navigation-item.component'; -import { NavigationComponent } from './navigation.component'; - -describe('NavigationComponent', () => { - let component: NavigationComponent; - let fixture: ComponentFixture<NavigationComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - NavigationComponent, - MockComponent(PostfachNavigationItemComponent), - MockComponent(OrganisationseinheitNavigationItemComponent), - ], - }).compileComponents(); - - fixture = TestBed.createComponent(NavigationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.ts b/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.ts deleted file mode 100644 index 0161a117a1692930d4335c882992892626744a6e..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/navigation/navigation.component.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'admin-navigation', - templateUrl: './navigation.component.html', - styleUrls: ['./navigation.component.scss'], -}) -export class NavigationComponent {} 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..1c5f9731bdfef401363bc423391cf1c7a2ed0529 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 '../organisationseinheit.service'; import { OrganisationseinheitContainerComponent } from './organisationseinheit-container.component'; import { OrganisationseinheitFormComponent } from './organisationseinheit-form/organisationseinheit-form.component'; import { OrganisationseinheitListComponent } from './organisationseinheit-list/organisationseinheit-list.component'; @@ -27,11 +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', - ); + const dialogOpenButtonSelector: string = getDataTestIdOf('organisationseinheit-open-dialog-button'); const spinnerSelector: string = getDataTestIdOf('organisationseinheit-spinner'); const organisationseinheitItems: Organisationseinheit[] = [createOrganisationseinheit()]; @@ -48,15 +44,13 @@ 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 - .fn() - .mockReturnValue(of(createOrganisationseinheitState(organisationseinheitItems))); + organisationseinheitService.get = jest.fn().mockReturnValue(of(createStateResource(organisationseinheitItems))); fixture.detectChanges(); formComponent = getElementFromFixtureByType(fixture, OrganisationseinheitFormComponent); @@ -99,13 +93,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..c8dc13a8eda3ff1f0825c7282020040695df6ecd 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 '../organisationseinheit.service'; 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.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts index 0418653a9be772a8adc99ba50aa1a7f4dfda142f..fd81569ecb52fceecb829096f4e859aeb73fad36 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts @@ -1,3 +1,4 @@ +import { createEmptyStateResource } from '@alfa-client/tech-shared'; import { dispatchEventFromFixture, getDebugElementFromFixtureByCss, @@ -7,29 +8,24 @@ import { } from '@alfa-client/test-utils'; import { DebugElement } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { - AbstractControl, - FormsModule, - ReactiveFormsModule, - UntypedFormGroup, -} from '@angular/forms'; +import { AbstractControl, FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; +import { ButtonWithSpinnerComponent } from '@ods/component'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent, ngMocks } from 'ng-mocks'; import { of, throwError } from 'rxjs'; import { createOrganisationseinheit } from '../../../../../test/user/user'; -import { PrimaryButtonComponent } from '../../../shared/primary-button/primary-button.component'; import { TextFieldComponent } from '../../../shared/text-field/text-field.component'; import { Organisationseinheit } from '../../../user/user.model'; -import { UserService } from '../../../user/user.service'; +import { OrganisationseinheitService } from '../../organisationseinheit.service'; import { OrganisationseinheitFormComponent } from './organisationseinheit-form.component'; -import { OrganisationseinheitFormservice } from './organisationseinheit.formservice'; +import { OrganisationseinheitFormService } from './organisationseinheit-form.service'; describe('OrganisationseinheitFormComponent', () => { let component: OrganisationseinheitFormComponent; let fixture: ComponentFixture<OrganisationseinheitFormComponent>; let form: UntypedFormGroup; - const userService: Mock<UserService> = mock(UserService); + const organisationseinheitService: Mock<OrganisationseinheitService> = mock(OrganisationseinheitService); const saveButtonSelector: string = getDataTestIdOf('organisationseinheit-save-button'); const closeButtonSelector: string = getDataTestIdOf('organisationseinheit-close-button'); @@ -39,11 +35,11 @@ describe('OrganisationseinheitFormComponent', () => { await TestBed.configureTestingModule({ declarations: [ OrganisationseinheitFormComponent, - MockComponent(PrimaryButtonComponent), MockComponent(TextFieldComponent), + MockComponent(ButtonWithSpinnerComponent), ], - imports: [ReactiveFormsModule, FormsModule], - providers: [{ provide: UserService, useValue: userService }], + imports: [ReactiveFormsModule, FormsModule, ButtonWithSpinnerComponent], + providers: [{ provide: OrganisationseinheitService, useValue: organisationseinheitService }], }).compileComponents(); fixture = TestBed.createComponent(OrganisationseinheitFormComponent); @@ -61,47 +57,30 @@ describe('OrganisationseinheitFormComponent', () => { describe('form element', () => { const fields: string[][] = [ - [ - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD, - 'Name', - 'organisationseinheit-name', - ], - [ - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD, - 'OrganisationseinheitID', - 'organisationseinheit-id', - ], + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD, 'Name', 'organisationseinheit-name'], + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD, 'OrganisationseinheitID', 'organisationseinheit-id'], ]; - it.each(fields)( - 'should have label for field "%s" with name "%s"', - (fieldName: string, text: string, inputId: string) => { - const textFieldElement = getElementFromFixture(fixture, getDataTestIdOf(inputId)); - expect(textFieldElement.getAttribute('label')).toBe(text); - }, - ); + it.each(fields)('should have label for field "%s" with name "%s"', (fieldName: string, text: string, inputId: string) => { + const textFieldElement = getElementFromFixture(fixture, getDataTestIdOf(inputId)); + expect(textFieldElement.getAttribute('label')).toBe(text); + }); it.each(fields)('should bind form for text-field "%s"', (fieldName, text, dataTestId) => { const fieldValue: string = `some text-field ${text}`; const formControl: AbstractControl = form.get(fieldName); - const textFieldComponent: DebugElement = getDebugElementFromFixtureByCss( - fixture, - getDataTestIdOf(dataTestId), - ); + const textFieldComponent: DebugElement = getDebugElementFromFixtureByCss(fixture, getDataTestIdOf(dataTestId)); ngMocks.change(textFieldComponent, fieldValue); expect(formControl.value).toBe(fieldValue); }); }); describe('save button', () => { - let saveButtonComponent: PrimaryButtonComponent; + let saveButtonComponent: ButtonWithSpinnerComponent; beforeEach(() => { - saveButtonComponent = getDebugElementFromFixtureByCss( - fixture, - saveButtonSelector, - ).componentInstance; + saveButtonComponent = getDebugElementFromFixtureByCss(fixture, saveButtonSelector).componentInstance; }); it('should call submit on click', () => { @@ -112,20 +91,20 @@ describe('OrganisationseinheitFormComponent', () => { expect(component.submit).toHaveBeenCalled(); }); - it('should be disabled while in progress', () => { - component.submitInProgress$ = of(true); + it('should be disabled while stateResource in loading', () => { + component.organisationseinheitItems = createEmptyStateResource(true); fixture.detectChanges(); - expect(saveButtonComponent.submitInProgress).toBe(true); + expect(saveButtonComponent.stateResource.loading).toBe(true); }); it('should be enabled while not in progress', () => { - component.submitInProgress$ = of(false); + component.organisationseinheitItems = createEmptyStateResource(false); fixture.detectChanges(); - expect(saveButtonComponent.submitInProgress).toBe(false); + expect(saveButtonComponent.stateResource.loading).toBe(false); }); }); @@ -157,15 +136,16 @@ describe('OrganisationseinheitFormComponent', () => { })); it.each([true, false])('should use submit progress "%s"', (progress) => { - component.submitInProgress$ = of(progress); + component.organisationseinheitItems = createEmptyStateResource(progress); fixture.detectChanges(); - const saveButtonComponent: PrimaryButtonComponent = getDebugElementFromFixtureByCss( + const saveButtonComponent: ButtonWithSpinnerComponent = getDebugElementFromFixtureByCss( fixture, saveButtonSelector, ).componentInstance; - expect(saveButtonComponent.submitInProgress).toBe(progress); + + expect(saveButtonComponent.stateResource.loading).toBe(progress); }); }); 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..4f4976209bc7976af17a5ce954585efbcffaff15 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,18 +1,21 @@ -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'; +import { OrganisationseinheitFormService } from './organisationseinheit-form.service'; @Component({ selector: 'admin-organisationseinheit-form', templateUrl: './organisationseinheit-form.component.html', - providers: [OrganisationseinheitFormservice], + 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'; - protected readonly OrganisationseinheitFormService = OrganisationseinheitFormservice; + protected readonly OrganisationseinheitFormService = OrganisationseinheitFormService; @ViewChild('OrganisationseinheitDialog') private dialogRef: ElementRef<HTMLDialogElement>; dialog: HTMLDialogElement; @@ -21,16 +24,14 @@ export class OrganisationseinheitFormComponent implements AfterViewInit { label: string; - constructor(public formService: OrganisationseinheitFormservice) {} + constructor(public formService: OrganisationseinheitFormService) {} ngAfterViewInit(): void { this.dialog = this.dialogRef.nativeElement; } public submit() { - this.submitInProgress$ = this.formService - .submit() - .pipe(tap((progress: boolean) => this.handleProgressChange(progress))); + this.submitInProgress$ = this.formService.submit().pipe(tap((progress: boolean) => this.handleProgressChange(progress))); } handleProgressChange(progress: boolean): void { 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-form.service.ts similarity index 67% rename from alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.ts rename to alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.service.ts index 850b5f775f723f0b27073ac5de0b99d7a2c877e8..fb65fa06cc6959e5df3f029f7e2894307d140d43 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-form.service.ts @@ -1,17 +1,15 @@ -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 { - Organisationseinheit, - OrganisationseinheitError, - OrganisationseinheitErrorType, -} from '../../../user/user.model'; -import { UserService } from '../../../user/user.service'; +import { catchError, Observable, of } from 'rxjs'; +import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType } from '../../../user/user.model'; import { getOrganisationseinheitErrorMessage } from '../../../user/user.util'; +import { OrganisationseinheitService } from '../../organisationseinheit.service'; @Injectable() -export class OrganisationseinheitFormservice { +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,11 +19,11 @@ export class OrganisationseinheitFormservice { constructor( private formBuilder: UntypedFormBuilder, - private userService: UserService, + private organisationsEinheitService: OrganisationseinheitService, ) { this.form = this.formBuilder.group({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: new FormControl(''), - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: new FormControl(''), + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: new FormControl(''), + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: new FormControl(''), }); } @@ -47,14 +45,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(), @@ -65,7 +63,7 @@ export class OrganisationseinheitFormservice { let valid: boolean = true; if (this.getOrganisationseinheitIds().length == 0) { - this.setError(OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD, { + this.setError(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD, { errorType: OrganisationseinheitErrorType.ID_MISSING, detail: '', }); @@ -76,14 +74,12 @@ export class OrganisationseinheitFormservice { } private getName(): string { - return this.getStringFromPotentiallyEmptyField( - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD, - ); + return this.getStringFromPotentiallyEmptyField(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD); } private getOrganisationseinheitIds(): string[] { return this.splitOrganisationseinheitIds( - this.form.get(OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD).value ?? '', + this.form.get(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD).value ?? '', ); } @@ -100,7 +96,7 @@ export class OrganisationseinheitFormservice { error.errorType === OrganisationseinheitErrorType.NAME_CONFLICT || error.errorType === OrganisationseinheitErrorType.NAME_MISSING ) { - this.setError(OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD, error); + this.setError(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD, error); } } @@ -122,9 +118,8 @@ export class OrganisationseinheitFormservice { this.reset(); this.source = organisationseinheit; this.form.patchValue({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: - organisationseinheit.organisationseinheitIds.join(', '), + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: organisationseinheit.organisationseinheitIds.join(', '), }); } diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts index c43144f6a8730dd6c81ed73aaef73aa4f0b4b5ef..30b0562d0e2ab18a08381eb1bded287beb3dcd68 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts @@ -1,36 +1,29 @@ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { Type } from '@angular/core'; import { fakeAsync, tick } from '@angular/core/testing'; import { AbstractControl, FormBuilder } from '@angular/forms'; import { hot } from 'jest-marbles'; import { singleCold, singleHot } from 'libs/tech-shared/test/marbles'; import { Observable, lastValueFrom, throwError } from 'rxjs'; -import { - createOrganisationseinheit, - createOrganisationseinheitError, -} from '../../../../../test/user/user'; -import { - Organisationseinheit, - OrganisationseinheitError, - OrganisationseinheitErrorType, -} from '../../../user/user.model'; -import { UserService } from '../../../user/user.service'; +import { createOrganisationseinheit, createOrganisationseinheitError } from '../../../../../test/user/user'; +import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType } from '../../../user/user.model'; import { getOrganisationseinheitErrorMessage } from '../../../user/user.util'; -import { OrganisationseinheitFormservice } from './organisationseinheit.formservice'; +import { OrganisationseinheitService } from '../../organisationseinheit.service'; +import { OrganisationseinheitFormService } from './organisationseinheit-form.service'; describe('OrganisationseinheitFormService', () => { - let formService: OrganisationseinheitFormservice; + let formService: OrganisationseinheitFormService; let organisationseinheit: Organisationseinheit; - const userService: Mock<UserService> = mock(UserService); + const organisationseinheitService: Mock<OrganisationseinheitService> = mockResourceService(OrganisationseinheitService); const formBuilder: FormBuilder = new FormBuilder(); beforeEach(() => { - formService = new OrganisationseinheitFormservice(formBuilder, useFromMock(userService)); + formService = new OrganisationseinheitFormService(formBuilder, useFromMock(organisationseinheitService)); organisationseinheit = createOrganisationseinheit(); formService.form.setValue({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: - organisationseinheit.organisationseinheitIds.join(','), + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: organisationseinheit.organisationseinheitIds.join(','), }); }); @@ -93,14 +86,13 @@ describe('OrganisationseinheitFormService', () => { const hasIdMissingError = () => formService.form.hasError( OrganisationseinheitErrorType.ID_MISSING, - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD, + OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD, ); describe('without organisationeinheitIds', () => { beforeEach(() => { formService.form.setValue({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: - organisationseinheit.name, - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: ',', + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: ',', }); }); @@ -171,24 +163,27 @@ describe('OrganisationseinheitFormService', () => { it('should call create organisationseinheit', () => { formService.create(); - expect(userService.createOrganisationseinheit).toHaveBeenCalledWith( - organisationseinheit.name, - organisationseinheit.organisationseinheitIds, - ); + expect(organisationseinheitService.create).toHaveBeenCalledWith({ + name: organisationseinheit.name, + organisationseinheitIds: organisationseinheit.organisationseinheitIds, + }); }); it('should call create organisationseinheit with empty form', () => { formService.form.setValue({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: null, - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: null, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: null, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: null, }); formService.create(); - expect(userService.createOrganisationseinheit).toHaveBeenCalledWith('', []); + expect(organisationseinheitService.create).toHaveBeenCalledWith({ + name: '', + organisationseinheitIds: [], + }); }); it('should return progress observable', () => { - userService.createOrganisationseinheit.mockReturnValue(singleCold(true)); + organisationseinheitService.create.mockReturnValue(singleCold(true)); const progressObservable: Observable<boolean> = formService.create(); @@ -202,7 +197,7 @@ describe('OrganisationseinheitFormService', () => { formService.save(); - expect(userService.saveOrganisationseinheit).toHaveBeenCalledWith({ + expect(organisationseinheitService.save).toHaveBeenCalledWith({ id: formService.source.id, name: organisationseinheit.name, organisationseinheitIds: organisationseinheit.organisationseinheitIds, @@ -212,12 +207,12 @@ describe('OrganisationseinheitFormService', () => { it('should call save organisationseinheit with empty form', () => { formService.source = createOrganisationseinheit(); formService.form.setValue({ - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD]: null, - [OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD]: null, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: null, + [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: null, }); formService.save(); - expect(userService.saveOrganisationseinheit).toHaveBeenCalledWith({ + expect(organisationseinheitService.save).toHaveBeenCalledWith({ id: formService.source.id, name: '', organisationseinheitIds: [], @@ -225,7 +220,7 @@ describe('OrganisationseinheitFormService', () => { }); it('should return progress observable', () => { - userService.saveOrganisationseinheit.mockReturnValue(singleCold(true)); + organisationseinheitService.save.mockReturnValue(singleCold(true)); const progressObservable: Observable<boolean> = formService.save(); @@ -234,22 +229,19 @@ describe('OrganisationseinheitFormService', () => { }); describe('handle error', () => { - it.each([ - OrganisationseinheitErrorType.NAME_CONFLICT, - OrganisationseinheitErrorType.NAME_MISSING, - ])('should set error on name field on %s', (type: OrganisationseinheitErrorType) => { - const error: OrganisationseinheitError = { - ...createOrganisationseinheitError(), - errorType: type, - }; - - formService.handleError(error); - const errorMessage = formService.form.getError( - type, - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD, - ); - expect(errorMessage).toEqual(getOrganisationseinheitErrorMessage(error)); - }); + it.each([OrganisationseinheitErrorType.NAME_CONFLICT, OrganisationseinheitErrorType.NAME_MISSING])( + 'should set error on name field on %s', + (type: OrganisationseinheitErrorType) => { + const error: OrganisationseinheitError = { + ...createOrganisationseinheitError(), + errorType: type, + }; + + formService.handleError(error); + const errorMessage = formService.form.getError(type, OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD); + expect(errorMessage).toEqual(getOrganisationseinheitErrorMessage(error)); + }, + ); it('should not set error on name field for unknown errors', () => { const unknownError: OrganisationseinheitError = createOrganisationseinheitError(undefined); @@ -280,33 +272,27 @@ describe('OrganisationseinheitFormService', () => { it('should set name', () => { formService.patch(organisationseinheit); - const formControl: AbstractControl = formService.form.get( - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_NAME_FIELD, - ); + const formControl: AbstractControl = formService.form.get(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD); expect(formControl.value).toEqual(organisationseinheit.name); }); it('should set organisationseinheitIds', () => { formService.patch(organisationseinheit); - const formControl: AbstractControl = formService.form.get( - OrganisationseinheitFormservice.ORGANISATIONSEINHEIT_IDS_FIELD, - ); + const formControl: AbstractControl = formService.form.get(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD); expect(formControl.value).toEqual(organisationseinheit.organisationseinheitIds.join(', ')); }); }); describe('split organisationseinheitIds by comma', () => { it('should return', () => { - const organisationseinheitIds: string[] = - formService.splitOrganisationseinheitIds('123, 555 , 666'); + const organisationseinheitIds: string[] = formService.splitOrganisationseinheitIds('123, 555 , 666'); expect(organisationseinheitIds).toEqual(['123', '555', '666']); }); it('should filter empty organisationseinheitIds', () => { - const organisationseinheitIds: string[] = - formService.splitOrganisationseinheitIds(',55,,66,'); + const organisationseinheitIds: string[] = formService.splitOrganisationseinheitIds(',55,,66,'); expect(organisationseinheitIds).toEqual(['55', '66']); }); @@ -352,3 +338,7 @@ describe('OrganisationseinheitFormService', () => { }); }); }); + +function mockResourceService<T>(service: Type<T>): Mock<T> { + return <Mock<T>>{ ...mock(service), create: jest.fn(), save: jest.fn() }; +} 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/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.html deleted file mode 100644 index 2e7a09a8541daa2e00417268ec67b4a43103702b..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.html +++ /dev/null @@ -1,5 +0,0 @@ -<admin-navigation-item - name="Organisationseinheiten" - imageSrc="/assets/organisationseinheit.svg" - link="/organisationseinheiten" -></admin-navigation-item> diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.spec.ts deleted file mode 100644 index b855265b14e0b5ec4e03e1db4b4af27cd1528b23..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NavigationItemComponent } from '@admin-client/admin-settings'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent } from 'ng-mocks'; -import { OrganisationseinheitNavigationItemComponent } from './organisationseinheit-navigation-item.component'; - -describe('OrganisationseinheitNavigationItemComponent', () => { - let component: OrganisationseinheitNavigationItemComponent; - let fixture: ComponentFixture<OrganisationseinheitNavigationItemComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - OrganisationseinheitNavigationItemComponent, - MockComponent(NavigationItemComponent), - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(OrganisationseinheitNavigationItemComponent); - component = fixture.componentInstance; - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.ts deleted file mode 100644 index 6e66cbe44e49519c4b169c18067059807d11216a..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-navigation-item/organisationseinheit-navigation-item.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'admin-organisationseinheit-navigation-item', - templateUrl: './organisationseinheit-navigation-item.component.html', -}) -export class OrganisationseinheitNavigationItemComponent {} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e16cd2a3421cac02ddcd38e472322fc5942e808 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.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 './organisationseinheit.service'; + +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/organisationseinheit.service.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d07d400cdb35c40955a164a99bcd66faf933b911 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.ts @@ -0,0 +1,30 @@ +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/postfach/postfach-navigation-item/postfach-navigation-item.component.html b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.html deleted file mode 100644 index c2dc7ea533bb1d807a8a73de12d273881e2e0053..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.html +++ /dev/null @@ -1,5 +0,0 @@ -<admin-navigation-item - name="Postfach" - imageSrc="/assets/mail.svg" - link="/postfach" -></admin-navigation-item> diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.scss b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts deleted file mode 100644 index 78ced8dcace6a475a7d5bda4c1f2d2be6a60171a..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NavigationItemComponent } from '@admin-client/admin-settings'; -import { getMockComponent } from '@alfa-client/test-utils'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent } from 'ng-mocks'; -import { SettingName } from '../../admin-settings.model'; -import { PostfachNavigationItemComponent } from './postfach-navigation-item.component'; - -describe('PostfachNavigationItemComponent', () => { - let component: PostfachNavigationItemComponent; - let fixture: ComponentFixture<PostfachNavigationItemComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [PostfachNavigationItemComponent, MockComponent(NavigationItemComponent)], - }).compileComponents(); - - fixture = TestBed.createComponent(PostfachNavigationItemComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('navigation item component', () => { - let navigationItemComponent: NavigationItemComponent; - - beforeEach(() => { - navigationItemComponent = getMockComponent(fixture, NavigationItemComponent); - }); - - it('should be called with name', () => { - expect(navigationItemComponent.name).toBe(SettingName.POSTFACH); - }); - - it('should be called with imageSrc', () => { - expect(navigationItemComponent.imageSrc).toBe('/assets/mail.svg'); - }); - - it('should be called with link', () => { - expect(navigationItemComponent.link).toBe('/postfach'); - }); - }); -}); diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.ts b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.ts deleted file mode 100644 index c5765cc3bf0b3170300d23383a9dead64714feb8..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-navigation-item/postfach-navigation-item.component.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'admin-postfach-navigation-item', - templateUrl: './postfach-navigation-item.component.html', - styleUrls: ['./postfach-navigation-item.component.scss'], -}) -export class PostfachNavigationItemComponent {} 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..421892f8c3e731bf0ba687c2468e844fb5fd460b --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.spec.ts @@ -0,0 +1,227 @@ +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 { Dummy, 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 id: string = faker.random.word(); + const emptyStateResource: StateResource<unknown[]> = createEmptyStateResource<unknown[]>(); + const dummyObject: Dummy = createDummy(); + const dummyAction: Observable<Dummy> = 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', () => { + it('should call doIfLoadingRequired', () => { + const doIfLoadingRequired: jest.SpyInstance<boolean> = jest.spyOn(resourceUtil, 'doIfLoadingRequired'); + + service.handleChanges(emptyStateResource); + + expect(doIfLoadingRequired).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: Dummy = 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(); + }); + }); + + describe('refreshAfterFirstEmit', () => { + it('should call refresh after first emit', fakeAsync(() => { + service.refresh = jest.fn(); + + service.refreshAfterFirstEmit(dummyAction).subscribe(); + tick(); + + expect(service.refresh).toHaveBeenCalled(); + })); + }); + + describe('progress', () => { + it('should emit true at the start and false after first parameter emit', () => { + const result: Observable<boolean> = 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..bc62384b90ed390dccad35c9acb296d74ca3a1e0 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/keycloak.resource.service.ts @@ -0,0 +1,82 @@ +import { createEmptyStateResource, createStateResource, doIfLoadingRequired, StateResource } from '@alfa-client/tech-shared'; +import { BehaviorSubject, first, map, Observable, startWith, tap } from 'rxjs'; + +export abstract class KeycloakResourceService<T> { + readonly stateResource: BehaviorSubject<StateResource<T[]>> = new BehaviorSubject(createEmptyStateResource()); + + public get(): Observable<StateResource<T[]>> { + return this.stateResource.asObservable().pipe(tap((stateResource) => this.handleChanges(stateResource))); + } + + handleChanges(stateResource: StateResource<T[]>) { + doIfLoadingRequired(stateResource, () => this.loadResource()); + } + + loadResource(): void { + this.setLoading(); + this.getItemsFromKeycloak() + .pipe(first()) + .subscribe((items) => this.updateResource(items)); + } + + abstract getItemsFromKeycloak(): Observable<T[]>; + + private updateResource(items: T[]): void { + 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/to-user-name.pipe.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/to-user-name.pipe.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..81012d2754951d89e18992d26fea0fd7b2248bf0 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/to-user-name.pipe.spec.ts @@ -0,0 +1,23 @@ +import { createUser } from '../../../test/user/user'; +import { ToUserNamePipe } from './to-user-name.pipe'; +import { User } from './user.model'; + +describe('UserNamePipe', () => { + it('should return user name', () => { + const user: User = { ...createUser(), firstName: 'Max', lastName: 'Mustermann' }; + const userNamePipe: ToUserNamePipe = new ToUserNamePipe(); + + const result: string = userNamePipe.transform(user); + + expect(result).toBe('Max Mustermann'); + }); + + it('should return username for user without firstName and lastName', () => { + const user: User = { ...createUser(), firstName: null, lastName: null }; + const userNamePipe: ToUserNamePipe = new ToUserNamePipe(); + + const result: string = userNamePipe.transform(user); + + expect(result).toBe(user.username); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/user/to-user-name.pipe.ts b/alfa-client/libs/admin/settings/src/lib/user/to-user-name.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..4711ff9e5d78eea721342923a296976dde6a0389 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/user/to-user-name.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { User } from './user.model'; + +@Pipe({ + name: 'toUserName', + standalone: true, +}) +export class ToUserNamePipe implements PipeTransform { + transform(user: User): string { + if (!user.firstName || !user.lastName) return user.username; + return `${user.firstName} ${user.lastName}`; + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.model.ts b/alfa-client/libs/admin/settings/src/lib/user/user.model.ts index 308e945aa9b0f900ccfc135eeb98f713efc7e5e7..742ca490a50a84a8f9026204e573bcf729bf3c30 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.model.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.model.ts @@ -4,12 +4,6 @@ export interface Organisationseinheit { organisationseinheitIds: string[]; } -export interface OrganisationseinheitState { - organisationseinheitItems: Organisationseinheit[]; - loading: boolean; - updateRequired: boolean; -} - export enum OrganisationseinheitErrorType { NAME_CONFLICT = 'name-conflict', NAME_MISSING = 'name-missing', @@ -20,3 +14,13 @@ export interface OrganisationseinheitError { errorType: OrganisationseinheitErrorType; detail: string; } + +export interface User { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + groups?: string[]; + roles?: string[]; +} 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 293806db24c520afa4b0907811092ee0bf023979..5415be30358d129923f64c6f837885a5d78e2fd4 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 @@ -2,9 +2,13 @@ import { Injectable } from '@angular/core'; import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client'; import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; +import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; +import RoleRepresentation from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { OAuthService } from 'angular-oauth2-oidc'; -import { Observable, OperatorFunction, catchError, from, map, throwError } from 'rxjs'; -import { Organisationseinheit, OrganisationseinheitError } from './user.model'; +import { isNil } from 'lodash-es'; +import { Observable, OperatorFunction, catchError, forkJoin, from, map, mergeMap, throwError } from 'rxjs'; +import { Organisationseinheit, OrganisationseinheitError, User } from './user.model'; import { KEYCLOAK_CREATE_GROUPS_ERROR_STATUS } from './user.util'; @Injectable({ @@ -35,9 +39,7 @@ export class UserRepository { public findOrganisationseinheitItems(): Observable<Organisationseinheit[]> { return from(this.kcAdminClient.groups.find({ briefRepresentation: false })).pipe( map((reps: GroupRepresentation[]) => - reps.map((rep: GroupRepresentation) => - this.mapGroupRepresentationToOrganisationseinheit(rep), - ), + reps.map((rep: GroupRepresentation) => this.mapGroupRepresentationToOrganisationseinheit(rep)), ), ); } @@ -62,21 +64,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(), @@ -93,4 +94,53 @@ export class UserRepository { detail: error.responseData['errorMessage'] ?? '', }; } + + public getUsers(): Observable<User[]> { + return from(this.kcAdminClient.users.find()).pipe( + map((userReps: UserRepresentation[]) => userReps.map((userReps) => this.mapToUser(userReps))), + mergeMap((users) => forkJoin(users.map((users) => this.addInformationToUser(users)))), + ); + } + + mapToUser(userRepresentation: UserRepresentation): User { + return { + id: userRepresentation.id, + email: userRepresentation.email, + username: userRepresentation.username, + firstName: userRepresentation.firstName, + lastName: userRepresentation.lastName, + groups: null, + roles: null, + }; + } + + private addInformationToUser(user: User): Observable<User> { + return forkJoin([this.getUserGroups(user), this.getAlfaClientRoles(user)]).pipe( + map(([groups, roles]) => ({ + ...user, + groups, + roles, + })), + ); + } + + private getUserGroups(user: User): Observable<string[]> { + return from(this.kcAdminClient.users.listGroups({ id: user.id })).pipe( + map((groups: GroupRepresentation[]) => groups.map((group) => group.name)), + ); + } + + private getAlfaClientRoles(user: User): Observable<string[]> { + return from(this.kcAdminClient.users.listRoleMappings({ id: user.id })).pipe( + map((clientMappings: MappingsRepresentation) => this.mapToAlfaClientRoleNames(clientMappings)), + ); + } + + private mapToAlfaClientRoleNames(roleMappings: MappingsRepresentation): string[] { + if (isNil(roleMappings?.clientMappings?.['alfa'])) { + return []; + } + + return roleMappings.clientMappings['alfa'].mappings.map((role: RoleRepresentation) => role.name); + } } diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.repository.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/user.repository.spec.ts index 47926c8e111e9ab28bc517ef13add588011b0d1e..ef000f8630a3fd9990283d882b023b30e253a060 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.repository.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.repository.spec.ts @@ -4,6 +4,7 @@ import { faker } from '@faker-js/faker'; import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client'; import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; +import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; import { Groups } from '@keycloak/keycloak-admin-client/lib/resources/groups'; import { OAuthService } from 'angular-oauth2-oidc'; import { Observable, OperatorFunction, catchError, firstValueFrom, of, throwError } from 'rxjs'; @@ -12,12 +13,9 @@ import { createNetworkError, createOrganisationseinheit, createOrganisationseinheitError, + createUser, } from '../../../test/user/user'; -import { - Organisationseinheit, - OrganisationseinheitError, - OrganisationseinheitErrorType, -} from './user.model'; +import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType, User } from './user.model'; import { UserRepository } from './user.repository.service'; describe('UserRepository', () => { @@ -62,42 +60,36 @@ describe('UserRepository', () => { }); describe('map organisationseinheit representation', () => { - let expectedOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); + const expectedOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); it('should map field "id"', () => { - const organisationseinheit: Organisationseinheit = - repository.mapGroupRepresentationToOrganisationseinheit({ - id: expectedOrganisationseinheit.id, - }); + const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ + id: expectedOrganisationseinheit.id, + }); expect(organisationseinheit.id).toEqual(expectedOrganisationseinheit.id); }); it('should map field "name"', () => { - const organisationseinheit: Organisationseinheit = - repository.mapGroupRepresentationToOrganisationseinheit({ - name: expectedOrganisationseinheit.name, - }); + const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ + name: expectedOrganisationseinheit.name, + }); expect(organisationseinheit.name).toEqual(expectedOrganisationseinheit.name); }); it('should map field "organisationseinheitIds"', () => { - const organisationseinheit: Organisationseinheit = - repository.mapGroupRepresentationToOrganisationseinheit({ - attributes: { - organisationseinheitId: expectedOrganisationseinheit.organisationseinheitIds, - }, - }); + const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ + attributes: { + organisationseinheitId: expectedOrganisationseinheit.organisationseinheitIds, + }, + }); - expect(organisationseinheit.organisationseinheitIds).toEqual( - expectedOrganisationseinheit.organisationseinheitIds, - ); + expect(organisationseinheit.organisationseinheitIds).toEqual(expectedOrganisationseinheit.organisationseinheitIds); }); it('should map missing organisationseinheitIds to empty list', () => { - const organisationseinheit: Organisationseinheit = - repository.mapGroupRepresentationToOrganisationseinheit({}); + const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({}); expect(organisationseinheit.organisationseinheitIds).toEqual([]); }); @@ -110,16 +102,13 @@ describe('UserRepository', () => { createOrganisationseinheit(), ]; - const groupReps: GroupRepresentation[] = - organisationseinheitItems.map(createGroupRepresentation); + const groupReps: GroupRepresentation[] = organisationseinheitItems.map(createGroupRepresentation); it('should return mapped organisationseinheit search result', async () => { const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps)); mockGroupsFunc('find', findMock); - const groupsResult: Organisationseinheit[] = await firstValueFrom( - repository.findOrganisationseinheitItems(), - ); + const groupsResult: Organisationseinheit[] = await firstValueFrom(repository.findOrganisationseinheitItems()); expect(groupsResult).toEqual(groupsResult); }); @@ -167,9 +156,7 @@ describe('UserRepository', () => { it('should pipe rethrowMappedGroupsError', (done) => { const updateMock: jest.Mock = jest.fn(() => Promise.reject(networkError)); mockGroupsFunc('update', updateMock); - repository.rethrowMappedGroupsError = jest - .fn() - .mockReturnValue(catchError(() => throwError(() => error))); + repository.rethrowMappedGroupsError = jest.fn().mockReturnValue(catchError(() => throwError(() => error))); repository.saveOrganisationseinheit(saveGroup).subscribe({ error: (err) => { @@ -188,10 +175,10 @@ describe('UserRepository', () => { mockGroupsFunc('create', createMock); await firstValueFrom( - repository.createOrganisationseinheit( - newOrganisationseinheit.name, - newOrganisationseinheit.organisationseinheitIds, - ), + repository.createOrganisationseinheit({ + name: newOrganisationseinheit.name, + organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, + }), ); expect(createMock).toHaveBeenCalledWith({ @@ -203,16 +190,14 @@ describe('UserRepository', () => { }); it('should return mapped organisationseinheit result', async () => { - const createMock: jest.Mock = jest.fn(() => - Promise.resolve({ id: newOrganisationseinheit.id }), - ); + const createMock: jest.Mock = jest.fn(() => Promise.resolve({ id: newOrganisationseinheit.id })); mockGroupsFunc('create', createMock); const newGroupResult: Organisationseinheit = await firstValueFrom( - repository.createOrganisationseinheit( - newOrganisationseinheit.name, - newOrganisationseinheit.organisationseinheitIds, - ), + repository.createOrganisationseinheit({ + name: newOrganisationseinheit.name, + organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, + }), ); expect(newGroupResult).toEqual(newOrganisationseinheit); @@ -221,15 +206,13 @@ describe('UserRepository', () => { it('should pipe rethrowMappedGroupsError', (done) => { const createMock: jest.Mock = jest.fn(() => Promise.reject(networkError)); mockGroupsFunc('create', createMock); - repository.rethrowMappedGroupsError = jest - .fn() - .mockReturnValue(catchError(() => throwError(() => error))); + repository.rethrowMappedGroupsError = jest.fn().mockReturnValue(catchError(() => throwError(() => error))); repository - .createOrganisationseinheit( - newOrganisationseinheit.name, - newOrganisationseinheit.organisationseinheitIds, - ) + .createOrganisationseinheit({ + name: newOrganisationseinheit.name, + organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, + }) .subscribe({ error: (err) => { expect(err).toBe(error); @@ -305,9 +288,7 @@ describe('UserRepository', () => { const deleteOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); it('should call kcAdminClient.groups.del', async () => { - const delMock: jest.Mock = jest.fn(() => - Promise.resolve({ id: deleteOrganisationseinheit.id }), - ); + const delMock: jest.Mock = jest.fn(() => Promise.resolve({ id: deleteOrganisationseinheit.id })); mockGroupsFunc('del', delMock); await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id)); @@ -323,11 +304,75 @@ describe('UserRepository', () => { jest.fn(() => Promise.resolve(null)), ); - const voidResult = await firstValueFrom( - repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id), - ); + const voidResult: void = await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id)); expect(voidResult).toBeNull(); }); }); + + describe('getUsers', () => { + const userRep: User = createUser(); + const userRepArray: User[] = [userRep, userRep, userRep]; + const group: string = faker.random.word(); + const role: string = faker.random.word(); + const groupRep: GroupRepresentation[] = [{ name: group }]; + const roleRep: MappingsRepresentation = { + clientMappings: { alfa: { mappings: [{ name: role }] } }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + kcAdminClient.users = <any>{ + find: jest.fn().mockReturnValue(Promise.resolve(userRepArray)), + listGroups: jest.fn().mockReturnValue(Promise.resolve(groupRep)), + listRoleMappings: jest.fn().mockReturnValue(Promise.resolve(roleRep)), + }; + }); + + it('should call kcAdminClient users find', () => { + repository.getUsers(); + + expect(kcAdminClient.users['find']).toHaveBeenCalled(); + }); + + it('should call mapToUser', fakeAsync(() => { + const mapToUser: jest.SpyInstance<User> = jest.spyOn(repository, 'mapToUser'); + + repository.getUsers().subscribe(); + tick(); + + expect(mapToUser).toBeCalledTimes(userRepArray.length); + })); + + it('should call kcadminClient listGroups for every user', fakeAsync(() => { + repository.getUsers().subscribe(); + tick(); + + expect(kcAdminClient.users['listGroups']).toBeCalledTimes(userRepArray.length); + })); + + it('should call kcadminClient listRoleMappings for every user', fakeAsync(() => { + repository.getUsers().subscribe(); + tick(); + + expect(kcAdminClient.users['listRoleMappings']).toBeCalledTimes(userRepArray.length); + })); + + it('should return users with groups and roles', (done) => { + repository.getUsers().subscribe((users: User[]) => { + users.forEach((user) => expect(user).toEqual({ ...userRep, groups: [group], roles: [role] })); + done(); + }); + }); + + it('should return users with empty groups and roles if they have none', (done) => { + kcAdminClient.users['listGroups'] = jest.fn().mockReturnValue(Promise.resolve([])); + kcAdminClient.users['listRoleMappings'] = jest.fn().mockReturnValue(Promise.resolve({})); + + repository.getUsers().subscribe((users: User[]) => { + users.forEach((user) => expect(user).toEqual({ ...userRep, groups: [], roles: [] })); + done(); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.service.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/user.service.spec.ts deleted file mode 100644 index 56ac9d24a8cf68804dce861d61b7a3cf551aeb54..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/user/user.service.spec.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { mock, Mock, useFromMock } from '@alfa-client/test-utils'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { cold } from 'jest-marbles'; -import { singleCold } from 'libs/tech-shared/test/marbles'; -import { firstValueFrom, lastValueFrom, Observable, of } from 'rxjs'; -import { - createOrganisationseinheit, - createOrganisationseinheitState, -} from '../../../test/user/user'; -import { Organisationseinheit } from './user.model'; -import { UserRepository } from './user.repository.service'; -import { UserService } from './user.service'; - -describe('UserService', () => { - let service: UserService; - let repository: Mock<UserRepository>; - const sortedNames: string[] = ['BBBB', 'CCCC', 'XXXX']; - const sortedOrganisationseinheitItems: Organisationseinheit[] = sortedNames.map((name) => ({ - ...createOrganisationseinheit(), - name, - })); - const unsortedOrganisationseinheitItems: Organisationseinheit[] = [ - sortedOrganisationseinheitItems[2], - sortedOrganisationseinheitItems[0], - sortedOrganisationseinheitItems[1], - ]; - let setOrganisationseinheitItemsSpy: jest.SpyInstance; - let setLoadingUntilFirstSpy: jest.SpyInstance; - - beforeEach(() => { - repository = mock(UserRepository); - service = new UserService(useFromMock(repository)); - setOrganisationseinheitItemsSpy = jest.spyOn(service, 'setOrganisationseinheit'); - setLoadingUntilFirstSpy = jest.spyOn(service, 'setLoadingUntilFirst'); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('initial state', () => { - it('should have empty group list', () => { - expect(service.organisationseinheitState$.value.organisationseinheitItems).toEqual([]); - }); - it('should not be loading', () => { - expect(service.organisationseinheitState$.value.loading).toEqual(false); - }); - it('should be requiring a reload', () => { - expect(service.organisationseinheitState$.value.updateRequired).toEqual(true); - }); - }); - - describe('get group state', () => { - it('should update if required', async () => { - service.updateIfRequired = jest.fn(); - - await firstValueFrom(service.getOrganisationseinheitState()); - - expect(service.updateIfRequired).toHaveBeenCalled(); - }); - - it('should call find groups', fakeAsync(() => { - repository.findOrganisationseinheitItems.mockReturnValue( - of(unsortedOrganisationseinheitItems), - ); - - service.getOrganisationseinheitState().subscribe(); - tick(); - - expect(repository.findOrganisationseinheitItems).toHaveBeenCalled(); - })); - }); - - describe('update if required', () => { - beforeEach(() => { - service.updateOrganisationseinheitItems = jest.fn(() => of([])); - }); - - it('should call update groups with initial values', () => { - service.updateIfRequired(); - - expect(service.updateOrganisationseinheitItems).toHaveBeenCalled(); - }); - - it('should not call update groups if loading', () => { - service.setLoading(); - - service.updateIfRequired(); - - expect(service.updateOrganisationseinheitItems).not.toHaveBeenCalled(); - }); - - it('should not call update groups if not requiring update', () => { - service.setUpdateRequired(false); - - service.updateIfRequired(); - - expect(service.updateOrganisationseinheitItems).not.toHaveBeenCalled(); - }); - }); - - describe('update groups', () => { - beforeEach(() => { - repository.findOrganisationseinheitItems.mockReturnValue( - of(unsortedOrganisationseinheitItems), - ); - }); - - it('should set loading', () => { - service.updateOrganisationseinheitItems(); - - expect(service.organisationseinheitState$.value.loading).toBeTruthy(); - }); - - it('should set sorted groups', async () => { - await lastValueFrom(service.updateOrganisationseinheitItems()); - - expect(service.organisationseinheitState$.value.organisationseinheitItems).toEqual( - sortedOrganisationseinheitItems, - ); - }); - - it('should not require update afterward', async () => { - await lastValueFrom(service.updateOrganisationseinheitItems()); - - expect(service.organisationseinheitState$.value.updateRequired).toBeFalsy(); - }); - }); - - describe('save organisationseinheit', () => { - const saveGroup: Organisationseinheit = { - ...createOrganisationseinheit(), - id: sortedOrganisationseinheitItems[0].id, - }; - - beforeEach(() => { - repository.saveOrganisationseinheit.mockReturnValue(of(null)); - }); - - it('should use set loading until first', fakeAsync(() => { - service.saveOrganisationseinheit(saveGroup).subscribe(); - tick(); - - expect(setLoadingUntilFirstSpy).toHaveBeenCalled(); - })); - - it('should call repository for save', fakeAsync(() => { - service.saveOrganisationseinheit(saveGroup).subscribe(); - tick(); - - expect(repository.saveOrganisationseinheit).toHaveBeenCalledWith(saveGroup); - })); - - it('should set organisationseinheit items with updated organisationseinheit', fakeAsync(() => { - service.setOrganisationseinheit(sortedOrganisationseinheitItems); - setOrganisationseinheitItemsSpy.mockClear(); - - service.saveOrganisationseinheit(saveGroup).subscribe(); - tick(); - - expect(setOrganisationseinheitItemsSpy).toHaveBeenCalledWith([ - saveGroup, - ...sortedOrganisationseinheitItems.slice(1), - ]); - })); - }); - - describe('create organisationseinheit', () => { - const organisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - beforeEach(() => { - repository.createOrganisationseinheit.mockReturnValue(of(organisationseinheit)); - }); - - it('should use set loading until first', () => { - service.createOrganisationseinheit( - organisationseinheit.name, - organisationseinheit.organisationseinheitIds, - ); - - expect(setLoadingUntilFirstSpy).toHaveBeenCalled(); - }); - - it('should call repository for save', () => { - service.createOrganisationseinheit( - organisationseinheit.name, - organisationseinheit.organisationseinheitIds, - ); - - expect(repository.createOrganisationseinheit).toHaveBeenCalledWith( - organisationseinheit.name, - organisationseinheit.organisationseinheitIds, - ); - }); - - it('should set organisationseinheit items with new group', fakeAsync(() => { - service.setOrganisationseinheit(sortedOrganisationseinheitItems); - setOrganisationseinheitItemsSpy.mockClear(); - - service - .createOrganisationseinheit( - organisationseinheit.name, - organisationseinheit.organisationseinheitIds, - ) - .subscribe(); - tick(); - - expect(setOrganisationseinheitItemsSpy).toHaveBeenCalledWith([ - ...sortedOrganisationseinheitItems, - organisationseinheit, - ]); - })); - }); - - describe('set organisationseinheit items', () => { - it('should sort groups by name', async () => { - service.setOrganisationseinheit(unsortedOrganisationseinheitItems); - - expect(service.organisationseinheitState$.value.organisationseinheitItems).toEqual( - sortedOrganisationseinheitItems, - ); - }); - }); - - describe('delete group', () => { - const deleteGroup: Organisationseinheit = unsortedOrganisationseinheitItems[1]; - - beforeEach(async () => { - repository.deleteOrganisationseinheit.mockReturnValue(of(null)); - service.organisationseinheitState$.next({ - ...service.organisationseinheitState$.value, - organisationseinheitItems: sortedOrganisationseinheitItems, - }); - }); - - it('should use set loading until first', () => { - const setLoadingUntilFirstSpy: jest.SpyInstance = jest.spyOn(service, 'setLoadingUntilFirst'); - service.deleteOrganisationseinheit(deleteGroup.id); - - expect(setLoadingUntilFirstSpy).toHaveBeenCalled(); - }); - - it('should call repository', () => { - service.deleteOrganisationseinheit(deleteGroup.id); - - expect(repository.deleteOrganisationseinheit).toHaveBeenCalledWith(deleteGroup.id); - }); - - describe('which exists', () => { - it('should not have group in list after delete', fakeAsync(() => { - service.deleteOrganisationseinheit(deleteGroup.id).subscribe(); - tick(); - - expect(service.organisationseinheitState$.value.organisationseinheitItems).not.toContain( - deleteGroup, - ); - })); - }); - - describe('which does not exist', () => { - it('should have no effect', fakeAsync(() => { - service.deleteOrganisationseinheit('unknown-id').subscribe(); - tick(); - - expect(service.organisationseinheitState$.value.organisationseinheitItems).toEqual( - sortedOrganisationseinheitItems, - ); - })); - }); - }); - - describe('set loading until first', () => { - it('should set loading', () => { - service.setLoadingUntilFirst(of(null), () => {}); - - expect(service.organisationseinheitState$.value.loading).toBe(true); - }); - - it('should unset loading', fakeAsync(() => { - service.setLoadingUntilFirst(of(null), () => {}).subscribe(); - tick(); - - expect(service.organisationseinheitState$.value.loading).toBe(false); - })); - - it('should emit loading before first and emit loading after first', () => { - const delayedNull = cold('-a|', { a: null }); - - const progressObservable = service.setLoadingUntilFirst(delayedNull, () => {}); - - expect(progressObservable).toBeObservable(cold('a(b|)', { a: true, b: false })); - }); - - it('should call tap function', fakeAsync(() => { - const tapFunction: jest.Mock = jest.fn(); - const value: string = 'abc'; - const delayedNull: Observable<string> = of(value); - - service.setLoadingUntilFirst(delayedNull, tapFunction).subscribe(); - tick(); - - expect(tapFunction).toHaveBeenCalledWith(value); - })); - }); - - describe('get organisationseinheit items', () => { - it('should return items of state', () => { - const organisationseinheitItems: Organisationseinheit[] = [ - createOrganisationseinheit(), - createOrganisationseinheit(), - ]; - service.getOrganisationseinheitState = jest - .fn() - .mockReturnValue(singleCold(createOrganisationseinheitState(organisationseinheitItems))); - - const itemsObservable: Observable<Organisationseinheit[]> = - service.getOrganisationseinheitItems(); - expect(itemsObservable).toBeObservable(singleCold(organisationseinheitItems)); - }); - }); -}); 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 deleted file mode 100644 index 9ec08995ed49a91e48c044690c9770f121fcf9a5..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/user/user.service.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable, first, map, startWith, tap } from 'rxjs'; -import { Organisationseinheit, OrganisationseinheitState } from './user.model'; -import { UserRepository } from './user.repository.service'; - -@Injectable({ - providedIn: 'root', -}) -export class UserService { - readonly organisationseinheitState$: BehaviorSubject<OrganisationseinheitState> = - new BehaviorSubject({ - organisationseinheitItems: [], - loading: false, - updateRequired: true, - }); - - constructor(private userRepository: UserRepository) {} - - public saveOrganisationseinheit(organisationseinheit: Organisationseinheit): Observable<boolean> { - return this.setLoadingUntilFirst( - this.userRepository.saveOrganisationseinheit(organisationseinheit), - () => { - this.updateExistingOrganisationseinheit(organisationseinheit); - }, - ); - } - - private updateExistingOrganisationseinheit(organisationseinheit: Organisationseinheit): void { - this.setOrganisationseinheit([ - organisationseinheit, - ...this.organisationseinheitState$.value.organisationseinheitItems.filter( - (otherOrganisationseinheit: Organisationseinheit) => - otherOrganisationseinheit.id !== organisationseinheit.id, - ), - ]); - } - - public createOrganisationseinheit( - name: string, - organisationseinheitIds: string[], - ): Observable<boolean> { - return this.setLoadingUntilFirst( - this.userRepository.createOrganisationseinheit(name, organisationseinheitIds), - (organisationseinheit: Organisationseinheit) => { - this.addOrganisationseinheit(organisationseinheit); - }, - ); - } - - public deleteOrganisationseinheit(id: string): Observable<boolean> { - return this.setLoadingUntilFirst(this.userRepository.deleteOrganisationseinheit(id), () => - this.removeOrganisationseinheit(id), - ); - } - - private removeOrganisationseinheit(id: string): void { - this.setOrganisationseinheit( - this.organisationseinheitState$.value.organisationseinheitItems.filter( - (organisationseinheit: Organisationseinheit) => organisationseinheit.id !== id, - ), - ); - } - - setLoadingUntilFirst<T>( - action: Observable<T>, - tapFunction: (value: T) => void, - ): Observable<boolean> { - this.setLoading(); - - return action.pipe( - first(), - tap(tapFunction), - tap(() => this.setLoading(false)), - map(() => this.organisationseinheitState$.value.loading), - startWith(this.organisationseinheitState$.value.loading), - ); - } - - private addOrganisationseinheit(organisationseinheit: Organisationseinheit): void { - this.setOrganisationseinheit([ - ...this.organisationseinheitState$.value.organisationseinheitItems, - organisationseinheit, - ]); - } - - setOrganisationseinheit(organisationseinheitItems: Organisationseinheit[]): void { - this.organisationseinheitState$.next({ - ...this.organisationseinheitState$.value, - organisationseinheitItems: this.sortedOrganisationseinheitItems(organisationseinheitItems), - }); - } - - private sortedOrganisationseinheitItems( - organisationseinheitItems: Organisationseinheit[], - ): Organisationseinheit[] { - return [...organisationseinheitItems].sort((a, b) => a.name.localeCompare(b.name)); - } - - public getOrganisationseinheitState(): Observable<OrganisationseinheitState> { - return this.organisationseinheitState$.pipe(tap(() => this.updateIfRequired())); - } - - updateIfRequired(): void { - if (this.isUpdateRequired(this.organisationseinheitState$.value)) { - this.updateOrganisationseinheitItems().pipe(first()).subscribe(); - } - } - - private isUpdateRequired(state: OrganisationseinheitState): boolean { - return state.updateRequired && !state.loading; - } - - updateOrganisationseinheitItems(): Observable<Organisationseinheit[]> { - this.setLoading(); - - return this.userRepository.findOrganisationseinheitItems().pipe( - tap(() => this.setUpdateRequired(false)), - tap((organisationseinheitItems: Organisationseinheit[]) => - this.setOrganisationseinheit(organisationseinheitItems), - ), - ); - } - - setUpdateRequired(updateRequired: boolean = true) { - this.organisationseinheitState$.next({ - ...this.organisationseinheitState$.value, - updateRequired, - }); - } - - setLoading(loading: boolean = true): void { - this.organisationseinheitState$.next({ - ...this.organisationseinheitState$.value, - loading, - }); - } - - public getOrganisationseinheitItems(): Observable<Organisationseinheit[]> { - return this.getOrganisationseinheitState().pipe( - map((state: OrganisationseinheitState) => state.organisationseinheitItems), - ); - } -} diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts b/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts index 7c0a60daa8b3f7e03fc2b974ada2d7b4ae2f4d13..f4d9cc168ecb9eaca45f28cead659716ff232845 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.util.spec.ts @@ -1,22 +1,43 @@ -import { createOrganisationseinheitError } from '../../../test/user/user'; -import { OrganisationseinheitError, OrganisationseinheitErrorType } from './user.model'; -import { KEYCLOAK_ERROR_MESSAGES, getOrganisationseinheitErrorMessage } from './user.util'; - -describe('get organisationseinheit error message', () => { - it('should map known error message', () => { - const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError( - OrganisationseinheitErrorType.NAME_CONFLICT, - ); - const expectedMessage: string = KEYCLOAK_ERROR_MESSAGES[nameConflictError.errorType]; - - const message: string = getOrganisationseinheitErrorMessage(nameConflictError); - expect(message).toEqual(expectedMessage); +import { EMPTY_STRING } from '@alfa-client/tech-shared'; +import { createOrganisationseinheitError, createUser } from '../../../test/user/user'; +import { OrganisationseinheitError, OrganisationseinheitErrorType, User } from './user.model'; +import { KEYCLOAK_ERROR_MESSAGES, getOrganisationseinheitErrorMessage, sortUsersByLastName } from './user.util'; + +describe('user util', () => { + describe('get organisationseinheit error message', () => { + it('should map known error message', () => { + const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError( + OrganisationseinheitErrorType.NAME_CONFLICT, + ); + const expectedMessage: string = KEYCLOAK_ERROR_MESSAGES[nameConflictError.errorType]; + + const message: string = getOrganisationseinheitErrorMessage(nameConflictError); + + expect(message).toEqual(expectedMessage); + }); + + it('should map unknown error message to empty string', () => { + const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError(null); + + const message: string = getOrganisationseinheitErrorMessage(nameConflictError); + + expect(message).toEqual(EMPTY_STRING); + }); }); - it('should map unknown error message to empty string', () => { - const nameConflictError: OrganisationseinheitError = createOrganisationseinheitError(null); + describe('sort users by last name', () => { + const users: User[] = [ + { ...createUser(), lastName: 'Müller' }, + { ...createUser(), lastName: 'Schmidt' }, + { ...createUser(), lastName: 'Anders' }, + ]; + + it('shoud sort users by last name alphabetically ', () => { + const sortedUsers: User[] = sortUsersByLastName(users); - const message: string = getOrganisationseinheitErrorMessage(nameConflictError); - expect(message).toEqual(''); + expect(sortedUsers[0].lastName).toBe('Anders'); + expect(sortedUsers[1].lastName).toBe('Müller'); + expect(sortedUsers[2].lastName).toBe('Schmidt'); + }); }); }); diff --git a/alfa-client/libs/admin/settings/src/lib/user/user.util.ts b/alfa-client/libs/admin/settings/src/lib/user/user.util.ts index 2a1da4ffbdac3353761518988c9c112cbc450ca1..9878f7021e501b38d8d0938228a3c2c7c1df51b9 100644 --- a/alfa-client/libs/admin/settings/src/lib/user/user.util.ts +++ b/alfa-client/libs/admin/settings/src/lib/user/user.util.ts @@ -1,10 +1,9 @@ -import { OrganisationseinheitError, OrganisationseinheitErrorType } from './user.model'; +import { OrganisationseinheitError, OrganisationseinheitErrorType, User } from './user.model'; export const KEYCLOAK_ERROR_MESSAGES: { [type: string]: string } = { [OrganisationseinheitErrorType.NAME_CONFLICT]: 'Der Name exisitert bereits.', [OrganisationseinheitErrorType.NAME_MISSING]: 'Bitte den Namen angeben.', - [OrganisationseinheitErrorType.ID_MISSING]: - 'Bitte mindestens eine Organisationseinheit ID angeben.', + [OrganisationseinheitErrorType.ID_MISSING]: 'Bitte mindestens eine Organisationseinheit ID angeben.', }; export const KEYCLOAK_CREATE_GROUPS_ERROR_STATUS: { @@ -17,3 +16,7 @@ export const KEYCLOAK_CREATE_GROUPS_ERROR_STATUS: { export function getOrganisationseinheitErrorMessage(error: OrganisationseinheitError): string { return KEYCLOAK_ERROR_MESSAGES[error.errorType] ?? ''; } + +export function sortUsersByLastName(users: User[]): User[] { + return users.sort((a, b) => (a.lastName ?? '').localeCompare(b.lastName ?? '')); +} diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.spec.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbc8a861f16886d2e481d220281bfca7c01b03db --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.spec.ts @@ -0,0 +1,37 @@ +import { mock, Mock, useFromMock } from '@alfa-client/test-utils'; +import { fakeAsync, tick } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { createUser } from '../../../test/user/user'; +import { User } from '../user/user.model'; +import { UserRepository } from '../user/user.repository.service'; +import * as UserUtil from '../user/user.util'; +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + let repository: Mock<UserRepository>; + + const user: User = createUser(); + + beforeEach(() => { + repository = { ...mock(UserRepository), getUsers: jest.fn().mockReturnValue(of([user])) }; + service = new UserService(useFromMock(repository)); + }); + + describe('getItemsFromKeycloak', () => { + it('should call getUsers from userRepository', () => { + service.getItemsFromKeycloak(); + + expect(repository.getUsers).toHaveBeenCalled(); + }); + + it('should call sortUsersByLastName', fakeAsync(() => { + const sortUsersByLastNameSpy: jest.SpyInstance<User[]> = jest.spyOn(UserUtil, 'sortUsersByLastName'); + + service.getItemsFromKeycloak().subscribe(); + tick(); + + expect(sortUsersByLastNameSpy).toHaveBeenCalled(); + })); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..83c56cc41ce3f12ec58f542164a9a113c19b75ef --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/user.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { KeycloakResourceService } from '../user/keycloak.resource.service'; +import { User } from '../user/user.model'; +import { UserRepository } from '../user/user.repository.service'; +import { sortUsersByLastName } from '../user/user.util'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService extends KeycloakResourceService<User> { + constructor(private userRepository: UserRepository) { + super(); + } + + getItemsFromKeycloak(): Observable<User[]> { + return this.userRepository.getUsers().pipe(map(sortUsersByLastName)); + } + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..2607f2a96ac2d8eb8cef3aeffa18aa709fefdaea --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html @@ -0,0 +1,60 @@ +<h1 class="heading-1">Benutzer & Rollen</h1> +<ods-button-with-spinner text="Benutzer hinzufügen" class="py-8" dataTestId="add-user-button" /> +<ng-container *ngIf="users$ | async as users"> + <ul class="divide-y divide-gray-300 rounded-md bg-background-50 text-text shadow-sm ring-1 ring-gray-300 empty:hidden"> + <li *ngFor="let user of users.resource"> + <a + href="#" + class="flex flex-col items-start justify-between gap-6 border-primary-600/50 px-6 py-4 hover:bg-background-150 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus lg:flex-row" + > + <div class="flex-1 basis-1/2"> + <div class="mb-2 flex flex-wrap items-center gap-3"> + <h3 class="text-md font-semibold">{{ user | toUserName }}</h3> + <dl class="flex flex-wrap gap-2"> + <dt class="sr-only">Rollen:</dt> + <dd + *ngFor="let role of user.roles" + class="inline-flex flex-shrink-0 items-center rounded-full bg-green-50 px-1.5 py-0.5 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-600/20" + > + {{ role }} + </dd> + </dl> + </div> + + <dl> + <div *ngIf="user.email" class="flex items-center gap-2"> + <dt> + <span class="sr-only">E-Mail:</span> + <ods-mailbox-icon size="small" class="stroke-gray-600" /> + </dt> + <dd>{{ user.email }}</dd> + </div> + <div class="flex items-center gap-2"> + <dt> + <span class="sr-only">Benutzername:</span> + <ods-person-icon /> + </dt> + <dd>{{ user.username }}</dd> + </div> + </dl> + </div> + + <div class="flex-1 basis-1/2"> + <h4 class="sr-only">Zuständige Stellen</h4> + + <ng-container *ngIf="user.groups.length > 0; else noGroups"> + <ul class="list-outside list-disc pl-4"> + <ng-container *ngFor="let group of user.groups | slice: 0 : GROUPS_TO_DISPLAY"> + <li>{{ group }}</li> + </ng-container> + </ul> + <p *ngIf="user.groups.length > GROUPS_TO_DISPLAY" class="pl-4 text-gray-500"> + und {{ user.groups.length - 3 }} weitere + </p> + </ng-container> + <ng-template #noGroups>keine zuständige Stelle zugewiesen</ng-template> + </div> + </a> + </li> + </ul> +</ng-container> diff --git a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..49182ee84673fa793f0cbe0ee14eb694ab6f2319 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.spec.ts @@ -0,0 +1,47 @@ +import { Mock, mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonWithSpinnerComponent } from '@ods/component'; +import { MailboxIconComponent, PersonIconComponent } from '@ods/system'; +import { MockComponent, MockPipe } from 'ng-mocks'; +import { ToUserNamePipe } from '../user/to-user-name.pipe'; +import { UserService } from './user.service'; +import { UsersRolesComponent } from './users-roles.component'; + +describe('UsersRolesComponent', () => { + let component: UsersRolesComponent; + let fixture: ComponentFixture<UsersRolesComponent>; + + const userService: Mock<UserService> = { + ...mock(UserService), + get: jest.fn(), + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UsersRolesComponent], + providers: [{ provide: UserService, useValue: userService }], + imports: [ + MockComponent(ButtonWithSpinnerComponent), + MockComponent(MailboxIconComponent), + MockComponent(PersonIconComponent), + MockPipe(ToUserNamePipe), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(UsersRolesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('template', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('constructor', () => { + it('should call userService.get', () => { + expect(userService.get).toHaveBeenCalled(); + }); + }); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..46003c627d6c355241eba06d875e49d4d7df0f2b --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.ts @@ -0,0 +1,18 @@ +import { StateResource } from '@alfa-client/tech-shared'; +import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { User } from '../user/user.model'; +import { UserService } from './user.service'; + +@Component({ + selector: 'admin-users-roles', + templateUrl: './users-roles.component.html', +}) +export class UsersRolesComponent { + public users$: Observable<StateResource<User[]>>; + public readonly GROUPS_TO_DISPLAY = 3; + + constructor(private userService: UserService) { + this.users$ = this.userService.get(); + } +} diff --git a/alfa-client/libs/admin/settings/test/user/user.ts b/alfa-client/libs/admin/settings/test/user/user.ts index 090c729b48a5a0bea9f37ea4ec790793bf49b1f5..8e9c06e922483b856f0b179d14e476d7b81ddc83 100644 --- a/alfa-client/libs/admin/settings/test/user/user.ts +++ b/alfa-client/libs/admin/settings/test/user/user.ts @@ -5,9 +5,21 @@ import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType, - OrganisationseinheitState, + User, } from '../../src/lib/user/user.model'; +export function createUser(): User { + return { + id: faker.random.word(), + username: faker.random.word(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + email: faker.internet.email(), + roles: null, + groups: null, + }; +} + export function createOrganisationseinheit(): Organisationseinheit { return { id: faker.random.alphaNumeric(16), @@ -28,16 +40,6 @@ export function createGroupRepresentation( }; } -export function createOrganisationseinheitState( - organisationseinheitItems: Organisationseinheit[] = [createOrganisationseinheit()], -): OrganisationseinheitState { - return { - organisationseinheitItems: organisationseinheitItems, - loading: false, - updateRequired: false, - }; -} - export function createOrganisationseinheitError( errorType: OrganisationseinheitErrorType = OrganisationseinheitErrorType.NAME_MISSING, ): OrganisationseinheitError { diff --git a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.html b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.html index 733601490e198e7d93d37626f597974081b8367a..f6f07ea8d41b856a7044732497a7a9ae381ef669 100644 --- a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.html +++ b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.html @@ -11,7 +11,7 @@ dataTestId="anfrage-erstellen-button" (clickEmitter)="showRequestForm.emit()" > - <ods-collaboration-icon icon /> + <ods-users-icon icon /> </ods-button> </ng-template> diff --git a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.spec.ts b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.spec.ts index 5b17cd273cc3b503a6accb79219bb149c351041a..ab7493dc471343f302d5f6053f5dff6dc20057da 100644 --- a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.spec.ts +++ b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component.spec.ts @@ -13,7 +13,7 @@ import { triggerEvent, } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ButtonComponent, CollaborationIconComponent, OfficeIconComponent } from '@ods/system'; +import { ButtonComponent, OfficeIconComponent, UsersIconComponent } from '@ods/system'; import { createCollaboration, createCollaborationListResource, @@ -41,7 +41,7 @@ describe('CollaborationInVorgangComponent', () => { HasLinkPipe, MockComponent(ButtonComponent), MockComponent(CollaborationRequestFormComponent), - MockComponent(CollaborationIconComponent), + MockComponent(UsersIconComponent), MockComponent(OfficeIconComponent), MockComponent(OrganisationsEinheitComponent), ], diff --git a/alfa-client/libs/collaboration/src/lib/collaboration.module.ts b/alfa-client/libs/collaboration/src/lib/collaboration.module.ts index 82a66754b9da100acdf4bf900a6669832f24a4ab..bff26cb02d9a14bef2898adce7a6ec64c779106c 100644 --- a/alfa-client/libs/collaboration/src/lib/collaboration.module.ts +++ b/alfa-client/libs/collaboration/src/lib/collaboration.module.ts @@ -11,11 +11,11 @@ import { import { ButtonComponent, CloseIconComponent, - CollaborationIconComponent, InstantSearchComponent, OfficeIconComponent, SaveIconComponent, SearchIconComponent, + UsersIconComponent, } from '@ods/system'; import { CollaborationInVorgangContainerComponent } from './collaboration-in-vorgang-container/collaboration-in-vorgang-container.component'; import { CollaborationInVorgangComponent } from './collaboration-in-vorgang-container/collaboration-in-vorgang/collaboration-in-vorgang.component'; @@ -34,7 +34,7 @@ import { SearchOrganisationsEinheitFormComponent } from './search-organisations- CloseIconComponent, SearchIconComponent, CollaborationSharedModule, - CollaborationIconComponent, + UsersIconComponent, TextEditorComponent, TextareaEditorComponent, FormsModule, 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..e8388104a49fcb510c88b01ba98e4ea57fa2135f 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() : this.stateResource; } get isLoading(): boolean { diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 9e23463ad6c6fd0d96953de7adb1b71075150ca2..9a450a885bbe052655354f1410a730ad501ea22e 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -19,7 +19,6 @@ export * from './lib/icons/attachment-icon/attachment-icon.component'; export * from './lib/icons/bescheid-generate-icon/bescheid-generate-icon.component'; export * from './lib/icons/bescheid-upload-icon/bescheid-upload-icon.component'; export * from './lib/icons/close-icon/close-icon.component'; -export * from './lib/icons/collaboration-icon/collaboration-icon.component'; export * from './lib/icons/exclamation-icon/exclamation-icon.component'; export * from './lib/icons/file-icon/file-icon.component'; export * from './lib/icons/iconVariants'; @@ -27,11 +26,13 @@ export * from './lib/icons/logout-icon/logout-icon.component'; export * from './lib/icons/mailbox-icon/mailbox-icon.component'; export * from './lib/icons/office-icon/office-icon.component'; export * from './lib/icons/orga-unit-icon/orga-unit-icon.component'; +export * from './lib/icons/person-icon/person-icon.component'; export * from './lib/icons/save-icon/save-icon.component'; export * from './lib/icons/search-icon/search-icon.component'; export * from './lib/icons/send-icon/send-icon.component'; export * from './lib/icons/spinner-icon/spinner-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component'; +export * from './lib/icons/users-icon/users-icon.component'; export * from './lib/instant-search/instant-search/instant-search.component'; export * from './lib/instant-search/instant-search/instant-search.model'; export * from './lib/navbar/nav-item/nav-item.component'; diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts deleted file mode 100644 index c1a2136a653c2b9dfc52bc039fb0aa1d48ce0733..0000000000000000000000000000000000000000 --- a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CollaborationIconComponent } from './collaboration-icon.component'; - -describe('CollaborationIconComponent', () => { - let component: CollaborationIconComponent; - let fixture: ComponentFixture<CollaborationIconComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CollaborationIconComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(CollaborationIconComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts index 84f8f446e56ed21aa7d1dfd2119a673b850897e9..07b28d630504aa66112c1af5fe9c030a273f1533 100644 --- a/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/mailbox-icon/mailbox-icon.component.ts @@ -15,14 +15,12 @@ import { IconVariants, iconVariants } from '../iconVariants'; > <path d="M20 4H4C2.89543 4 2 4.89543 2 6V18C2 19.1046 2.89543 20 4 20H20C21.1046 20 22 19.1046 22 18V6C22 4.89543 21.1046 4 20 4Z" - stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> <path d="M22 7L13.03 12.7C12.7213 12.8934 12.3643 12.996 12 12.996C11.6357 12.996 11.2787 12.8934 10.97 12.7L2 7" - stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" diff --git a/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..652bfa8bc67ef5656971452b96f76272c06ecbdc --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PersonIconComponent } from './person-icon.component'; + +describe('PersonIconComponent', () => { + let component: PersonIconComponent; + let fixture: ComponentFixture<PersonIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PersonIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PersonIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..54a353e5f0ff022713350d113556d0953fdeeed3 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.component.ts @@ -0,0 +1,33 @@ +import { NgClass } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; + +import { IconVariants, iconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-person-icon', + standalone: true, + imports: [NgClass], + template: `<svg + xmlns="http://www.w3.org/2000/svg" + [ngClass]="[twMerge(iconVariants({ size }), 'stroke-gray-600', class)]" + aria-hidden="true" + viewBox="0 0 24 24" + fill="none" + > + <path + d="M20.4 22.8v-2.4a4.8 4.8 0 00-4.8-4.8H8.4a4.8 4.8 0 00-4.8 4.8v2.4m8.4-12a4.8 4.8 0 100-9.6 4.8 4.8 0 000 9.6z" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + fill="none" + /> + </svg>`, +}) +export class PersonIconComponent { + @Input() size: IconVariants['size'] = 'small'; + @Input() class: string = undefined; + + public readonly iconVariants = iconVariants; + public readonly twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..61ef6271736032c9a31a5695d62303bc77fdd373 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/person-icon/person-icon.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { PersonIconComponent } from './person-icon.component'; + +const meta: Meta<PersonIconComponent> = { + title: 'Icons/Person icon', + component: PersonIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<PersonIconComponent>; + +export const Default: Story = { + args: { size: 'large' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'small' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b12f0a04fd5c34a508c040a28ff59772577af746 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UsersIconComponent } from './users-icon.component'; + +describe('UsersIconComponent', () => { + let component: UsersIconComponent; + let fixture: ComponentFixture<UsersIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UsersIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UsersIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.ts similarity index 95% rename from alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts rename to alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.ts index 8565574eac373499fcedaea5b514e7f616b344e1..d7cc5a858b275dc0e33c1caecbf3ac265c89e818 100644 --- a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.component.ts @@ -4,7 +4,7 @@ import { twMerge } from 'tailwind-merge'; import { IconVariants, iconVariants } from '../iconVariants'; @Component({ - selector: 'ods-collaboration-icon', + selector: 'ods-users-icon', standalone: true, imports: [CommonModule], template: `<svg @@ -44,7 +44,7 @@ import { IconVariants, iconVariants } from '../iconVariants'; /> </svg>`, }) -export class CollaborationIconComponent { +export class UsersIconComponent { @Input() size: IconVariants['size'] = 'medium'; @Input() class: string = undefined; diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.stories.ts similarity index 64% rename from alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts rename to alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.stories.ts index dbc1440939f920bb66062062c12ac343a347ddce..702d6b13e5e6ff34a2da6549618f3c71891f6e96 100644 --- a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts +++ b/alfa-client/libs/design-system/src/lib/icons/users-icon/users-icon.stories.ts @@ -1,16 +1,16 @@ import type { Meta, StoryObj } from '@storybook/angular'; -import { CollaborationIconComponent } from './collaboration-icon.component'; +import { UsersIconComponent } from './users-icon.component'; -const meta: Meta<CollaborationIconComponent> = { - title: 'Icons/Collaboration icon', - component: CollaborationIconComponent, +const meta: Meta<UsersIconComponent> = { + title: 'Icons/Users icon', + component: UsersIconComponent, excludeStories: /.*Data$/, tags: ['autodocs'], }; export default meta; -type Story = StoryObj<CollaborationIconComponent>; +type Story = StoryObj<UsersIconComponent>; export const Default: Story = { args: { size: 'medium' }, diff --git a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts index 14acc1a1b2c6f3c6ca800bc30710d5a7adf48cdc..df3cc837b44933211f471f15420d87d88a95985b 100644 --- a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts +++ b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts @@ -9,11 +9,9 @@ import { RouterLink, RouterLinkActive } from '@angular/router'; template: `<a [routerLink]="to" routerLinkActive="bg-selected-light border-selected" - [ngClass]="[ - 'flex min-h-8 items-center gap-2 rounded-2xl px-4 py-2', - 'border border-transparent hover:border-primary', - 'outline-2 outline-offset-4 outline-focus focus-visible:border-background-200', - ]" + class="flex min-h-8 items-center gap-2 rounded-2xl border border-transparent + px-4 py-2 outline-2 outline-offset-2 outline-focus hover:border-primary + focus-visible:border-background-200 focus-visible:outline" [attr.data-test-id]="'link-to-' + to" > <ng-content select="[icon]" /> diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index 17394d5b91f600d6882f82fca324a97cdb046449..4691f7998807aa6e0e35dbcea7d84a5fd4a09885 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -85,7 +85,7 @@ export function doOnValidStateResource( if (isValidStateResource(stateResource)) actionOnValid(); } -export function isValidStateResource(stateResource: StateResource<Resource>): boolean { +export function isValidStateResource(stateResource: StateResource<unknown>): boolean { return stateResource.loaded && isNotNull(stateResource.resource); } diff --git a/alfa-client/libs/tech-shared/test/dummy.ts b/alfa-client/libs/tech-shared/test/dummy.ts index 33e1ab1611c9df963f722a8c340641e10e0c2d33..5eb5d3f6fb787bd42180b1b61a773079a6bcf7f4 100644 --- a/alfa-client/libs/tech-shared/test/dummy.ts +++ b/alfa-client/libs/tech-shared/test/dummy.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -class Dummy {} +export class Dummy {} export function createDummy(): Dummy { return new Dummy();