diff --git a/alfa-client/apps/admin/src/app/app.module.ts b/alfa-client/apps/admin/src/app/app.module.ts index 089a1851424f1c2eca17a73b15d0b0ad3f132588..27a97c2883ef53397aee6da6db68ac930a0b2147 100644 --- a/alfa-client/apps/admin/src/app/app.module.ts +++ b/alfa-client/apps/admin/src/app/app.module.ts @@ -41,9 +41,6 @@ import { appRoutes } from './app.routes'; HttpClientModule, ApiRootModule, EnvironmentModule, - environment.production ? [] : StoreDevtoolsModule.instrument(), - StoreModule.forRoot({}), - EffectsModule.forRoot(), StoreRouterConnectingModule.forRoot(), FormsModule, ReactiveFormsModule, @@ -54,6 +51,18 @@ import { appRoutes } from './app.routes'; }, }), TechSharedModule, + StoreModule.forRoot( + {}, + { + metaReducers: [], + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true, + }, + }, + ), + EffectsModule.forRoot([]), + environment.production ? [] : StoreDevtoolsModule.instrument(), ], providers: [ { diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings-resource.service.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings-resource.service.ts index 67ded869c2c64e15d7a382d8ede37ee6e3eb458b..6d2906574daafaab16ccbbf0ea972277430f9482 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings-resource.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings-resource.service.ts @@ -30,5 +30,6 @@ function buildConfig( createLinkRel: SettingListLinkRel.CREATE, listLinkRel: ConfigurationLinkRel.SETTING, listResourceListLinkRel: SettingListLinkRel.LIST, + saveLinkRel: ConfigurationLinkRel.SETTING, }; } 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 99b04e136805e4fec867cfe801aa82ab6ded3936..f74bece4f9f65ebd88722e7fe354ec92868ee97b 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 @@ -6,12 +6,17 @@ import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import KcAdminClient from '@keycloak/keycloak-admin-client'; +import { Store, StoreModule } from '@ngrx/store'; +import { EffectService } from 'libs/tech-shared/src/lib/ngrx/effects.service'; +import { ReducerService } from 'libs/tech-shared/src/lib/ngrx/reducer.service'; import { createSettingListResourceService, SettingListResourceService, } from './admin-settings-resource.service'; import { SettingsService } from './admin-settings.service'; import { + CONFIGURATION_FEATURE_KEY, + configurationReducers, ConfigurationResourceService, createConfigurationResourceService, } from './configuration/configuration-resource.service'; @@ -26,6 +31,8 @@ import { PostfachFormComponent } from './postfach/postfach-container/postfach-fo import { PostfachNavigationItemComponent } from './postfach/postfach-navigation-item/postfach-navigation-item.component'; import { createPostfachResourceService, + POSTFACH_FEATURE_KEY, + postfachReducers, PostfachResourceService, } from './postfach/postfach-resource.service'; import { PostfachService } from './postfach/postfach.service'; @@ -55,7 +62,14 @@ import { TextFieldComponent } from './shared/text-field/text-field.component'; MoreItemButtonComponent, SpinnerComponent, ], - imports: [CommonModule, TechSharedModule, RouterModule, ReactiveFormsModule], + imports: [ + CommonModule, + TechSharedModule, + RouterModule, + ReactiveFormsModule, + StoreModule.forFeature(CONFIGURATION_FEATURE_KEY, configurationReducers), + StoreModule.forFeature(POSTFACH_FEATURE_KEY, postfachReducers), + ], exports: [ PostfachContainerComponent, OrganisationseinheitContainerComponent, @@ -75,21 +89,21 @@ import { TextFieldComponent } from './shared/text-field/text-field.component'; }), deps: [ENVIRONMENT_CONFIG], }, - { - provide: PostfachResourceService, - useFactory: createPostfachResourceService, - deps: [ResourceRepository, SettingsService], - }, { provide: ConfigurationResourceService, useFactory: createConfigurationResourceService, - deps: [ResourceRepository, ApiRootService], + deps: [ResourceRepository, ApiRootService, Store, ReducerService, EffectService], }, { provide: SettingListResourceService, useFactory: createSettingListResourceService, deps: [ResourceRepository, ConfigurationService], }, + { + provide: PostfachResourceService, + useFactory: createPostfachResourceService, + deps: [ResourceRepository, SettingsService, Store, ReducerService, EffectService], + }, ], }) export class AdminSettingsModule {} diff --git a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts index 10697e4a4b6e128637d423fa7340b8f6dec11c15..e357b7b8e43b527bc7f90c88b97a75cf9f7b0094 100644 --- a/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/admin-settings.service.ts @@ -1,4 +1,4 @@ -import { StateResource } from '@alfa-client/tech-shared'; +import { StateResource, isNotNil, mapToResource } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { Observable, map } from 'rxjs'; import { SettingListResourceService } from './admin-settings-resource.service'; @@ -9,6 +9,10 @@ import { PostfachResource } from './postfach/postfach.model'; export class SettingsService { constructor(private settingListResourceService: SettingListResourceService) {} + public existPostfach(): Observable<boolean> { + return this.getPostfach().pipe(mapToResource<PostfachResource>(), map(isNotNil)); + } + public getPostfach(): Observable<StateResource<PostfachResource>> { return this.settingListResourceService.getList().pipe(map(getPostfachResource)); } diff --git a/alfa-client/libs/admin-settings/src/lib/configuration/configuration-resource.service.ts b/alfa-client/libs/admin-settings/src/lib/configuration/configuration-resource.service.ts index f73458583b348631669fe5479cbcc597f0cf5c61..bd2cc74a1b0a25c7edc861c10a72ad0e724fdca7 100644 --- a/alfa-client/libs/admin-settings/src/lib/configuration/configuration-resource.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/configuration/configuration-resource.service.ts @@ -1,11 +1,21 @@ import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { ApiResourceService, + ResourceReducer, ResourceRepository, ResourceServiceConfig, + ResourceState, + createResourceAction, } from '@alfa-client/tech-shared'; +import { Action, ActionReducerMap, Store } from '@ngrx/store'; +import { EffectService } from 'libs/tech-shared/src/lib/ngrx/effects.service'; +import { ReducerService } from 'libs/tech-shared/src/lib/ngrx/reducer.service'; import { ConfigurationResource } from './configuration.model'; +export const CONFIGURATION_FEATURE_KEY = 'ConfigurationState'; + +export const CONFIGURATION_PATH = 'configuration'; + export class ConfigurationResourceService extends ApiResourceService< ApiRootResource, ConfigurationResource @@ -14,13 +24,39 @@ export class ConfigurationResourceService extends ApiResourceService< export function createConfigurationResourceService( repository: ResourceRepository, apiRootService: ApiRootService, + store: Store, + reducerService: ReducerService, + effectService: EffectService, ) { - return new ApiResourceService(buildConfig(apiRootService), repository); + return new ApiResourceService( + buildConfig(apiRootService, store), + repository, + reducerService, + effectService, + ); } -function buildConfig(apiRootService: ApiRootService): ResourceServiceConfig<ApiRootResource> { +function buildConfig( + apiRootService: ApiRootService, + store: Store, +): ResourceServiceConfig<ApiRootResource> { return { + stateInfo: { + store, + name: CONFIGURATION_FEATURE_KEY, + resourcePath: CONFIGURATION_PATH, + }, resource: apiRootService.getApiRoot(), getLinkRel: ApiRootLinkRel.CONFIGURATION, }; } + +export function configurationResourceReducer(state: ResourceState, action: Action) { + const resourceReducer: ResourceReducer = new ResourceReducer( + createResourceAction(CONFIGURATION_FEATURE_KEY), + ); + return resourceReducer.reducer(state, action); +} +export const configurationReducers: ActionReducerMap<any> = { + [CONFIGURATION_PATH]: configurationResourceReducer, +}; diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.html b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.html index 8a68a1e11400f2ece06013fc60f1d2a13f1d1b50..f3e350a3bee48069b475bf0933a06dbba4400e7b 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.html +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.html @@ -1 +1,6 @@ -<postfach-form [postfachStateResource]="postfachStateResource$ | async"></postfach-form> +<ng-container *ngIf="postfachStateResource$ | async as postfachStateResource"> + <postfach-form + *ngIf="postfachStateResource.resource" + [postfachStateResource]="postfachStateResource" + ></postfach-form> +</ng-container> diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.ts index abba013042591826c89ef4adde6db5b5aae54a28..c20ceef730b0ce421418bcc5995e8ab5a512a3a5 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-container.component.ts @@ -1,6 +1,7 @@ -import { StateResource } from '@alfa-client/tech-shared'; +import { StateResource, createEmptyStateResource, isLoaded } from '@alfa-client/tech-shared'; import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, filter, of, tap } from 'rxjs'; +import { SettingsService } from '../../admin-settings.service'; import { PostfachResource } from '../postfach.model'; import { PostfachService } from '../postfach.service'; @@ -9,11 +10,25 @@ import { PostfachService } from '../postfach.service'; templateUrl: './postfach-container.component.html', }) export class PostfachContainerComponent implements OnInit { - postfachStateResource$: Observable<StateResource<PostfachResource>>; + postfachStateResource$: Observable<StateResource<PostfachResource>> = of( + createEmptyStateResource<PostfachResource>(), + ); - constructor(private postfachService: PostfachService) {} + constructor( + private settingService: SettingsService, + private postfachService: PostfachService, + ) {} ngOnInit(): void { - this.postfachStateResource$ = this.postfachService.get(); + // this.settingService.existPostfach().subscribe((exists) => { + // if (exists) { + this.postfachStateResource$ = this.postfachService.get().pipe( + filter(isLoaded), + tap((postfach) => { + console.info('Container get postfach...', postfach); + }), + ); + // } + // }); } } diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts index c6d801bdd90f6f27101dd9d621c497083c57f6ac..185236551853342f58358801e47dc0ac52145134 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-container/postfach-form/postfach.formservice.ts @@ -26,6 +26,7 @@ export class PostfachFormService extends AbstractFormService { constructor( formBuilder: UntypedFormBuilder, private postfachService: PostfachService, + // private settingService: SettingsService, ) { super(formBuilder); } @@ -48,6 +49,7 @@ export class PostfachFormService extends AbstractFormService { if (this.shouldSkipAbsender(value)) { delete value.absender; } + // return this.settingService.savePostfach(value); return this.postfachService.save(value); } diff --git a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-resource.service.ts b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-resource.service.ts index 07f664fc2293fa9b9e5418d0ffc4a5cfe847a084..9fe53d9cc509ca3e85f99cc03022a406e773c591 100644 --- a/alfa-client/libs/admin-settings/src/lib/postfach/postfach-resource.service.ts +++ b/alfa-client/libs/admin-settings/src/lib/postfach/postfach-resource.service.ts @@ -1,12 +1,22 @@ import { ApiResourceService, + ResourceReducer, ResourceRepository, ResourceServiceConfig, + ResourceState, + createResourceAction, } from '@alfa-client/tech-shared'; +import { Action, ActionReducerMap, Store } from '@ngrx/store'; +import { EffectService } from 'libs/tech-shared/src/lib/ngrx/effects.service'; +import { ReducerService } from 'libs/tech-shared/src/lib/ngrx/reducer.service'; import { SettingsService } from '../admin-settings.service'; import { PostfachLinkRel } from './postfach.linkrel'; import { PostfachResource } from './postfach.model'; +export const POSTFACH_FEATURE_KEY = 'PostfachState'; + +export const POSTFACH_PATH = 'postfach'; + export class PostfachResourceService extends ApiResourceService< PostfachResource, PostfachResource @@ -15,14 +25,36 @@ export class PostfachResourceService extends ApiResourceService< export function createPostfachResourceService( repository: ResourceRepository, settingService: SettingsService, + store: Store, + reducerService: ReducerService, + effectService: EffectService, ) { - return new ApiResourceService(buildConfig(settingService), repository); + return new ApiResourceService( + buildConfig(settingService, store), + repository, + reducerService, + effectService, + ); } -function buildConfig(settingService: SettingsService): ResourceServiceConfig<PostfachResource> { +function buildConfig( + settingService: SettingsService, + store: Store, +): ResourceServiceConfig<PostfachResource> { return { + stateInfo: { store, name: POSTFACH_FEATURE_KEY, resourcePath: POSTFACH_PATH }, resource: settingService.getPostfach(), getLinkRel: PostfachLinkRel.SELF, edit: { linkRel: PostfachLinkRel.SELF }, }; } + +export function postfachResourceReducer(state: ResourceState, action: Action) { + const resourceReducer: ResourceReducer = new ResourceReducer( + createResourceAction(POSTFACH_FEATURE_KEY), + ); + return resourceReducer.reducer(state, action); +} +export const postfachReducers: ActionReducerMap<any> = { + [POSTFACH_PATH]: postfachResourceReducer, +}; diff --git a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.spec.ts b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.spec.ts index 67058f2fbdf29b3c9d0fd5623dc297961760b677..02fe16deb88e7b419b134c3d6825550b9eab8a9a 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.spec.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.spec.ts @@ -5,7 +5,7 @@ import { Action } from '@ngrx/store'; import { createCommandResource } from 'libs/command-shared/test/command'; import { createApiError } from 'libs/tech-shared/test/error'; import { createVorgangWithEingangResource } from 'libs/vorgang-shared/test/vorgang'; -import { BescheidState, initialState, reducer } from './bescheid.reducer'; +import { BescheidState, bescheidReducer, initialState } from './bescheid.reducer'; import * as CommandActions from '../../../../command-shared/src/lib/+state/command.actions'; @@ -14,7 +14,7 @@ describe('Bescheid Reducer', () => { it('should return current state', () => { const action: Action = {} as Action; - const result = reducer(initialState, action); + const result: BescheidState = bescheidReducer(initialState, action); expect(result).toBe(initialState); }); @@ -30,7 +30,7 @@ describe('Bescheid Reducer', () => { command: { ...createCommandResource(), order: CommandOrder.CREATE_BESCHEID }, }); - const state: BescheidState = reducer(initialState, action); + const state: BescheidState = bescheidReducer(initialState, action); expect(state.bescheidCommand.loading).toBeTruthy(); }); @@ -44,7 +44,7 @@ describe('Bescheid Reducer', () => { }; const action: Action = CommandActions.createCommandSuccess({ command }); - const state: BescheidState = reducer(initialState, action); + const state: BescheidState = bescheidReducer(initialState, action); expect(state.bescheidCommand.resource).toBe(command); }); @@ -63,7 +63,7 @@ describe('Bescheid Reducer', () => { error: { error: apiError }, }); - const state: BescheidState = reducer(initialState, action); + const state: BescheidState = bescheidReducer(initialState, action); expect(state.bescheidCommand.error).toBe(apiError); }); diff --git a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.ts b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.ts index 77d5d488988eb02164f9aab52f09c6b5a3553131..824344e4e489e869cce0a34abb55c3607599a0ed 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.reducer.ts @@ -6,22 +6,23 @@ import { } from '@alfa-client/command-shared'; import { ApiError, + ResourceReducer, StateResource, createEmptyStateResource, createErrorStateResource, + createResourceAction, createStateResource, } from '@alfa-client/tech-shared'; import { HttpErrorResponse } from '@angular/common/http'; -import { Action, ActionReducer, createReducer, on } from '@ngrx/store'; +import { Action, ActionReducer, ActionReducerMap, createReducer, on } from '@ngrx/store'; import { isCreateBescheidCommand } from '../bescheid.util'; import * as CommandActions from '../../../../command-shared/src/lib/+state/command.actions'; export const BESCHEID_FEATURE_KEY = 'BescheidState'; -export interface BescheidPartialState { - readonly [BESCHEID_FEATURE_KEY]: BescheidState; -} +export const BESCHEID_PATH = 'bescheid'; +export const BESCHEID_DRAFT_PATH = 'bescheidDraft'; export interface BescheidState { bescheidCommand: StateResource<CommandResource>; @@ -31,11 +32,12 @@ export const initialState: BescheidState = { bescheidCommand: createEmptyStateResource(), }; -const bescheidReducer: ActionReducer<BescheidState, Action> = createReducer( +export const bescheidReducer: ActionReducer<BescheidState, Action> = createReducer( initialState, on( CommandActions.createCommand, (state: BescheidState, props: CreateCommandProps): BescheidState => { + console.info('CREATE command: ', state); return isCreateBescheidCommand(props.command.order) ? { ...state, bescheidCommand: { ...state.bescheidCommand, loading: true } } : state; @@ -67,3 +69,14 @@ const bescheidReducer: ActionReducer<BescheidState, Action> = createReducer( export function reducer(state: BescheidState, action: Action): BescheidState { return bescheidReducer(state, action); } + +export function bescheidResourceReducer(state: BescheidState, action: Action) { + const resourceReducer: ResourceReducer = new ResourceReducer( + createResourceAction(BESCHEID_FEATURE_KEY), + ); + return resourceReducer.reducer(state, action); +} +export const reducers: ActionReducerMap<any> = { + [BESCHEID_PATH]: bescheidReducer, + [BESCHEID_DRAFT_PATH]: bescheidResourceReducer, +}; diff --git a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.spec.ts b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.spec.ts index 04f7dfd10e8da45695ae20069e5d11713af0e6b0..fc2c03fec85eeba366a92196a05dd50173da81d9 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.spec.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.spec.ts @@ -1,30 +1,29 @@ import { CommandResource } from '@alfa-client/command-shared'; import { createStateResource, StateResource } from '@alfa-client/tech-shared'; import { createCommandResource } from 'libs/command-shared/test/command'; -import { BescheidPartialState, initialState } from './bescheid.reducer'; +import { BescheidResourceState, initialState } from './bescheid.reducer'; import * as BescheidSelectors from './bescheid.selectors'; describe('Bescheid Selectors', () => { - let state: BescheidPartialState; + let state: BescheidResourceState; const bescheidCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); beforeEach(() => { state = { - BescheidState: { + bescheid: { ...initialState, bescheidCommand, }, + bescheidDraft: null, }; }); describe('bescheidCommand', () => { it('should return bescheidCommand from state', () => { - expect(BescheidSelectors.bescheidCommand.projector(state.BescheidState)).toEqual( - bescheidCommand, - ); + expect(BescheidSelectors.bescheidCommand.projector(state)).toEqual(bescheidCommand); }); }); }); diff --git a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.ts b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.ts index 78facfef40dad11ac7ae2cb68ce9679e41301e5b..45bda37da9694bb5a19e9cf464128e3cfc612287 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/+state/bescheid.selectors.ts @@ -9,4 +9,7 @@ export const getBescheidState: MemoizedSelector<object, BescheidState> = export const bescheidCommand: MemoizedSelector< BescheidState, StateResource<CommandResource> -> = createSelector(getBescheidState, (state: BescheidState) => state.bescheidCommand); +> = createSelector(getBescheidState, (state: BescheidState) => { + console.info('bescheid command selector: ', state); + return (<any>state).bescheid.bescheidCommand; +}); diff --git a/alfa-client/libs/bescheid-shared/src/lib/bescheid-shared.module.ts b/alfa-client/libs/bescheid-shared/src/lib/bescheid-shared.module.ts index 9d1ae76fdae87d4475e12e7acc14eea3e518dc0f..4f55bc5fdac79bd196a7fd02a433cf9668fe9c41 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/bescheid-shared.module.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/bescheid-shared.module.ts @@ -1,9 +1,10 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; -import { BESCHEID_FEATURE_KEY, reducer } from './+state/bescheid.reducer'; +import { BESCHEID_FEATURE_KEY, reducers } from './+state/bescheid.reducer'; @NgModule({ - imports: [CommonModule, StoreModule.forFeature(BESCHEID_FEATURE_KEY, reducer)], + imports: [CommonModule, StoreModule.forFeature(BESCHEID_FEATURE_KEY, reducers)], + // imports: [CommonModule, StoreModule.forFeature(BESCHEID_FEATURE_KEY, reducer)], }) export class BescheidSharedModule {} diff --git a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts index 787659b0cf6b97dd6c777b83b9703f2d47184109..6bd7bb36ea6ecb5497a32c1e1c001be5b7ee20e2 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts @@ -11,7 +11,6 @@ import { } from '@alfa-client/command-shared'; import { ApiError, - EMPTY_STRING, HttpError, StateResource, createEmptyStateResource, @@ -27,6 +26,7 @@ import { } from '@alfa-client/vorgang-shared'; import { fakeAsync, tick } from '@angular/core/testing'; import faker from '@faker-js/faker'; +import { Store } from '@ngrx/store'; import { ResourceUri, getUrl } from '@ngxp/rest'; import { cold } from 'jest-marbles'; import { CommandLinkRel } from 'libs/command-shared/src/lib/command.linkrel'; @@ -78,6 +78,7 @@ describe('BescheidService', () => { let commandService: Mock<CommandService>; let vorgangCommandService: Mock<VorgangCommandService>; let binaryFileService: Mock<BinaryFileService>; + let store: Mock<Store>; const vorgangWithEingangStateResource: StateResource<VorgangWithEingangResource> = createStateResource(createVorgangWithEingangResource()); @@ -87,6 +88,7 @@ describe('BescheidService', () => { resourceRepository = mock(ResourceRepository); commandService = mock(CommandService); vorgangCommandService = mock(VorgangCommandService); + store = mock(Store); vorgangService = mock(VorgangService); vorgangService.getVorgangWithEingang.mockReturnValue(of(vorgangWithEingangStateResource)); @@ -100,6 +102,7 @@ describe('BescheidService', () => { useFromMock(vorgangCommandService), useFromMock(binaryFileService), useFromMock(resourceRepository), + useFromMock(store), ); }); @@ -352,6 +355,7 @@ describe('BescheidService', () => { const commandStateResource: StateResource<CommandResource> = createCommandStateResource(); beforeEach(() => { + service.bescheidDraftService.exists = jest.fn().mockReturnValue(of(true)); vorgangCommandService.abschliessen.mockReturnValue(of(commandStateResource)); }); @@ -365,41 +369,29 @@ describe('BescheidService', () => { done(); }); }); - - it('should return command', () => { - const command$: Observable<StateResource<CommandResource>> = - service.bescheidErstellungUeberspringen(vorgangWithEingangResource); - - expect(command$).toBeObservable(cold('(a|)', { a: commandStateResource })); - }); }); describe('delete bescheid', () => { const bescheidResource: BescheidResource = createBescheidResource(); - it('should create command', () => { - service.deleteBescheid(bescheidResource); + const commandStateResource: StateResource<CommandResource> = + createStateResource(createCommandResource()); - const expectedProps: CreateCommandProps = { - resource: bescheidResource, - linkRel: BescheidLinkRel.DELETE, - command: { - order: CommandOrder.DELETE_BESCHEID, - body: null, - }, - snackBarMessage: EMPTY_STRING, - }; - expect(commandService.createCommandByProps).toHaveBeenCalledWith(expectedProps); + beforeEach(() => { + service.bescheidDraftService.delete = jest.fn().mockReturnValue(of(commandStateResource)); }); - it('should return command', () => { - const commandStateResource: StateResource<CommandResource> = createEmptyStateResource(); - commandService.createCommandByProps.mockReturnValue(commandStateResource); + it('should call service delete', () => { + service.deleteBescheid(bescheidResource); - const createdCommand: Observable<StateResource<CommandResource>> = + expect(service.bescheidDraftService.delete).toHaveBeenCalled(); + }); + + it('should return value', () => { + const deleteCommandStateResource$: Observable<StateResource<CommandResource>> = service.deleteBescheid(bescheidResource); - expect(createdCommand).toEqual(commandStateResource); + expect(deleteCommandStateResource$).toBeObservable(singleColdCompleted(commandStateResource)); }); }); @@ -418,7 +410,9 @@ describe('BescheidService', () => { buildUpdateBescheidCommandPropsSpy = jest .spyOn(BescheidUtil, 'buildUpdateBescheidCommandProps') .mockReturnValue(createCommandProps); - service.bescheidDraftService.stateResource.next(createStateResource(bescheidResource)); + service.bescheidDraftService.get = jest + .fn() + .mockReturnValue(of(createStateResource(bescheidResource))); commandService.createCommandByProps.mockReturnValue(of(commandStateResource)); service.bescheidDraftService.setResourceByUri = jest.fn(); }); @@ -426,10 +420,7 @@ describe('BescheidService', () => { it('should build update bescheid command props', () => { service.updateBescheid(bescheid); - expect(buildUpdateBescheidCommandPropsSpy).toHaveBeenCalledWith( - service.bescheidDraftService.getResource(), - bescheid, - ); + expect(buildUpdateBescheidCommandPropsSpy).toHaveBeenCalledWith(bescheidResource, bescheid); }); it('should create command', () => { @@ -943,7 +934,7 @@ describe('BescheidService', () => { beforeEach(() => { commandService.createCommandByProps.mockReturnValue(of(commandStateResource)); - service.bescheidDraftService.getResource = jest.fn().mockReturnValue(bescheidResource); + service.getResource = jest.fn().mockReturnValue(bescheidResource); buildCreateBescheidDocumentCommandPropsSpy = jest .spyOn(BescheidUtil, 'buildCreateBescheidDocumentCommandProps') .mockReturnValue(createCommandProps); @@ -1104,16 +1095,17 @@ describe('BescheidService', () => { const commandStateResource: StateResource<CommandResource> = createStateResource(command); beforeEach(() => { + service.bescheidDraftService.get = jest.fn().mockReturnValue(of(commandStateResource)); service.deleteBescheid = jest.fn().mockReturnValue(of(commandStateResource)); service.deleteBescheidDocument = jest.fn(); }); it('should get resource', () => { - service.bescheidDraftService.getResource = jest.fn(); + service.getResource = jest.fn(); service.bescheidVerwerfen().pipe(first()).subscribe(); - expect(service.bescheidDraftService.getResource).toHaveBeenCalled(); + expect(service.getResource).toHaveBeenCalled(); }); it('should delete bescheid', (done) => { diff --git a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts index dc682d6238b3bc2325ac3a09d505843544c13aa7..f74441d8fbc599fc645747403747f3fabcc53bd5 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts @@ -32,7 +32,10 @@ import { } from '@alfa-client/vorgang-shared'; import { getEmpfaenger } from '@alfa-client/vorgang-shared-ui'; import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { ResourceUri, getUrl, hasLink } from '@ngxp/rest'; +import { EffectService } from 'libs/tech-shared/src/lib/ngrx/effects.service'; +import { ReducerService } from 'libs/tech-shared/src/lib/ngrx/reducer.service'; import { BehaviorSubject, Observable, @@ -49,8 +52,8 @@ import { ResourceServiceConfig, } from '../../../tech-shared/src/lib/resource/resource.model'; import { ResourceRepository } from '../../../tech-shared/src/lib/resource/resource.repository'; -import { ResourceService } from '../../../tech-shared/src/lib/resource/resource.service'; import { BescheidFacade } from './+state/bescheid.facade'; +import { BESCHEID_DRAFT_PATH, BESCHEID_FEATURE_KEY, reducer } from './+state/bescheid.reducer'; import { BescheidLinkRel, BescheidListLinkRel } from './bescheid.linkrel'; import { Bescheid, @@ -63,7 +66,6 @@ import { buildCreateBescheidCommand, buildCreateBescheidDocumentCommandProps, buildCreateBescheidDocumentFromFileProps, - buildDeleteBescheidCommandProps, buildSendBescheidCommandProps, buildUpdateBescheidCommandProps, } from './bescheid.util'; @@ -72,7 +74,7 @@ import { DocumentResource } from './document.model'; @Injectable({ providedIn: 'root' }) export class BescheidService { - bescheidDraftService: ResourceService<VorgangWithEingangResource, BescheidResource>; + bescheidDraftService: CommandResourceService<VorgangWithEingangResource, BescheidResource>; bescheidListService: ResourceListService< VorgangWithEingangResource, BescheidListResource, @@ -107,10 +109,14 @@ export class BescheidService { private readonly vorgangCommandService: VorgangCommandService, private readonly binaryFileService: BinaryFileService, private readonly repository: ResourceRepository, + private readonly effectService: EffectService, + private readonly store: Store, + private readonly reducerService: ReducerService, ) { this.bescheidDraftService = new CommandResourceService( this.buildBescheidDraftServiceConfig(), - repository, + this.reducerService, + this.effectService, this.commandService, ); this.bescheidListService = new ResourceListService( @@ -121,6 +127,12 @@ export class BescheidService { buildBescheidDraftServiceConfig(): ResourceServiceConfig<VorgangWithEingangResource> { return { + stateInfo: { + store: this.store, + name: BESCHEID_FEATURE_KEY, + resourcePath: BESCHEID_DRAFT_PATH, + additionalReducer: [reducer], + }, resource: this.vorgangService.getVorgangWithEingang(), getLinkRel: VorgangWithEingangLinkRel.BESCHEID_DRAFT, delete: { linkRel: BescheidLinkRel.DELETE, order: CommandOrder.DELETE_BESCHEID }, @@ -137,11 +149,12 @@ export class BescheidService { } public init(): void { - this.bescheidDraftService = new CommandResourceService( - this.buildBescheidDraftServiceConfig(), - this.repository, - this.commandService, - ); + // this.bescheidDraftService = new CommandResourceService( + // this.buildBescheidDraftServiceConfig(), + // this.reducerService, + // this.effectService, + // this.commandService, + // ); this.bescheidDocumentFile$.next(createEmptyStateResource()); this.bescheidDocumentUri$.next(null); } @@ -212,7 +225,7 @@ export class BescheidService { } public updateBescheid(bescheid: Bescheid): Observable<StateResource<CommandResource>> { - return this.doUpdateBescheid(this.bescheidDraftService.getResource(), bescheid).pipe( + return this.doUpdateBescheid(this.getResource(), bescheid).pipe( tapOnCommandSuccessfullyDone((commandStateResource: StateResource<CommandResource>) => { this.updateBescheidDraft(commandStateResource.resource); this.clearCreateBescheidDocumentInProgress(); @@ -410,7 +423,7 @@ export class BescheidService { doCreateBescheidDocument(): Observable<StateResource<CommandResource>> { return this.commandService.createCommandByProps( - buildCreateBescheidDocumentCommandProps(this.bescheidDraftService.getResource()), + buildCreateBescheidDocumentCommandProps(this.getResource()), ); } @@ -457,7 +470,7 @@ export class BescheidService { } public bescheidVerwerfen(): Observable<StateResource<CommandResource>> { - return this.deleteBescheid(this.bescheidDraftService.getResource()).pipe( + return this.deleteBescheid(this.getResource()).pipe( tapOnCommandSuccessfullyDone(() => { this.deleteBescheidDocument(); this.vorgangService.reloadCurrentVorgang(); @@ -465,8 +478,25 @@ export class BescheidService { ); } + /** + * @returns @deprecated Don't use this Function + */ + getResource(): BescheidResource { + let resource: StateResource<BescheidResource> = undefined; + const selected = this.bescheidDraftService.get().pipe( + filter((stateResource: StateResource<BescheidResource>) => !stateResource.loading), + take(1), + ); + const sub = selected.subscribe( + (stateResource: StateResource<BescheidResource>) => (resource = stateResource), + ); + sub.unsubscribe(); + + return resource.resource; + } + deleteBescheid(bescheid: BescheidResource): Observable<StateResource<CommandResource>> { - return this.commandService.createCommandByProps(buildDeleteBescheidCommandProps(bescheid)); + return this.bescheidDraftService.delete(); } public reloadCurrentVorgang(): void { 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 73c60da70421cec72dc85f7be6a696a3e7884aae..cb9d833441eaf4b4a7671a7594e986e7c54ec002 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 @@ -4,16 +4,19 @@ import { ResourceRepository, ResourceServiceConfig, StateResource, + createEmptyStateResource, createStateResource, } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { Store } from '@ngrx/store'; import { Resource } from '@ngxp/rest'; +import { cold } from 'jest-marbles'; import { Observable, of } from 'rxjs'; import { createCommandResource } from '../../../command-shared/test/command'; -import { singleCold, singleHot } from '../../../tech-shared/test/marbles'; +import { singleHot } from '../../../tech-shared/test/marbles'; import { createDummyResource } from '../../../tech-shared/test/resource'; import { CommandResourceService } from './command-resource.service'; -import { CommandResource } from './command.model'; +import { CommandResource, CreateCommandProps } from './command.model'; import { CommandService } from './command.service'; describe('CommandResourceService', () => { @@ -21,26 +24,30 @@ describe('CommandResourceService', () => { let config: ResourceServiceConfig<Resource>; let repository: Mock<ResourceRepository>; let commandService: Mock<CommandService>; + let store: Mock<Store>; const configResource: Resource = createDummyResource(); const configStateResource: StateResource<Resource> = createStateResource(configResource); const configStateResource$: Observable<StateResource<Resource>> = of(configStateResource); - const editLinkRel: string = 'dummyEditLinkRel'; + const editLinkRel: LinkRelationName = 'dummyEditLinkRel'; const getLinkRel: LinkRelationName = 'dummyGetLinkRel'; - const deleteOrder: string = 'dummyDeleteOrder'; - const deleteLinkRel: string = 'dummyDeleteLinkRel'; + const deleteOrder: LinkRelationName = 'dummyDeleteOrder'; + const deleteLinkRel: LinkRelationName = 'dummyDeleteLinkRel'; beforeEach(() => { + repository = mock(ResourceRepository); + commandService = mock(CommandService); + store = mock(Store); + config = { + stateInfo: { store: useFromMock(store), name: EMPTY_STRING }, resource: configStateResource$, getLinkRel, edit: { linkRel: editLinkRel }, delete: { order: deleteOrder, linkRel: deleteLinkRel }, }; - repository = mock(ResourceRepository); - commandService = mock(CommandService); service = new CommandResourceService( config, @@ -54,44 +61,43 @@ describe('CommandResourceService', () => { }); describe('delete', () => { - const resourceWithDeleteLinkRel: Resource = createDummyResource([deleteLinkRel]); - const stateResourceWithDeleteLink: StateResource<Resource> = - createStateResource(resourceWithDeleteLinkRel); + const commandResourceWithDeleteLink: CommandResource = createCommandResource([deleteLinkRel]); + const commandStateResource: StateResource<CommandResource> = createStateResource( + commandResourceWithDeleteLink, + ); beforeEach(() => { - commandService.createCommandByProps.mockReturnValue( - of(createStateResource(createCommandResource())), - ); - service.stateResource.next(stateResourceWithDeleteLink); + commandService.createCommandByProps.mockReturnValue(of(commandStateResource)); + store.select.mockReturnValue(of(commandStateResource)); }); - it('should throw error if delete linkRel not exists on current stateresource', () => { - service.stateResource.next(createStateResource(createDummyResource())); - - expect(() => service.delete()).toThrowError( - 'No delete link exists on current stateresource.', - ); - }); - - it('should call command service', () => { - service.delete(); - - expect(commandService.createCommandByProps).toHaveBeenCalledWith({ - resource: resourceWithDeleteLinkRel, + it('should call command service', (done) => { + const expectedCreateCommandByProps: CreateCommandProps = { + resource: commandResourceWithDeleteLink, linkRel: deleteLinkRel, command: { order: deleteOrder, body: null }, snackBarMessage: EMPTY_STRING, + }; + + service.delete().subscribe(() => { + expect(commandService.createCommandByProps).toHaveBeenCalledWith( + expectedCreateCommandByProps, + ); + done(); }); }); it('should return value', () => { - const deleteResource: Resource = createDummyResource(); - const deleteStateResource: StateResource<Resource> = createStateResource(deleteResource); - commandService.createCommandByProps.mockReturnValue(singleHot(deleteStateResource)); + commandService.createCommandByProps.mockReturnValue( + singleHot(commandResourceWithDeleteLink, '-a'), + ); - const deletedResource: Observable<StateResource<CommandResource>> = service.delete(); + const deletedCommandStateResource$: Observable<StateResource<CommandResource>> = + service.delete(); - expect(deletedResource).toBeObservable(singleCold(deleteStateResource)); + expect(deletedCommandStateResource$).toBeObservable( + cold('ab', { a: createEmptyStateResource(true), b: commandResourceWithDeleteLink }), + ); }); }); }); 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 0eb9f03aa4979479a1bcb27c9d79d7d959d6ff2d..c9546c10bce58fd38d3fa79ab578efafd2fd104b 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 @@ -1,44 +1,48 @@ import { EMPTY_STRING, - ResourceRepository, ResourceService, ResourceServiceConfig, StateResource, createEmptyStateResource, + isStateResoureStable, } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { EffectService } from 'libs/tech-shared/src/lib/ngrx/effects.service'; +import { ReducerService } from 'libs/tech-shared/src/lib/ngrx/reducer.service'; +import { Observable, filter, startWith, switchMap } from 'rxjs'; import { CommandResource, CreateCommandProps } from './command.model'; +import { tapOnCommandSuccessfullyDone } from './command.rxjs.operator'; import { CommandService } from './command.service'; export class CommandResourceService<B extends Resource, T extends Resource> extends ResourceService< B, T > { - deleteStateCommandResource: BehaviorSubject<StateResource<CommandResource>> = new BehaviorSubject< - StateResource<CommandResource> - >(createEmptyStateResource()); - constructor( protected config: ResourceServiceConfig<B>, - protected repository: ResourceRepository, + protected reducerService: ReducerService, + protected effectService: EffectService, private commandService: CommandService, ) { - super(config, repository); + super(config, reducerService, effectService); } - doSave(resource: T, toSave: unknown): Observable<T> { - throw new Error('Method not implemented.'); + public delete(): Observable<StateResource<CommandResource>> { + return this.getResource().pipe( + switchMap((stateResource: StateResource<T>) => this.doDelete(stateResource.resource)), + tapOnCommandSuccessfullyDone(() => this.clearStateResource()), + filter(isStateResoureStable), + startWith(createEmptyStateResource<CommandResource>(true)), + ); } - public delete(): Observable<StateResource<CommandResource>> { - this.verifyDeleteLinkRel(); - return this.commandService.createCommandByProps(this.buildDeleteCommandProps()); + private doDelete(resource: T): Observable<StateResource<CommandResource>> { + return this.commandService.createCommandByProps(this.buildDeleteCommandProps(resource)); } - private buildDeleteCommandProps(): CreateCommandProps { - return { - resource: this.stateResource.value.resource, + private buildDeleteCommandProps(resource: T): CreateCommandProps { + return <any>{ + resource, linkRel: this.config.delete.linkRel, command: { order: this.config.delete.order, body: null }, snackBarMessage: EMPTY_STRING, diff --git a/alfa-client/libs/command-shared/src/lib/command.rxjs.operator.ts b/alfa-client/libs/command-shared/src/lib/command.rxjs.operator.ts index 5feb8a4455d112986ef77a8b4cebf3bdd05f0421..d0b866bebd3e5ce0eb867fe8d33e3b1c2991f731 100644 --- a/alfa-client/libs/command-shared/src/lib/command.rxjs.operator.ts +++ b/alfa-client/libs/command-shared/src/lib/command.rxjs.operator.ts @@ -1,4 +1,4 @@ -import { StateResource } from '@alfa-client/tech-shared'; +import { StateResource, isNotNil } from '@alfa-client/tech-shared'; import { Observable, of, switchMap, tap } from 'rxjs'; import { CommandResource } from './command.model'; import { isSuccessfulDone } from './command.util'; @@ -29,7 +29,7 @@ export function switchMapCommandSuccessfullyDone( ): Observable<StateResource<CommandResource>> => { return source.pipe( switchMap((commandStateResource: StateResource<CommandResource>) => { - if (isSuccessfulDone(commandStateResource.resource)) { + if (isNotNil(commandStateResource) && isSuccessfulDone(commandStateResource.resource)) { return runnable(commandStateResource); } else { return of(commandStateResource); diff --git a/alfa-client/libs/tech-shared/src/index.ts b/alfa-client/libs/tech-shared/src/index.ts index fd72ff72fc272d489207273fcbc805abe4f8b2f0..42a4680e0a720fc879458ef15443ce752577a5c0 100644 --- a/alfa-client/libs/tech-shared/src/index.ts +++ b/alfa-client/libs/tech-shared/src/index.ts @@ -31,6 +31,8 @@ export * from './lib/form.util'; export * from './lib/http.util'; export * from './lib/message-code'; export * from './lib/ngrx/actions'; +export * from './lib/ngrx/resource.reducer'; +export * from './lib/ngrx/resource.state'; export * from './lib/pipe/convert-for-data-test.pipe'; export * from './lib/pipe/convert-to-boolean.pipe'; export * from './lib/pipe/enum-to-label.pipe'; diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/actions.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/actions.ts index 907ccaf7749c9f9cb7438addf98a0da8b56de6dc..43baadf366f04420513f674de3e867153590bad8 100644 --- a/alfa-client/libs/tech-shared/src/lib/ngrx/actions.ts +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/actions.ts @@ -21,10 +21,10 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Action, ActionCreator } from '@ngrx/store'; +import { Action, ActionCreator, createAction, props } from '@ngrx/store'; import { TypedAction } from '@ngrx/store/src/models'; -import { ResourceUri } from '@ngxp/rest'; -import { ApiError } from '../tech.model'; +import { Resource, ResourceUri } from '@ngxp/rest'; +import { ApiError, HttpError } from '../tech.model'; export const EMPTY_ACTION: Action = {} as Action; @@ -32,11 +32,59 @@ export interface TypedActionCreatorWithProps<T> extends ActionCreator<string, (props: T) => T & TypedAction<string>> {} export interface TypedActionCreator extends ActionCreator<string, () => TypedAction<string>> {} +//TODO rename ApiErrorProps export interface ApiErrorAction { - //TODO rename ApiErrorProps apiError: ApiError; } export interface ResourceUriProps { resourceUri: ResourceUri; + resourcePath?: string; +} + +export interface LoadResourceSuccessProps { + resource: Resource; + resourcePath?: string; +} + +export interface LoadResourceFailureProps { + error: HttpError; + resourcePath?: string; +} + +export interface LoadResourceSuccessProps {} + +export interface ResourceActions { + loadAction: TypedActionCreatorWithProps<ResourceUriProps>; + loadSuccessAction: TypedActionCreatorWithProps<LoadResourceSuccessProps>; + loadFailureAction: TypedActionCreatorWithProps<LoadResourceFailureProps>; + + clearAction: TypedActionCreator; + reloadAction: TypedActionCreator; +} + +export function createResourceAction(featureName: string): ResourceActions { + const loadAction: TypedActionCreatorWithProps<ResourceUriProps> = createAction( + createActionType(featureName, 'Load Resource'), + props<ResourceUriProps>(), + ); + const loadSuccessAction: TypedActionCreatorWithProps<LoadResourceSuccessProps> = createAction( + createActionType(featureName, 'Load Resource Success'), + props<LoadResourceSuccessProps>(), + ); + const loadFailureAction: TypedActionCreatorWithProps<LoadResourceFailureProps> = createAction( + createActionType(featureName, 'Load Resource Failure'), + props<LoadResourceFailureProps>(), + ); + const clearAction: TypedActionCreator = createAction( + createActionType(featureName, 'Clear Resource'), + ); + const reloadAction: TypedActionCreator = createAction( + createActionType(featureName, 'Reload Resource'), + ); + return { loadAction, loadSuccessAction, loadFailureAction, clearAction, reloadAction }; +} + +function createActionType(stateName: string, message: string): string { + return `[${stateName}] ${message}`; } diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/effects.service.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/effects.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..91e9112aa8215fe731064df4e9455430c95957d9 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/effects.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Actions, EffectSources } from '@ngrx/effects'; +import { ResourceRepository } from '../resource/resource.repository'; +import { ResourceActions } from './actions'; +import { ResourceEffects } from './resource.effects'; + +@Injectable({ + providedIn: 'root', +}) +export class EffectService { + constructor( + private actions$: Actions, + private effectSources: EffectSources, + private repository: ResourceRepository, + ) {} + + public addEffects(resourceActions: ResourceActions): void { + const effect = new ResourceEffects(this.actions$, this.repository, resourceActions); + this.effectSources.addEffects({ ...effect }); + } +} diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/reducer.service.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/reducer.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1922c286d839502a6f66bd202a65dd08e4ce909f --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/reducer.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { Action, ActionReducer, ActionReducerMap, ReducerManager } from '@ngrx/store'; +import { StateInfo } from '../resource/resource.model'; +import { ResourceActions } from './actions'; +import { ResourceReducer } from './resource.reducer'; +import { ResourceState } from './resource.state'; + +@Injectable({ + providedIn: 'root', +}) +export class ReducerService { + constructor(private reducerManager: ReducerManager) {} + + public addFeatureReducer<T>(stateInfo: StateInfo, resourceActions: ResourceActions) { + const resourceReducer = new ResourceReducer(resourceActions); + //Funktioniert aber überschreibt die bestehenden reducer :( + // this.reducerManager.addReducer(stateInfo.name, resourceReducer.reducer); + + //2 + // this.addSubReducer(stateInfo.name, 'resource', resourceReducer.reducer); + //3 + // const wrappedReducer = dynamicReducerWrapper(resourceReducer); + // this.reducerManager.addReducer('feature', wrappedReducer); + + // this.addSubReducerTest2(stateInfo.name, 'resource', resourceReducer.reducer); + } + + private addSubReducer( + featureStateName: string, + attributeName: string, + reducer: ActionReducer<any, Action>, + ): void { + console.info('ADD SUB_REDUCER'); + const allReducer: ActionReducerMap<any> = this.reducerManager.currentReducers; + console.info('ALL REDUCER: ', allReducer); + + const existingReducer = allReducer[featureStateName]; + + this.reducerManager.addReducer(featureStateName, (state: ResourceState, action: Action) => { + console.info('State in addReducer: ', state); + const newState = { + ...state, + ['bescheidCommand']: { ...state['bescheidCommand'] }, + ['bescheidDraft']: reducer(state[attributeName], action), + }; + + // const newState = combineReducers(existingReducer, reducer); + return newState; + }); + } + + // addSubReducerTest2(parentKey, subKey: string, reducer: any) { + // // Wrap the existing parent reducer if it isn't already wrapped + // const existingReducer = this.reducerManager.getReducer(parentKey as string); + // if (!existingReducer.wrapped) { + // const wrappedReducer = dynamicReducerWrapper(existingReducer); + // this.reducerManager.addReducer(parentKey as string, wrappedReducer); + // } + + // // Add the new sub-reducer + // addDynamicReducer(subKey, reducer); + // } +} + +// const dynamicReducers: { [key: string]: ActionReducer<any> } = {}; +// export function dynamicReducerWrapper<S>(reducer: ActionReducer<S>): ActionReducer<S> { +// return function (state: S, action: Action): S { +// // First, run the base reducer +// let newState = reducer(state, action); + +// // Then, run any dynamically added sub-reducers +// for (const key in dynamicReducers) { +// newState = { +// ...newState, +// [key]: dynamicReducers[key](newState[key], action), +// }; +// } + +// return newState; +// }; +// } + +// export function addDynamicReducer<S>(key: string, dynamicReducer: ActionReducer<any>) { +// dynamicReducers[key] = dynamicReducer; +// } diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effect.spec.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effect.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc207846a5556605b28b0f7c34b54c595f8e2d24 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effect.spec.ts @@ -0,0 +1,93 @@ +import { Mock, mock } from '@alfa-client/test-utils'; +import { HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import faker from '@faker-js/faker'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Action } from '@ngrx/store'; +import { TypedAction } from '@ngrx/store/src/models'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { Resource, ResourceUri } from '@ngxp/rest'; +import { cold, hot } from 'jest-marbles'; +import { createApiError, createHttpErrorResponse } from 'libs/tech-shared/test/error'; +import { createDummyResource } from 'libs/tech-shared/test/resource'; +import { Observable, of } from 'rxjs'; +import { ResourceRepository } from '../resource/resource.repository'; +import { ApiError } from '../tech.model'; +import { ResourceEffects } from './resource.effects'; + +import * as ResourceActions from './actions'; + +describe('ResourceEffects', () => { + let actions: Observable<Action>; + let effects: ResourceEffects<Resource>; + let store: MockStore; + + const resourceRepository: Mock<ResourceRepository> = mock(ResourceRepository); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ResourceEffects, + provideMockActions(() => actions), + provideMockStore(), + { + provide: ResourceRepository, + useValue: resourceRepository, + }, + ], + }); + + effects = TestBed.inject(ResourceEffects); + + store = TestBed.inject(MockStore); + }); + + describe('load resource by uri', () => { + const resourceUri: ResourceUri = faker.internet.url(); + + const action: TypedAction<string> = ResourceActions.loadResource({ resourceUri }); + const resource: Resource = createDummyResource(); + + it('should call repository', () => { + actions = of(action); + + effects.loadByUri$.subscribe(); + + expect(resourceRepository.getResource).toHaveBeenCalledWith(resourceUri); + }); + + describe('on success', () => { + beforeEach(() => { + resourceRepository.getResource.mockReturnValue(of(resource)); + }); + + it('should dispatch action', () => { + actions = hot('-a-|', { a: action }); + + const expected = hot('-a-|', { + a: ResourceActions.loadResourceSuccess({ resource }), + }); + expect(effects.loadByUri$).toBeObservable(expected); + }); + }); + + describe('on failure', () => { + const apiError: ApiError = createApiError(); + const errorResponse: HttpErrorResponse = createHttpErrorResponse(apiError); + + beforeEach(() => { + const errorResponse$ = cold('-#', {}, errorResponse); + resourceRepository.getResource = jest.fn(() => errorResponse$); + }); + + it('should dispatch action', () => { + actions = hot('-a', { a: action }); + + const expected = cold('--c', { + c: ResourceActions.loadResourceFailure({ error: <any>errorResponse }), + }); + expect(effects.loadByUri$).toBeObservable(expected); + }); + }); + }); +}); diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effects.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effects.ts new file mode 100644 index 0000000000000000000000000000000000000000..9811c4f4146c2c7987c606f9f32af79ce41c2a36 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.effects.ts @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { Resource } from '@ngxp/rest'; +import { of } from 'rxjs'; +import { catchError, map, switchMap, tap } from 'rxjs/operators'; +import { ResourceRepository } from '../resource/resource.repository'; +import { ResourceActions, ResourceUriProps } from './actions'; + +export class ResourceEffects<T extends Resource> { + constructor( + private actions$: Actions, + private repository: ResourceRepository, + private resourceActions: ResourceActions, + ) {} + + loadByUri$ = createEffect(() => + this.actions$.pipe( + tap((action) => console.info('Effect triggered for:', action.type)), + ofType(this.resourceActions.loadAction), + tap(() => console.info('Effect execute...')), + switchMap((props: ResourceUriProps) => { + return this.repository.getResource<T>(props.resourceUri).pipe( + map((resource: T) => + this.resourceActions.loadSuccessAction({ + resource, + resourcePath: props.resourcePath, + }), + ), + catchError((error) => + of( + this.resourceActions.loadFailureAction({ + error, + resourcePath: props.resourcePath, + }), + ), + ), + ); + }), + ), + ); +} diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/resource.reducer.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..107f31b41148c569bf80cda9a5c65e390714d4d4 --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.reducer.ts @@ -0,0 +1,66 @@ +import { Action, ActionReducer, createReducer, on } from '@ngrx/store'; +import { + createEmptyStateResource, + createErrorStateResource, + createStateResource, +} from '../resource/resource.util'; +import { LoadResourceFailureProps, LoadResourceSuccessProps, ResourceActions } from './actions'; +import { ResourceState, initialResourceState } from './resource.state'; + +export class ResourceReducer { + public reducer: ActionReducer<any, Action>; + + constructor(private actions: ResourceActions) { + this.init(); + } + + private init(): void { + this.reducer = createReducer( + initialResourceState, + on(this.actions.loadAction, (state: ResourceState): ResourceState => { + console.info('State after LOAD', { + ...state, + resource: { ...state.resource, loading: true, reload: false }, + }); + return { + ...state, + resource: { ...state.resource, loading: true }, + }; + }), + on( + this.actions.loadSuccessAction, + (state: ResourceState, props: LoadResourceSuccessProps): ResourceState => { + console.info('State after SUCCESS', { + ...state, + resource: createStateResource(props.resource), + }); + return { + ...state, + resource: createStateResource(props.resource), + }; + }, + ), + on( + this.actions.loadFailureAction, + (state: ResourceState, props: LoadResourceFailureProps): ResourceState => ({ + ...state, + resource: createErrorStateResource(props.error), + }), + ), + on( + this.actions.clearAction, + (state: ResourceState): ResourceState => ({ + ...state, + resource: createEmptyStateResource(), + }), + ), + on( + this.actions.reloadAction, + (state: ResourceState): ResourceState => ({ + ...state, + resource: { ...state.resource, reload: true }, + }), + ), + ); + } +} diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/resource.selector.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..71fd1d8e323afdf3f03d267a090dec1dc1f3dd7f --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.selector.ts @@ -0,0 +1,56 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import { isUndefined } from 'lodash-es'; +import { StateInfo } from '../resource/resource.model'; +import { ResourceState } from './resource.state'; + +function getState<T>(stateInfo: StateInfo, state: any): ResourceState { + console.info(stateInfo.name + ' - GetState Selector: ', state); + return ( + isUndefined(stateInfo.resourcePath) ? state + : state.hasOwnProperty(stateInfo.resourcePath) ? state[stateInfo.resourcePath] + : state + ); +} + +export const selectResource = <T>(stateInfo: StateInfo) => + createSelector( + createFeatureSelector<ResourceState>(stateInfo.name), + (state: ResourceState) => <T>getState(stateInfo, state).resource, + ); + +export const existResource = <T>(stateInfo: StateInfo) => + createSelector( + createFeatureSelector<ResourceState>(stateInfo.name), + (state: ResourceState) => <T>(<any>getState(stateInfo, state).resource)?.loaded, + ); + +// export class ResourceSelector { +// public featureSelector; +// public resourceSelector; +// public existResourceSelector; +// private stateData: StateInfo; +// constructor(stateData: StateInfo) { +// this.stateData = stateData; +// this.featureSelector = createFeatureSelector(this.stateData.name); +// this.resourceSelector = this.createResourceSelector(); +// this.existResourceSelector = this.createExistResourceSelector(); +// } +// private createResourceSelector() { +// return <T>(stateInfo: StateInfo) => +// createSelector( +// this.featureSelector, +// (state: ResourceState | ResourceSubState) => <T>this.getState(stateInfo, state).resource, +// ); +// } +// private createExistResourceSelector() { +// return <T>(stateInfo: StateInfo) => +// createSelector( +// this.featureSelector, +// (state: ResourceState | ResourceSubState) => +// <T>this.getState(stateInfo, state).resource.loaded, +// ); +// } +// private getState<T>(stateInfo: StateInfo, state: any): ResourceState<T> { +// return isUndefined(stateInfo.resourcePath) ? state : state[stateInfo.resourcePath]; +// } +// } diff --git a/alfa-client/libs/tech-shared/src/lib/ngrx/resource.state.ts b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.state.ts new file mode 100644 index 0000000000000000000000000000000000000000..597f431f8788c5470747e6cb31ea0a6fae19967c --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/ngrx/resource.state.ts @@ -0,0 +1,10 @@ +import { Resource } from '@ngxp/rest'; +import { StateResource, createEmptyStateResource } from '../resource/resource.util'; + +export interface ResourceState { + resource?: StateResource<Resource>; +} + +export const initialResourceState: ResourceState = { + resource: createEmptyStateResource<Resource>(), +}; 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 2f328bcfd4ada2264cfc13c8df28b33a2ac81691..9828e168fe43b50be97f98e4ad1403b2ac141920 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 @@ -1,8 +1,13 @@ +import { HttpErrorResponse } from '@angular/common/http'; import { Resource } from '@ngxp/rest'; -import { Observable } from 'rxjs'; +import { Observable, catchError, map, startWith, switchMap, tap } from 'rxjs'; +import { EffectService } from '../ngrx/effects.service'; +import { ReducerService } from '../ngrx/reducer.service'; +import { HttpError } from '../tech.model'; import { ResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { ResourceService } from './resource.service'; +import { StateResource, createEmptyStateResource, createStateResource } from './resource.util'; export class ApiResourceService<B extends Resource, T extends Resource> extends ResourceService< B, @@ -11,20 +16,41 @@ export class ApiResourceService<B extends Resource, T extends Resource> extends constructor( protected config: ResourceServiceConfig<B>, protected repository: ResourceRepository, + protected reducerService: ReducerService, + protected effectService: EffectService, ) { - super(config, repository); + super(config, reducerService, effectService); } - doSave(resource: T, toSave: unknown): Observable<T> { - return <Observable<T>>this.repository.save({ - resource, - linkRel: this.config.edit.linkRel, - toSave, - }); + public save(toSave: unknown): Observable<StateResource<T | HttpError>> { + return this.getResource().pipe( + switchMap((stateResource: StateResource<T>) => { + console.info('Save stateResource: ', stateResource); + return this.doSave(stateResource.resource, toSave).pipe( + tap((response: StateResource<T>) => { + console.info('Saved Response: ', response); + // this.store.dispatch( + // ResourceActions.loadResourceSuccess({ resource: response.resource }), + // ); + }), + catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)), + ); + }), + startWith(createEmptyStateResource<T | HttpError>(true)), + ); + } + + doSave(resource: T, toSave: unknown): Observable<StateResource<T>> { + return this.repository + .save({ + resource, + linkRel: this.config.edit.linkRel, + toSave, + }) + .pipe(map((value: T) => createStateResource<T>(value))); } public delete(): Observable<Resource> { - this.verifyDeleteLinkRel(); - return this.repository.delete(this.getResource(), this.config.delete.linkRel); + throw new Error('Function not implemented yet'); } } 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..a0d8b5fec5007d6405999dc1d3bf865511277589 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,21 @@ +import { HttpErrorResponse } from '@angular/common/http'; import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; import { isEqual, isNull } from 'lodash-es'; import { BehaviorSubject, Observable, + catchError, combineLatest, filter, first, map, + of, startWith, tap, + throwError, } from 'rxjs'; +import { isUnprocessableEntity } from '../http.util'; +import { HttpError } from '../tech.model'; import { isNotNull, isNotUndefined } from '../tech.util'; import { CreateResourceData, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; @@ -18,6 +24,7 @@ import { ListResource, StateResource, createEmptyStateResource, + createErrorStateResource, createStateResource, getEmbeddedResources, isEmptyStateResource, @@ -195,10 +202,12 @@ export class ResourceListService< } setStateResourceLoading(): void { + console.info('Update State set listResource loading...'); this.listResource.next(createEmptyStateResource(true)); } updateListResource(listResource: T): void { + console.info('Update State listResource: ', listResource); this.listResource.next(createStateResource(listResource)); } @@ -241,4 +250,39 @@ export class ResourceListService< ), ); } + + public saveItem( + itemResource: ListItemResource, + toSave: unknown, + ): Observable<StateResource<any | HttpError>> { + return this.doSave(itemResource, toSave).pipe( + // filter(isLoaded), + tap((response: StateResource<T>) => { + // if (isLoaded(response)) { + console.info('Saved Response: ', response); + // this.loadListResource(this.getListResource(), linkRel); + // this.listResource.next({ ...this.listResource.value, reload: true }); + // } + }), + catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)), + startWith(createEmptyStateResource<T | HttpError>(true)), + ); + } + + handleError(errorResponse: HttpErrorResponse): Observable<StateResource<HttpError>> { + if (isUnprocessableEntity(errorResponse.status)) { + return of(createErrorStateResource((<any>errorResponse) as HttpError)); + } + return throwError(() => errorResponse); + } + + doSave(resource: ListItemResource, toSave: unknown): Observable<StateResource<T>> { + return this.repository + .save({ + resource, + linkRel: 'self', + toSave, + }) + .pipe(map((value: T) => createStateResource<T>(value))); + } } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.model.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.model.ts index 5fa5264f32b11a057af8755641f61d02b001fc46..b0fbf7528b983c5a06eca2a2bb5b5b4841a56dda 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.model.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.model.ts @@ -1,3 +1,4 @@ +import { Action, ActionReducer, Store } from '@ngrx/store'; import { Resource } from '@ngxp/rest'; import { Observable } from 'rxjs'; import { StateResource } from './resource.util'; @@ -7,6 +8,7 @@ export interface ListResourceServiceConfig<B> { listLinkRel: LinkRelationName; listResourceListLinkRel: LinkRelationName; createLinkRel?: LinkRelationName; + saveLinkRel?: LinkRelationName; } export interface CreateResourceData<T> { @@ -24,7 +26,15 @@ export interface SaveResourceData<T> { export interface ListItemResource extends Resource {} export declare type LinkRelationName = string; +export interface StateInfo { + name: string; + store?: Store; + resourcePath?: string; + additionalReducer?: ActionReducer<any, Action>[]; +} + export interface ResourceServiceConfig<B> { + stateInfo?: StateInfo; resource: Observable<StateResource<B>>; getLinkRel: LinkRelationName; delete?: { linkRel: LinkRelationName; order?: string }; diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.itcase.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.itcase.spec.ts index cff64cc4e17979a6d14e59a8f017e1b7cd2c9076..4f93ff1a6e3ac76a971a5b163d5dfc2805b174ea 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.service.itcase.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.service.itcase.spec.ts @@ -9,7 +9,7 @@ import { ResourceRepository } from './resource.repository'; import { DummyResourceService } from './resource.service.spec'; import { StateResource, createEmptyStateResource, createStateResource } from './resource.util'; -describe.skip('FIXME: mocking.ts issue due to module test | ResourceService ITCase', () => { +describe.skip('FIXME: An die Ngrx Umsetzung anpassen | ResourceService ITCase', () => { let service: DummyResourceService<Resource, Resource>; let config: ResourceServiceConfig<Resource>; let repository: Mock<ResourceRepository>; @@ -40,7 +40,8 @@ describe.skip('FIXME: mocking.ts issue due to module test | ResourceService ITCa service = new DummyResourceService<Resource, Resource>(config, useFromMock(repository)); repository.getResource.mockReturnValueOnce(of(loadedResource)); - service.stateResource.next(createEmptyStateResource()); + //TODO Auf Ngrx/Store umstellen + // service.stateResource.next(createEmptyStateResource()); }); describe('get', () => { 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 797f3dd37cff70809fffaee12dd5af9fd8bec528..bd723a6f1a9d1cbd76581273b76e3a36f2e09122 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 @@ -1,31 +1,51 @@ +// import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +// import { HttpErrorResponse } from '@angular/common/http'; +// import { fakeAsync, tick } from '@angular/core/testing'; +// import { faker } from '@faker-js/faker'; +// import { Resource, ResourceUri, getUrl } from '@ngxp/rest'; +// import { cold } from 'jest-marbles'; +// import { Observable, lastValueFrom, of, throwError } from 'rxjs'; +// import { createProblemDetail } from '../../../test//error'; +// import { singleCold, singleHot } from '../../../test/marbles'; +// import { createDummyResource } from '../../../test/resource'; +// import { HttpError, 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 { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { HttpErrorResponse } from '@angular/common/http'; import { fakeAsync, tick } from '@angular/core/testing'; -import { faker } from '@faker-js/faker'; +import faker from '@faker-js/faker'; +import { Store } from '@ngrx/store'; import { Resource, ResourceUri, getUrl } from '@ngxp/rest'; import { cold } from 'jest-marbles'; +import { createHttpErrorResponse, createProblemDetail } from 'libs/tech-shared/test/error'; +import { singleHot } from 'libs/tech-shared/test/marbles'; +import { createDummyResource } from 'libs/tech-shared/test/resource'; import { Observable, lastValueFrom, of, throwError } from 'rxjs'; -import { createProblemDetail } from '../../../test//error'; -import { singleCold, singleHot } from '../../../test/marbles'; -import { createDummyResource } from '../../../test/resource'; import { HttpError, ProblemDetail } from '../tech.model'; -import { LinkRelationName, ResourceServiceConfig } from './resource.model'; +import { LinkRelationName, ResourceServiceConfig, StateInfo } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { ResourceService } from './resource.service'; -import { - StateResource, - createEmptyStateResource, - createErrorStateResource, - createStateResource, -} from './resource.util'; +import { StateResource, createEmptyStateResource, createStateResource } from './resource.util'; +import * as Actions from '../ngrx/actions'; import * as ResourceUtil from './resource.util'; describe('ResourceService', () => { let service: DummyResourceService<Resource, Resource>; + let stateInfo: StateInfo; let config: ResourceServiceConfig<Resource>; let repository: Mock<ResourceRepository>; + let store: Mock<Store>; const configResource: Resource = createDummyResource(); const configStateResource: StateResource<Resource> = createStateResource(configResource); @@ -35,14 +55,19 @@ describe('ResourceService', () => { const getLinkRel: LinkRelationName = 'dummyGetLinkRel'; const deleteLinkRel: LinkRelationName = 'dummyDeleteLinkRel'; + const stateName: string = 'DummyState'; + beforeEach(() => { + repository = mock(ResourceRepository); + store = mock(Store); + stateInfo = { store: useFromMock(store), name: stateName }; config = { + stateInfo, resource: configStateResource$, getLinkRel, edit: { linkRel: editLinkRel }, delete: { linkRel: deleteLinkRel }, }; - repository = mock(ResourceRepository); service = new DummyResourceService(config, useFromMock(repository)); }); @@ -56,15 +81,18 @@ describe('ResourceService', () => { let isInvalidResourceCombinationSpy: jest.SpyInstance; beforeEach(() => { - service.stateResource.next(stateResource); - - service.handleNullConfigResource = jest.fn(); service.handleConfigResourceChanged = jest.fn(); isInvalidResourceCombinationSpy = jest .spyOn(ResourceUtil, 'isInvalidResourceCombination') .mockReturnValue(true); + store.select.mockReturnValue(of(stateResource)); }); + it('should get resource from store', fakeAsync(() => { + service.get().subscribe(); + tick(); + })); + it('should handle config resource changed', fakeAsync(() => { service.get().subscribe(); tick(); @@ -75,13 +103,6 @@ describe('ResourceService', () => { ); })); - it('should handle null configresource', fakeAsync(() => { - service.get().subscribe(); - tick(); - - expect(service.handleNullConfigResource).toHaveBeenCalledWith(configResource); - })); - it('should call isInvalidResourceCombinationSpy', fakeAsync(() => { service.get().subscribe(); tick(); @@ -90,9 +111,7 @@ describe('ResourceService', () => { })); it('should return initial value', () => { - service.stateResource.asObservable = jest - .fn() - .mockReturnValue(singleHot(stateResource, '-a')); + store.select.mockReturnValue(singleHot(stateResource, '-a')); const apiRootStateResource$: Observable<StateResource<Resource>> = service.get(); @@ -115,12 +134,16 @@ describe('ResourceService', () => { expect(service.configResource).toBe(changedConfigResource); }); - it('should set state resource reload', () => { + it('should call update state resource by config resource', () => { + service.updateStateResourceByConfigResource = jest.fn(); service.configResource = createDummyResource(); service.handleConfigResourceChanged(stateResource, changedConfigResource); - expect(service.stateResource.value.reload).toBeTruthy(); + expect(service.updateStateResourceByConfigResource).toHaveBeenCalledWith( + stateResource, + changedConfigResource, + ); }); }); @@ -159,56 +182,77 @@ describe('ResourceService', () => { }); }); - describe('handle null config resource', () => { - const resource: Resource = createDummyResource(); - const stateResource: StateResource<Resource> = createStateResource(resource); - - beforeEach(() => { - service.shouldClearStateResource = jest.fn(); - service.stateResource.next(stateResource); + describe('update state resource by config resource', () => { + it('should TODO', () => { + //TODO }); - it('should call shouldClearStateResource', () => { - service.handleNullConfigResource(null); + // it('should set state resource reload', () => { + // service.configResource = createDummyResource(); - expect(service.shouldClearStateResource).toHaveBeenCalledWith(null); - }); + // service.handleConfigResourceChanged(stateResource, changedConfigResource); - it('should clear stateresource if shouldClearStateResource is true', () => { - service.shouldClearStateResource = jest.fn().mockReturnValue(true); + // expect(service.stateResource.value.reload).toBeTruthy(); + // }); + }); - service.handleNullConfigResource(null); + // describe('handle null config resource', () => { + // const resource: Resource = createDummyResource(); + // const stateResource: StateResource<Resource> = createStateResource(resource); - expect(service.stateResource.value).toEqual(createEmptyStateResource()); - }); + // beforeEach(() => { + // service.shouldClearStateResource = jest.fn(); + // service.stateResource.next(stateResource); + // }); + // it('should call shouldClearStateResource', () => { + // service.handleNullConfigResource(null); - it('should keep stateresource if shouldClearStateResource is false', () => { - service.shouldClearStateResource = jest.fn().mockReturnValue(false); + // expect(service.shouldClearStateResource).toHaveBeenCalledWith(null); + // }); - service.handleNullConfigResource(null); + // it('should clear stateresource if shouldClearStateResource is true', () => { + // service.shouldClearStateResource = jest.fn().mockReturnValue(true); - expect(service.stateResource.value).toBe(stateResource); - }); - }); + // service.handleNullConfigResource(null); + + // expect(service.stateResource.value).toEqual(createEmptyStateResource()); + // }); + + // it('should keep stateresource if shouldClearStateResource is false', () => { + // service.shouldClearStateResource = jest.fn().mockReturnValue(false); + + // service.handleNullConfigResource(null); + + // expect(service.stateResource.value).toBe(stateResource); + // }); + // }); describe('should clear stateresource', () => { const resource: Resource = createDummyResource(); const stateResource: StateResource<Resource> = createStateResource(resource); it('should return true on null configresource and filled stateresource', () => { - service.stateResource.next(stateResource); - - const shouldClear: boolean = service.shouldClearStateResource(null); + const shouldClear: boolean = service.shouldClearStateResource(stateResource, null); expect(shouldClear).toBeTruthy(); }); it('should return false on null configresource and empty stateresource', () => { - service.stateResource.next(createEmptyStateResource()); - - const shouldClear: boolean = service.shouldClearStateResource(null); + const shouldClear: boolean = service.shouldClearStateResource( + createEmptyStateResource(), + null, + ); expect(shouldClear).toBeFalsy(); }); + + it('should return true on configresource without get link and filled stateresource', () => { + const shouldClear: boolean = service.shouldClearStateResource( + stateResource, + createDummyResource(), + ); + + expect(shouldClear).toBeTruthy(); + }); }); describe('should load resource', () => { @@ -247,12 +291,6 @@ describe('ResourceService', () => { describe('load resource', () => { const configResourceWithGetLinkRel: Resource = createDummyResource([getLinkRel]); - it('should throw error if getLinkRel not exists', () => { - expect(() => service.loadResource(configResource)).toThrowError( - 'No get link exists on configresource.', - ); - }); - it('should call do load resource', () => { service.doLoadResource = jest.fn(); @@ -264,53 +302,21 @@ describe('ResourceService', () => { }); }); - describe('set state resource loading', () => { - it('should set loading true', () => { - service.stateResource.next(createStateResource(createDummyResource())); - - service.setStateResourceLoading(); - - expect(service.stateResource.value.loading).toBeTruthy(); - }); - - it('should set reload false', () => { - service.stateResource.next({ ...createStateResource(createDummyResource()), reload: true }); - - service.setStateResourceLoading(); - - expect(service.stateResource.value.reload).toBeFalsy(); - }); - }); - describe('do load resource', () => { - let resourceUri: ResourceUri; - let loadedResource: Resource; + const resourceUri: ResourceUri = faker.internet.url(); + // let loadedResource: Resource; beforeEach(() => { - service.setStateResourceLoading = jest.fn(); - resourceUri = faker.internet.url(); - loadedResource = createDummyResource(); - repository.getResource.mockReturnValue(of(loadedResource)); - }); - - it('should set state resource to loading', () => { - service.doLoadResource(resourceUri); - - expect(service.setStateResourceLoading).toHaveBeenCalled(); + // service.setStateResourceLoading = jest.fn(); + // resourceUri = faker.internet.url(); + // loadedResource = createDummyResource(); + // repository.getResource.mockReturnValue(of(loadedResource)); }); - it('should call repository', () => { + it('should dispatch action', () => { service.doLoadResource(resourceUri); - expect(repository.getResource).toHaveBeenCalledWith(resourceUri); - }); - - it('should update stateresource', () => { - service.updateStateResource = jest.fn(); - - service.doLoadResource(resourceUri); - - expect(service.updateStateResource).toHaveBeenCalledWith(loadedResource); + expect(store.dispatch).toHaveBeenCalledWith(Actions.loadResource({ resourceUri })); }); }); @@ -325,57 +331,42 @@ describe('ResourceService', () => { }); }); - describe('update stateresource', () => { - const resourceToBeSet: Resource = createDummyResource(); - - it('should set resource as stateresource', () => { - service.stateResource.next(createEmptyStateResource()); - - service.updateStateResource(resourceToBeSet); - - expect(service.stateResource.value).toEqual(createStateResource(resourceToBeSet)); - }); - }); - describe('save', () => { const dummyToSave: unknown = {}; const loadedResource: Resource = createDummyResource(); + const stateResource: StateResource<Resource> = createStateResource(loadedResource); - const resourceWithEditLinkRel: Resource = createDummyResource([editLinkRel]); + beforeEach(() => { + store.select.mockReturnValue(of(stateResource)); + }); - it('should throw error if edit link not exists', () => { - service.stateResource.next(createStateResource(createDummyResource())); + it('should get resource from store', fakeAsync(() => { + service.save(dummyToSave).subscribe(); + tick(); - expect(() => service.save(dummyToSave)).toThrowError( - 'No edit link exists on current stateresource.', - ); - }); + expect(store.select).toHaveBeenCalled(); + })); 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(stateResource)); service.save(dummyToSave).subscribe(); tick(); - expect(doSaveMock).toHaveBeenCalledWith(resourceWithEditLinkRel, dummyToSave); + expect(doSaveMock).toHaveBeenCalledWith(loadedResource, dummyToSave); })); - it('should return saved object', () => { - service.stateResource.next(createStateResource(resourceWithEditLinkRel)); - service.doSave = jest.fn().mockReturnValue(singleHot(loadedResource)); + // it('should return saved object', () => { + // service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + // service.doSave = jest.fn().mockReturnValue(singleHot(loadedResource)); - const saved: Observable<StateResource<Resource | HttpError>> = service.save(dummyToSave); + // const saved: Observable<StateResource<Resource | HttpError>> = service.save(dummyToSave); - expect(saved).toBeObservable(singleCold(createStateResource(loadedResource))); - }); + // expect(saved).toBeObservable(singleCold(createStateResource(loadedResource))); + // }); it('should call handleError', () => { - service.stateResource.next(createStateResource(createDummyResource([config.edit.linkRel]))); - const errorResponse: ProblemDetail = createProblemDetail(); + const errorResponse: HttpErrorResponse = createHttpErrorResponse(); service.doSave = jest.fn().mockReturnValue(throwError(() => errorResponse)); service.handleError = jest.fn(); @@ -384,15 +375,36 @@ describe('ResourceService', () => { expect(service.handleError).toHaveBeenCalledWith(errorResponse); }); - it('should update state resource subject', fakeAsync(() => { - service.stateResource.next(createStateResource(resourceWithEditLinkRel)); - service.doSave = jest.fn().mockReturnValue(of(loadedResource)); + // it('should update state resource subject', fakeAsync(() => { + // service.stateResource.next(createStateResource(resourceWithEditLinkRel)); + // service.doSave = jest.fn().mockReturnValue(of(loadedResource)); - service.save(dummyToSave).subscribe(); - tick(); + // service.save(dummyToSave).subscribe(); + // tick(); - expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); - })); + // expect(service.stateResource.value).toEqual(createStateResource(loadedResource)); + // })); + }); + + describe('get resource', () => { + const stateResource: StateResource<Resource> = createStateResource(createDummyResource()); + + beforeEach(() => { + store.select.mockReturnValue(of(stateResource)); + }); + + it('should select from store', (done) => { + service.getResource().subscribe(() => { + expect(store.select).toHaveBeenCalled(); + done(); + }); + }); + it('should return state value', (done) => { + service.getResource().subscribe((response) => { + expect(response).toBe(stateResource); + done(); + }); + }); }); describe('handleError', () => { @@ -402,7 +414,7 @@ describe('ResourceService', () => { service .handleError(<HttpErrorResponse>(<any>error)) .subscribe((responseError: StateResource<HttpError>) => { - expect(responseError).toEqual(createErrorStateResource(error)); + expect(responseError).toEqual(ResourceUtil.createErrorStateResource(error)); done(); }); }); @@ -425,75 +437,25 @@ describe('ResourceService', () => { service.loadResource = jest.fn(); }); - it('should set reload true on statresource', () => { - service.stateResource.next(createStateResource(createDummyResource())); - + it('should dispatch reload action', () => { service.refresh(); - expect(service.stateResource.value.reload).toBeTruthy(); + expect(store.dispatch(Actions.reloadResource())); }); }); - describe('can edit', () => { - it('should return true if link is present', () => { - const resource: StateResource<Resource> = createStateResource( - createDummyResource([editLinkRel]), - ); - service.stateResource.next(resource); - - const canEdit: boolean = service.canEdit(); - - expect(canEdit).toBeTruthy(); - }); - - it('should return false if link is NOT present', () => { - const resource: StateResource<Resource> = createStateResource(createDummyResource()); - service.stateResource.next(resource); - - const canEdit: boolean = service.canEdit(); - - expect(canEdit).toBeFalsy(); - }); - }); - - describe('can delete', () => { - it('should return true if link is present', () => { - const resource: StateResource<Resource> = createStateResource( - createDummyResource([deleteLinkRel]), - ); - service.stateResource.next(resource); - - const canEdit: boolean = service.canDelete(); - - expect(canEdit).toBeTruthy(); - }); - - it('should return false if link is NOT present', () => { - const resource: StateResource<Resource> = createStateResource(createDummyResource()); - service.stateResource.next(resource); - - const canEdit: boolean = service.canDelete(); - - expect(canEdit).toBeFalsy(); + describe('exists', () => { + beforeEach(() => { + store.select.mockReturnValue(of(true)); }); - }); - - describe('get resource', () => { - it('should return resource from stateResource', () => { - const resource: Resource = createDummyResource(); - const stateResource: StateResource<Resource> = createStateResource(resource); - service.stateResource.next(stateResource); - const result: Resource = service.getResource(); - - expect(result).toBe(resource); + it('should select from store', (done) => { + service.exists().subscribe(() => { + expect(store.select).toHaveBeenCalled(); + done(); + }); }); - }); - - describe('exists', () => { it('should return true', (done) => { - service.updateStateResource(createDummyResource()); - service.exists().subscribe((response) => { expect(response).toBeTruthy(); done(); @@ -501,7 +463,7 @@ describe('ResourceService', () => { }); it('should return false', (done) => { - service.updateStateResource(null); + store.select.mockReturnValue(of(false)); service.exists().subscribe((response) => { expect(response).toBeFalsy(); @@ -522,7 +484,8 @@ export class DummyResourceService<B extends Resource, T extends Resource> extend super(config, repository); } - doSave(resource: T, toSave: unknown): Observable<T> { - return of(resource); + doSave(resource: T, toSave: unknown): Observable<StateResource<T>> { + return of(createStateResource(resource)); + // throw new Error('Method not implemented.'); } } 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 dec67942889bebdabac69a7b09ce67c20f0f8db0..692f810cfcbe3f783f85ca930a1f2bb79ba8a257 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,59 +1,68 @@ import { HttpErrorResponse } from '@angular/common/http'; -import { getUrl, hasLink, Resource, ResourceUri } from '@ngxp/rest'; +import { Store } from '@ngrx/store'; +import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; import { isEqual, isNull } from 'lodash-es'; -import { - BehaviorSubject, - catchError, - combineLatest, - filter, - first, - map, - Observable, - of, - startWith, - tap, - throwError, -} from 'rxjs'; +import { Observable, combineLatest, filter, of, startWith, tap, throwError } from 'rxjs'; import { isUnprocessableEntity } from '../http.util'; +import { ResourceActions, createResourceAction } from '../ngrx/actions'; +import { EffectService } from '../ngrx/effects.service'; +import { ReducerService } from '../ngrx/reducer.service'; import { HttpError } from '../tech.model'; import { isNotNull } from '../tech.util'; import { ResourceServiceConfig } from './resource.model'; -import { ResourceRepository } from './resource.repository'; import { mapToFirst, mapToResource } from './resource.rxjs.operator'; import { + StateResource, createEmptyStateResource, createErrorStateResource, - createStateResource, isInvalidResourceCombination, isLoadingRequired, - StateResource, - throwErrorOn, + isStateResoureStable, } from './resource.util'; +import * as ResourceSelectors from '../ngrx/resource.selector'; + /** * B = Type of baseresource * 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(), - ); + configResource: B = null; - configResource: B; + resourceActions: ResourceActions; constructor( protected config: ResourceServiceConfig<B>, - protected repository: ResourceRepository, - ) {} + protected reducerService: ReducerService, + protected effectService: EffectService, + ) { + this.initActions(); + this.initReducer(); + this.addEffect(); + } + + initActions() { + this.resourceActions = createResourceAction(this.config.stateInfo.name); + } + + private addEffect(): void { + console.info('add effects for: ', this.config.stateInfo.name); + this.effectService.addEffects(this.resourceActions); + } + + private initReducer(): void { + console.info('add reducer for: ', this.config.stateInfo.name); + this.reducerService.addFeatureReducer(this.config.stateInfo, this.resourceActions); + } public get(): Observable<StateResource<T>> { - return combineLatest([this.stateResource.asObservable(), this.getConfigResource()]).pipe( + return combineLatest([this.getResource(), this.getConfigResource()]).pipe( tap(([stateResource, configResource]) => - this.handleConfigResourceChanged(stateResource, configResource), + this.handleConfigResourceChanged(<StateResource<T>>stateResource, configResource), ), - tap(([, configResource]) => this.handleNullConfigResource(configResource)), filter( - ([stateResource]) => !isInvalidResourceCombination(stateResource, this.configResource), + ([stateResource, configResource]) => + !isInvalidResourceCombination(<StateResource<T>>stateResource, configResource), ), mapToFirst<T, B>(), startWith(createEmptyStateResource<T>(true)), @@ -61,81 +70,90 @@ 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, - ), - mapToResource<B>(), - ); + return this.config.resource.pipe(filter(isStateResoureStable), mapToResource<B>()); } handleConfigResourceChanged(stateResource: StateResource<T>, configResource: B): void { if (!isEqual(this.configResource, configResource)) { this.configResource = configResource; - this.stateResource.next({ ...this.stateResource.value, reload: true }); + this.updateStateResourceByConfigResource(stateResource, configResource); } else if (this.shouldLoadResource(stateResource, configResource)) { this.loadResource(configResource); } } - shouldLoadResource(stateResource: StateResource<T>, configResource: B): boolean { - return isNotNull(configResource) && isLoadingRequired(stateResource); + //TDD + updateStateResourceByConfigResource(stateResource: StateResource<T>, configResource: B): void { + if (!isStateResoureStable(stateResource)) { + return; + } + if (this.shouldClearStateResource(stateResource, configResource)) { + this.clearStateResource(); + } else if (isNotNull(configResource) && hasLink(configResource, this.config.getLinkRel)) { + this.loadResource(configResource); + } } - loadResource(configResource: B): void { - this.verifyGetLink(configResource); - this.doLoadResource(getUrl(configResource, this.config.getLinkRel)); + clearStateResource(): void { + this.store.dispatch(this.resourceActions.clearAction()); } + // - private verifyGetLink(configResource: B): void { - throwErrorOn( - !hasLink(configResource, this.config.getLinkRel), - 'No get link exists on configresource.', + shouldClearStateResource(stateResource: StateResource<T>, configResource: B): boolean { + return ( + (isNull(configResource) || this.hasNotGetLink(configResource)) && + !this.isStateResourceEmpty(stateResource) ); } - //TODO rename to reloadByResourceUri - public setResourceByUri(resourceUri: ResourceUri): void { - this.doLoadResource(resourceUri); + private hasNotGetLink(configResource: B) { + return isNotNull(configResource) && !hasLink(configResource, this.config.getLinkRel); } - doLoadResource(resourceUri: ResourceUri): void { - this.setStateResourceLoading(); - this.repository - .getResource(resourceUri) - .pipe(first()) - .subscribe((loadedResource: T) => this.updateStateResource(loadedResource)); + private isStateResourceEmpty(stateResource: StateResource<T>): boolean { + return isEqual(stateResource, createEmptyStateResource()); } - setStateResourceLoading(): void { - this.stateResource.next({ ...createEmptyStateResource(true), reload: false }); + shouldLoadResource(stateResource: StateResource<T>, configResource: B): boolean { + return ( + isNotNull(configResource) && + hasLink(configResource, this.config.getLinkRel) && + isLoadingRequired(stateResource) + ); } - updateStateResource(resource: T): void { - this.stateResource.next(createStateResource(resource)); + loadResource(configResource: B): void { + this.doLoadResource(this.getGetUrl(configResource)); } - handleNullConfigResource(configResource: B): void { - if (this.shouldClearStateResource(configResource)) { - this.stateResource.next(createEmptyStateResource()); - } + private getGetUrl(configResource: B): string { + return getUrl(configResource, this.config.getLinkRel); } - shouldClearStateResource(configResource: B): boolean { - return isNull(configResource) && !isEqual(this.stateResource.value, createEmptyStateResource()); + //TODO rename to reloadByResourceUri + public setResourceByUri(resourceUri: ResourceUri): void { + this.doLoadResource(resourceUri); } - public save(toSave: unknown): Observable<StateResource<T | HttpError>> { - this.verifyEditLinkRel(); - const previousResource: T = this.stateResource.value.resource; - return this.doSave(previousResource, toSave).pipe( - tap((loadedResource: T) => this.stateResource.next(createStateResource(loadedResource))), - map(() => this.stateResource.value), - catchError((errorResponse: HttpErrorResponse) => this.handleError(errorResponse)), + doLoadResource(resourceUri: ResourceUri): void { + // const action = Actions.loadResource({ + // resourceUri, + // resourcePath: this.config.stateInfo.resourcePath, + // }); + this.store.dispatch( + this.resourceActions.loadAction({ + resourceUri, + resourcePath: this.config.stateInfo.resourcePath, + }), + // Actions.loadResource({ resourceUri, resourcePath: this.config.stateInfo.resourcePath }), ); } + getResource(): Observable<StateResource<T>> { + // return this.store.select(this.selector.resourceSelector); + return this.store.select(ResourceSelectors.selectResource(this.config.stateInfo)); + } + handleError(errorResponse: HttpErrorResponse): Observable<StateResource<HttpError>> { if (isUnprocessableEntity(errorResponse.status)) { return of(createErrorStateResource((<any>errorResponse) as HttpError)); @@ -143,51 +161,19 @@ export abstract class ResourceService<B extends Resource, T extends Resource> { return throwError(() => errorResponse); } - public verifyEditLinkRel(): void { - throwErrorOn( - !this.hasLinkRel(this.config.edit.linkRel), - 'No edit link exists on current stateresource.', - ); - } - - abstract doSave(resource: T, toSave: unknown): Observable<T>; + // abstract doSave(resource: T, toSave: unknown): Observable<StateResource<T>>; public refresh(): void { - this.stateResource.next({ ...this.stateResource.value, reload: true }); - } - - /** - * @deprecated - */ - public canEdit(): boolean { - return this.hasLinkRel(this.config.edit.linkRel); + this.store.dispatch(this.resourceActions.reloadAction()); + // this.store.dispatch(ResourceActions.reloadResource()); } - protected hasLinkRel(linkRel: string): boolean { - return hasLink(this.getResource(), linkRel); - } - - /** - * @deprecated - */ - public getResource(): T { - return this.stateResource.value.resource; - } - - /** - * @deprecated - */ - public canDelete(): boolean { - return this.hasLinkRel(this.config.delete.linkRel); - } - - protected verifyDeleteLinkRel(): void { - throwErrorOn(!this.canDelete(), 'No delete link exists on current stateresource.'); + public exists(): Observable<boolean> { + // return this.store.select(this.selector.existResourceSelector); + return this.store.select(ResourceSelectors.existResource(this.config.stateInfo)); } - public exists(): Observable<boolean> { - return this.stateResource - .asObservable() - .pipe(map((stateResource: StateResource<T>) => stateResource.loaded)); + private get store(): Store { + return this.config.stateInfo.store; } } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts index deaa71132a606f656d62efd87deded4ba3e71f2b..a93b99306d00bb44db800c5a537fcde7355f5e50 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.ts @@ -24,7 +24,7 @@ import { Resource, ResourceUri, getEmbeddedResource, getUrl, hasLink } from '@ngxp/rest'; import { isEqual, isNil, isNull } from 'lodash-es'; import { HttpError } from '../tech.model'; -import { encodeUrlForEmbedding, isNotNull } from '../tech.util'; +import { EMPTY_ARRAY, encodeUrlForEmbedding, isNotNull } from '../tech.util'; export interface ListResource extends Resource { _embedded; @@ -94,7 +94,7 @@ export function getEmbeddedResources<T>( linkRel: string, ): T[] { if (isNil(stateResource) || isNil(stateResource.resource)) { - return []; + return EMPTY_ARRAY; } return getEmbeddedResource<T[]>(stateResource.resource, linkRel); }