diff --git a/alfa-client/apps/admin/src/app/app.component.html b/alfa-client/apps/admin/src/app/app.component.html index 69e4b50b55c887010dbdea890b1f62c3b275478f..f2b6f351107b9ad56a829832c750d7b9f6956033 100644 --- a/alfa-client/apps/admin/src/app/app.component.html +++ b/alfa-client/apps/admin/src/app/app.component.html @@ -11,31 +11,26 @@ > <ods-admin-logo-icon /> </a> - <user-profile-button-container - data-test-id="user-profile-button" - ></user-profile-button-container> + <user-profile-button-container data-test-id="user-profile-button"></user-profile-button-container> </header> <div class="flex h-screen w-full justify-center overflow-y-auto"> <ods-navbar data-test-id="navigation"> <ng-container *ngIf="apiRoot | hasLink: ApiRootLinkRel.CONFIGURATION"> - <!-- <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-nav-item caption="Benutzer & Rollen" path="/benutzer_und_rollen"> <ods-users-icon class="stroke-text" icon /> </ods-nav-item> <hr /> - <ods-nav-item caption="Postfach" to="/postfach"> + <ods-nav-item caption="Postfach" path="/postfach"> <ods-mailbox-icon icon /> </ods-nav-item> + <ods-nav-item caption="Organisationseinheiten" path="/organisationseinheiten"> + <ods-orga-unit-icon icon /> + </ods-nav-item> </ng-container> </ods-navbar> <main class="flex-1 overflow-y-auto bg-white px-6 py-4"> <router-outlet - *ngIf=" - apiRoot | hasLink: ApiRootLinkRel.CONFIGURATION; - else configurationResourceLinkNotAvailable - " + *ngIf="apiRoot | hasLink: ApiRootLinkRel.CONFIGURATION; else configurationResourceLinkNotAvailable" data-test-id="router-outlet" ></router-outlet> <ng-template #configurationResourceLinkNotAvailable> diff --git a/alfa-client/apps/admin/src/app/app.module.ts b/alfa-client/apps/admin/src/app/app.module.ts index 7c9c63cf24edf02b34858e9a0f9f1ce98b2ffdcd..29e55f5de54331beb3a2195b83e6265c056dcb5b 100644 --- a/alfa-client/apps/admin/src/app/app.module.ts +++ b/alfa-client/apps/admin/src/app/app.module.ts @@ -28,7 +28,8 @@ import { OAuthModule } from 'angular-oauth2-oidc'; import { HttpUnauthorizedInterceptor } from 'libs/authentication/src/lib/http-unauthorized.interceptor'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { environment } from '../environments/environment'; -import { OrganisationseinheitPageComponent } from '../pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component'; +import { OrganisationsEinheitFormPageComponent } from '../pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component'; +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'; @@ -40,7 +41,8 @@ import { appRoutes } from './app.routes'; AppComponent, PostfachPageComponent, UserRolesPageComponent, - OrganisationseinheitPageComponent, + OrganisationsEinheitPageComponent, + OrganisationsEinheitFormPageComponent, UserProfileButtonContainerComponent, UnavailablePageComponent, ], diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index 9594ce3ee2736d7a990774c0b57db4f75e3a7582..4225458cdaeb73330edbafa34f76c46209ebfc27 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -1,4 +1,6 @@ import { Route } from '@angular/router'; +import { OrganisationsEinheitFormPageComponent } from '../pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component'; +import { OrganisationsEinheitPageComponent } from '../pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; import { UserRolesPageComponent } from '../pages/users-roles/user-roles-page/user-roles-page.component'; @@ -18,9 +20,14 @@ export const appRoutes: Route[] = [ component: UserRolesPageComponent, title: 'Admin | Benutzer & Rollen', }, - // { - // path: 'organisationseinheiten', - // component: OrganisationseinheitPageComponent, - // title: 'Admin | Organisationseinheiten', - // }, + { + path: 'organisationseinheiten', + component: OrganisationsEinheitPageComponent, + title: 'Admin | Organisationseinheiten', + }, + { + path: 'organisationseinheiten/:organisationsEinheitUrl', + component: OrganisationsEinheitFormPageComponent, + title: 'Admin | Organisationseinheit', + }, ]; diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2449a303b186022b68d062f7b59ba73c5cb6a790 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html @@ -0,0 +1 @@ +<admin-organisationseinheit-form-container/> \ No newline at end of file diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ad49ca4595e598e66a9e6b0d40b46cca0d606b5 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts @@ -0,0 +1,26 @@ +import { OrganisationsEinheitFormContainerComponent } from '@admin-client/admin-settings'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { OrganisationsEinheitFormPageComponent } from './organisationseinheit-form-page.component'; + +describe('OrganisationsEinheitFormPageComponent', () => { + let component: OrganisationsEinheitFormPageComponent; + let fixture: ComponentFixture<OrganisationsEinheitFormPageComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OrganisationsEinheitFormPageComponent, MockComponent(OrganisationsEinheitFormContainerComponent)], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OrganisationsEinheitFormPageComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7c9e0ea72a6ccd313239a4461f3a75692be5d9d --- /dev/null +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'organisationseinheit-form-page', + templateUrl: './organisationseinheit-form-page.component.html', +}) +export class OrganisationsEinheitFormPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html index ff86abb3c13e73397ead229e205a03fa87c3f944..792930158756ab6bea2bf0e8fbd5c164c5037c0a 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html @@ -1 +1 @@ -<admin-organisationseinheit-container></admin-organisationseinheit-container> +<admin-organisationseinheit-container/> diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts index e5c52e8433943e7eaedee4c2cc541400b7eb873c..59ddda81ab9c26fbac8f8bd976298596c9283b5a 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts @@ -1,23 +1,20 @@ -import { OrganisationseinheitContainerComponent } from '@admin-client/admin-settings'; +import { OrganisationsEinheitContainerComponent } from '@admin-client/admin-settings'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent } from 'ng-mocks'; -import { OrganisationseinheitPageComponent } from './organisationseinheit-page.component'; +import { OrganisationsEinheitPageComponent } from './organisationseinheit-page.component'; -describe('OrganisationseinheitPageComponent', () => { - let component: OrganisationseinheitPageComponent; - let fixture: ComponentFixture<OrganisationseinheitPageComponent>; +describe('OrganisationsEinheitPageComponent', () => { + let component: OrganisationsEinheitPageComponent; + let fixture: ComponentFixture<OrganisationsEinheitPageComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - OrganisationseinheitPageComponent, - MockComponent(OrganisationseinheitContainerComponent), - ], + declarations: [OrganisationsEinheitPageComponent, MockComponent(OrganisationsEinheitContainerComponent)], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(OrganisationseinheitPageComponent); + fixture = TestBed.createComponent(OrganisationsEinheitPageComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts index a87b271644db44803dd63e84c99ca71b0be15443..4d653b6ed7b3701a4f74ce763f313cc93ca29dc6 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts @@ -4,4 +4,4 @@ import { Component } from '@angular/core'; selector: 'organisationseinheit-page', templateUrl: './organisationseinheit-page.component.html', }) -export class OrganisationseinheitPageComponent {} +export class OrganisationsEinheitPageComponent {} diff --git a/alfa-client/libs/admin/settings/src/index.ts b/alfa-client/libs/admin/settings/src/index.ts index c4fb8070ef7b2b2df0fcfbe70f30c84a06235327..a13f84185d2480694cbb145fb300e553cc668381 100644 --- a/alfa-client/libs/admin/settings/src/index.ts +++ b/alfa-client/libs/admin/settings/src/index.ts @@ -1,5 +1,7 @@ export * from './lib/admin-settings.module'; +export * from './lib/organisationseinheit/organisations-einheit.model'; export * from './lib/organisationseinheit/organisationseinheit-container/organisationseinheit-container.component'; +export * from './lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-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 321e7c04c5850b85027d827f2bc43e971cdef1b1..ad13c6426adbf09d39909c175b215c4e79f6c932 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 @@ -1,20 +1,39 @@ import { ApiRootService } from '@alfa-client/api-root-shared'; import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; +import { NavigationSharedModule } from '@alfa-client/navigation-shared'; import { ResourceRepository, TechSharedModule } from '@alfa-client/tech-shared'; +import { UiModule } from '@alfa-client/ui'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import KcAdminClient from '@keycloak/keycloak-admin-client'; import { ButtonWithSpinnerComponent, TextareaEditorComponent } from '@ods/component'; -import { MailboxIconComponent, PersonIconComponent, TextInputComponent } from '@ods/system'; +import { + ExclamationIconComponent, + ListComponent, + ListItemComponent, + 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 { ConfigurationService } from './configuration/configuration.service'; -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 { + createOrganisationsEinheitListResourceService, + OrganisationsEinheitListResourceService, +} from './organisationseinheit/organisations-einheit-list-resource.service'; +import { + createOrganisationsEinheitResourceService, + OrganisationsEinheitResourceService, +} from './organisationseinheit/organisations-einheit-resource.service'; +import { OrganisationsEinheitContainerComponent } from './organisationseinheit/organisationseinheit-container/organisationseinheit-container.component'; +import { OrganisationsEinheitListComponent } from './organisationseinheit/organisationseinheit-container/organisationseinheit-list/organisationseinheit-list.component'; +import { OrganisationsEinheitFormContainerComponent } from './organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component'; +import { OrganisationsEinheitFormComponent } from './organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component'; +import { OrganisationsEinheitSignaturComponent } from './organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.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'; @@ -37,11 +56,13 @@ import { UsersRolesComponent } from './users-roles/users-roles.component'; PostfachSignaturComponent, NavigationItemComponent, TextFieldComponent, - OrganisationseinheitContainerComponent, - OrganisationseinheitFormComponent, + OrganisationsEinheitContainerComponent, + OrganisationsEinheitListComponent, + OrganisationsEinheitFormContainerComponent, + OrganisationsEinheitFormComponent, + OrganisationsEinheitSignaturComponent, PrimaryButtonComponent, SecondaryButtonComponent, - OrganisationseinheitListComponent, MoreMenuComponent, MoreItemButtonComponent, SpinnerComponent, @@ -58,8 +79,19 @@ import { UsersRolesComponent } from './users-roles/users-roles.component'; MailboxIconComponent, PersonIconComponent, ToUserNamePipe, + ListComponent, + ListItemComponent, + ExclamationIconComponent, + UiModule, + NavigationSharedModule, + ], + exports: [ + PostfachContainerComponent, + OrganisationsEinheitContainerComponent, + OrganisationsEinheitFormContainerComponent, + NavigationItemComponent, + UsersRolesComponent, ], - exports: [PostfachContainerComponent, OrganisationseinheitContainerComponent, NavigationItemComponent, UsersRolesComponent], providers: [ ConfigurationService, SettingsService, @@ -88,6 +120,16 @@ import { UsersRolesComponent } from './users-roles/users-roles.component'; useFactory: createSettingListResourceService, deps: [ResourceRepository, ConfigurationService], }, + { + provide: OrganisationsEinheitListResourceService, + useFactory: createOrganisationsEinheitListResourceService, + deps: [ResourceRepository, ApiRootService], + }, + { + provide: OrganisationsEinheitResourceService, + useFactory: createOrganisationsEinheitResourceService, + deps: [ResourceRepository, OrganisationsEinheitListResourceService], + }, ], }) export class AdminSettingsModule {} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-list-resource.service.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-list-resource.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..6fd977e0f5cffdd20fc701931e73e023279efafc --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-list-resource.service.ts @@ -0,0 +1,24 @@ +import { AdminOrganisationsEinheitItemResource, AdminOrganisationsEinheitListResource } from '@admin-client/admin-settings'; +import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; +import { ListResourceServiceConfig, ResourceListService, ResourceRepository } from '@alfa-client/tech-shared'; +import { ConfigurationResource } from 'libs/admin/settings/src/lib/configuration/configuration.model'; +import { OrganisationsEinheitListLinkRel } from './organisations-einheit.linkrel'; + +export class OrganisationsEinheitListResourceService extends ResourceListService< + ApiRootResource, + AdminOrganisationsEinheitListResource, + AdminOrganisationsEinheitItemResource +> {} + +export function createOrganisationsEinheitListResourceService(repository: ResourceRepository, apiRootService: ApiRootService) { + return new ResourceListService(buildConfig(apiRootService), repository); +} + +function buildConfig(apiRootService: ApiRootService): ListResourceServiceConfig<ConfigurationResource> { + return { + baseResource: apiRootService.getApiRoot(), + createLinkRel: 'TODO', + listLinkRel: ApiRootLinkRel.ORGANISATIONS_EINHEIT, + listResourceListLinkRel: OrganisationsEinheitListLinkRel.LIST, + }; +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-resource.service.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-resource.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..7722ed1bc822c933a67f33f74b7175ae2e7e2f47 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit-resource.service.ts @@ -0,0 +1,25 @@ +import { AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { ApiResourceService, ResourceRepository, ResourceServiceConfig } from '@alfa-client/tech-shared'; +import { ConfigurationResource } from 'libs/admin/settings/src/lib/configuration/configuration.model'; +import { OrganisationsEinheitListResourceService } from './organisations-einheit-list-resource.service'; +import { OrganisationsEinheitLinkRel } from './organisations-einheit.linkrel'; + +export class OrganisationsEinheitResourceService extends ApiResourceService< + AdminOrganisationsEinheitResource, + AdminOrganisationsEinheitResource +> {} + +export function createOrganisationsEinheitResourceService( + repository: ResourceRepository, + listResourceService: OrganisationsEinheitListResourceService, +) { + return new ApiResourceService(buildConfig(listResourceService), repository); +} + +function buildConfig(listResourceService: OrganisationsEinheitListResourceService): ResourceServiceConfig<ConfigurationResource> { + return { + resource: listResourceService.getSelected(), + getLinkRel: OrganisationsEinheitLinkRel.SELF, + edit: { linkRel: OrganisationsEinheitLinkRel.SELF }, + }; +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.linkrel.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.linkrel.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e941e1fc3ac3406b32d44085fcde10284fde8ba --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.linkrel.ts @@ -0,0 +1,7 @@ +export enum OrganisationsEinheitListLinkRel { + LIST = 'organisationsEinheitList', +} + +export enum OrganisationsEinheitLinkRel { + SELF = 'self', +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.model.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..f74a83a382373287cfc21afe8654c9023d67e341 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisations-einheit.model.ts @@ -0,0 +1,26 @@ +import { ListResource } from '@alfa-client/tech-shared'; +import { Resource } from '@ngxp/rest'; + +export interface AdminOrganisationsEinheit { + name: string; + organisationsEinheitId: string; + syncResult: AdminOrganisationsEinheitSyncResult; + settings?: AdminOrganisationsEinheitSettings; + isChild?: boolean; +} + +export enum AdminOrganisationsEinheitSyncResult { + OK = 'OK', + NOT_FOUND_IN_PVOG = 'NOT_FOUND_IN_PVOG', + NAME_MISMATCH = 'NAME_MISMATCH', + ORGANISATIONSEINHEIT_ID_NOT_UNIQUE = 'ORGANISATIONSEINHEIT_ID_NOT_UNIQUE', + DELETED = 'DELETED', +} + +export interface AdminOrganisationsEinheitSettings { + signatur?: string; +} + +export interface AdminOrganisationsEinheitResource extends AdminOrganisationsEinheit, Resource {} +export interface AdminOrganisationsEinheitListResource extends ListResource {} +export declare type AdminOrganisationsEinheitItemResource = Resource & AdminOrganisationsEinheit; 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 3e7af75f493ada03112994766ea980b90033e282..5dc954e2c97ce3b5bc3cef43b0cee24532dc987c 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,27 +1,7 @@ -<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> +<h1 class="heading-1 pb-4">Organisationseinheiten</h1> - <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-organisationseinheit-list - [organisationseinheitItems]="organisationseinheitItems" - (editOrganisationseinheit)="edit($event)" - (deleteOrganisationseinheit)="delete($event)" - data-test-id="organisationseinheit-list" - ></admin-organisationseinheit-list> -</ng-container> +<admin-organisationseinheit-list + *ngIf="organisationsEinheitResources$ | async as organisationsEinheitResources" + [organisationsEinheitResources]="organisationsEinheitResources" + data-test-id="organisations-einheit-list" +/> 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 1c5f9731bdfef401363bc423391cf1c7a2ed0529..43ee626c39887b1f882a4b5af5d23619171a0a63 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,126 +1,91 @@ +import { AdminOrganisationsEinheitResource, OrganisationsEinheitContainerComponent } from '@admin-client/admin-settings'; import { createStateResource } from '@alfa-client/tech-shared'; -import { - Mock, - dispatchEventFromFixture, - existsAsHtmlElement, - getElementFromFixtureByType, - mock, - notExistsAsHtmlElement, -} from '@alfa-client/test-utils'; +import { Mock, existsAsHtmlElement, mock } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { singleCold } from 'libs/tech-shared/test/marbles'; +import { ButtonWithSpinnerComponent } from '@ods/component'; import { MockComponent } from 'ng-mocks'; -import { of } from 'rxjs'; -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 { 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'; - -describe('OrganisationseinheitContainerComponent', () => { - let component: OrganisationseinheitContainerComponent; - let fixture: ComponentFixture<OrganisationseinheitContainerComponent>; - - const organisationseinheitService: Mock<OrganisationseinheitService> = mock(OrganisationseinheitService); - - const dialogOpenButtonSelector: string = getDataTestIdOf('organisationseinheit-open-dialog-button'); - const spinnerSelector: string = getDataTestIdOf('organisationseinheit-spinner'); +import { Observable, of } from 'rxjs'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { + createAdminOrganisationsEinheitListResource, + createAdminOrganisationsEinheitResource, +} from '../../../../test/organisations-einheit/organisations-einheit'; +import { OrganisationsEinheitService } from '../organisationseinheit.service'; +import { OrganisationsEinheitListComponent } from './organisationseinheit-list/organisationseinheit-list.component'; - const organisationseinheitItems: Organisationseinheit[] = [createOrganisationseinheit()]; +describe('OrganisationsEinheitContainerComponent', () => { + let component: OrganisationsEinheitContainerComponent; + let fixture: ComponentFixture<OrganisationsEinheitContainerComponent>; - let formComponent: OrganisationseinheitFormComponent; - let listComponent: OrganisationseinheitListComponent; + const organisationsEinheitService: Mock<OrganisationsEinheitService> = mock(OrganisationsEinheitService); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - OrganisationseinheitContainerComponent, - MockComponent(SecondaryButtonComponent), - MockComponent(OrganisationseinheitFormComponent), - MockComponent(OrganisationseinheitListComponent), - MockComponent(SpinnerComponent), - ], - providers: [{ provide: OrganisationseinheitService, useValue: organisationseinheitService }], + declarations: [OrganisationsEinheitContainerComponent, MockComponent(OrganisationsEinheitListComponent)], + imports: [ButtonWithSpinnerComponent], + providers: [{ provide: OrganisationsEinheitService, useValue: organisationsEinheitService }], }).compileComponents(); - fixture = TestBed.createComponent(OrganisationseinheitContainerComponent); + fixture = TestBed.createComponent(OrganisationsEinheitContainerComponent); component = fixture.componentInstance; - organisationseinheitService.get = jest.fn().mockReturnValue(of(createStateResource(organisationseinheitItems))); - fixture.detectChanges(); + organisationsEinheitService.getList = jest + .fn() + .mockReturnValue(of(createStateResource(createAdminOrganisationsEinheitListResource()))); - formComponent = getElementFromFixtureByType(fixture, OrganisationseinheitFormComponent); - listComponent = getElementFromFixtureByType(fixture, OrganisationseinheitListComponent); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should open form on new organisationseinheit button', () => { - formComponent.open = jest.fn(); - - dispatchEventFromFixture(fixture, dialogOpenButtonSelector, 'clickEmitter'); - - expect(formComponent.open).toHaveBeenCalled(); - }); - - describe('organisationseinheit list', () => { - it('should open form for editing on editOrganisationseinheit event', () => { - const organisationseinheit: Organisationseinheit = organisationseinheitItems[0]; - formComponent.openEdit = jest.fn(); - - listComponent.editOrganisationseinheit.emit(organisationseinheit); - - expect(formComponent.openEdit).toHaveBeenCalledWith(organisationseinheit); - }); - - it('should call deleteOrganisationseinheit form on deleteOrganisationseinheit event', () => { - const organisationseinheit: Organisationseinheit = organisationseinheitItems[0]; - component.delete = jest.fn(); - - listComponent.deleteOrganisationseinheit.emit(organisationseinheit); - - expect(component.delete).toHaveBeenCalledWith(organisationseinheit); - }); - }); - - describe('delete', () => { - const organisationseinheit: Organisationseinheit = organisationseinheitItems[0]; - - beforeEach(() => { - organisationseinheitService.delete = jest.fn().mockReturnValue(singleCold(true)); + describe('component', () => { + describe('ngOnInit', () => { + beforeEach(() => { + component.loadOrganisationsEinheitResources = jest.fn().mockReturnValue(of([createAdminOrganisationsEinheitResource()])); + }); + + it('should call loadOrganisationsEinheitResources', () => { + component.organisationsEinheitResources$.subscribe(() => { + expect(component.loadOrganisationsEinheitResources).toHaveBeenCalled(); + }); + }); + + it('should set organisationsEinheitResources', () => { + component.ngOnInit(); + + component.organisationsEinheitResources$.subscribe( + (organisationsEinheitResources: AdminOrganisationsEinheitResource[]) => { + expect(organisationsEinheitResources.length).toBe(1); + }, + ); + }); }); - it('should call service method', () => { - component.delete(organisationseinheit); + describe('loadOrganisationsEinheitResources', () => { + it('should call organisationsEinheitService getList', () => { + component.loadOrganisationsEinheitResources(); - expect(organisationseinheitService.delete).toHaveBeenCalledWith(organisationseinheit.id); - }); + expect(organisationsEinheitService.getList).toHaveBeenCalled(); + }); - it('should assign delete progress observable', () => { - component.delete(organisationseinheit); + it('should set organisationsEinheitResources', () => { + const organisationsEinheitResources$: Observable<AdminOrganisationsEinheitResource[]> = + component.loadOrganisationsEinheitResources(); - expect(component.deleteInProgress$).toBeObservable(singleCold(true)); + organisationsEinheitResources$.subscribe((organisationsEinheitResources: AdminOrganisationsEinheitResource[]) => { + expect(organisationsEinheitResources.length).toBe(1); + }); + }); }); }); - describe('spinner', () => { - it('should not show if delete in not progress', () => { - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, spinnerSelector); - }); - it('should show if delete in progress', () => { - component.deleteInProgress$ = of(true); - - fixture.detectChanges(); - - existsAsHtmlElement(fixture, spinnerSelector); + describe('template', () => { + describe('organisationsEinheiten list', () => { + it('should show list', () => { + existsAsHtmlElement(fixture, getDataTestIdOf('organisations-einheit-list')); + }); }); }); }); 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 c8dc13a8eda3ff1f0825c7282020040695df6ecd..ce5610b8b2d295c591265cc930f96f4b5c1684a4 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,36 +1,33 @@ -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 { OrganisationseinheitService } from '../organisationseinheit.service'; -import { OrganisationseinheitFormComponent } from './organisationseinheit-form/organisationseinheit-form.component'; +import { getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; +import { Component, OnInit } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { OrganisationsEinheitListLinkRel } from '../organisations-einheit.linkrel'; +import { AdminOrganisationsEinheitListResource, AdminOrganisationsEinheitResource } from '../organisations-einheit.model'; +import { OrganisationsEinheitService } from '../organisationseinheit.service'; @Component({ selector: 'admin-organisationseinheit-container', templateUrl: './organisationseinheit-container.component.html', }) -export class OrganisationseinheitContainerComponent implements OnInit { - organisationseinheitItems$: Observable<StateResource<Organisationseinheit[]>>; - deleteInProgress$: Observable<boolean> = of(false); +export class OrganisationsEinheitContainerComponent implements OnInit { + organisationsEinheitResources$: Observable<AdminOrganisationsEinheitResource[]>; - @ViewChild(OrganisationseinheitFormComponent) - private form!: OrganisationseinheitFormComponent; - - constructor(private organisationseinheitService: OrganisationseinheitService) {} + constructor(private organisationsEinheitService: OrganisationsEinheitService) {} ngOnInit(): void { - this.organisationseinheitItems$ = this.organisationseinheitService.get(); - } - - public openDialogForNewGroup(): void { - this.form.open(); - } - - public edit(organisationseinheit: Organisationseinheit): void { - this.form.openEdit(organisationseinheit); + this.organisationsEinheitResources$ = this.loadOrganisationsEinheitResources(); } - public delete(organisationseinheit: Organisationseinheit): void { - this.deleteInProgress$ = this.organisationseinheitService.delete(organisationseinheit.id); + loadOrganisationsEinheitResources(): Observable<AdminOrganisationsEinheitResource[]> { + return this.organisationsEinheitService + .getList() + .pipe( + map((organisationsEinheitListResource: StateResource<AdminOrganisationsEinheitListResource>) => + getEmbeddedResources<AdminOrganisationsEinheitResource>( + organisationsEinheitListResource, + OrganisationsEinheitListLinkRel.LIST, + ), + ), + ); } } 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 deleted file mode 100644 index 3bd257438d0545a41b7b791b9574b1cf9184b009..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.html +++ /dev/null @@ -1,34 +0,0 @@ -<ng-container *ngIf="submitInProgress$ | async"></ng-container> -<dialog #OrganisationseinheitDialog data-test-id="organisationseinheit-dialog" class="bg-gray-50"> - <button - (click)="OrganisationseinheitDialog.close()" - data-test-id="organisationseinheit-close-button" - class="absolute right-3 top-1 text-2xl text-black hover:font-bold active:text-black/80" - > - ✕ - </button> - <form [formGroup]="formService.form" class="m-5 grid grid-cols-1 gap-5"> - <h1 class="text-2xl" data-test-id="organisationseinheit-form-header"> - {{ label }} - </h1> - <text-field - label="Name" - data-test-id="organisationseinheit-name" - [formControlName]="OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD" - ></text-field> - <text-field - label="OrganisationseinheitID" - data-test-id="organisationseinheit-id" - [formControlName]="OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD" - ></text-field> - - <ods-button-with-spinner - data-test-id="organisationseinheit-save-button" - class="justify-self-end" - (clickEmitter)="submit()" - [stateResource]="organisationseinheitItems" - text="Speichern" - > - </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 deleted file mode 100644 index fd81569ecb52fceecb829096f4e859aeb73fad36..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { createEmptyStateResource } from '@alfa-client/tech-shared'; -import { - dispatchEventFromFixture, - getDebugElementFromFixtureByCss, - getElementFromFixture, - mock, - Mock, -} 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 { 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 { TextFieldComponent } from '../../../shared/text-field/text-field.component'; -import { Organisationseinheit } from '../../../user/user.model'; -import { OrganisationseinheitService } from '../../organisationseinheit.service'; -import { OrganisationseinheitFormComponent } from './organisationseinheit-form.component'; -import { OrganisationseinheitFormService } from './organisationseinheit-form.service'; - -describe('OrganisationseinheitFormComponent', () => { - let component: OrganisationseinheitFormComponent; - let fixture: ComponentFixture<OrganisationseinheitFormComponent>; - let form: UntypedFormGroup; - - const organisationseinheitService: Mock<OrganisationseinheitService> = mock(OrganisationseinheitService); - - const saveButtonSelector: string = getDataTestIdOf('organisationseinheit-save-button'); - const closeButtonSelector: string = getDataTestIdOf('organisationseinheit-close-button'); - const headerSelector: string = getDataTestIdOf('organisationseinheit-form-header'); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ - OrganisationseinheitFormComponent, - MockComponent(TextFieldComponent), - MockComponent(ButtonWithSpinnerComponent), - ], - imports: [ReactiveFormsModule, FormsModule, ButtonWithSpinnerComponent], - providers: [{ provide: OrganisationseinheitService, useValue: organisationseinheitService }], - }).compileComponents(); - - fixture = TestBed.createComponent(OrganisationseinheitFormComponent); - component = fixture.componentInstance; - form = fixture.componentInstance.formService.form; - fixture.detectChanges(); - - component.dialog.showModal = jest.fn(); - component.dialog.close = jest.fn(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('form element', () => { - const fields: string[][] = [ - [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 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)); - ngMocks.change(textFieldComponent, fieldValue); - expect(formControl.value).toBe(fieldValue); - }); - }); - - describe('save button', () => { - let saveButtonComponent: ButtonWithSpinnerComponent; - - beforeEach(() => { - saveButtonComponent = getDebugElementFromFixtureByCss(fixture, saveButtonSelector).componentInstance; - }); - - it('should call submit on click', () => { - component.submit = jest.fn(); - - dispatchEventFromFixture(fixture, saveButtonSelector, 'clickEmitter'); - - expect(component.submit).toHaveBeenCalled(); - }); - - it('should be disabled while stateResource in loading', () => { - component.organisationseinheitItems = createEmptyStateResource(true); - - fixture.detectChanges(); - - expect(saveButtonComponent.stateResource.loading).toBe(true); - }); - - it('should be enabled while not in progress', () => { - component.organisationseinheitItems = createEmptyStateResource(false); - - fixture.detectChanges(); - - expect(saveButtonComponent.stateResource.loading).toBe(false); - }); - }); - - describe('submit', () => { - beforeEach(() => { - component.handleProgressChange = jest.fn(); - }); - - it('should not call complete on submit error', fakeAsync(() => { - component.formService.submit = () => throwError(() => new Error('some error')); - - component.submit(); - component.submitInProgress$.subscribe({ - error: () => {}, - }); - tick(); - - expect(component.handleProgressChange).not.toHaveBeenCalled(); - })); - - it('should call complete on submit event', fakeAsync(() => { - component.formService.submit = () => of(false); - - component.submit(); - component.submitInProgress$.subscribe(); - tick(); - - expect(component.handleProgressChange).toHaveBeenCalled(); - })); - - it.each([true, false])('should use submit progress "%s"', (progress) => { - component.organisationseinheitItems = createEmptyStateResource(progress); - - fixture.detectChanges(); - - const saveButtonComponent: ButtonWithSpinnerComponent = getDebugElementFromFixtureByCss( - fixture, - saveButtonSelector, - ).componentInstance; - - expect(saveButtonComponent.stateResource.loading).toBe(progress); - }); - }); - - describe('handle progress change', () => { - it('should call complete if no errors with progress false', () => { - component.completeIfNoErrors = jest.fn(); - - component.handleProgressChange(false); - - expect(component.completeIfNoErrors).toHaveBeenCalled(); - }); - - it('should call complete if no errors with progress true', () => { - component.completeIfNoErrors = jest.fn(); - - component.handleProgressChange(true); - - expect(component.completeIfNoErrors).not.toHaveBeenCalled(); - }); - }); - - describe('complete if no errors', () => { - beforeEach(() => { - component.complete = jest.fn(); - }); - - it('should call not complete with errors', () => { - component.formService.isInvalid = jest.fn().mockReturnValue(true); - - component.completeIfNoErrors(); - - expect(component.complete).not.toHaveBeenCalled(); - }); - - it('should call complete without errors', () => { - component.formService.isInvalid = jest.fn().mockReturnValue(false); - - component.completeIfNoErrors(); - - expect(component.complete).toHaveBeenCalled(); - }); - }); - - describe('complete', () => { - beforeEach(() => { - component.dialog.close = jest.fn(); - component.formService.reset = jest.fn(); - }); - - it('should close dialog', () => { - component.complete(); - - expect(component.dialog.close).toHaveBeenCalled(); - }); - it('should reset form', () => { - component.complete(); - - expect(component.formService.reset).toHaveBeenCalled(); - }); - }); - - describe('close button', () => { - it('should call to close dialog', () => { - component.dialog.close = jest.fn(); - - dispatchEventFromFixture(fixture, closeButtonSelector, 'click'); - - expect(component.dialog.close).toHaveBeenCalled(); - }); - }); - - describe('open', () => { - beforeEach(() => { - form.markAsTouched(); - }); - - it('should set create label', () => { - component.open(); - - expect(component.label).toEqual(OrganisationseinheitFormComponent.CREATE_LABEL); - }); - - it('should open form', () => { - component.open(); - - expect(component.dialog.showModal).toHaveBeenCalled(); - }); - - it('should reset form', () => { - component.formService.reset = jest.fn(); - - component.open(); - - expect(component.formService.reset).toHaveBeenCalled(); - }); - }); - - describe('open edit', () => { - const organisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - it('should set edit label', () => { - component.openEdit(organisationseinheit); - - expect(component.label).toEqual(OrganisationseinheitFormComponent.EDIT_LABEL); - }); - - it('should open dialog', () => { - component.open(); - - expect(component.dialog.showModal).toHaveBeenCalled(); - }); - - it('should patch form', () => { - component.formService.patch = jest.fn(); - - component.openEdit(organisationseinheit); - - expect(component.formService.patch).toHaveBeenCalledWith(organisationseinheit); - }); - }); - - describe('header', () => { - it('should show label text', () => { - const text: string = 'test-text'; - component.label = text; - - fixture.detectChanges(); - - const headerElement: HTMLElement = getElementFromFixture(fixture, headerSelector); - expect(headerElement.textContent.trim()).toEqual(text); - }); - }); -}); 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 deleted file mode 100644 index 4f4976209bc7976af17a5ce954585efbcffaff15..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.component.ts +++ /dev/null @@ -1,65 +0,0 @@ -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-form.service'; - -@Component({ - selector: 'admin-organisationseinheit-form', - templateUrl: './organisationseinheit-form.component.html', - 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; - - @ViewChild('OrganisationseinheitDialog') private dialogRef: ElementRef<HTMLDialogElement>; - dialog: HTMLDialogElement; - - submitInProgress$: Observable<boolean> = of(false); - - label: string; - - 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))); - } - - handleProgressChange(progress: boolean): void { - if (!progress) { - this.completeIfNoErrors(); - } - } - - completeIfNoErrors(): void { - if (!this.formService.isInvalid()) { - this.complete(); - } - } - - public open(): void { - this.label = OrganisationseinheitFormComponent.CREATE_LABEL; - this.formService.reset(); - this.dialog.showModal(); - } - - public openEdit(organisationseinheit: Organisationseinheit): void { - this.label = OrganisationseinheitFormComponent.EDIT_LABEL; - this.formService.patch(organisationseinheit); - this.dialog.showModal(); - } - - complete(): void { - this.dialog.close(); - this.formService.reset(); - } -} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.service.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.service.ts deleted file mode 100644 index fb65fa06cc6959e5df3f029f7e2894307d140d43..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit-form.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { createStateResource, isNotNil, StateResource } from '@alfa-client/tech-shared'; -import { Injectable, Input } from '@angular/core'; -import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -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 { - @Input() organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([]); - - public static readonly ORGANISATIONSEINHEIT_NAME_FIELD: string = 'name'; - public static readonly ORGANISATIONSEINHEIT_IDS_FIELD: string = 'organisationseinheit'; - - form: UntypedFormGroup; - - source: Organisationseinheit; - - constructor( - private formBuilder: UntypedFormBuilder, - private organisationsEinheitService: OrganisationseinheitService, - ) { - this.form = this.formBuilder.group({ - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: new FormControl(''), - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: new FormControl(''), - }); - } - - public submit(): Observable<boolean> { - if (this.validate()) { - return this.callService().pipe( - catchError((error: OrganisationseinheitError) => { - this.handleError(error); - return of(false); - }), - ); - } else { - return of(false); - } - } - - callService(): Observable<boolean> { - return this.isPatch() ? this.save() : this.create(); - } - - create(): Observable<boolean> { - return this.organisationsEinheitService.create({ - name: this.getName(), - organisationseinheitIds: this.getOrganisationseinheitIds(), - }); - } - - save(): Observable<boolean> { - return this.organisationsEinheitService.save({ - ...this.source, - name: this.getName(), - organisationseinheitIds: this.getOrganisationseinheitIds(), - }); - } - - validate(): boolean { - let valid: boolean = true; - - if (this.getOrganisationseinheitIds().length == 0) { - this.setError(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD, { - errorType: OrganisationseinheitErrorType.ID_MISSING, - detail: '', - }); - valid = false; - } - - return valid; - } - - private getName(): string { - return this.getStringFromPotentiallyEmptyField(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD); - } - - private getOrganisationseinheitIds(): string[] { - return this.splitOrganisationseinheitIds( - this.form.get(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD).value ?? '', - ); - } - - private getStringFromPotentiallyEmptyField(fieldName: string): string { - return this.form.get(fieldName).value ?? ''; - } - - public isPatch(): boolean { - return isNotNil(this.source); - } - - handleError(error: OrganisationseinheitError): void { - if ( - error.errorType === OrganisationseinheitErrorType.NAME_CONFLICT || - error.errorType === OrganisationseinheitErrorType.NAME_MISSING - ) { - this.setError(OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD, error); - } - } - - private setError(controlName: string, error: OrganisationseinheitError): void { - const control: AbstractControl = this.form.get(controlName); - control.setErrors({ - [error.errorType]: getOrganisationseinheitErrorMessage(error), - }); - } - - splitOrganisationseinheitIds(organisationseinheitIdsString: string): string[] { - return organisationseinheitIdsString - .split(',') - .map((organisationseinheitId) => organisationseinheitId.trim()) - .filter((organisationseinheitId) => organisationseinheitId.length > 0); - } - - public patch(organisationseinheit: Organisationseinheit): void { - this.reset(); - this.source = organisationseinheit; - this.form.patchValue({ - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: organisationseinheit.organisationseinheitIds.join(', '), - }); - } - - public reset(): void { - this.source = null; - this.form.reset(); - this.form.markAsUntouched(); - } - - public isInvalid(): boolean { - return this.form.invalid; - } -} 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 deleted file mode 100644 index 30b0562d0e2ab18a08381eb1bded287beb3dcd68..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts +++ /dev/null @@ -1,344 +0,0 @@ -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 { getOrganisationseinheitErrorMessage } from '../../../user/user.util'; -import { OrganisationseinheitService } from '../../organisationseinheit.service'; -import { OrganisationseinheitFormService } from './organisationseinheit-form.service'; - -describe('OrganisationseinheitFormService', () => { - let formService: OrganisationseinheitFormService; - let organisationseinheit: Organisationseinheit; - const organisationseinheitService: Mock<OrganisationseinheitService> = mockResourceService(OrganisationseinheitService); - - const formBuilder: FormBuilder = new FormBuilder(); - - beforeEach(() => { - formService = new OrganisationseinheitFormService(formBuilder, useFromMock(organisationseinheitService)); - organisationseinheit = createOrganisationseinheit(); - formService.form.setValue({ - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: organisationseinheit.organisationseinheitIds.join(','), - }); - }); - - it('should create', () => { - expect(formService).toBeTruthy(); - }); - - describe('submit', () => { - beforeEach(() => { - formService.handleError = jest.fn(); - }); - - describe('with successful validation', () => { - beforeEach(() => { - formService.validate = jest.fn().mockReturnValue(true); - }); - it('should call handle error with service call observable', fakeAsync(() => { - const error: OrganisationseinheitError = createOrganisationseinheitError(); - formService.callService = jest.fn().mockReturnValue(throwError(() => error)); - - formService.submit().subscribe(); - tick(); - - expect(formService.handleError).toHaveBeenCalledWith(error); - })); - - it('should emit emit progress as false on error', () => { - const error: OrganisationseinheitError = createOrganisationseinheitError(); - formService.callService = jest.fn().mockReturnValue(hot('a#', { a: true }, error)); - - const progressObservable: Observable<boolean> = formService.submit(); - - expect(progressObservable).toBeObservable(hot('a(b|)', { a: true, b: false })); - }); - - it('should return progress observable', () => { - formService.callService = jest.fn().mockReturnValue(singleCold(true)); - - const progressObservable: Observable<boolean> = formService.submit(); - - expect(progressObservable).toBeObservable(singleCold(true)); - }); - }); - describe('with unsuccessful validation', () => { - beforeEach(() => { - formService.validate = jest.fn().mockReturnValue(false); - }); - - it('should return progress observable with false', async () => { - const progressObservable: Observable<boolean> = formService.submit(); - - const progress: boolean = await lastValueFrom(progressObservable); - - expect(progress).toBeFalsy(); - }); - }); - }); - - describe('validate', () => { - const hasIdMissingError = () => - formService.form.hasError( - OrganisationseinheitErrorType.ID_MISSING, - OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD, - ); - describe('without organisationeinheitIds', () => { - beforeEach(() => { - formService.form.setValue({ - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: organisationseinheit.name, - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: ',', - }); - }); - - it('should set id-missing error', () => { - formService.validate(); - - expect(hasIdMissingError()).toBeTruthy(); - }); - it('should be invalid', () => { - const valid: boolean = formService.validate(); - - expect(valid).toBeFalsy(); - }); - }); - describe('with organisationeinheitIds', () => { - it('should not set error', () => { - formService.validate(); - - expect(hasIdMissingError()).toBeFalsy(); - }); - - it('should be valid', () => { - const valid: boolean = formService.validate(); - - expect(valid).toBeTruthy(); - }); - }); - }); - - describe('call service', () => { - it('should use create if is not patch', () => { - formService.isPatch = jest.fn().mockReturnValue(false); - formService.create = jest.fn().mockReturnValue(singleHot(true)); - - const progressObservable: Observable<boolean> = formService.callService(); - - expect(progressObservable).toBeObservable(singleHot(true)); - }); - - it('should use save if is patch', () => { - formService.isPatch = jest.fn().mockReturnValue(true); - formService.save = jest.fn().mockReturnValue(singleHot(true)); - - const progressObservable: Observable<boolean> = formService.callService(); - - expect(progressObservable).toBeObservable(singleHot(true)); - }); - }); - - describe('is patch', () => { - it('should return false without source', () => { - formService.source = null; - - const isPatch: boolean = formService.isPatch(); - - expect(isPatch).toBeFalsy(); - }); - it('should return true with source', () => { - formService.source = createOrganisationseinheit(); - - const isPatch: boolean = formService.isPatch(); - - expect(isPatch).toBeTruthy(); - }); - }); - - describe('create', () => { - it('should call create organisationseinheit', () => { - formService.create(); - - 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, - }); - formService.create(); - - expect(organisationseinheitService.create).toHaveBeenCalledWith({ - name: '', - organisationseinheitIds: [], - }); - }); - - it('should return progress observable', () => { - organisationseinheitService.create.mockReturnValue(singleCold(true)); - - const progressObservable: Observable<boolean> = formService.create(); - - expect(progressObservable).toBeObservable(singleCold(true)); - }); - }); - - describe('save', () => { - it('should call save organisationseinheit', () => { - formService.source = createOrganisationseinheit(); - - formService.save(); - - expect(organisationseinheitService.save).toHaveBeenCalledWith({ - id: formService.source.id, - name: organisationseinheit.name, - organisationseinheitIds: organisationseinheit.organisationseinheitIds, - }); - }); - - it('should call save organisationseinheit with empty form', () => { - formService.source = createOrganisationseinheit(); - formService.form.setValue({ - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_NAME_FIELD]: null, - [OrganisationseinheitFormService.ORGANISATIONSEINHEIT_IDS_FIELD]: null, - }); - formService.save(); - - expect(organisationseinheitService.save).toHaveBeenCalledWith({ - id: formService.source.id, - name: '', - organisationseinheitIds: [], - }); - }); - - it('should return progress observable', () => { - organisationseinheitService.save.mockReturnValue(singleCold(true)); - - const progressObservable: Observable<boolean> = formService.save(); - - expect(progressObservable).toBeObservable(singleCold(true)); - }); - }); - - 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('should not set error on name field for unknown errors', () => { - const unknownError: OrganisationseinheitError = createOrganisationseinheitError(undefined); - - formService.handleError(unknownError); - - expect(formService.form.errors).toBeNull(); - }); - }); - - describe('patch', () => { - const organisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - it('should reset', () => { - formService.reset = jest.fn(); - - formService.patch(organisationseinheit); - - expect(formService.reset).toHaveBeenCalled(); - }); - - it('should set source', () => { - formService.patch(organisationseinheit); - - expect(formService.source).toBe(organisationseinheit); - }); - - it('should set name', () => { - formService.patch(organisationseinheit); - - 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); - expect(formControl.value).toEqual(organisationseinheit.organisationseinheitIds.join(', ')); - }); - }); - - describe('split organisationseinheitIds by comma', () => { - it('should return', () => { - 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,'); - - expect(organisationseinheitIds).toEqual(['55', '66']); - }); - }); - - describe('reset', () => { - it('should set source to null', () => { - formService.form.reset = jest.fn(); - - formService.reset(); - - expect(formService.source).toBeNull(); - }); - - it('should call form reset', () => { - formService.form.reset = jest.fn(); - - formService.reset(); - - expect(formService.form.reset).toHaveBeenCalled(); - }); - - it('should mark as untouched', () => { - formService.patch(organisationseinheit); - - expect(formService.form.untouched).toBeTruthy(); - }); - }); - - describe('is invalid', () => { - it('should return true', () => { - formService.form.setErrors({ some: 'message' }); - - const invalid: boolean = formService.isInvalid(); - - expect(invalid).toBeTruthy(); - }); - - it('should return false', () => { - const invalid: boolean = formService.isInvalid(); - - expect(invalid).toBeFalsy(); - }); - }); -}); - -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 35f04a2f2e65d9cd6354ccc65e4ebe980c7ad8bb..a42709e0e2447833b65c7c6b658cbd2bc56b6b15 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,54 +1,29 @@ -<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" +<ods-list *ngIf="organisationsEinheitResources.length > 0" data-test-id="organisations-einheit-list"> + <ods-list-item + *ngFor="let organisationsEinheitResource of organisationsEinheitResources" + [routerLink]="getEncodedSelfLink(organisationsEinheitResource)" + [class.text-red-500]="syncResultIsNotOk(organisationsEinheitResource)" + data-test-id="organisations-einheit-list-item" > - <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> + <dl class="flex-1 basis-3/4 font-semibold" [class.pl-4]="organisationsEinheitResource.isChild"> + <dt class="sr-only">Name</dt> + <dd data-test-id="organisations-einheit-name">{{ organisationsEinheitResource.name }}</dd> + </dl> + + <dl class="flex-1 basis-3/12"> + <dt class="sr-only">Organisationseinheit-ID</dt> + <dd data-test-id="organisations-einheit-id">{{ organisationsEinheitResource.organisationsEinheitId }}</dd> + </dl> + + <dl class="flex-1 basis-1/12"> + <dt class="sr-only">Synchronisationsergebnis</dt> + <dd class="mt-1"> + <ods-exclamation-icon + *ngIf="syncResultIsNotOk(organisationsEinheitResource)" + matTooltip="Organisationseinheit wurde nicht in den PVOG-Daten gefunden." + size="small" + /> + </dd> + </dl> + </ods-list-item> +</ods-list> 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 53a1347003495d3b5501794c11b04af3f60008c7..fa4d2943bd5e3e46e03c4d532379ac3a077290c9 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,48 +1,42 @@ +import { AdminOrganisationsEinheitResource, AdminOrganisationsEinheitSyncResult } from '@admin-client/admin-settings'; +import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; import { - ConvertForDataTestPipe, - StateResource, - convertForDataTest, - createStateResource, -} from '@alfa-client/tech-shared'; -import { - dispatchEventFromFixture, existsAsHtmlElement, getElementFromFixture, + getElementsFromFixture, + mock, notExistsAsHtmlElement, } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { ActivatedRoute } from '@angular/router'; +import { ResourceUri } from '@ngxp/rest'; +import { ExclamationIconComponent, ListComponent, ListItemComponent } from '@ods/system'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent } from 'ng-mocks'; -import { createOrganisationseinheit } from '../../../../../test/user/user'; -import { MoreItemButtonComponent } from '../../../shared/more-menu/more-item-button/more-item-button.component'; -import { MoreMenuComponent } from '../../../shared/more-menu/more-menu.component'; -import { Organisationseinheit } from '../../../user/user.model'; -import { OrganisationseinheitListComponent } from './organisationseinheit-list.component'; - -describe('OrganisationseinheitListComponent', () => { - let component: OrganisationseinheitListComponent; - let fixture: ComponentFixture<OrganisationseinheitListComponent>; - - const emptyMessageSelector: string = getDataTestIdOf('organisationseinheit-empty-message'); - const tableSelector: string = getDataTestIdOf('organisationseinheit-table'); - - const organisationseinheitElementSelector = ( - organisationseinheit: Organisationseinheit, - cell: string, - ) => - getDataTestIdOf(convertForDataTest(`organisationseinheit-${cell}-${organisationseinheit.id}`)); +import { MockModule } from 'ng-mocks'; +import { createAdminOrganisationsEinheitResource } from '../../../../../test/organisations-einheit/organisations-einheit'; +import { OrganisationsEinheitListComponent } from './organisationseinheit-list.component'; + +describe('OrganisationsEinheitListComponent', () => { + let component: OrganisationsEinheitListComponent; + let fixture: ComponentFixture<OrganisationsEinheitListComponent>; + + const listSelector: string = getDataTestIdOf('organisations-einheit-list'); + const listItemSelector: string = getDataTestIdOf('organisations-einheit-list-item'); beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - OrganisationseinheitListComponent, - ConvertForDataTestPipe, - MockComponent(MoreMenuComponent), - MockComponent(MoreItemButtonComponent), + providers: [ + { + provide: ActivatedRoute, + useValue: mock(ActivatedRoute), + }, ], + declarations: [OrganisationsEinheitListComponent, ConvertForDataTestPipe, MockModule(MatTooltipModule)], + imports: [ListComponent, ListItemComponent, ExclamationIconComponent], }).compileComponents(); - fixture = TestBed.createComponent(OrganisationseinheitListComponent); + fixture = TestBed.createComponent(OrganisationsEinheitListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -51,104 +45,127 @@ describe('OrganisationseinheitListComponent', () => { expect(component).toBeTruthy(); }); - describe('table rows', () => { - describe('without organisationseinheit items', () => { - it('should show empty message', () => { - existsAsHtmlElement(fixture, emptyMessageSelector); - }); - it('should not show table', () => { - notExistsAsHtmlElement(fixture, tableSelector); - }); - }); + describe('input', () => { + describe('organisationsEinheitResources', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource( + AdminOrganisationsEinheitSyncResult.NAME_MISMATCH, + ); - describe('with organisationseinheit items', () => { - const organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([ - createOrganisationseinheit(), - createOrganisationseinheit(), - ]); beforeEach(() => { - component.organisationseinheitItems = organisationseinheitItems; + component.organisationsEinheitResources = [organisationsEinheitResource]; fixture.detectChanges(); }); - it('should show table', () => { - existsAsHtmlElement(fixture, tableSelector); + it('should set organisationsEinheitResource name', () => { + const listItemElement: HTMLElement = getElementFromFixture(fixture, listItemSelector); + const nameElement: Element = listItemElement.querySelector('[data-test-id="organisations-einheit-name"]'); + + expect(nameElement.textContent).toBe(organisationsEinheitResource.name); }); - it('should not show empty message', () => { - notExistsAsHtmlElement(fixture, emptyMessageSelector); + + it('should set organisationsEinheitResource organisationsEinheitId', () => { + const listItemElement: HTMLElement = getElementFromFixture(fixture, listItemSelector); + const idElement: Element = listItemElement.querySelector('[data-test-id="organisations-einheit-id"]'); + + expect(idElement.textContent).toBe(organisationsEinheitResource.organisationsEinheitId); }); - it('should show rows in order', () => { - const tableElement: HTMLTableElement = getElementFromFixture(fixture, tableSelector); - const rows: HTMLTableRowElement[] = Array.from(tableElement.querySelectorAll('tr[id]')); - const rowIds: string[] = rows.map((row) => row.id); + it('should set exclamation icon', () => { + const listItemElement: HTMLElement = getElementFromFixture(fixture, listItemSelector); + const iconElement: Element = listItemElement.querySelector('ods-exclamation-icon'); + + expect(iconElement).toBeTruthy(); + }); + }); + }); - expect(rowIds).toEqual( - organisationseinheitItems.resource.map( - (organisationseinheit: Organisationseinheit) => organisationseinheit.id, - ), + describe('component', () => { + describe('moveChildrenIntoParentLevel', () => { + it('should move children one level up', () => { + const childList: AdminOrganisationsEinheitResource[] = [ + createAdminOrganisationsEinheitResource(), + createAdminOrganisationsEinheitResource(), + ]; + const item1: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + const item2: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + item2['_embedded'] = { childList }; + component.organisationsEinheitResources = [item1, item2]; + + expect(component.organisationsEinheitResources.length).toBe(4); + }); + + it('should set isChild property at moved children', () => { + const childList: AdminOrganisationsEinheitResource[] = [ + createAdminOrganisationsEinheitResource(), + createAdminOrganisationsEinheitResource(), + ]; + const item1: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + const item2: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + item2['_embedded'] = { childList }; + component.organisationsEinheitResources = [item1, item2]; + + expect(component.organisationsEinheitResources[2].isChild).toBeTruthy(); + }); + }); + describe('syncResultIsNotOk', () => { + it('should return true', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource( + AdminOrganisationsEinheitSyncResult.NAME_MISMATCH, ); + const result: boolean = component.syncResultIsNotOk(organisationsEinheitResource); + + expect(result).toBeTruthy(); }); - it.each(organisationseinheitItems.resource)( - 'should show name of organisationseinheit %#', - (organisationseinheit: Organisationseinheit) => { - const nameTableCell = getElementFromFixture( - fixture, - organisationseinheitElementSelector(organisationseinheit, 'name'), - ); + it('should return false', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource( + AdminOrganisationsEinheitSyncResult.OK, + ); + const result: boolean = component.syncResultIsNotOk(organisationsEinheitResource); - expect(nameTableCell.textContent.trim()).toBe(organisationseinheit.name); - }, - ); + expect(result).toBeFalsy(); + }); + }); - it.each(organisationseinheitItems.resource)( - 'should show organisationseinheitId of organisationseinheit %#', - (organisationseinheit: Organisationseinheit) => { - const attrTableCell = getElementFromFixture( - fixture, - organisationseinheitElementSelector(organisationseinheit, 'attr'), - ); - - expect(attrTableCell.textContent.trim()).toBe( - `OrganisationseinheitID: ${organisationseinheit.organisationseinheitIds.join(', ')}`, - ); - }, - ); + describe('getEncodedSelfLink', () => { + it('should return encoded self link', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + const result: ResourceUri = component.getEncodedSelfLink(organisationsEinheitResource); + + expect(result).toBeDefined(); + }); + }); + }); - it.each(organisationseinheitItems.resource)( - 'should emit editOrganisationseinheit %# on edit button click ', - (organisationseinheit: Organisationseinheit) => { - component.editOrganisationseinheit.emit = jest.fn(); + describe('template', () => { + describe('organisationsEinheiten list', () => { + describe('without organisationsEinheiten', () => { + it('should not show list', () => { + notExistsAsHtmlElement(fixture, listSelector); + }); + }); - dispatchEventFromFixture( - fixture, - organisationseinheitElementSelector(organisationseinheit, 'edit'), - 'clickEmitter', - ); + describe('with organisationsEinheiten', () => { + const organisationsEinheiten: AdminOrganisationsEinheitResource[] = [ + createAdminOrganisationsEinheitResource(), + createAdminOrganisationsEinheitResource(), + ]; - expect(component.editOrganisationseinheit.emit).toHaveBeenCalledWith( - organisationseinheit, - ); - }, - ); + beforeEach(() => { + component.organisationsEinheitResources = organisationsEinheiten; + fixture.detectChanges(); + }); - it.each(organisationseinheitItems.resource)( - 'should emit deleteOrganisationseinheit %# on delete button click ', - (organisationseinheit: Organisationseinheit) => { - component.deleteOrganisationseinheit.emit = jest.fn(); + it('should show list', () => { + existsAsHtmlElement(fixture, listSelector); + }); - dispatchEventFromFixture( - fixture, - organisationseinheitElementSelector(organisationseinheit, 'delete'), - 'clickEmitter', - ); + it('should show rows', () => { + const rows = getElementsFromFixture(fixture, listItemSelector); - expect(component.deleteOrganisationseinheit.emit).toHaveBeenCalledWith( - organisationseinheit, - ); - }, - ); + expect(rows.length).toEqual(2); + }); + }); }); }); }); 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 39b105cb05759aad5be906fb7b78e7b0b167dcb6..aab7414ed051b840b30523baa114828fba2a4fc6 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,18 +1,44 @@ -import { createStateResource, StateResource } from '@alfa-client/tech-shared'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { Organisationseinheit } from '../../../user/user.model'; +import { AdminOrganisationsEinheitResource, AdminOrganisationsEinheitSyncResult } from '@admin-client/admin-settings'; +import { encodeUrlForEmbedding } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; +import { ResourceUri, getLink } from '@ngxp/rest'; +import { OrganisationsEinheitLinkRel } from '../../organisations-einheit.linkrel'; @Component({ selector: 'admin-organisationseinheit-list', templateUrl: './organisationseinheit-list.component.html', }) -export class OrganisationseinheitListComponent { +export class OrganisationsEinheitListComponent { + private _organisationsEinheitResources: AdminOrganisationsEinheitResource[] = []; + @Input() - organisationseinheitItems: StateResource<Organisationseinheit[]> = createStateResource([]); + public get organisationsEinheitResources(): AdminOrganisationsEinheitResource[] { + return this._organisationsEinheitResources; + } + + public set organisationsEinheitResources(list: AdminOrganisationsEinheitResource[]) { + this.moveChildrenIntoParentLevel(list); + } + + moveChildrenIntoParentLevel(list: AdminOrganisationsEinheitResource[]): void { + list.forEach((parent) => { + this._organisationsEinheitResources.push(parent); + + if (parent._embedded && Array.isArray(parent._embedded.childList)) { + parent._embedded.childList.forEach((child: AdminOrganisationsEinheitResource) => { + child.isChild = true; + this._organisationsEinheitResources.push(child); + }); + } + }); + } - @Output() - editOrganisationseinheit: EventEmitter<Organisationseinheit> = new EventEmitter(); + syncResultIsNotOk(organisationsEinheitResource: AdminOrganisationsEinheitResource): boolean { + return organisationsEinheitResource.syncResult !== AdminOrganisationsEinheitSyncResult.OK; + } - @Output() - deleteOrganisationseinheit: EventEmitter<Organisationseinheit> = new EventEmitter(); + getEncodedSelfLink(organisationsEinheitResource: AdminOrganisationsEinheitResource): ResourceUri { + const resourceUri: ResourceUri = getLink(organisationsEinheitResource, OrganisationsEinheitLinkRel.SELF).href; + return encodeUrlForEmbedding(resourceUri); + } } diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f31e92e3c557900090fce127c00d206ce8f19d51 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.html @@ -0,0 +1,8 @@ +<h1 class="heading-1" data-test-id="organisations-form-container-headline"> + {{ (organisationsEinheitStateResource$ | async)?.resource?.name }} +</h1> + +<admin-organisationseinheit-form + [organisationsEinheitStateResource]="organisationsEinheitStateResource$ | async" + data-test-id="organisations-form" +/> \ No newline at end of file diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0854bf5313ee7fc349c7049dc8f7befe498c869 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.spec.ts @@ -0,0 +1,73 @@ +import { AdminOrganisationsEinheitResource, OrganisationsEinheitFormContainerComponent } from '@admin-client/admin-settings'; +import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { Mock, existsAsHtmlElement, mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { createAdminOrganisationsEinheitResource } from '../../../../test/organisations-einheit/organisations-einheit'; +import { OrganisationsEinheitService } from '../organisationseinheit.service'; +import { OrganisationsEinheitFormComponent } from './organisationseinheit-form/organisationseinheit-form.component'; + +describe('OrganisationsEinheitFormContainerComponent', () => { + let component: OrganisationsEinheitFormContainerComponent; + let fixture: ComponentFixture<OrganisationsEinheitFormContainerComponent>; + + const organisationsEinheitService: Mock<OrganisationsEinheitService> = mock(OrganisationsEinheitService); + + const organisationsEinheitStateResource = createStateResource(createAdminOrganisationsEinheitResource()); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OrganisationsEinheitFormContainerComponent, MockComponent(OrganisationsEinheitFormComponent)], + providers: [{ provide: OrganisationsEinheitService, useValue: organisationsEinheitService }], + }).compileComponents(); + + fixture = TestBed.createComponent(OrganisationsEinheitFormContainerComponent); + component = fixture.componentInstance; + + organisationsEinheitService.get = jest.fn().mockReturnValue(of(organisationsEinheitStateResource)); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + describe('ngOnInit', () => { + it('should call organisationsEinheitService get', () => { + component.ngOnInit(); + + component.organisationsEinheitStateResource$.subscribe(() => { + expect(organisationsEinheitService.get).toHaveBeenCalled(); + }); + }); + + it('should set organisationsEinheitStateResource', () => { + component.ngOnInit(); + + component.organisationsEinheitStateResource$.subscribe( + (organisationsEinheitStateResource: StateResource<AdminOrganisationsEinheitResource>) => { + expect(organisationsEinheitStateResource).toEqual(organisationsEinheitStateResource); + }, + ); + }); + }); + }); + + describe('template', () => { + describe('headline', () => { + it('should show organisationsEinheit name', () => { + existsAsHtmlElement(fixture, getDataTestIdOf('organisations-form-container-headline')); + }); + }); + + describe('organisationsEinheit form', () => { + it('should show organisationsEinheit form', () => { + existsAsHtmlElement(fixture, getDataTestIdOf('organisations-form')); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc46ee5d86126541187b2ebd9d41dce928d553e8 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form-container.component.ts @@ -0,0 +1,19 @@ +import { StateResource } from '@alfa-client/tech-shared'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AdminOrganisationsEinheitResource } from '../organisations-einheit.model'; +import { OrganisationsEinheitService } from '../organisationseinheit.service'; + +@Component({ + selector: 'admin-organisationseinheit-form-container', + templateUrl: './organisationseinheit-form-container.component.html', +}) +export class OrganisationsEinheitFormContainerComponent implements OnInit { + organisationsEinheitStateResource$: Observable<StateResource<AdminOrganisationsEinheitResource>>; + + constructor(private organisationsEinheitService: OrganisationsEinheitService) {} + + ngOnInit(): void { + this.organisationsEinheitStateResource$ = this.organisationsEinheitService.get(); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..3e8708c6939b72e0796be10b943baf18bc9cf892 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.html @@ -0,0 +1,18 @@ +<form class="form flex-col" [formGroup]="formService.form"> + <admin-organisationseinheit-signatur class="mb-6 block" data-test-id="organisations-einheit-signatur-component" /> + + <ods-button-with-spinner + data-test-id="save-button" + text="Speichern" + [stateResource]="submitInProgress$ | async" + (clickEmitter)="submit()" + ></ods-button-with-spinner> + + <span + *ngIf="formService.invalidEmpty" + data-test-id="invalid-empty-message-span" + class="m-2 text-red-500" + > + *Es müssen alle Felder ausgefüllt sein. + </span> +</form> \ No newline at end of file diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1976562c9a9fa7aa270e230278e4ce3093775503 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.spec.ts @@ -0,0 +1,120 @@ +import { AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { createEmptyStateResource, createStateResource, ProblemDetail } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, Mock, mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ButtonWithSpinnerComponent } from '@ods/component'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { createInvalidParam, createProblemDetail } from '../../../../../../../tech-shared/test/error'; +import { createAdminOrganisationsEinheitResource } from '../../../../../test/organisations-einheit/organisations-einheit'; +import { TextFieldComponent } from '../../../shared/text-field/text-field.component'; +import { OrganisationsEinheitService } from '../../organisationseinheit.service'; +import { OrganisationsEinheitFormComponent } from './organisationseinheit-form.component'; +import { OrganisationsEinheitSignaturComponent } from './organisationseinheit-signatur/organisationseinheit-signatur.component'; +import { OrganisationsEinheitFormService } from './organisationseinheit.formservice'; + +describe('OrganisationsEinheitFormComponent', () => { + let component: OrganisationsEinheitFormComponent; + let fixture: ComponentFixture<OrganisationsEinheitFormComponent>; + let formService: OrganisationsEinheitFormService; + + const organisationsEinheitService: Mock<OrganisationsEinheitService> = mock(OrganisationsEinheitService); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + OrganisationsEinheitFormComponent, + MockComponent(TextFieldComponent), + MockComponent(OrganisationsEinheitSignaturComponent), + MockComponent(ButtonWithSpinnerComponent), + ], + imports: [ReactiveFormsModule, FormsModule], + providers: [{ provide: OrganisationsEinheitService, useValue: organisationsEinheitService }], + }).compileComponents(); + + fixture = TestBed.createComponent(OrganisationsEinheitFormComponent); + component = fixture.componentInstance; + formService = component.formService; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('input', () => { + describe('organisationsEinheitStateResource', () => { + it('should return resource', () => { + const updateOrganisationsEinheitResourceFn: jest.SpyInstance = jest.spyOn( + component, + 'updateOrganisationsEinheitResource', + ); + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + + component.organisationsEinheitStateResource = createStateResource(organisationsEinheitResource); + + expect(updateOrganisationsEinheitResourceFn).toHaveBeenCalledWith(organisationsEinheitResource); + }); + }); + }); + + describe('component', () => { + describe('updateOrganisationsEinheitResource', () => { + it('should call formService patch', () => { + formService.patch = jest.fn(); + + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + + component.updateOrganisationsEinheitResource(organisationsEinheitResource); + + expect(formService.patch).toHaveBeenCalledWith(organisationsEinheitResource.settings); + }); + }); + + describe('submit', () => { + it('should call formService submit', () => { + formService.submit = jest.fn().mockReturnValue(of(createEmptyStateResource())); + + component.submit(); + + expect(formService.submit).toHaveBeenCalled(); + }); + }); + }); + + describe('template', () => { + describe('organisationsEinheit signatur component', () => { + it('should show signatur component', () => { + existsAsHtmlElement(fixture, getDataTestIdOf('organisations-einheit-signatur-component')); + }); + }); + + describe('save button', () => { + it('should show save button', () => { + existsAsHtmlElement(fixture, getDataTestIdOf('save-button')); + }); + }); + + describe('invalid message', () => { + const invalidMessageSpan: string = getDataTestIdOf('invalid-empty-message-span'); + + it('should show if form invalidEmpty', () => { + const problemDetail: ProblemDetail = { + ...createProblemDetail(), + invalidParams: [{ ...createInvalidParam(), name: 'settingBody.signatur' }], + }; + formService.setErrorByProblemDetail(problemDetail); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, invalidMessageSpan); + }); + + it('should not show if form valid', () => { + notExistsAsHtmlElement(fixture, invalidMessageSpan); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e23d78e48258b6444fa11e4375ce8259f46742fd --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-form.component.ts @@ -0,0 +1,31 @@ +import { AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { StateResource, createEmptyStateResource, isNotNil } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; +import { Resource } from '@ngxp/rest'; +import { Observable, of } from 'rxjs'; +import { OrganisationsEinheitFormService } from './organisationseinheit.formservice'; + +@Component({ + selector: 'admin-organisationseinheit-form', + templateUrl: './organisationseinheit-form.component.html', + providers: [OrganisationsEinheitFormService], +}) +export class OrganisationsEinheitFormComponent { + submitInProgress$: Observable<StateResource<Resource>> = of(createEmptyStateResource<Resource>()); + + @Input() set organisationsEinheitStateResource(stateResource: StateResource<AdminOrganisationsEinheitResource>) { + this.updateOrganisationsEinheitResource(stateResource.resource); + } + + updateOrganisationsEinheitResource(organisationsEinheitResource: AdminOrganisationsEinheitResource): void { + if (isNotNil(organisationsEinheitResource)) { + this.formService.patch(organisationsEinheitResource.settings); + } + } + + constructor(public formService: OrganisationsEinheitFormService) {} + + public submit(): void { + this.submitInProgress$ = this.formService.submit(); + } +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.html b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.html new file mode 100644 index 0000000000000000000000000000000000000000..08db87e61bb3198d993070fdb54ab8ae5a68eb0c --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.html @@ -0,0 +1,16 @@ +<form [formGroup]="formService.form" class="max-w-[960px]"> + <h2 class="heading-2">Signatur</h2> + <p id="signatur-desc"> + Diese Signatur gilt für die ausgewählte Organisationseinheit und wird bei allen Nachrichten an den Antragsteller angezeigt. + </p> + <ods-textarea-editor + [formControlName]="formServiceClass.ORGANISATIONSEINHEIT_SIGNATUR_FIELD" + [isResizable]="false" + [showLabel]="false" + data-test-id="signatur-text" + label="signature" + rows="6" + class="w-full" + aria-describedby="signatur-desc" + /> +</form> \ No newline at end of file diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..da98a3b87601a0830743d69fbe6ed99cb2325f1f --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.spec.ts @@ -0,0 +1,57 @@ +import { NavigationSharedModule } from '@alfa-client/navigation-shared'; +import { getElementFromFixture, mock, useFromMock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; +import { TextareaEditorComponent } from '@ods/component'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { OrganisationsEinheitService } from '../../../organisationseinheit.service'; +import { OrganisationsEinheitFormService } from '../organisationseinheit.formservice'; +import { OrganisationsEinheitSignaturComponent } from './organisationseinheit-signatur.component'; + +describe('OrganisationsEinheitSignaturComponent', () => { + let component: OrganisationsEinheitSignaturComponent; + let fixture: ComponentFixture<OrganisationsEinheitSignaturComponent>; + + const formService: OrganisationsEinheitFormService = new OrganisationsEinheitFormService( + new FormBuilder(), + useFromMock(mock(OrganisationsEinheitService)), + ); + + const signaturTextarea = getDataTestIdOf('signatur-text'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, NavigationSharedModule, StoreModule.forRoot({}), EffectsModule.forRoot([])], + declarations: [OrganisationsEinheitSignaturComponent, MockComponent(TextareaEditorComponent)], + providers: [ + { + provide: OrganisationsEinheitFormService, + useValue: formService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(OrganisationsEinheitSignaturComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('ods-textarea-editor', () => { + describe('input', () => { + it('should set signatur field', () => { + const textAreaEditor = getElementFromFixture(fixture, signaturTextarea); + + expect(textAreaEditor.getAttribute('rows')).toEqual('6'); + }); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..112353d7b5cf1a613f3f81fd77662b7b11c8a2c0 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit-signatur/organisationseinheit-signatur.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; +import { OrganisationsEinheitFormService } from '../organisationseinheit.formservice'; + +@Component({ + selector: 'admin-organisationseinheit-signatur', + templateUrl: './organisationseinheit-signatur.component.html', +}) +export class OrganisationsEinheitSignaturComponent { + protected readonly formServiceClass = OrganisationsEinheitFormService; + + constructor(public formService: OrganisationsEinheitFormService) {} +} diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..249af7021abcd360680e7c7318c2123763089d20 --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.spec.ts @@ -0,0 +1,42 @@ +import { AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { FormBuilder } from '@angular/forms'; +import { of } from 'rxjs'; +import { createAdminOrganisationsEinheitResource } from '../../../../../test/organisations-einheit/organisations-einheit'; +import { OrganisationsEinheitService } from '../../organisationseinheit.service'; +import { OrganisationsEinheitFormService } from './organisationseinheit.formservice'; + +describe('OrganisationsEinheitFormService', () => { + let service: OrganisationsEinheitFormService; + let organisationsEinheitService: Mock<OrganisationsEinheitService>; + const formBuilder: FormBuilder = new FormBuilder(); + + beforeEach(() => { + organisationsEinheitService = mock(OrganisationsEinheitService); + service = new OrganisationsEinheitFormService(formBuilder, useFromMock(organisationsEinheitService)); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + describe('submit', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + + beforeEach(() => { + const stateResource: StateResource<AdminOrganisationsEinheitResource> = createStateResource(organisationsEinheitResource); + organisationsEinheitService.patch.mockReturnValue(of(stateResource)); + organisationsEinheitService.get.mockReturnValue(of(stateResource)); + service.form.setValue({ + [OrganisationsEinheitFormService.ORGANISATIONSEINHEIT_SIGNATUR_FIELD]: organisationsEinheitResource.settings.signatur, + }); + }); + + it('should call organisationsEinheitService patch', () => { + service.submit(); + + expect(organisationsEinheitService.patch).toHaveBeenCalledWith(organisationsEinheitResource.settings); + }); + }); +}); diff --git a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.ts b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.ts new file mode 100644 index 0000000000000000000000000000000000000000..82beca0dfb6fd1cb9210090f7a11cac3c2b497eb --- /dev/null +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit-form-container/organisationseinheit-form/organisationseinheit.formservice.ts @@ -0,0 +1,36 @@ +import { AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { AbstractFormService, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; +import { Injectable } from '@angular/core'; +import { FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Observable } from 'rxjs'; +import { OrganisationsEinheitService } from '../../organisationseinheit.service'; + +@Injectable() +export class OrganisationsEinheitFormService extends AbstractFormService { + public static readonly ORGANISATIONSEINHEIT_SIGNATUR_FIELD: string = 'signatur'; + + constructor( + formBuilder: UntypedFormBuilder, + private organisationsEinheitService: OrganisationsEinheitService, + ) { + super(formBuilder); + } + + protected initForm(): UntypedFormGroup { + return this.formBuilder.group({ + [OrganisationsEinheitFormService.ORGANISATIONSEINHEIT_SIGNATUR_FIELD]: new FormControl(EMPTY_STRING), + }); + } + + protected doSubmit(): Observable<StateResource<AdminOrganisationsEinheitResource>> { + return this.organisationsEinheitService.patch(this.getFormValue()); + } + + protected getPathPrefix(): string { + return 'settingBody'; + } + + public get invalidEmpty(): boolean { + return this.form.invalid; + } +} 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 index 4e16cd2a3421cac02ddcd38e472322fc5942e808..77d2d39309893316e9a99db8d6e6796fcd1a9d05 100644 --- 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 @@ -1,48 +1,155 @@ -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'; +import { AdminOrganisationsEinheitListResource, AdminOrganisationsEinheitResource } from '@admin-client/admin-settings'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { SnackBarService } from '@alfa-client/ui'; +import { singleColdCompleted } from 'libs/tech-shared/test/marbles'; +import { Observable, of } from 'rxjs'; +import { + createAdminOrganisationsEinheitListResource, + createAdminOrganisationsEinheitResource, +} from '../../../test/organisations-einheit/organisations-einheit'; +import { OrganisationsEinheitListResourceService } from './organisations-einheit-list-resource.service'; +import { OrganisationsEinheitResourceService } from './organisations-einheit-resource.service'; +import { OrganisationsEinheitService } from './organisationseinheit.service'; -describe('OrganisationseinheitService', () => { - let service: OrganisationseinheitService; - let repository: Mock<UserRepository>; +jest.mock('./organisations-einheit-list-resource.service'); +jest.mock('./organisations-einheit-resource.service'); +jest.mock('../../../../../navigation-shared/src/lib/navigation.service'); +jest.mock('../../../../../ui/src/lib/snackbar/snackbar.service'); - const organisationseinheit = createOrganisationseinheit(); +describe('OrganisationsEinheitService', () => { + let service: OrganisationsEinheitService; + let listResourceService: Mock<OrganisationsEinheitListResourceService>; + let resourceService: Mock<OrganisationsEinheitResourceService>; + let navigationService: Mock<NavigationService>; + let snackBarService: Mock<SnackBarService>; beforeEach(() => { - repository = mock(UserRepository); - service = new OrganisationseinheitService(useFromMock(repository)); + listResourceService = mock(OrganisationsEinheitListResourceService); + resourceService = mock(OrganisationsEinheitResourceService); + + navigationService = mock(NavigationService); + navigationService.urlChanged.mockReturnValue(of({})); + + snackBarService = mock(SnackBarService); + service = new OrganisationsEinheitService( + useFromMock(listResourceService), + useFromMock(resourceService), + useFromMock(navigationService), + useFromMock(snackBarService), + ); }); - describe('getItemsFromKeycloak', () => { - it('should call findOrganisationseinheitItems from userRepository', () => { - service.getItemsFromKeycloak(); + describe('onNavigation', () => { + it('should not call listResourceService select', () => { + service.onNavigation({}); + + expect(listResourceService.select).not.toHaveBeenCalled(); + }); + + it('should call listResourceService select', () => { + service.onNavigation({ [OrganisationsEinheitService.ORGANISATIONS_EINHEIT_URL]: 'some-uri' }); - expect(repository.findOrganisationseinheitItems).toHaveBeenCalled(); + expect(listResourceService.select).toHaveBeenCalled(); + }); + + it('should call getOrganisationsEinheitUrl', () => { + (<any>service).getOrganisationsEinheitUrl = jest.fn(); + + service.onNavigation({ [OrganisationsEinheitService.ORGANISATIONS_EINHEIT_URL]: 'some-uri' }); + + expect((<any>service).getOrganisationsEinheitUrl).toHaveBeenCalled(); }); }); - describe('saveInKeycloak', () => { - it('should call saveOrganisationseinheit from userRepository', () => { - service.saveInKeycloak(organisationseinheit); + describe('getOrganisationsEinheitUrl', () => { + it('should call navigationService getDecodedParam', () => { + service.getOrganisationsEinheitUrl(); - expect(repository.saveOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit); + expect(navigationService.getDecodedParam).toHaveBeenCalled(); + }); + + it('should return uri', () => { + navigationService.getDecodedParam.mockReturnValue('some-uri'); + + expect(service.getOrganisationsEinheitUrl()).toBe('some-uri'); }); }); - describe('createInKeycloak', () => { - it('should call createOrganisationseinheit from userRepository', () => { - service.createInKeycloak(organisationseinheit); + describe('getList', () => { + const organisationsEinheitListResource: AdminOrganisationsEinheitListResource = createAdminOrganisationsEinheitListResource(); + const organisationsEinheitStateListResource: StateResource<AdminOrganisationsEinheitListResource> = + createStateResource(organisationsEinheitListResource); + + beforeEach(() => { + listResourceService.getList.mockReturnValue(of(organisationsEinheitStateListResource)); + }); + + it('should call listResourceService', () => { + service.getList(); - expect(repository.createOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit); + expect(listResourceService.getList).toHaveBeenCalled(); + }); + + it('should return value', () => { + const list$: Observable<StateResource<AdminOrganisationsEinheitListResource>> = service.getList(); + + expect(list$).toBeObservable(singleColdCompleted(organisationsEinheitStateListResource)); }); }); - describe('deleteInKeycloak', () => { - it('should call deleteOrganisationseinheit from userRepository', () => { - service.deleteInKeycloak(organisationseinheit.id); + describe('get', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + const organisationsEinheitStateResource: StateResource<AdminOrganisationsEinheitResource> = + createStateResource(organisationsEinheitResource); + + beforeEach(() => { + resourceService.get.mockReturnValue(of(organisationsEinheitStateResource)); + }); + + it('should call resourceService', () => { + service.get(); + + expect(resourceService.get).toHaveBeenCalled(); + }); + + it('should return value', () => { + const resource$: Observable<StateResource<AdminOrganisationsEinheitResource>> = service.get(); + + expect(resource$).toBeObservable(singleColdCompleted(organisationsEinheitStateResource)); + }); + }); + + describe('patch', () => { + const organisationsEinheitResource: AdminOrganisationsEinheitResource = createAdminOrganisationsEinheitResource(); + const organisationsEinheitStateResource: StateResource<AdminOrganisationsEinheitResource> = + createStateResource(organisationsEinheitResource); + + beforeEach(() => { + resourceService.patch.mockReturnValue(of(organisationsEinheitStateResource)); + }); + + it('should call resourceService', () => { + service.patch(organisationsEinheitResource.settings).subscribe(); + + expect(resourceService.patch).toHaveBeenCalled(); + }); + + it('should call snackBarService showInfo', () => { + service.patch(organisationsEinheitResource.settings).subscribe(); + + expect(snackBarService.showInfo).toHaveBeenCalled(); + }); + + it('should return value', () => { + const resource$: Observable<StateResource<AdminOrganisationsEinheitResource>> = service.patch( + organisationsEinheitResource.settings, + ); - expect(repository.deleteOrganisationseinheit).toHaveBeenCalledWith(organisationseinheit.id); + resource$.subscribe((result: StateResource<AdminOrganisationsEinheitResource>) => { + expect(result).toEqual(organisationsEinheitStateResource); + }); }); }); }); 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 index d07d400cdb35c40955a164a99bcd66faf933b911..d20e30cc1f824e8f912a624540ff8047b3771041 100644 --- a/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.ts +++ b/alfa-client/libs/admin/settings/src/lib/organisationseinheit/organisationseinheit.service.ts @@ -1,30 +1,81 @@ +import { + AdminOrganisationsEinheitListResource, + AdminOrganisationsEinheitResource, + AdminOrganisationsEinheitSettings, +} from '@admin-client/admin-settings'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { StateResource, createEmptyStateResource, isNotUndefined } from '@alfa-client/tech-shared'; +import { SnackBarService } from '@alfa-client/ui'; 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'; +import { Params } from '@angular/router'; +import { ResourceUri } from '@ngxp/rest'; +import { isNil } from 'lodash-es'; +import { Observable, Subscription, startWith, tap } from 'rxjs'; +import { OrganisationsEinheitListResourceService } from './organisations-einheit-list-resource.service'; +import { OrganisationsEinheitResourceService } from './organisations-einheit-resource.service'; @Injectable({ providedIn: 'root', }) -export class OrganisationseinheitService extends KeycloakResourceService<Organisationseinheit> { - constructor(private userRepository: UserRepository) { - super(); +export class OrganisationsEinheitService { + static ORGANISATIONS_EINHEIT_URL: string = 'organisationsEinheitUrl'; + + private subscription: Subscription; + + constructor( + private listResourceService: OrganisationsEinheitListResourceService, + private resourceService: OrganisationsEinheitResourceService, + private navigationService: NavigationService, + private snackBarService: SnackBarService, + ) { + this.listenToNavigation(); + } + + private listenToNavigation(): void { + this.unsubscribe(); + this.subscription = this.navigationService.urlChanged().subscribe((params: Params) => this.onNavigation(params)); + } + + private unsubscribe(): void { + if (!isNil(this.subscription)) { + this.subscription.unsubscribe(); + } + } + + onNavigation(params: Params): void { + if (this.navigateToOrganisationsEinheitDetailPage(params)) { + this.listResourceService.select(this.getOrganisationsEinheitUrl()); + } + } + + private navigateToOrganisationsEinheitDetailPage(params: Params): boolean { + return isNotUndefined(params[OrganisationsEinheitService.ORGANISATIONS_EINHEIT_URL]); + } + + getOrganisationsEinheitUrl(): ResourceUri { + return this.navigationService.getDecodedParam(OrganisationsEinheitService.ORGANISATIONS_EINHEIT_URL); } - getItemsFromKeycloak(): Observable<Organisationseinheit[]> { - return this.userRepository.findOrganisationseinheitItems(); + public getList(): Observable<StateResource<AdminOrganisationsEinheitListResource>> { + return this.listResourceService.getList(); } - saveInKeycloak(organisationseinheit: Organisationseinheit): Observable<void> { - return this.userRepository.saveOrganisationseinheit(organisationseinheit); + public get(): Observable<StateResource<AdminOrganisationsEinheitResource>> { + return this.resourceService.get(); } - createInKeycloak(organisationseinheit: { name: string; organisationseinheitIds: string[] }): Observable<Organisationseinheit> { - return this.userRepository.createOrganisationseinheit(organisationseinheit); + public patch( + organisationsEinheitSettings: AdminOrganisationsEinheitSettings, + ): Observable<StateResource<AdminOrganisationsEinheitResource>> { + return this.resourceService.patch(organisationsEinheitSettings).pipe( + tap((stateResource: StateResource<AdminOrganisationsEinheitResource>) => this.showInfoAfterPatch(stateResource)), + startWith(createEmptyStateResource<AdminOrganisationsEinheitResource>(true)), + ); } - deleteInKeycloak(id: string): Observable<void> { - return this.userRepository.deleteOrganisationseinheit(id); + private showInfoAfterPatch(stateResource: StateResource<AdminOrganisationsEinheitResource>) { + if (!stateResource.loading) { + this.snackBarService.showInfo('Die Signatur wurde erfolgreich gespeichert.'); + } } } 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 742ca490a50a84a8f9026204e573bcf729bf3c30..43f4181229bb301da6124cde831ab9f7346c5ea9 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 @@ -1,20 +1,3 @@ -export interface Organisationseinheit { - id: string; - name: string; - organisationseinheitIds: string[]; -} - -export enum OrganisationseinheitErrorType { - NAME_CONFLICT = 'name-conflict', - NAME_MISSING = 'name-missing', - ID_MISSING = 'id-missing', -} - -export interface OrganisationseinheitError { - errorType: OrganisationseinheitErrorType; - detail: string; -} - export interface User { id: string; username: 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 5415be30358d129923f64c6f837885a5d78e2fd4..f7da378621af67111a8ce3640a4a86e566cc1f14 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 @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client'; +import KcAdminClient 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'; @@ -7,9 +7,8 @@ import RoleRepresentation from '@keycloak/keycloak-admin-client/lib/defs/roleRep import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { OAuthService } from 'angular-oauth2-oidc'; 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'; +import { Observable, forkJoin, from, map, mergeMap } from 'rxjs'; +import { User } from './user.model'; @Injectable({ providedIn: 'root', @@ -32,69 +31,6 @@ export class UserRepository { }; } - public deleteOrganisationseinheit(id: string): Observable<void> { - return from(this.kcAdminClient.groups.del({ id })); - } - - public findOrganisationseinheitItems(): Observable<Organisationseinheit[]> { - return from(this.kcAdminClient.groups.find({ briefRepresentation: false })).pipe( - map((reps: GroupRepresentation[]) => - reps.map((rep: GroupRepresentation) => this.mapGroupRepresentationToOrganisationseinheit(rep)), - ), - ); - } - - mapGroupRepresentationToOrganisationseinheit(group: GroupRepresentation): Organisationseinheit { - return { - id: group.id, - name: group.name, - organisationseinheitIds: group.attributes?.organisationseinheitId ?? [], - }; - } - - public saveOrganisationseinheit(organisationseinheit: Organisationseinheit): Observable<void> { - return from( - this.kcAdminClient.groups.update( - { id: organisationseinheit.id }, - { - name: organisationseinheit.name, - attributes: { organisationseinheitId: organisationseinheit.organisationseinheitIds }, - }, - ), - ).pipe(this.rethrowMappedGroupsError()); - } - - public createOrganisationseinheit(organisationseinheit: { - name: string; - organisationseinheitIds: string[]; - }): Observable<Organisationseinheit> { - return from( - this.kcAdminClient.groups.create({ - name: organisationseinheit.name, - attributes: { organisationseinheitId: organisationseinheit.organisationseinheitIds }, - }), - ).pipe( - map( - ({ id }): Organisationseinheit => ({ - ...organisationseinheit, - id, - }), - ), - this.rethrowMappedGroupsError(), - ); - } - - rethrowMappedGroupsError<T>(): OperatorFunction<T, T> { - return catchError((err) => throwError(() => this.mapCreateGroupsNetworkError(err))); - } - - mapCreateGroupsNetworkError(error: NetworkError): OrganisationseinheitError { - return { - errorType: KEYCLOAK_CREATE_GROUPS_ERROR_STATUS[error.response.status], - detail: error.responseData['errorMessage'] ?? '', - }; - } - public getUsers(): Observable<User[]> { return from(this.kcAdminClient.users.find()).pipe( map((userReps: UserRepresentation[]) => userReps.map((userReps) => this.mapToUser(userReps))), 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 ef000f8630a3fd9990283d882b023b30e253a060..812c138f9bf00724b2f260a7e3a6247b863beb3f 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 @@ -1,39 +1,23 @@ import { Mock, mock } from '@alfa-client/test-utils'; import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; -import KcAdminClient, { NetworkError } from '@keycloak/keycloak-admin-client'; +import KcAdminClient 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'; -import { - createGroupRepresentation, - createNetworkError, - createOrganisationseinheit, - createOrganisationseinheitError, - createUser, -} from '../../../test/user/user'; -import { Organisationseinheit, OrganisationseinheitError, OrganisationseinheitErrorType, User } from './user.model'; +import { createUser } from '../../../test/user/user'; +import { User } from './user.model'; import { UserRepository } from './user.repository.service'; describe('UserRepository', () => { const accessToken: string = faker.random.alphaNumeric(40); - const error: OrganisationseinheitError = createOrganisationseinheitError(); - const networkError: NetworkError = createNetworkError(400, ''); let repository: UserRepository; let kcAdminClient: Mock<KcAdminClient>; let oAuthService: Mock<OAuthService>; - const mockGroupsFunc = (groupsKey: keyof Groups, implementation: jest.Mock) => { - kcAdminClient.groups = <any>{ - [groupsKey]: implementation, - }; - }; - beforeEach(() => { kcAdminClient = mock(KcAdminClient); oAuthService = mock(OAuthService); @@ -59,257 +43,6 @@ describe('UserRepository', () => { }); }); - describe('map organisationseinheit representation', () => { - const expectedOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - it('should map field "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, - }); - - expect(organisationseinheit.name).toEqual(expectedOrganisationseinheit.name); - }); - - it('should map field "organisationseinheitIds"', () => { - const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({ - attributes: { - organisationseinheitId: expectedOrganisationseinheit.organisationseinheitIds, - }, - }); - - expect(organisationseinheit.organisationseinheitIds).toEqual(expectedOrganisationseinheit.organisationseinheitIds); - }); - - it('should map missing organisationseinheitIds to empty list', () => { - const organisationseinheit: Organisationseinheit = repository.mapGroupRepresentationToOrganisationseinheit({}); - - expect(organisationseinheit.organisationseinheitIds).toEqual([]); - }); - }); - - describe('find organisationseinheitItems', () => { - const organisationseinheitItems: Organisationseinheit[] = [ - createOrganisationseinheit(), - createOrganisationseinheit(), - createOrganisationseinheit(), - ]; - - 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()); - - expect(groupsResult).toEqual(groupsResult); - }); - - it('should call with brief representation', fakeAsync(() => { - const findMock: jest.Mock = jest.fn().mockReturnValue(Promise.resolve(groupReps)); - mockGroupsFunc('find', findMock); - - repository.findOrganisationseinheitItems().subscribe(); - tick(); - - expect(findMock).toHaveBeenCalledWith({ briefRepresentation: false }); - })); - }); - - describe('save organisationseinheit', () => { - const saveGroup: Organisationseinheit = createOrganisationseinheit(); - - it('should call kcAdminClient.groups.save', async () => { - const updateMock: jest.Mock = jest.fn(() => of(null)); - mockGroupsFunc('update', updateMock); - - await firstValueFrom(repository.saveOrganisationseinheit(saveGroup)); - - expect(updateMock).toHaveBeenCalledWith( - { id: saveGroup.id }, - { - name: saveGroup.name, - attributes: { - organisationseinheitId: saveGroup.organisationseinheitIds, - }, - }, - ); - }); - - it('should return organisationseinheit save observable', async () => { - const updateMock: jest.Mock = jest.fn(() => Promise.resolve(null)); - mockGroupsFunc('update', updateMock); - - const voidResult = await firstValueFrom(repository.saveOrganisationseinheit(saveGroup)); - - expect(voidResult).toBe(null); - }); - - 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.saveOrganisationseinheit(saveGroup).subscribe({ - error: (err) => { - expect(err).toBe(error); - done(); - }, - }); - }); - }); - - describe('create organisationseinheit', () => { - const newOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - it('should call kcAdminClient.groups.create', async () => { - const createMock: jest.Mock = jest.fn(() => of({ id: newOrganisationseinheit.id })); - mockGroupsFunc('create', createMock); - - await firstValueFrom( - repository.createOrganisationseinheit({ - name: newOrganisationseinheit.name, - organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, - }), - ); - - expect(createMock).toHaveBeenCalledWith({ - name: newOrganisationseinheit.name, - attributes: { - organisationseinheitId: newOrganisationseinheit.organisationseinheitIds, - }, - }); - }); - - it('should return mapped organisationseinheit result', async () => { - const createMock: jest.Mock = jest.fn(() => Promise.resolve({ id: newOrganisationseinheit.id })); - mockGroupsFunc('create', createMock); - - const newGroupResult: Organisationseinheit = await firstValueFrom( - repository.createOrganisationseinheit({ - name: newOrganisationseinheit.name, - organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, - }), - ); - - expect(newGroupResult).toEqual(newOrganisationseinheit); - }); - - 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 - .createOrganisationseinheit({ - name: newOrganisationseinheit.name, - organisationseinheitIds: newOrganisationseinheit.organisationseinheitIds, - }) - .subscribe({ - error: (err) => { - expect(err).toBe(error); - done(); - }, - }); - }); - }); - - describe('rethrow mapped groups error', () => { - let networkErrorObservable: Observable<never>; - - beforeEach(() => { - repository.mapCreateGroupsNetworkError = jest.fn().mockReturnValue(error); - networkErrorObservable = throwError(() => networkError); - }); - - it('should throw mapped error', (done) => { - const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError(); - - networkErrorObservable.pipe(rethrowOperator).subscribe({ - error: (err) => { - expect(err).toBe(error); - done(); - }, - }); - }); - - it('should call mapCreateGroupsNetworkError', (done) => { - const rethrowOperator: OperatorFunction<never, never> = repository.rethrowMappedGroupsError(); - - networkErrorObservable.pipe(rethrowOperator).subscribe({ - error: () => { - expect(repository.mapCreateGroupsNetworkError).toHaveBeenCalledWith(networkError); - done(); - }, - }); - }); - }); - - describe('map create groups network error', () => { - it('should interpret 409 status as name conflict', () => { - const keycloakError: OrganisationseinheitError = createOrganisationseinheitError( - OrganisationseinheitErrorType.NAME_CONFLICT, - ); - const networkError: NetworkError = createNetworkError(409, keycloakError.detail); - - const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); - - expect(error).toEqual(keycloakError); - }); - - it('should interpret 400 status as name missing', () => { - const keycloakError: OrganisationseinheitError = createOrganisationseinheitError( - OrganisationseinheitErrorType.NAME_MISSING, - ); - const networkError: NetworkError = createNetworkError(400, keycloakError.detail); - - const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); - - expect(error).toEqual(keycloakError); - }); - - it('should map missing errorMessage to empty string', () => { - const networkError: NetworkError = createNetworkError(500, undefined); - - const error: OrganisationseinheitError = repository.mapCreateGroupsNetworkError(networkError); - expect(error.detail).toEqual(''); - }); - }); - - describe('delete organisationseinheit', () => { - const deleteOrganisationseinheit: Organisationseinheit = createOrganisationseinheit(); - - it('should call kcAdminClient.groups.del', async () => { - const delMock: jest.Mock = jest.fn(() => Promise.resolve({ id: deleteOrganisationseinheit.id })); - mockGroupsFunc('del', delMock); - - await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id)); - - expect(delMock).toHaveBeenCalledWith({ - id: deleteOrganisationseinheit.id, - }); - }); - - it('should return void', async () => { - mockGroupsFunc( - 'del', - jest.fn(() => Promise.resolve(null)), - ); - - const voidResult: void = await firstValueFrom(repository.deleteOrganisationseinheit(deleteOrganisationseinheit.id)); - - expect(voidResult).toBeNull(); - }); - }); - describe('getUsers', () => { const userRep: User = createUser(); const userRepArray: User[] = [userRep, userRep, userRep]; 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 f4d9cc168ecb9eaca45f28cead659716ff232845..7ede2e6daccb6281372e0f3a161fed776447c8bb 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,30 +1,8 @@ -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'; +import { createUser } from '../../../test/user/user'; +import { User } from './user.model'; +import { 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); - }); - }); - describe('sort users by last name', () => { const users: User[] = [ { ...createUser(), lastName: 'Müller' }, 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 9878f7021e501b38d8d0938228a3c2c7c1df51b9..f801d3276c4ce3cdf05b67ab9412bcda0dde7fcd 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,21 +1,4 @@ -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.', -}; - -export const KEYCLOAK_CREATE_GROUPS_ERROR_STATUS: { - [status: number]: OrganisationseinheitErrorType; -} = { - 409: OrganisationseinheitErrorType.NAME_CONFLICT, - 400: OrganisationseinheitErrorType.NAME_MISSING, -}; - -export function getOrganisationseinheitErrorMessage(error: OrganisationseinheitError): string { - return KEYCLOAK_ERROR_MESSAGES[error.errorType] ?? ''; -} +import { User } from './user.model'; 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/users-roles.component.html b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html index c177ace12d14e3cd467b795ef256a70f1831d229..e22ebee9686297df2df289a14d42508c0d008ad7 100644 --- a/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html +++ b/alfa-client/libs/admin/settings/src/lib/users-roles/users-roles.component.html @@ -1,62 +1,51 @@ <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="#" - (click)="(false)" - 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" - [attr.data-test-id]="'user-entry-' + user.username" - > - <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> +<ods-list *ngIf="users$ | async as users"> + <ods-list-item *ngFor="let user of users.resource" [routerLink]="user.username"> + <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> + <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> + <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 *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> - <ng-template #noGroups>keine zuständige Stelle zugewiesen</ng-template> - </div> - </a> - </li> - </ul> -</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> + </ods-list-item> +</ods-list> diff --git a/alfa-client/libs/admin/settings/test/organisations-einheit/organisations-einheit.ts b/alfa-client/libs/admin/settings/test/organisations-einheit/organisations-einheit.ts new file mode 100644 index 0000000000000000000000000000000000000000..600e815386e1d52dce43597b5187f1310a1be713 --- /dev/null +++ b/alfa-client/libs/admin/settings/test/organisations-einheit/organisations-einheit.ts @@ -0,0 +1,41 @@ +import { faker } from '@faker-js/faker'; +import { times } from 'lodash-es'; +import { toResource } from '../../../../tech-shared/test/resource'; +import { + AdminOrganisationsEinheit, + AdminOrganisationsEinheitListResource, + AdminOrganisationsEinheitResource, + AdminOrganisationsEinheitSyncResult, +} from '../../src'; +import { OrganisationsEinheitListLinkRel } from '../../src/lib/organisationseinheit/organisations-einheit.linkrel'; + +export function createAdminOrganisationsEinheit(syncResult?: AdminOrganisationsEinheitSyncResult): AdminOrganisationsEinheit { + return { + name: faker.random.word(), + organisationsEinheitId: faker.random.word(), + syncResult: syncResult ?? AdminOrganisationsEinheitSyncResult.OK, + settings: { + signatur: faker.random.words(5), + }, + }; +} + +export function createAdminOrganisationsEinheitResource( + syncResult?: AdminOrganisationsEinheitSyncResult, + linkRel: string[] = [], +): AdminOrganisationsEinheitResource { + return toResource(createAdminOrganisationsEinheit(syncResult), linkRel); +} + +export function createAdminOrganisationsEinheitResources(linkRelations: string[] = []): AdminOrganisationsEinheitResource[] { + return times(10, () => toResource(createAdminOrganisationsEinheit(), [...linkRelations])); +} + +export function createAdminOrganisationsEinheitListResource( + resources?: AdminOrganisationsEinheitResource[], + linkRelations: string[] = [], +): AdminOrganisationsEinheitListResource { + return toResource({}, [...linkRelations], { + [OrganisationsEinheitListLinkRel.LIST]: resources ? resources : createAdminOrganisationsEinheitResources(), + }); +} diff --git a/alfa-client/libs/admin/settings/test/user/user.ts b/alfa-client/libs/admin/settings/test/user/user.ts index 8e9c06e922483b856f0b179d14e476d7b81ddc83..275d726ad9dcdae99412a3e936cc0777dffe1197 100644 --- a/alfa-client/libs/admin/settings/test/user/user.ts +++ b/alfa-client/libs/admin/settings/test/user/user.ts @@ -1,12 +1,5 @@ import { faker } from '@faker-js/faker'; -import { NetworkError } from '@keycloak/keycloak-admin-client'; -import type GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; -import { - Organisationseinheit, - OrganisationseinheitError, - OrganisationseinheitErrorType, - User, -} from '../../src/lib/user/user.model'; +import { User } from '../../src/lib/user/user.model'; export function createUser(): User { return { @@ -19,41 +12,3 @@ export function createUser(): User { groups: null, }; } - -export function createOrganisationseinheit(): Organisationseinheit { - return { - id: faker.random.alphaNumeric(16), - name: faker.name.jobTitle(), - organisationseinheitIds: [faker.random.numeric(10), faker.random.numeric(10)], - }; -} - -export function createGroupRepresentation( - organisationseinheit: Organisationseinheit = createOrganisationseinheit(), -): GroupRepresentation { - return { - id: organisationseinheit.id, - name: organisationseinheit.name, - attributes: { - organisationseinheitId: organisationseinheit.organisationseinheitIds, - }, - }; -} - -export function createOrganisationseinheitError( - errorType: OrganisationseinheitErrorType = OrganisationseinheitErrorType.NAME_MISSING, -): OrganisationseinheitError { - return { - errorType, - detail: faker.lorem.sentence(10), - }; -} - -export function createNetworkError(status: number, errorMessage: string | undefined): NetworkError { - return new NetworkError('...', { - response: { status } as Response, - responseData: { - errorMessage, - }, - }); -} diff --git a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts index fed6b6cc56cb6ded4a3d5b9f19a68578ec28edcb..231bfb91adcc94fad96c9727f06992960376dc3f 100644 --- a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts +++ b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts @@ -61,4 +61,5 @@ export enum ApiRootLinkRel { DOCUMENTATIONS = 'documentations', HINTS = 'hints', RESOURCE = 'resource', + ORGANISATIONS_EINHEIT = 'organisationsEinheiten', } diff --git a/alfa-client/libs/command-shared/src/lib/command-resource.service.spec.ts b/alfa-client/libs/command-shared/src/lib/command-resource.service.spec.ts index 01af08536ee53c2b081a2da1a34d0823958d5e5c..282d49ecc2c827b47980b536a152681c9df26a3c 100644 --- a/alfa-client/libs/command-shared/src/lib/command-resource.service.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/command-resource.service.spec.ts @@ -42,26 +42,31 @@ describe('CommandResourceService', () => { repository = mock(ResourceRepository); commandService = mock(CommandService); - service = new CommandResourceService( - config, - useFromMock(repository), - useFromMock(commandService), - ); + service = new CommandResourceService(config, useFromMock(repository), useFromMock(commandService)); }); it('should be created', () => { expect(service).toBeTruthy(); }); + describe('doSave', () => { + it('should throw error', () => { + expect(() => service.doSave(configResource, {})).toThrowError('Method not implemented.'); + }); + }); + + describe('doPatch', () => { + it('should throw error', () => { + expect(() => service.doPatch(configResource, {})).toThrowError('Method not implemented.'); + }); + }); + describe('delete', () => { const resourceWithDeleteLinkRel: Resource = createDummyResource([deleteLinkRel]); - const stateResourceWithDeleteLink: StateResource<Resource> = - createStateResource(resourceWithDeleteLinkRel); + const stateResourceWithDeleteLink: StateResource<Resource> = createStateResource(resourceWithDeleteLinkRel); beforeEach(() => { - commandService.createCommandByProps.mockReturnValue( - of(createStateResource(createCommandResource())), - ); + commandService.createCommandByProps.mockReturnValue(of(createStateResource(createCommandResource()))); service.stateResource.next(stateResourceWithDeleteLink); }); diff --git a/alfa-client/libs/command-shared/src/lib/command-resource.service.ts b/alfa-client/libs/command-shared/src/lib/command-resource.service.ts index 9a564442fd23862a0c85f645a6a93573ddca3612..4ec5a90e60890c26a04a3fd2d8aeed14794176d4 100644 --- a/alfa-client/libs/command-shared/src/lib/command-resource.service.ts +++ b/alfa-client/libs/command-shared/src/lib/command-resource.service.ts @@ -11,10 +11,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { CommandResource, CreateCommandProps } from './command.model'; import { CommandService } from './command.service'; -export class CommandResourceService<B extends Resource, T extends Resource> extends ResourceService< - B, - T -> { +export class CommandResourceService<B extends Resource, T extends Resource> extends ResourceService<B, T> { deleteStateCommandResource: BehaviorSubject<StateResource<CommandResource>> = new BehaviorSubject< StateResource<CommandResource> >(createEmptyStateResource()); @@ -31,6 +28,10 @@ export class CommandResourceService<B extends Resource, T extends Resource> exte throw new Error('Method not implemented.'); } + doPatch(resource: T, toPatch: unknown): Observable<T> { + throw new Error('Method not implemented.'); + } + public delete(): Observable<StateResource<CommandResource>> { return this.commandService.createCommandByProps(this.buildDeleteCommandProps()); } diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 79ec5d05a2a170233a108690d7d7046a4dbddd7c..dae1c087a831f7b46b22c93f751404403aef90bd 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -38,6 +38,8 @@ 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/list/list-item/list-item.component'; +export * from './lib/list/list.component'; export * from './lib/navbar/nav-item/nav-item.component'; export * from './lib/navbar/navbar/navbar.component'; export * from './lib/testbtn/testbtn.component'; diff --git a/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecec5321cbfbc0feb43f59e6523a67612db186a3 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.spec.ts @@ -0,0 +1,40 @@ +import { getElementFromFixture } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import faker from '@faker-js/faker'; +import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; +import { ListItemComponent } from './list-item.component'; + +describe('ListItemComponent', () => { + let component: ListItemComponent; + let fixture: ComponentFixture<ListItemComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ListItemComponent], + providers: [provideRouter([])], + }).compileComponents(); + + fixture = TestBed.createComponent(ListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('input', () => { + describe('routerLink', () => { + it('should set href attribute', () => { + component.routerLink = faker.system.filePath(); + const resultingLink: string = 'http://localhost' + component.routerLink; + const linkElement: HTMLLinkElement = getElementFromFixture(fixture, getDataTestClassOf('list-item-link')); + + fixture.detectChanges(); + + expect(linkElement.href).toBe(resultingLink); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.ts b/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..89e51332eda0375bfd8cfc77a383dca0a3f70175 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/list/list-item/list-item.component.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +@Component({ + selector: 'ods-list-item', + standalone: true, + imports: [CommonModule, RouterLink], + template: `<li> + <a + [routerLink]="routerLink" + data-test-class="list-item-link" + 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" + ><ng-content + /></a> + </li>`, + styles: [':host { @apply block w-full }'], +}) +export class ListItemComponent { + @Input() routerLink: string; +} diff --git a/alfa-client/libs/design-system/src/lib/list/list.component.spec.ts b/alfa-client/libs/design-system/src/lib/list/list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..35076e4e0ed34c166da97e496571c43c642f14a0 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/list/list.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: ListComponent; + let fixture: ComponentFixture<ListComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/list/list.component.ts b/alfa-client/libs/design-system/src/lib/list/list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4e903c1990ebc67a3bcb51f7e3596d0391c011e --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/list/list.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { ListItemComponent } from './list-item/list-item.component'; + +@Component({ + selector: 'ods-list', + standalone: true, + imports: [CommonModule, ListItemComponent], + template: ` + <ul class="divide-y divide-gray-300 rounded-md bg-background-50 text-text shadow-sm ring-1 ring-gray-300 empty:hidden"> + <ng-content /> + </ul> + `, +}) +export class ListComponent {} diff --git a/alfa-client/libs/design-system/src/lib/list/list.stories.ts b/alfa-client/libs/design-system/src/lib/list/list.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3164eabfb7facd2ec68b48b1199caf8d49296fe --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/list/list.stories.ts @@ -0,0 +1,42 @@ +import { APP_BASE_HREF } from '@angular/common'; +import { importProvidersFrom } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { applicationConfig, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { ListItemComponent } from './list-item/list-item.component'; +import { ListComponent } from './list.component'; + +const meta: Meta<ListComponent> = { + title: 'List', + component: ListComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], + decorators: [ + applicationConfig({ + providers: [importProvidersFrom(RouterModule.forRoot([]))], + }), + moduleMetadata({ + imports: [ListItemComponent], + providers: [ + { + provide: APP_BASE_HREF, + useValue: '/', + }, + ], + }), + ], +}; + +export default meta; +type Story = StoryObj<ListComponent>; + +export const Default: Story = { + args: {}, + render: () => ({ + template: ` + <ods-list> + <ods-list-item routerLink="/pfad/1">List Item 1</ods-list-item> + <ods-list-item routerLink="/pfad/2">List Item 2</ods-list-item> + <ods-list-item routerLink="/pfad/3">List Item 3</ods-list-item> + </ods-list>`, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts index 88cde90f95160002d9f799f93bbd318cc6b87195..220af3f589fc2bb8ad7eea2817af77a01ccf98e5 100644 --- a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts @@ -1,8 +1,8 @@ +import { convertForDataTest, TechSharedModule } from '@alfa-client/tech-shared'; import { getElementFromFixture, Mock, mock } from '@alfa-client/test-utils'; import { importProvidersFrom } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router, RouterModule } from '@angular/router'; -import { ConvertForDataTestPipe } from 'libs/tech-shared/src/lib/pipe/convert-for-data-test.pipe'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { NavItemComponent } from './nav-item.component'; @@ -14,7 +14,7 @@ describe('NavItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConvertForDataTestPipe, NavItemComponent], + imports: [NavItemComponent, TechSharedModule], providers: [ { provide: Router, @@ -37,21 +37,23 @@ describe('NavItemComponent', () => { describe('caption', () => { it('should set link text', () => { component.caption = 'Test caption'; - fixture.detectChanges(); + const testId: string = 'caption-' + convertForDataTest(component.caption); - const captionElement: HTMLParagraphElement = getElementFromFixture(fixture, getDataTestIdOf('link-caption')); + fixture.detectChanges(); + const captionElement: HTMLParagraphElement = getElementFromFixture(fixture, getDataTestIdOf(testId)); expect(captionElement.innerHTML).toBe('Test caption'); }); }); - describe('to', () => { + describe('path', () => { it('should set href', () => { - component.to = '/'; - fixture.detectChanges(); + component.path = 'pfad'; + const testId: string = 'link-path-' + convertForDataTest(component.path); - const linkElement: HTMLAnchorElement = getElementFromFixture(fixture, getDataTestIdOf('link-to-/')); + fixture.detectChanges(); + const linkElement: HTMLAnchorElement = getElementFromFixture(fixture, getDataTestIdOf(testId)); expect(linkElement).toHaveProperty('href'); }); }); 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 67ecdbddabe3133079b97dc26c2249b62b3f5ce7..0bf2f5580404c802a94c8f25d167638a9a5bf901 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 @@ -8,20 +8,20 @@ import { RouterLink, RouterLinkActive } from '@angular/router'; standalone: true, imports: [CommonModule, RouterLink, RouterLinkActive, TechSharedModule], template: `<a - [routerLink]="to" + [routerLink]="path" routerLinkActive="bg-selected-light border-selected" 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 | convertForDataTest" + [attr.data-test-id]="'link-path-' + path | convertForDataTest" > <ng-content select="[icon]" /> - <p class="text-left text-sm text-text" [attr.data-test-id]="'nav-item-' + caption | convertForDataTest">{{ caption }}</p> + <p class="text-left text-sm text-text" [attr.data-test-id]="'caption-' + caption | convertForDataTest">{{ caption }}</p> </a>`, }) export class NavItemComponent { @Input({ required: true }) caption!: string; - @Input() to: string; + @Input() path: string; - @HostBinding('attr.role') role = 'menuitem'; + @HostBinding('attr.role') role: string = 'menuitem'; } diff --git a/alfa-client/libs/design-system/src/lib/navbar/navbar/navbar.stories.ts b/alfa-client/libs/design-system/src/lib/navbar/navbar/navbar.stories.ts index fa5b213b2e0f14fe9de3643ce21e123f042df68c..8ae3842b6143820d0340035f9522a285610018d6 100644 --- a/alfa-client/libs/design-system/src/lib/navbar/navbar/navbar.stories.ts +++ b/alfa-client/libs/design-system/src/lib/navbar/navbar/navbar.stories.ts @@ -34,10 +34,10 @@ export const Default: Story = { args: {}, render: () => ({ template: `<ods-navbar> - <ods-nav-item caption="First link" to="/"><ods-office-icon icon /></ods-nav-item> - <ods-nav-item caption="Second link" to="/second"><ods-office-icon icon /></ods-nav-item> + <ods-nav-item caption="First link" path="/"><ods-office-icon icon /></ods-nav-item> + <ods-nav-item caption="Second link" path="/second"><ods-office-icon icon /></ods-nav-item> <hr /> - <ods-nav-item caption="Third link" to="/third"><ods-office-icon icon /></ods-nav-item> + <ods-nav-item caption="Third link" path="/third"><ods-office-icon icon /></ods-nav-item> </ods-navbar>`, }), }; diff --git a/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.spec.ts index 977db3428026e075b535d45b53e6cdd9865579fb..e608186534a4f3573d8a78a4fdda739926d7c7fb 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.spec.ts @@ -91,4 +91,56 @@ describe('ApiResourceService', () => { expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); })); }); + + describe('patch', () => { + const dummyToPatch: unknown = {}; + const loadedResource: Resource = createDummyResource(); + + const resourceWithEditLinkRel: Resource = createDummyResource([editLinkRel]); + + it('should call repository', fakeAsync(() => { + service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + repository.patch.mockReturnValue(of(loadedResource)); + + service.patch(dummyToPatch).subscribe(); + tick(); + + const expectedSaveResourceData: SaveResourceData<Resource> = { + resource: resourceWithEditLinkRel, + linkRel: editLinkRel, + toSave: dummyToPatch, + }; + expect(repository.patch).toHaveBeenCalledWith(expectedSaveResourceData); + })); + + it('should return patched object', () => { + service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + repository.patch.mockReturnValue(singleHot(loadedResource)); + + const saved: Observable<StateResource<Resource>> = service.patch(dummyToPatch); + + expect(saved).toBeObservable(singleCold(createStateResource(loadedResource))); + }); + + it('should call handleError', () => { + service.stateResource.next(createStateResource(createDummyResource([config.edit.linkRel]))); + const errorResponse: ProblemDetail = createProblemDetail(); + repository.patch.mockReturnValue(throwError(() => errorResponse)); + service.handleError = jest.fn(); + + service.patch(<any>{}).subscribe(); + + expect(service.handleError).toHaveBeenCalledWith(errorResponse); + }); + + it('should update state resource subject', fakeAsync(() => { + service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + repository.patch.mockReturnValue(of(loadedResource)); + + service.patch(dummyToPatch).subscribe(); + tick(); + + expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); + })); + }); }); diff --git a/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.ts index d19c4a6fdc52813b270661c560bf76bed7f3ba24..f27414058aae8584e3ce77dd8668735232c33acd 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/api-resource.service.ts @@ -4,10 +4,7 @@ import { ResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { ResourceService } from './resource.service'; -export class ApiResourceService<B extends Resource, T extends Resource> extends ResourceService< - B, - T -> { +export class ApiResourceService<B extends Resource, T extends Resource> extends ResourceService<B, T> { constructor( protected config: ResourceServiceConfig<B>, protected repository: ResourceRepository, @@ -22,4 +19,12 @@ export class ApiResourceService<B extends Resource, T extends Resource> extends toSave, }); } + + doPatch(resource: T, toPatch: unknown): Observable<T> { + return <Observable<T>>this.repository.patch({ + resource, + linkRel: this.config.edit.linkRel, + toSave: toPatch, + }); + } } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts index 9983f8acf61e11a6c642761e7b0d6e8e3351c253..a2aa79b4339145dbe72c5d3bd4b0be155a9ab354 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts @@ -4,29 +4,14 @@ import faker from '@faker-js/faker'; import { Resource, ResourceUri } from '@ngxp/rest'; import { cold } from 'jest-marbles'; import { DummyLinkRel, DummyListLinkRel } from 'libs/tech-shared/test/dummy'; -import { - createDummyListResource, - createDummyResource, - createFilledDummyListResource, -} from 'libs/tech-shared/test/resource'; +import { createDummyListResource, createDummyResource, createFilledDummyListResource } from 'libs/tech-shared/test/resource'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { singleCold, singleHot } from '../../../test/marbles'; +import { EMPTY_ARRAY } from '../tech.util'; import { ResourceListService } from './list-resource.service'; -import { - CreateResourceData, - LinkRelationName, - ListItemResource, - ListResourceServiceConfig, -} from './resource.model'; +import { CreateResourceData, LinkRelationName, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; -import { - ListResource, - StateResource, - createEmptyStateResource, - createStateResource, -} from './resource.util'; - -import { EMPTY_ARRAY } from '../tech.util'; +import { ListResource, StateResource, createEmptyStateResource, createStateResource } from './resource.util'; import * as ResourceUtil from './resource.util'; @@ -42,9 +27,9 @@ describe('ListResourceService', () => { const baseResource: Resource = createDummyResource(); const baseStateResource: StateResource<Resource> = createStateResource(baseResource); - const baseResourceSubj: BehaviorSubject<StateResource<Resource>> = new BehaviorSubject< - StateResource<Resource> - >(baseStateResource); + const baseResourceSubj: BehaviorSubject<StateResource<Resource>> = new BehaviorSubject<StateResource<Resource>>( + baseStateResource, + ); beforeEach(() => { config = { @@ -63,8 +48,7 @@ describe('ListResourceService', () => { }); describe('getList', () => { - const listStateResource: StateResource<ListResource> = - createStateResource(createDummyListResource()); + const listStateResource: StateResource<ListResource> = createStateResource(createDummyListResource()); let isInvalidResourceCombinationSpy: jest.SpyInstance; @@ -73,9 +57,7 @@ describe('ListResourceService', () => { service.handleNullConfigResource = jest.fn(); service.handleChanges = jest.fn(); - isInvalidResourceCombinationSpy = jest - .spyOn(ResourceUtil, 'isInvalidResourceCombination') - .mockReturnValue(true); + isInvalidResourceCombinationSpy = jest.spyOn(ResourceUtil, 'isInvalidResourceCombination').mockReturnValue(true); }); it('should handle config resource changed', fakeAsync(() => { @@ -104,15 +86,12 @@ describe('ListResourceService', () => { const apiRootStateResource$: Observable<StateResource<Resource>> = service.getList(); - expect(apiRootStateResource$).toBeObservable( - cold('a', { a: createEmptyStateResource(true) }), - ); + expect(apiRootStateResource$).toBeObservable(cold('a', { a: createEmptyStateResource(true) })); }); }); describe('handle changes', () => { - const listStateResource: StateResource<ListResource> = - createStateResource(createDummyListResource()); + const listStateResource: StateResource<ListResource> = createStateResource(createDummyListResource()); const changedConfigResource: Resource = createDummyResource(); describe('on different config resource', () => { @@ -127,8 +106,7 @@ describe('ListResourceService', () => { }); describe('on same config resource', () => { - const listStateResource: StateResource<ListResource> = - createStateResource(createDummyListResource()); + const listStateResource: StateResource<ListResource> = createStateResource(createDummyListResource()); beforeEach(() => { service.baseResource = baseResource; @@ -210,8 +188,7 @@ describe('ListResourceService', () => { it('should keep current current list resource on unstable state resource', () => { jest.spyOn(ResourceUtil, 'isStateResoureStable').mockReturnValue(false); - const currentListStateResource: StateResource<ListResource> = - createStateResource(createDummyListResource()); + const currentListStateResource: StateResource<ListResource> = createStateResource(createDummyListResource()); service.listResource.next(currentListStateResource); const configResuorceWithoutLink: Resource = createDummyListResource(); @@ -318,18 +295,6 @@ describe('ListResourceService', () => { resourceRepository.getResource.mockReturnValue(of(loadedResource)); }); - it('should throw error if listResource is not valid', () => { - service.listResource.next(createEmptyStateResource()); - - expect(() => service.select(selfHref)).toThrowError('No list resource available.'); - }); - - it('should throw error if uri not exists in list resource', () => { - expect(() => service.select('uriNotExistsInListResource')).toThrowError( - 'No entry match with given uri.', - ); - }); - it('should set resource loading', () => { service.setSelectedResourceLoading = jest.fn(); @@ -375,8 +340,7 @@ describe('ListResourceService', () => { describe('getSelected', () => { it('should return selected resource', (done) => { - const dummyStateResource: StateResource<Resource> = - createStateResource(createDummyResource()); + const dummyStateResource: StateResource<Resource> = createStateResource(createDummyResource()); service.getSelected().subscribe((selected) => { expect(selected).toEqual(dummyStateResource); @@ -411,10 +375,7 @@ describe('ListResourceService', () => { service.prev(); - expect(resourceRepository.getListResource).toHaveBeenCalledWith( - listResource, - service.prevLink, - ); + expect(resourceRepository.getListResource).toHaveBeenCalledWith(listResource, service.prevLink); }); it('should update listResource', () => { @@ -443,10 +404,7 @@ describe('ListResourceService', () => { service.next(); - expect(resourceRepository.getListResource).toHaveBeenCalledWith( - listResourceWithPrevLink, - service.nextLink, - ); + expect(resourceRepository.getListResource).toHaveBeenCalledWith(listResourceWithPrevLink, service.nextLink); }); it('should update listResource', () => { @@ -485,9 +443,7 @@ describe('ListResourceService', () => { describe('get items', () => { const listResourceItems: ListItemResource[] = [createDummyResource()]; - const stateListResource: StateResource<ListResource> = createStateResource( - createFilledDummyListResource(listResourceItems), - ); + const stateListResource: StateResource<ListResource> = createStateResource(createFilledDummyListResource(listResourceItems)); let getListSpy: jest.SpyInstance; beforeEach(() => { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts index 631e1c685eba0e9f96e5a4e1d97e1314a1cb7e9f..4bb6beecc38df96224fa5adf1b4ede5ed4729039 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts @@ -1,15 +1,6 @@ import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; import { isEqual, isNull } from 'lodash-es'; -import { - BehaviorSubject, - Observable, - combineLatest, - filter, - first, - map, - startWith, - tap, -} from 'rxjs'; +import { BehaviorSubject, Observable, combineLatest, filter, first, map, startWith, tap } from 'rxjs'; import { isNotNull, isNotUndefined } from '../tech.util'; import { CreateResourceData, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; @@ -31,21 +22,13 @@ import { * T = Type of listresource * I = Type of items in listresource */ -export class ResourceListService< - B extends Resource, - T extends ListResource, - I extends ListItemResource, -> { +export class ResourceListService<B extends Resource, T extends ListResource, I extends ListItemResource> { readonly nextLink: string = 'next'; readonly prevLink: string = 'prev'; - readonly listResource: BehaviorSubject<StateResource<T>> = new BehaviorSubject( - createEmptyStateResource(), - ); + readonly listResource: BehaviorSubject<StateResource<T>> = new BehaviorSubject(createEmptyStateResource()); - readonly selectedResource: BehaviorSubject<StateResource<I>> = new BehaviorSubject( - createEmptyStateResource(), - ); + readonly selectedResource: BehaviorSubject<StateResource<I>> = new BehaviorSubject(createEmptyStateResource()); baseResource: B = null; @@ -56,7 +39,9 @@ export class ResourceListService< public getList(): Observable<StateResource<T>> { return combineLatest([this.listResource.asObservable(), this.getConfigResource()]).pipe( - tap(([stateResource, configResource]) => this.handleChanges(stateResource, configResource)), + tap(([stateResource, configResource]) => { + this.handleChanges(stateResource, configResource); + }), tap(([, configResource]) => this.handleNullConfigResource(configResource)), filter(([stateResource]) => !isInvalidResourceCombination(stateResource, this.baseResource)), mapToFirst<T, B>(), @@ -109,9 +94,7 @@ export class ResourceListService< public create(toCreate: unknown): Observable<Resource> { this.verifyBeforeCreation(); - return this.repository.createResource( - this.buildCreateResourceData(toCreate, this.config.createLinkRel), - ); + return this.repository.createResource(this.buildCreateResourceData(toCreate, this.config.createLinkRel)); } private verifyBeforeCreation(): void { @@ -132,7 +115,6 @@ export class ResourceListService< } public select(uri: ResourceUri): void { - this.verifyBeforeSelection(uri); this.setSelectedResourceLoading(); this.repository .getResource(uri) @@ -156,10 +138,7 @@ export class ResourceListService< } existsUriInList(uri: ResourceUri): boolean { - const listResources: Resource[] = getEmbeddedResources( - this.listResource.value, - this.config.listLinkRel, - ); + const listResources: Resource[] = getEmbeddedResources(this.listResource.value, this.config.listLinkRel); return isNotUndefined(listResources.find((resource) => getUrl(resource) === uri)); } @@ -234,10 +213,7 @@ export class ResourceListService< return this.getList().pipe( filter((listStateResource: StateResource<T>) => !listStateResource.loading), map((listStateResource: StateResource<T>) => - getEmbeddedResources<ListItemResource>( - listStateResource, - this.config.listResourceListLinkRel, - ), + getEmbeddedResources<ListItemResource>(listStateResource, this.config.listResourceListLinkRel), ), ); } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.spec.ts index 2c8c97bfd4304f4bad5545a11cbe97dcb8a7258f..dfbdeb0e8b3889808c4839004012f82e65b89875 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.spec.ts @@ -13,7 +13,7 @@ describe('ResourceRepository', () => { let repository: ResourceRepository; let resourceFactory = mock(ResourceFactory); - let resourceWrapper = { get: jest.fn(), post: jest.fn(), put: jest.fn(), delete: jest.fn() }; + let resourceWrapper = { get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn() }; beforeEach(() => { resourceFactory.from.mockReturnValue(resourceWrapper); @@ -42,9 +42,7 @@ describe('ResourceRepository', () => { it('should call get url without parameter', () => { repository.getListResource(baseResource, listLinkRel); - expect(repository.getUrlWithoutParameter).toHaveBeenCalledWith( - getUrl(baseResource, listLinkRel), - ); + expect(repository.getUrlWithoutParameter).toHaveBeenCalledWith(getUrl(baseResource, listLinkRel)); }); it('should call resourceFactory with uri', () => { @@ -131,7 +129,7 @@ describe('ResourceRepository', () => { expect(resourceFactory.from).toHaveBeenCalledWith(resource); }); - it('should call resourceWrapper with linkel and object to create', () => { + it('should call resourceWrapper with linkRel and object to create', () => { repository.createResource(createResourceData); expect(resourceWrapper.post).toHaveBeenCalledWith(linkRel, toCreate); @@ -162,7 +160,7 @@ describe('ResourceRepository', () => { expect(resourceFactory.from).toHaveBeenCalledWith(resource); }); - it('should call resourceWrapper with linkel and object to save', () => { + it('should call resourceWrapper with linkRel and object to save', () => { repository.save(saveResourceData); expect(resourceWrapper.put).toHaveBeenCalledWith(linkRel, toSave); @@ -175,6 +173,37 @@ describe('ResourceRepository', () => { }); }); + describe('patch', () => { + const toPatch: unknown = {}; + const linkRel: string = DummyLinkRel.DUMMY; + const resource: Resource = createDummyResource([linkRel]); + const patchResourceData: SaveResourceData<Resource> = { toSave: toPatch, linkRel, resource }; + + const patchedResource: Resource = createDummyResource(); + + beforeEach(() => { + resourceWrapper.patch.mockReturnValue(singleCold(patchedResource)); + }); + + it('should call resourceFactory with resource', () => { + repository.patch(patchResourceData); + + expect(resourceFactory.from).toHaveBeenCalledWith(resource); + }); + + it('should call resourceWrapper with linkRel and object to patch', () => { + repository.patch(patchResourceData); + + expect(resourceWrapper.patch).toHaveBeenCalledWith(linkRel, toPatch); + }); + + it('should return value', () => { + const result: Observable<Resource> = repository.patch(patchResourceData); + + expect(result).toBeObservable(singleHot(patchedResource)); + }); + }); + describe('delete', () => { const deleteLinkRel: LinkRelationName = faker.random.word(); const resourceToDelete: Resource = createDummyResource([deleteLinkRel]); @@ -191,7 +220,7 @@ describe('ResourceRepository', () => { expect(resourceFactory.from).toHaveBeenCalledWith(resourceToDelete); }); - it('should call resourceWrapper with linkel', () => { + it('should call resourceWrapper with linkRel', () => { repository.delete(resourceToDelete, deleteLinkRel); expect(resourceWrapper.delete).toHaveBeenCalledWith(deleteLinkRel); @@ -218,10 +247,7 @@ describe('ResourceRepository', () => { repository.search(dummyResource, linkRel, searchBy); - expect(repository.buildSearchUri).toHaveBeenCalledWith( - new URL(getUrl(dummyResource, linkRel)), - searchBy, - ); + expect(repository.buildSearchUri).toHaveBeenCalledWith(new URL(getUrl(dummyResource, linkRel)), searchBy); }); it('should call resourceFactory', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.ts index b0fcb3995f0961983b8caef53160762ad3fc4416..fe199d6e0f55c932f385ecfa4d3dac96dd930230 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.repository.ts @@ -23,9 +23,7 @@ export class ResourceRepository { } public createResource(createResourceData: CreateResourceData<Resource>): Observable<Resource> { - return this.resourceFactory - .from(createResourceData.resource) - .post(createResourceData.linkRel, createResourceData.toCreate); + return this.resourceFactory.from(createResourceData.resource).post(createResourceData.linkRel, createResourceData.toCreate); } public getResource<T>(uri: ResourceUri): Observable<T> { @@ -33,9 +31,11 @@ export class ResourceRepository { } public save(saveResourceData: SaveResourceData<Resource>): Observable<Resource> { - return this.resourceFactory - .from(saveResourceData.resource) - .put(saveResourceData.linkRel, saveResourceData.toSave); + return this.resourceFactory.from(saveResourceData.resource).put(saveResourceData.linkRel, saveResourceData.toSave); + } + + public patch(patchResourceData: SaveResourceData<Resource>): Observable<Resource> { + return this.resourceFactory.from(patchResourceData.resource).patch(patchResourceData.linkRel, patchResourceData.toSave); } public delete(resource: Resource, linkRel: LinkRelationName): Observable<Resource> { @@ -43,9 +43,7 @@ export class ResourceRepository { } public search<T>(resource: Resource, linkRel: LinkRelationName, searchBy: string): Observable<T> { - return this.resourceFactory - .fromId(this.buildSearchUri(new URL(getUrl(resource, linkRel)), searchBy)) - .get(); + return this.resourceFactory.fromId(this.buildSearchUri(new URL(getUrl(resource, linkRel)), searchBy)).get(); } buildSearchUri(url: URL, searchBy: string): ResourceUri { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts index 61f1ae622b0c4621da50136718665d0c5a94756e..1a7c4254c31475dde3488c115c80245b758d0145 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.spec.ts @@ -12,12 +12,7 @@ import { ProblemDetail } from '../tech.model'; import { LinkRelationName, ResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { ResourceService } from './resource.service'; -import { - StateResource, - createEmptyStateResource, - createErrorStateResource, - createStateResource, -} from './resource.util'; +import { StateResource, createEmptyStateResource, createErrorStateResource, createStateResource } from './resource.util'; import * as ResourceUtil from './resource.util'; @@ -62,9 +57,7 @@ describe('ResourceService', () => { service.stateResource.next(stateResource); service.handleResourceChanges = jest.fn(); - isInvalidResourceCombinationSpy = jest - .spyOn(ResourceUtil, 'isInvalidResourceCombination') - .mockReturnValue(true); + isInvalidResourceCombinationSpy = jest.spyOn(ResourceUtil, 'isInvalidResourceCombination').mockReturnValue(true); }); it('should handle config resource changed', fakeAsync(() => { @@ -82,15 +75,11 @@ describe('ResourceService', () => { })); it('should return initial value', () => { - service.stateResource.asObservable = jest - .fn() - .mockReturnValue(singleHot(stateResource, '-a')); + service.stateResource.asObservable = jest.fn().mockReturnValue(singleHot(stateResource, '-a')); const apiRootStateResource$: Observable<StateResource<Resource>> = service.get(); - expect(apiRootStateResource$).toBeObservable( - cold('a', { a: createEmptyStateResource(true) }), - ); + expect(apiRootStateResource$).toBeObservable(cold('a', { a: createEmptyStateResource(true) })); }); }); @@ -167,10 +156,7 @@ describe('ResourceService', () => { it('should update stateresource by configresource', () => { service.handleResourceChanges(stateResource, configResource); - expect(service.updateStateResourceByConfigResource).toHaveBeenCalledWith( - stateResource, - configResource, - ); + expect(service.updateStateResourceByConfigResource).toHaveBeenCalledWith(stateResource, configResource); }); }); @@ -235,10 +221,7 @@ describe('ResourceService', () => { }); it('should return true if configresource has no get link', () => { - const shouldClear: boolean = service.shouldClearStateResource( - dummyStateResource, - createDummyResource(), - ); + const shouldClear: boolean = service.shouldClearStateResource(dummyStateResource, createDummyResource()); expect(shouldClear).toBeTruthy(); }); @@ -246,19 +229,13 @@ describe('ResourceService', () => { describe('on empty stateresource', () => { it('should return false', () => { - const shouldClear: boolean = service.shouldClearStateResource( - createEmptyStateResource(), - null, - ); + const shouldClear: boolean = service.shouldClearStateResource(createEmptyStateResource(), null); expect(shouldClear).toBeFalsy(); }); it('should return false if configresource has no get link', () => { - const shouldClear: boolean = service.shouldClearStateResource( - createEmptyStateResource(), - createDummyResource(), - ); + const shouldClear: boolean = service.shouldClearStateResource(createEmptyStateResource(), createDummyResource()); expect(shouldClear).toBeFalsy(); }); @@ -306,9 +283,7 @@ describe('ResourceService', () => { service.loadResource(configResourceWithGetLinkRel); - expect(service.doLoadResource).toHaveBeenCalledWith( - getUrl(configResourceWithGetLinkRel, config.getLinkRel), - ); + expect(service.doLoadResource).toHaveBeenCalledWith(getUrl(configResourceWithGetLinkRel, config.getLinkRel)); }); }); @@ -394,9 +369,7 @@ describe('ResourceService', () => { it('should do save', fakeAsync(() => { const stateResource: StateResource<Resource> = createStateResource(resourceWithEditLinkRel); service.stateResource.next(stateResource); - const doSaveMock: jest.Mock = (service.doSave = jest.fn()).mockReturnValue( - of(loadedResource), - ); + const doSaveMock: jest.Mock = (service.doSave = jest.fn()).mockReturnValue(of(loadedResource)); service.save(dummyToSave).subscribe(); tick(); @@ -435,16 +408,62 @@ describe('ResourceService', () => { })); }); + describe('patch', () => { + const dummyToPatch: unknown = {}; + const loadedResource: Resource = createDummyResource(); + + const resourceWithEditLinkRel: Resource = createDummyResource([editLinkRel]); + + it('should do patch', fakeAsync(() => { + const stateResource: StateResource<Resource> = createStateResource(resourceWithEditLinkRel); + service.stateResource.next(stateResource); + const doPatchMock: jest.Mock = (service.doPatch = jest.fn()).mockReturnValue(of(loadedResource)); + + service.patch(dummyToPatch).subscribe(); + tick(); + + expect(doPatchMock).toHaveBeenCalledWith(resourceWithEditLinkRel, dummyToPatch); + })); + + it('should return patched object', () => { + service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + service.doPatch = jest.fn().mockReturnValue(singleHot(loadedResource)); + + const patched: Observable<StateResource<Resource>> = service.patch(dummyToPatch); + + expect(patched).toBeObservable(singleCold(createStateResource(loadedResource))); + }); + + it('should call handleError', () => { + service.stateResource.next(createStateResource(createDummyResource([config.edit.linkRel]))); + const errorResponse: ProblemDetail = createProblemDetail(); + service.doPatch = jest.fn().mockReturnValue(throwError(() => errorResponse)); + service.handleError = jest.fn(); + + service.patch(<any>{}).subscribe(); + + expect(service.handleError).toHaveBeenCalledWith(errorResponse); + }); + + it('should update state resource subject', fakeAsync(() => { + service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + service.doPatch = jest.fn().mockReturnValue(of(loadedResource)); + + service.patch(dummyToPatch).subscribe(); + tick(); + + expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); + })); + }); + describe('handleError', () => { it('should return error stateresource on problem unprocessable entity', (done: jest.DoneCallback) => { const error: ProblemDetail = createProblemDetail(); - service - .handleError(<HttpErrorResponse>(<any>error)) - .subscribe((responseError: StateResource<unknown>) => { - expect(responseError).toEqual(createErrorStateResource(error)); - done(); - }); + service.handleError(<HttpErrorResponse>(<any>error)).subscribe((responseError: StateResource<unknown>) => { + expect(responseError).toEqual(createErrorStateResource(error)); + done(); + }); }); it('should rethrow error', () => { @@ -503,10 +522,7 @@ describe('ResourceService', () => { }); }); -export class DummyResourceService<B extends Resource, T extends Resource> extends ResourceService< - B, - T -> { +export class DummyResourceService<B extends Resource, T extends Resource> extends ResourceService<B, T> { constructor( protected config: ResourceServiceConfig<B>, protected repository: ResourceRepository, @@ -517,4 +533,8 @@ export class DummyResourceService<B extends Resource, T extends Resource> extend doSave(resource: T, toSave: unknown): Observable<T> { return of(resource); } + + doPatch(resource: T, toPatch: unknown): Observable<T> { + return of(resource); + } } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts index 6c9eb0a56f13fb81555b915c436119b742b2ec45..02abe975e853fe01912920f1bff836914c4779aa 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.ts @@ -1,19 +1,7 @@ import { HttpErrorResponse } from '@angular/common/http'; import { getUrl, hasLink, Resource, ResourceUri } from '@ngxp/rest'; import { isEqual, isNull } from 'lodash-es'; -import { - BehaviorSubject, - catchError, - combineLatest, - filter, - first, - map, - Observable, - of, - startWith, - tap, - throwError, -} from 'rxjs'; +import { BehaviorSubject, catchError, combineLatest, filter, first, map, Observable, of, startWith, tap, throwError } from 'rxjs'; import { isUnprocessableEntity } from '../http.util'; import { HttpError } from '../tech.model'; import { isNotNull } from '../tech.util'; @@ -35,9 +23,7 @@ import { * T = Type of the resource which is working on */ export abstract class ResourceService<B extends Resource, T extends Resource> { - readonly stateResource: BehaviorSubject<StateResource<T>> = new BehaviorSubject( - createEmptyStateResource(), - ); + readonly stateResource: BehaviorSubject<StateResource<T>> = new BehaviorSubject(createEmptyStateResource()); configResource: B = null; @@ -48,12 +34,8 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { public get(): Observable<StateResource<T>> { return combineLatest([this.stateResource.asObservable(), this.getConfigResource()]).pipe( - tap(([stateResource, configResource]) => - this.handleResourceChanges(stateResource, configResource), - ), - filter( - ([stateResource]) => !isInvalidResourceCombination(stateResource, this.configResource), - ), + tap(([stateResource, configResource]) => this.handleResourceChanges(stateResource, configResource)), + filter(([stateResource]) => !isInvalidResourceCombination(stateResource, this.configResource)), mapToFirst<T, B>(), startWith(createEmptyStateResource<T>(true)), ); @@ -61,10 +43,7 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { private getConfigResource(): Observable<B> { return this.config.resource.pipe( - filter( - (configStateResource: StateResource<B>) => - !configStateResource.loading && !configStateResource.reload, - ), + filter((configStateResource: StateResource<B>) => !configStateResource.loading && !configStateResource.reload), mapToResource<B>(), ); } @@ -93,10 +72,7 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { } shouldClearStateResource(stateResource: StateResource<T>, configResource: B): boolean { - return ( - (isNull(configResource) || this.hasNotGetLink(configResource)) && - !this.isStateResourceEmpty(stateResource) - ); + return (isNull(configResource) || this.hasNotGetLink(configResource)) && !this.isStateResourceEmpty(stateResource); } private hasNotGetLink(configResource: B): boolean { @@ -152,6 +128,15 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { ); } + public patch(toPatch: unknown): Observable<StateResource<T>> { + const previousResource: T = this.stateResource.value.resource; + return this.doPatch(previousResource, toPatch).pipe( + tap((loadedResource: T) => this.stateResource.next(createStateResource(loadedResource))), + map(() => this.stateResource.value), + catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)), + ); + } + handleError(errorResponse: HttpErrorResponse): Observable<StateResource<T>> { if (isUnprocessableEntity(errorResponse.status)) { return of(createErrorStateResource((<any>errorResponse) as HttpError)); @@ -161,6 +146,8 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { abstract doSave(resource: T, toSave: unknown): Observable<T>; + abstract doPatch(resource: T, toPatch: unknown): Observable<T>; + public refresh(): void { this.stateResource.next({ ...this.stateResource.value, reload: true }); } diff --git a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.model.ts b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.model.ts index aafc23fbbce95dae57f511413b8e1c0dedd936ad..46320868558c4bcb58c907bcb3ad1ec3cd766b73 100644 --- a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.model.ts +++ b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.model.ts @@ -13,8 +13,6 @@ export interface Anschrift { ort: string; } -export interface OrganisationsEinheitResource - extends OrganisationsEinheit, - Resource, - ListItemResource {} +export interface OrganisationsEinheitResource extends OrganisationsEinheit, Resource, ListItemResource {} export interface OrganisationsEinheitListResource extends ListResource {} +export declare type OrganisationsEinheitItemResource = Resource & OrganisationsEinheit; diff --git a/alfa-client/libs/zustaendige-stelle/src/lib/search-zustaendige-stelle-dialog/search-zustaendige-stelle-form/search-zustaendige-stelle-form.component.spec.ts b/alfa-client/libs/zustaendige-stelle/src/lib/search-zustaendige-stelle-dialog/search-zustaendige-stelle-form/search-zustaendige-stelle-form.component.spec.ts index f5a101175b95b57d2a75449fb53a487e17286166..6f6328bf87d2a6eec944f8b968dcbeb114238f50 100644 --- a/alfa-client/libs/zustaendige-stelle/src/lib/search-zustaendige-stelle-dialog/search-zustaendige-stelle-form/search-zustaendige-stelle-form.component.spec.ts +++ b/alfa-client/libs/zustaendige-stelle/src/lib/search-zustaendige-stelle-dialog/search-zustaendige-stelle-form/search-zustaendige-stelle-form.component.spec.ts @@ -100,8 +100,8 @@ describe('SearchZustaendigeStelleFormComponent', () => { }; }); - it('on searchResultClosed output', () => { - eventData = { ...eventData, name: 'searchResultClosed' }; + it('on searchClosed output', () => { + eventData = { ...eventData, name: 'searchClosed' }; triggerEvent(eventData); @@ -109,7 +109,7 @@ describe('SearchZustaendigeStelleFormComponent', () => { }); it('on searchQueryCleared output', () => { - eventData = { ...eventData, name: 'searchResultClosed' }; + eventData = { ...eventData, name: 'searchClosed' }; triggerEvent(eventData);