diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail-error.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail-error.cy.ts index 7248a1727b40c6ad4ae2d5165660c7bda3b2394c..7a882428f3725ae4b096f258426f040d10098d55 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail-error.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail-error.cy.ts @@ -21,9 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { E2EAppHelper } from 'apps/alfa-e2e/src/helper/app.helper'; import { E2EPostfachNachrichtVerifier } from 'apps/alfa-e2e/src/helper/postfach-nachricht/postfach-nachricht.verifier'; -import { E2EVorgangNavigator } from 'apps/alfa-e2e/src/helper/vorgang/vorgang.navigator'; +import { APP_HELPER, VORGANG_NAVIGATOR } from 'apps/alfa-e2e/src/support/e2e'; import { createPostfachNachrichtAttachedItem, createPostfachNachrichtReplyItem, @@ -42,7 +41,7 @@ import { PostfachNachrichtSnackbarMessageE2E, VorgangAttachedItemE2E, } from '../../../model/vorgang-attached-item'; -import { containsSpinner, MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; +import { containsSpinner, MainPage, notContainsSpinner, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; import { PostfachMailPage } from '../../../page-objects/postfach-mail.component.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; import { contains, exist, notExist } from '../../../support/cypress.util'; @@ -51,9 +50,6 @@ import { initVorgangAttachedItem } from '../../../support/vorgang-attached-item- import { createVorgang, initVorgang, objectIds } from '../../../support/vorgang-util'; describe('PostfachMail error', () => { - const appHelper: E2EAppHelper = new E2EAppHelper(); - const vorgangNavigator: E2EVorgangNavigator = new E2EVorgangNavigator(); - const mainPage: MainPage = new MainPage(); const snackbar: SnackBarE2EComponent = mainPage.getSnackBar(); @@ -94,12 +90,12 @@ describe('PostfachMail error', () => { initVorgang(vorgang); initVorgangAttachedItem([vorgangAttachedItem, vorgangAttachedItem2]); - appHelper.loginAsSabine(); + APP_HELPER.loginAsSabine(); }); describe('navigate to vorgang detail', () => { it('should open vorgang detail', () => { - vorgangNavigator.openVorgang(vorgang.name); + VORGANG_NAVIGATOR.openVorgang(vorgang.name); waitForSpinnerToDisappear(); }); @@ -131,6 +127,7 @@ describe('PostfachMail error', () => { it('click on resend button should hide error', () => { listItem.getResendButton().click(); containsSpinner(listItem.getResendButton()); + notContainsSpinner(postfachMailPage.getListItem(postfachNachricht2.subject).getResendButton()); waitForSpinnerToDisappear(); notExist(listItem.getSendErrorIcon()); diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-pending-send.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-pending-send.cy.ts index d2c1d45bf3f18d1c4712b357719aa7c46dd05da2..ffcd5734aabf4f08ca1fe3f15ae19f8eef9972d2 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-pending-send.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-pending-send.cy.ts @@ -1,28 +1,31 @@ import { PostfachMailE2EComponent } from 'apps/alfa-e2e/src/components/postfach/postfach-mail.e2e.component'; -import { E2EAppHelper } from 'apps/alfa-e2e/src/helper/app.helper'; -import { E2EVorgangNavigator } from 'apps/alfa-e2e/src/helper/vorgang/vorgang.navigator'; import { CommandE2E, CommandOrderE2E, CommandStatusE2E } from 'apps/alfa-e2e/src/model/command'; -import { createCommand, initCommand } from 'apps/alfa-e2e/src/support/command-util'; +import { createCommand, initCommands } from 'apps/alfa-e2e/src/support/command-util'; +import { APP_HELPER, VORGANG_NAVIGATOR } from 'apps/alfa-e2e/src/support/e2e'; import { createPostfachNachrichtAttachedItem, + createPostfachNachrichtReplyItem, createSendPostfachNachrichtItem, } from 'apps/alfa-e2e/src/support/postfach-nachricht.util'; +import { getUserSabineId } from 'apps/alfa-e2e/src/support/user-util'; import { initVorgangAttachedItem } from 'apps/alfa-e2e/src/support/vorgang-attached-item-util'; import { VorgangE2E } from '../../../model/vorgang'; -import { PostfachMailItemE2E, VorgangAttachedItemE2E } from '../../../model/vorgang-attached-item'; +import { + DirectionE2E, + PostfachMailItemE2E, + PostfachNachrichtMessageCodeE2E, + VorgangAttachedItemE2E, +} from '../../../model/vorgang-attached-item'; import { containsSpinner } from '../../../page-objects/main.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; import { exist } from '../../../support/cypress.util'; -import { createVorgang, initVorgang, objectIds } from '../../../support/vorgang-util'; - -describe('Postfach Nachricht pending send', () => { - const appHelper: E2EAppHelper = new E2EAppHelper(); - - const vorgangNavigator: E2EVorgangNavigator = new E2EVorgangNavigator(); +import { buildVorgang, createVorgang, initVorgaenge, objectIds } from '../../../support/vorgang-util'; +describe('Postfach Nachricht pending', () => { const vorgangPage: VorgangPage = new VorgangPage(); const postfachMailContainer: PostfachMailE2EComponent = vorgangPage.getPostfachMailcontainer(); + //SEND const vorgangWithPendingSendCommand: VorgangE2E = { ...createVorgang(), name: 'VorgangWithPendingSendCommand' }; const postfachSendNachrichtItem: PostfachMailItemE2E = { ...createSendPostfachNachrichtItem(), @@ -40,17 +43,63 @@ describe('Postfach Nachricht pending send', () => { body: postfachSendNachrichtItem, }; + //RESEND 1 + const vorgangWithResendCommand: VorgangE2E = buildVorgang(objectIds[1], 'VorgangWithResendCommand'); + const resendPostfachNachricht1: PostfachMailItemE2E = { + ...createPostfachNachrichtReplyItem(), + subject: 'Failed Outgoing Postfach Nachricht 1', + createdBy: getUserSabineId(), + direction: DirectionE2E.OUT, + sentAt: '2022-12-02T15:00:00.790Z[UTC]', + sentSuccessful: false, + messageCode: PostfachNachrichtMessageCodeE2E.PROCESSING_FAILED, + }; + const resendPostfachNachrichtVorgangAttachedItem1: VorgangAttachedItemE2E = { + ...createPostfachNachrichtAttachedItem(objectIds[2], vorgangWithResendCommand._id.$oid), + item: resendPostfachNachricht1, + }; + const pendingResendCommand1: CommandE2E = { + ...createCommand(), + _id: { $oid: objectIds[1] }, + vorgangId: vorgangWithResendCommand._id.$oid, + order: CommandOrderE2E.RESEND_POSTFACH_NACHRICHT, + status: CommandStatusE2E.PENDING, + finishedAt: null, + body: resendPostfachNachricht1, + relationId: resendPostfachNachrichtVorgangAttachedItem1._id.$oid, + }; + + //RESEND 2 + const resendPostfachNachricht2: PostfachMailItemE2E = { + ...resendPostfachNachricht1, + subject: 'Failed Outgoing Postfach Nachricht 2', + }; + const resendPostfachNachrichtVorgangAttachedItem2: VorgangAttachedItemE2E = { + ...createPostfachNachrichtAttachedItem(objectIds[3], vorgangWithResendCommand._id.$oid), + item: resendPostfachNachricht2, + }; + const pendingResendCommand2: CommandE2E = { + ...pendingResendCommand1, + _id: { $oid: objectIds[2] }, + body: resendPostfachNachricht2, + relationId: resendPostfachNachrichtVorgangAttachedItem2._id.$oid, + }; + before(() => { - initVorgang(vorgangWithPendingSendCommand); - initVorgangAttachedItem([postfachNachrichtAttachedItem]); - initCommand(pendingSendCommand); + initVorgaenge([vorgangWithPendingSendCommand, vorgangWithResendCommand]); + initVorgangAttachedItem([ + postfachNachrichtAttachedItem, + resendPostfachNachrichtVorgangAttachedItem1, + resendPostfachNachrichtVorgangAttachedItem2, + ]); + initCommands([pendingSendCommand, pendingResendCommand1, pendingResendCommand2]); - appHelper.loginAsSabine(); + APP_HELPER.loginAsSabine(); }); - describe('area in vorgang detail', () => { + describe('send command on area in vorgang detail', () => { it('should have item in list', () => { - vorgangNavigator.openVorgang(vorgangWithPendingSendCommand.name); + VORGANG_NAVIGATOR.openVorgang(vorgangWithPendingSendCommand.name); exist(postfachMailContainer.getListItem(postfachSendNachrichtItem.subject).getRoot()); }); @@ -64,4 +113,18 @@ describe('Postfach Nachricht pending send', () => { exist(postfachMailContainer.getListItem(postfachSendNachrichtItem.subject).getEditButton()); }); }); + + describe('resend commands area in vorgang detail', () => { + it('should have items in list', () => { + VORGANG_NAVIGATOR.openVorgang(vorgangWithResendCommand.name); + + exist(postfachMailContainer.getListItem(resendPostfachNachricht1.subject).getRoot()); + exist(postfachMailContainer.getListItem(resendPostfachNachricht2.subject).getRoot()); + }); + + it('should show spinner on buttons', () => { + containsSpinner(postfachMailContainer.getListItem(resendPostfachNachricht1.subject).getResendButton()); + containsSpinner(postfachMailContainer.getListItem(resendPostfachNachricht2.subject).getResendButton()); + }); + }); }); diff --git a/alfa-client/apps/alfa-e2e/src/page-objects/main.po.ts b/alfa-client/apps/alfa-e2e/src/page-objects/main.po.ts index d47b4e62dc617d1f1035616d60fdfabda5c31f28..69da85ac9291080b631d691d73a81c43dd9859be 100644 --- a/alfa-client/apps/alfa-e2e/src/page-objects/main.po.ts +++ b/alfa-client/apps/alfa-e2e/src/page-objects/main.po.ts @@ -111,6 +111,10 @@ export function containsSpinner(rootElement: Cypress.Chainable<JQuery<Element>>) exist(rootElement.findTestElementWithClass(MainPage.SPINNER)); } +export function notContainsSpinner(rootElement: Cypress.Chainable<JQuery<Element>>): void { + notExist(rootElement.findTestElementWithClass(MainPage.SPINNER)); +} + //TODO Funktion loeschen export function waitforSpinnerToAppear(): void { // exist(cy.getTestElementWithClass('spinner')); diff --git a/alfa-client/apps/alfa-e2e/src/support/e2e.ts b/alfa-client/apps/alfa-e2e/src/support/e2e.ts index 6334e8ea63eb9e62470295a521b509af2cd38df8..0d31b33108885bdbc1cc4de347681850f8c5a0fb 100644 --- a/alfa-client/apps/alfa-e2e/src/support/e2e.ts +++ b/alfa-client/apps/alfa-e2e/src/support/e2e.ts @@ -40,6 +40,8 @@ import 'cypress-mochawesome-reporter/register'; import 'cypress-real-events'; import 'cypress-timestamps/support'; +import { E2EAppHelper } from '../helper/app.helper'; +import { E2EVorgangNavigator } from '../helper/vorgang/vorgang.navigator'; import './commands'; import { dropCollections } from './cypress-helper'; import './file-upload'; @@ -73,3 +75,7 @@ after(() => { dropCollections(); cy.log('...cleanup done.'); }); + +export const APP_HELPER: E2EAppHelper = new E2EAppHelper(); + +export const VORGANG_NAVIGATOR: E2EVorgangNavigator = new E2EVorgangNavigator(); diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.actions.ts b/alfa-client/libs/command-shared/src/lib/+state/command.actions.ts index e573a10f4afe9c28cc3a67fc76f30e4fade26b89..e899a6df087b97784e3784274dc5147a9d4998ae 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.actions.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.actions.ts @@ -29,18 +29,11 @@ import { TypedActionCreatorWithProps, } from '@alfa-client/tech-shared'; import { HttpErrorResponse } from '@angular/common/http'; -import { createAction, props, Action } from '@ngrx/store'; -import { Resource } from '@ngxp/rest'; -import { - CommandListResource, - CommandResource, - CreateCommand, - CreateCommandProps, -} from '../command.model'; +import { Action, createAction, props } from '@ngrx/store'; +import { Resource, ResourceUri } from '@ngxp/rest'; +import { CommandListResource, CommandResource, CreateCommand, CreateCommandProps } from '../command.model'; -export const publishConcurrentModificationAction: TypedActionCreator = createAction( - '[Command/API] Concurrent Modification', -); +export const publishConcurrentModificationAction: TypedActionCreator = createAction('[Command/API] Concurrent Modification'); export interface CommandStateResourceProps { commandStateResource: StateResource<CommandResource>; @@ -52,9 +45,7 @@ export interface LoadCommandListSuccessProps { export interface LoadCommandListProps { resource: Resource; linkRel: string; - successAction: ( - commandList: CommandListResource, - ) => LoadCommandListSuccessProps & Action<string>; + successAction: (commandList: CommandListResource) => LoadCommandListSuccessProps & Action<string>; failureAction: (apiError: ApiError) => ApiErrorAction & Action<string>; } @@ -63,6 +54,11 @@ export const loadCommandList: TypedActionCreatorWithProps<LoadCommandListProps> props<LoadCommandListProps>(), ); +export interface UpdateCommandProps { + relatedResourceUri: ResourceUri; + command: CommandResource; +} + export interface CommandProps { command: CommandResource; } @@ -82,7 +78,13 @@ export interface RevokeCommandFailureProps { error: ApiError | unknown; } +export interface CreateCommandSuccessProps { + createCommandProps: CreateCommandProps; + command: CommandResource; +} + export interface CreateCommandFailureProps { + createCommandProps: CreateCommandProps; command: CreateCommand; error: ApiError | HttpErrorResponse | unknown; } @@ -91,12 +93,14 @@ export const createCommand: TypedActionCreatorWithProps<CreateCommandProps> = cr '[Command] Create command', props<CreateCommandProps>(), ); -export const createCommandSuccess: TypedActionCreatorWithProps<CommandProps> = createAction( +export const createCommandSuccess: TypedActionCreatorWithProps<CreateCommandSuccessProps> = createAction( '[Command] Create command Success', - props<CommandProps>(), + props<CreateCommandSuccessProps>(), +); +export const createCommandFailure: TypedActionCreatorWithProps<CreateCommandFailureProps> = createAction( + '[Command] Create command Failure', + props<CreateCommandFailureProps>(), ); -export const createCommandFailure: TypedActionCreatorWithProps<CreateCommandFailureProps> = - createAction('[Command] Create command Failure', props<CreateCommandFailureProps>()); export const pollCreatedCommand: TypedActionCreatorWithProps<PollCommandProps> = createAction( '[Command] Poll created command', props<PollCommandProps>(), @@ -119,8 +123,10 @@ export const revokeCommandSuccess: TypedActionCreatorWithProps<CommandProps> = c '[Command] Revoke command Success', props<CommandProps>(), ); -export const revokeCommandFailure: TypedActionCreatorWithProps<RevokeCommandFailureProps> = - createAction('[Command] Revoke command Failure', props<RevokeCommandFailureProps>()); +export const revokeCommandFailure: TypedActionCreatorWithProps<RevokeCommandFailureProps> = createAction( + '[Command] Revoke command Failure', + props<RevokeCommandFailureProps>(), +); export const pollRevokedCommand: TypedActionCreatorWithProps<PollCommandProps> = createAction( '[Command] Poll revoked command', props<PollCommandProps>(), diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.effects.spec.ts b/alfa-client/libs/command-shared/src/lib/+state/command.effects.spec.ts index b1cb1673a7a835539822c4475ea48db445c31e58..1b2da8085396e38292373dfb70520bd7b42dde77 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.effects.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.effects.spec.ts @@ -21,12 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - ApiError, - ApiErrorAction, - EMPTY_STRING, - TypedActionCreatorWithProps, -} from '@alfa-client/tech-shared'; +import { ApiError, ApiErrorAction, EMPTY_STRING, TypedActionCreatorWithProps } from '@alfa-client/tech-shared'; import { Mock, mock } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { TestBed } from '@angular/core/testing'; @@ -36,11 +31,7 @@ import { provideMockStore } from '@ngrx/store/testing'; import { Resource } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; import { ColdObservable } from 'jest-marbles/typings/src/rxjs/cold-observable'; -import { - createCommandListResource, - createCommandResource, - createCreateCommandProps, -} from 'libs/command-shared/test/command'; +import { createCommandListResource, createCommandResource, createCreateCommandProps } from 'libs/command-shared/test/command'; import { createApiError } from 'libs/tech-shared/test/error'; import { Observable, of } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; @@ -48,12 +39,7 @@ import { CommandLinkRel } from '../command.linkrel'; import { CREATE_COMMAND_MESSAGE_BY_ORDER, CommandErrorMessage } from '../command.message'; import { CommandListResource, CommandResource, CreateCommandProps } from '../command.model'; import { CommandRepository } from '../command.repository'; -import { - CommandProps, - LoadCommandListSuccessProps, - SnackBarProps, - createCommandFailure, -} from './command.actions'; +import { CommandProps, LoadCommandListSuccessProps, SnackBarProps, createCommandFailure } from './command.actions'; import { CommandEffects } from './command.effects'; import * as CommandActions from './command.actions'; @@ -121,7 +107,7 @@ describe('CommandEffects', () => { const commandListResource: CommandListResource = createCommandListResource(); beforeEach(() => { - repository.getPendingCommands.mockReturnValue(of(commandListResource)); + repository.getCommands.mockReturnValue(of(commandListResource)); }); it('should call repository', () => { @@ -129,7 +115,7 @@ describe('CommandEffects', () => { effects.loadCommandList$.subscribe(); - expect(repository.getPendingCommands).toHaveBeenCalledWith(resource, linkRel); + expect(repository.getCommands).toHaveBeenCalledWith(resource, linkRel); }); it('should dispatch success action on no error', () => { @@ -145,7 +131,7 @@ describe('CommandEffects', () => { const apiError: ApiError = createApiError(); const error = { error: apiError }; const errorResponse = cold('-#', {}, error); - repository.getPendingCommands = jest.fn(() => errorResponse); + repository.getCommands = jest.fn(() => errorResponse); actions = hot('-a', { a: loadCommandList }); @@ -156,8 +142,7 @@ describe('CommandEffects', () => { describe('createCommand', () => { const createCommandProps: CreateCommandProps = createCreateCommandProps(); - const createCommandAction: Action<string> = - CommandActions.createCommand(createCommandProps); + const createCommandAction: Action<string> = CommandActions.createCommand(createCommandProps); const commandResource: CommandResource = createCommandResource(); @@ -184,10 +169,7 @@ describe('CommandEffects', () => { effects.createCommand$.subscribe(); - expect(effects.handleCreatedCommand).toHaveBeenCalledWith( - addCreateCommandActionType(createCommandProps), - commandResource, - ); + expect(effects.handleCreatedCommand).toHaveBeenCalledWith(addCreateCommandActionType(createCommandProps), commandResource); }); function addCreateCommandActionType(createCommandProps: CreateCommandProps) { @@ -202,7 +184,11 @@ describe('CommandEffects', () => { actions = hot('-a', { a: createCommandAction }); const expected: ColdObservable = cold('--b', { - b: CommandActions.createCommandFailure({ error, command: createCommandProps.command }), + b: CommandActions.createCommandFailure({ + createCommandProps: addCreateCommandActionType(createCommandProps), + error, + command: createCommandProps.command, + }), }); expect(effects.createCommand$).toBeObservable(expected); }); @@ -214,10 +200,7 @@ describe('CommandEffects', () => { it('should return pollCreatedCommand action on pending command', () => { const command: CommandResource = createCommandResource([CommandLinkRel.UPDATE]); - const actions: Action<string>[] = effects.handleCreatedCommand( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreatedCommand(createCommandProps, command); expect(actions.length).toBe(1); expect(actions[0].type).toBe(CommandActions.pollCreatedCommand.type); @@ -285,7 +268,7 @@ describe('CommandEffects', () => { actions = hot('-a', { a: pollCreateCommandAction }); expectObservable(effects.pollCreatedCommand$).toBe(delay + ' --c', { - c: createCommandFailure({ command, error }), + c: createCommandFailure({ createCommandProps: null, command, error }), }); }); }); @@ -298,10 +281,7 @@ describe('CommandEffects', () => { it('should return createCommandSuccess action', () => { const command: CommandResource = createCommandResource(); - const actions: Action<string>[] = effects.handleCreateCommandSuccess( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreateCommandSuccess(createCommandProps, command); expect(actions.length).toBe(2); const createCommandSuccessAction: CommandActions.CommandProps = <any>actions[0]; @@ -312,10 +292,7 @@ describe('CommandEffects', () => { it('should return showSnackbar action', () => { const command: CommandResource = createCommandResource(); - const actions: Action<string>[] = effects.handleCreateCommandSuccess( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreateCommandSuccess(createCommandProps, command); expect(actions.length).toBe(2); const showSnackbarAction: CommandActions.SnackBarProps = <any>actions[1]; @@ -328,10 +305,7 @@ describe('CommandEffects', () => { it('should return createCommandSuccess action', () => { const command: CommandResource = createCommandResource([CommandLinkRel.REVOKE]); - const actions: Action<string>[] = effects.handleCreateCommandSuccess( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreateCommandSuccess(createCommandProps, command); expect(actions.length).toBe(2); expect(actions[0].type).toBe(CommandActions.createCommandSuccess.type); @@ -341,10 +315,7 @@ describe('CommandEffects', () => { it('should return showRevokeSnackbar action', () => { const command: CommandResource = createCommandResource([CommandLinkRel.REVOKE]); - const actions: Action<string>[] = effects.handleCreateCommandSuccess( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreateCommandSuccess(createCommandProps, command); expect(actions.length).toBe(2); expect(actions[1].type).toBe(CommandActions.showRevokeSnackbar.type); @@ -372,10 +343,7 @@ describe('CommandEffects', () => { errorMessage: CommandErrorMessage.CONCURRENT_MODIFICATION, }; - const actions: Action<string>[] = effects.handleCreateCommandSuccess( - createCommandProps, - command, - ); + const actions: Action<string>[] = effects.handleCreateCommandSuccess(createCommandProps, command); expect(actions.length).toBe(2); expect(actions[0].type).toBe(CommandActions.createCommandSuccess.type); @@ -428,10 +396,7 @@ describe('CommandEffects', () => { effects.handleSnackbarByCommand(snackBarProps); - expect(snackBarService.show).toHaveBeenCalledWith( - command, - createCommandProps.snackBarMessage, - ); + expect(snackBarService.show).toHaveBeenCalledWith(command, createCommandProps.snackBarMessage); }); it('should show snackBar on undefined snackBarMessage', () => { @@ -442,10 +407,7 @@ describe('CommandEffects', () => { effects.handleSnackbarByCommand(snackBarProps); - expect(snackBarService.show).toHaveBeenCalledWith( - command, - CREATE_COMMAND_MESSAGE_BY_ORDER[command.order], - ); + expect(snackBarService.show).toHaveBeenCalledWith(command, CREATE_COMMAND_MESSAGE_BY_ORDER[command.order]); }); it('should NOT show snackBar on empty snackBarMessage', () => { diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.effects.ts b/alfa-client/libs/command-shared/src/lib/+state/command.effects.ts index 6b13e23b3f49c05e1bec4067d4cb12cecfae1eb2..3df93f0045a699329e7c6ed391bf6197d8ddb108 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.effects.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.effects.ts @@ -25,19 +25,14 @@ import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { Store, Action } from '@ngrx/store'; +import { Action, Store } from '@ngrx/store'; import { isEmpty, isEqual, isUndefined } from 'lodash-es'; import { of } from 'rxjs'; import { catchError, delay, map, mergeMap, switchMap, tap } from 'rxjs/operators'; import { COMMAND_ERROR_MESSAGES, CREATE_COMMAND_MESSAGE_BY_ORDER } from '../command.message'; import { CommandListResource, CommandResource, CreateCommandProps } from '../command.model'; import { CommandRepository } from '../command.repository'; -import { - hasCommandError, - isConcurrentModification, - isPending, - isRevokeable, -} from '../command.util'; +import { hasCommandError, isConcurrentModification, isPending, isRevokeable } from '../command.util'; import { CommandProps, LoadCommandListProps, @@ -72,7 +67,7 @@ export class CommandEffects { this.actions$.pipe( ofType(loadCommandList), switchMap((props: LoadCommandListProps) => - this.repository.getPendingCommands(props.resource, props.linkRel).pipe( + this.repository.getCommands(props.resource, props.linkRel).pipe( map((commandList: CommandListResource) => props.successAction(commandList)), catchError((error) => of(props.failureAction(error.error))), ), @@ -86,7 +81,7 @@ export class CommandEffects { switchMap((props: CreateCommandProps) => this.repository.createCommand(props.resource, props.linkRel, props.command).pipe( mergeMap((command: CommandResource) => this.handleCreatedCommand(props, command)), - catchError((error) => of(createCommandFailure({ error, command: props.command }))), + catchError((error) => of(createCommandFailure({ createCommandProps: props, error, command: props.command }))), ), ), ), @@ -98,39 +93,34 @@ export class CommandEffects { delay(CommandEffects.POLL_DELAY), switchMap((props: PollCommandProps) => this.repository.getCommand(props.command).pipe( - mergeMap((command: CommandResource) => - this.handleCreatedCommand(props.createCommandProps, command), + mergeMap((command: CommandResource) => this.handleCreatedCommand(props.createCommandProps, command)), + catchError((error) => + of(createCommandFailure({ createCommandProps: props.createCommandProps, command: props.command, error })), ), - catchError((error) => of(createCommandFailure({ command: props.command, error }))), ), ), ), ); - handleCreatedCommand( - createCommandProps: CreateCommandProps, - command: CommandResource, - ): Action<string>[] { + handleCreatedCommand(createCommandProps: CreateCommandProps, command: CommandResource): Action<string>[] { if (isPending(command)) { return [pollCreatedCommand({ createCommandProps, command })]; } return this.handleCreateCommandSuccess(createCommandProps, command); } - handleCreateCommandSuccess( - createCommandProps: CreateCommandProps, - command: CommandResource, - ): Action<string>[] { + handleCreateCommandSuccess(createCommandProps: CreateCommandProps, command: CommandResource): Action<string>[] { if (isRevokeable(command)) { - return [createCommandSuccess({ command }), showRevokeSnackbar({ command })]; + return [createCommandSuccess({ createCommandProps, command }), showRevokeSnackbar({ command })]; } + if (hasCommandError(command) && isConcurrentModification(command.errorMessage)) { this.showError(command); //FIXME Anstelle der createCommandSucess Action sollte eine createCommandFailure Action geworfen werden. //Hierzu muss ein HttpErrorResponse für die errorMessage definieren werden - return [createCommandSuccess({ command }), publishConcurrentModificationAction()]; + return [createCommandSuccess({ createCommandProps, command }), publishConcurrentModificationAction()]; } - return [createCommandSuccess({ command }), showSnackbar({ createCommandProps, command })]; + return [createCommandSuccess({ createCommandProps, command }), showSnackbar({ createCommandProps, command })]; } private showError(command: CommandResource): void { @@ -166,10 +156,7 @@ export class CommandEffects { ); handleSnackbarByCommand(props: SnackBarProps): void { - if ( - !isEqual(props.createCommandProps.snackBarMessage, EMPTY_STRING) && - isEmpty(props.command.errorMessage) - ) { + if (!isEqual(props.createCommandProps.snackBarMessage, EMPTY_STRING) && isEmpty(props.command.errorMessage)) { this.snackbarService.show(props.command, this.getSnackBarMessage(props)); } } diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.reducer.spec.ts b/alfa-client/libs/command-shared/src/lib/+state/command.reducer.spec.ts index 9a3735965f667bbe02ca6d3f87e6baabc75e1469..161bddb6780496664c3503a678f0cc2fcedcbc22 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.reducer.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.reducer.spec.ts @@ -26,21 +26,27 @@ import { createEmptyStateResource, createErrorStateResource, createStateResource, + LinkRelationName, } from '@alfa-client/tech-shared'; import { HttpErrorResponse } from '@angular/common/http'; +import { faker } from '@faker-js/faker'; import { Action } from '@ngrx/store'; -import { Resource, ResourceUri } from '@ngxp/rest'; +import { getUrl, Resource, ResourceUri } from '@ngxp/rest'; import { createApiError, createHttpErrorResponse } from '../../../../tech-shared/test/error'; import { createDummyResource } from '../../../../tech-shared/test/resource'; -import { createCommandResource, createCreateCommand } from '../../../test/command'; -import { CommandResource, CreateCommand } from '../command.model'; -import { CommandState, initialState, reducer } from './command.reducer'; - -import { faker } from '@faker-js/faker'; +import { createCommandResource, createCreateCommand, createCreateCommandProps } from '../../../test/command'; +import { CommandResource, CreateCommand, CreateCommandProps } from '../command.model'; +import { buildCommandStateKey, CommandState, initialState, reducer } from './command.reducer'; import * as CommandActions from '../+state/command.actions'; describe('Command Reducer', () => { + const linkRel: LinkRelationName = faker.string.alpha(6); + const resource: Resource = createDummyResource([linkRel]); + const uri: ResourceUri = getUrl(resource, linkRel); + const commandResource: CommandResource = createCommandResource(); + const createCommandProps: CreateCommandProps = { ...createCreateCommandProps(), resource, linkRel }; + describe('unknown action', () => { it('should return current state', () => { const action: Action = {} as Action; @@ -52,42 +58,48 @@ describe('Command Reducer', () => { }); describe('createCommand', () => { - const resource: Resource = createDummyResource(); - const linkRel: ResourceUri = faker.internet.url(); const command: CreateCommand = createCreateCommand(); it('should create empty loading map entry', () => { - const action = CommandActions.createCommand({ resource, linkRel, command }); + const action: Action<string> = CommandActions.createCommand({ resource, linkRel, command }); const state: CommandState = reducer(initialState, action); - expect(state.commandByOrderMap[command.order]).toEqual(createEmptyStateResource(true)); + expect(state.commandMap[buildCommandStateKey(command.order, uri)]).toEqual(createEmptyStateResource(true)); }); }); describe('createCommandSuccess', () => { - const command: CommandResource = createCommandResource(); - it('should create stateResource entry by loaded resource', () => { - const action = CommandActions.createCommandSuccess({ command }); + const action: Action<string> = CommandActions.createCommandSuccess({ + createCommandProps, + command: commandResource, + }); const state: CommandState = reducer(initialState, action); - expect(state.commandByOrderMap[command.order]).toEqual(createStateResource(command)); + expect(state.commandMap[buildCommandStateKey(createCommandProps.command.order, uri)]).toEqual( + createStateResource(commandResource), + ); }); }); describe('createCommandFailure', () => { - const command: CommandResource = createCommandResource(); const error: ApiError = createApiError(); const httpErrorResponse: HttpErrorResponse = { ...createHttpErrorResponse(), error }; it('should create errorStateResource entry by occured error', () => { - const action = CommandActions.createCommandFailure({ command, error: httpErrorResponse }); + const action: Action<string> = CommandActions.createCommandFailure({ + createCommandProps, + command: commandResource, + error: httpErrorResponse, + }); const state: CommandState = reducer(initialState, action); - expect(state.commandByOrderMap[command.order]).toEqual(createErrorStateResource(error)); + expect(state.commandMap[buildCommandStateKey(createCommandProps.command.order, uri)]).toEqual( + createErrorStateResource(error), + ); }); }); }); diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.reducer.ts b/alfa-client/libs/command-shared/src/lib/+state/command.reducer.ts index bce701b1df56880f91f80bdd6cd210ebea1fc36a..3c9e84e51d884bf3ac6bad0a5788a37c28b7f0ec 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.reducer.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.reducer.ts @@ -21,16 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - StateResource, - createEmptyStateResource, - createErrorStateResource, - createStateResource, -} from '@alfa-client/tech-shared'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { HttpErrorResponse } from '@angular/common/http'; import { Action, ActionReducer, createReducer, on } from '@ngrx/store'; -import { CommandResource, CreateCommandProps } from '../command.model'; +import { getUrl, ResourceUri } from '@ngxp/rest'; +import { CommandOrder, CommandResource, CreateCommandProps } from '../command.model'; +import { CreateCommandFailureProps, CreateCommandSuccessProps } from './command.actions'; -import { HttpErrorResponse } from '@angular/common/http'; import * as Actions from './command.actions'; export const COMMAND_FEATURE_KEY = 'CommandState'; @@ -40,11 +37,11 @@ export interface CommandPartialState { } export interface CommandState { - commandByOrderMap: { [order: string]: StateResource<CommandResource> }; + commandMap: { [key: string]: StateResource<CommandResource> }; } export const initialState: CommandState = { - commandByOrderMap: {}, + commandMap: {}, }; const commandReducer: ActionReducer<CommandState, Action> = createReducer( @@ -53,29 +50,29 @@ const commandReducer: ActionReducer<CommandState, Action> = createReducer( Actions.createCommand, (state: CommandState, props: CreateCommandProps): CommandState => ({ ...state, - commandByOrderMap: { - ...state.commandByOrderMap, - [props.command.order]: createEmptyStateResource(true), + commandMap: { + ...state.commandMap, + [createCommandStateKey(props)]: createEmptyStateResource(true), }, }), ), on( Actions.createCommandSuccess, - (state: CommandState, props: Actions.CommandProps): CommandState => ({ + (state: CommandState, props: CreateCommandSuccessProps): CommandState => ({ ...state, - commandByOrderMap: { - ...state.commandByOrderMap, - [props.command.order]: createStateResource(props.command), + commandMap: { + ...state.commandMap, + [createCommandStateKey(props.createCommandProps)]: createStateResource(props.command), }, }), ), on( Actions.createCommandFailure, - (state: CommandState, props: Actions.CreateCommandFailureProps): CommandState => ({ + (state: CommandState, props: CreateCommandFailureProps): CommandState => ({ ...state, - commandByOrderMap: { - ...state.commandByOrderMap, - [props.command.order]: createErrorStateResource((<HttpErrorResponse>props.error).error), + commandMap: { + ...state.commandMap, + [createCommandStateKey(props.createCommandProps)]: createErrorStateResource((<HttpErrorResponse>props.error).error), }, }), ), @@ -84,3 +81,11 @@ const commandReducer: ActionReducer<CommandState, Action> = createReducer( export function reducer(state: CommandState, action: Action): CommandState { return commandReducer(state, action); } + +function createCommandStateKey(props: CreateCommandProps): string { + return buildCommandStateKey(props.command.order, getUrl(props.resource, props.linkRel)); +} + +export function buildCommandStateKey(order: CommandOrder, uri: ResourceUri): string { + return `${order}_${uri}`; +} diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.selectors.spec.ts b/alfa-client/libs/command-shared/src/lib/+state/command.selectors.spec.ts index cc0f81f2f5012401a3a1d7793001d22ff6a2a9c7..d2e786a6f8a566a0673ed8112401bb03b816971a 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.selectors.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.selectors.spec.ts @@ -21,10 +21,12 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { StateResource, createEmptyStateResource } from '@alfa-client/tech-shared'; +import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; +import { faker } from '@faker-js/faker/.'; +import { ResourceUri } from '@ngxp/rest'; import { createCreateCommand } from 'libs/command-shared/test/command'; import { CommandOrder, CommandResource, CreateCommand } from '../command.model'; -import { CommandPartialState, initialState } from './command.reducer'; +import { buildCommandStateKey, CommandPartialState, initialState } from './command.reducer'; import * as Selectors from './command.selectors'; @@ -32,38 +34,37 @@ describe('Command Selectors', () => { let state: CommandPartialState; const commandInState: CreateCommand = createCreateCommand(); + const uri: ResourceUri = faker.internet.url(); + const commandInStateOrder: CommandOrder = CommandOrder.VORGANG_ANNEHMEN; - const commandByOrderMap: any = { [commandInStateOrder]: commandInState }; + const commandMap: any = { [buildCommandStateKey(commandInStateOrder, uri)]: commandInState }; beforeEach(() => { state = { CommandState: { ...initialState, - commandByOrderMap, + commandMap, }, }; }); - describe('commandByOrderMap', () => { - it('should select commandByOrderMap', () => { - const result: any = Selectors.commandByOrderMap.projector(state.CommandState); + describe('commandMap', () => { + it('should select commandMap', () => { + const result: any = Selectors.commandMap.projector(state.CommandState); - expect(result).toBe(commandByOrderMap); + expect(result).toBe(commandMap); }); }); - describe('commandByOrder', () => { + describe('command', () => { it('should return command from map by order', () => { - const result: StateResource<CommandResource> = - Selectors.commandByOrder(commandInStateOrder).projector(commandByOrderMap); + const result: StateResource<CommandResource> = Selectors.command(commandInStateOrder, uri).projector(commandMap); expect(result).toBe(commandInState); }); it('should return empty state resource on empty object', () => { - const result: StateResource<CommandResource> = Selectors.commandByOrder( - commandInStateOrder, - ).projector({}); + const result: StateResource<CommandResource> = Selectors.command(commandInStateOrder, uri).projector({}); expect(result).toEqual(createEmptyStateResource()); }); diff --git a/alfa-client/libs/command-shared/src/lib/+state/command.selectors.ts b/alfa-client/libs/command-shared/src/lib/+state/command.selectors.ts index db561e1aec818b3808373e68ec34b3964973859b..2e31f077a03dc161373bf2c36109dfb539066bc6 100644 --- a/alfa-client/libs/command-shared/src/lib/+state/command.selectors.ts +++ b/alfa-client/libs/command-shared/src/lib/+state/command.selectors.ts @@ -22,21 +22,21 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { createEmptyStateResource } from '@alfa-client/tech-shared'; -import { MemoizedSelector, createFeatureSelector, createSelector } from '@ngrx/store'; +import { createFeatureSelector, createSelector, MemoizedSelector } from '@ngrx/store'; +import { ResourceUri } from '@ngxp/rest'; import { isUndefined } from 'lodash-es'; import { CommandOrder } from '../command.model'; -import { COMMAND_FEATURE_KEY, CommandState } from './command.reducer'; +import { buildCommandStateKey, COMMAND_FEATURE_KEY, CommandState } from './command.reducer'; -const getCommandState: MemoizedSelector<object, CommandState> = - createFeatureSelector<CommandState>(COMMAND_FEATURE_KEY); +const getCommandState: MemoizedSelector<object, CommandState> = createFeatureSelector<CommandState>(COMMAND_FEATURE_KEY); -export const commandByOrderMap: MemoizedSelector<CommandState, any> = createSelector( +export const commandMap: MemoizedSelector<CommandState, any> = createSelector( getCommandState, - (state: CommandState) => state.commandByOrderMap, + (state: CommandState) => state.commandMap, ); -export const commandByOrder = (order: CommandOrder) => - createSelector(commandByOrderMap, (commandByOrderMapInState: any) => - isUndefined(commandByOrderMapInState[order]) ? - createEmptyStateResource() - : commandByOrderMapInState[order], +export const command = (order: CommandOrder, uri: ResourceUri) => + createSelector(commandMap, (commandMapInState: any) => + isUndefined(commandMapInState[buildCommandStateKey(order, uri)]) ? createEmptyStateResource() : ( + commandMapInState[buildCommandStateKey(order, uri)] + ), ); diff --git a/alfa-client/libs/command-shared/src/lib/command.linkrel.ts b/alfa-client/libs/command-shared/src/lib/command.linkrel.ts index 2b92fb7620ff8bc6a63da16f535b76aef682146b..5ef51639bae02523c8bd3d9137591a7174b20af7 100644 --- a/alfa-client/libs/command-shared/src/lib/command.linkrel.ts +++ b/alfa-client/libs/command-shared/src/lib/command.linkrel.ts @@ -24,6 +24,7 @@ export enum CommandLinkRel { CREATED_BY = 'createdBy', EFFECTED_RESOURCE = 'effected_resource', + RELATED_RESOURCE = 'related_resource', REVOKE = 'revoke', SELF = 'self', UPDATE = 'update', diff --git a/alfa-client/libs/command-shared/src/lib/command.model.ts b/alfa-client/libs/command-shared/src/lib/command.model.ts index b86ac920fbeab00514c3cf45b7ae0f88e7ff7ce3..cc598f0540b1d0777921ff752592fa8d231d43b1 100644 --- a/alfa-client/libs/command-shared/src/lib/command.model.ts +++ b/alfa-client/libs/command-shared/src/lib/command.model.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ListResource } from '@alfa-client/tech-shared'; +import { ListResource, StateResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; export interface CreateCommand { @@ -101,3 +101,7 @@ export interface CreateCommandProps { } export type CreateCommandPropsWithoutResource = Omit<CreateCommandProps, 'resource'>; + +export interface PendingCommandMap { + [relatedResourceUri: string]: StateResource<CommandResource>; +} diff --git a/alfa-client/libs/command-shared/src/lib/command.repository.spec.ts b/alfa-client/libs/command-shared/src/lib/command.repository.spec.ts index 2d422eba29a49de7321e82919b5494b0eacf69a0..c4cd71b0c6ebd224545462d1cfd7f1e248854bbe 100644 --- a/alfa-client/libs/command-shared/src/lib/command.repository.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/command.repository.spec.ts @@ -21,15 +21,12 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { faker } from '@faker-js/faker'; +import { LinkRelationName } from '@alfa-client/tech-shared'; import { mock, useFromMock } from '@alfa-client/test-utils'; +import { faker } from '@faker-js/faker'; import { Resource, ResourceFactory } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; -import { - createCommand, - createCommandListResource, - createCommandResource, -} from 'libs/command-shared/test/command'; +import { createCommand, createCommandListResource, createCommandResource } from 'libs/command-shared/test/command'; import { toResource } from 'libs/tech-shared/test/resource'; import { CommandLinkRel } from './command.linkrel'; import { Command, CommandListResource, CommandResource, CommandStatus } from './command.model'; @@ -41,11 +38,10 @@ describe('CommandRepository', () => { let resourceWrapper = { post: jest.fn(), get: jest.fn(), patch: jest.fn() }; const command: Command = createCommand(); - const commandResource: CommandResource = createCommandResource(); + + const linkRel: LinkRelationName = 'dummyLinkRel'; + const commandResource: CommandResource = createCommandResource([linkRel]); const commandListResource: CommandListResource = createCommandListResource(); - const commandResourceWithPendingCommand: CommandResource = createCommandResource([ - 'pendingCommands', - ]); beforeEach(() => { repository = new CommandRepository(useFromMock(resourceFactory)); @@ -131,27 +127,25 @@ describe('CommandRepository', () => { }); }); - describe('get pending commands', () => { - const linkRel = 'pendingCommands'; - + describe('get commands', () => { beforeEach(() => { resourceWrapper.get.mockReturnValue(hot('a', { a: commandListResource })); }); it('should call resourceFactory with resource', () => { - repository.getPendingCommands(commandResourceWithPendingCommand, linkRel); + repository.getCommands(commandResource, linkRel); - expect(resourceFactory.from).toHaveBeenCalledWith(commandResourceWithPendingCommand); + expect(resourceFactory.from).toHaveBeenCalledWith(commandResource); }); it('should call resourceWrapper with linkRel', () => { - repository.getPendingCommands(commandResourceWithPendingCommand, linkRel); + repository.getCommands(commandResource, linkRel); expect(resourceWrapper.get).toHaveBeenCalledWith(linkRel); }); it('should return value', () => { - const result = repository.getPendingCommands(commandResourceWithPendingCommand, linkRel); + const result = repository.getCommands(commandResource, linkRel); expect(result).toBeObservable(cold('a', { a: commandListResource })); }); diff --git a/alfa-client/libs/command-shared/src/lib/command.repository.ts b/alfa-client/libs/command-shared/src/lib/command.repository.ts index 88f4ac7457ac948af0ae7cb49f2d84c78848964e..83c5ec53fcf74a558d56646f474a24a4c6a6a188 100644 --- a/alfa-client/libs/command-shared/src/lib/command.repository.ts +++ b/alfa-client/libs/command-shared/src/lib/command.repository.ts @@ -25,22 +25,13 @@ import { Injectable } from '@angular/core'; import { Resource, ResourceFactory } from '@ngxp/rest'; import { Observable } from 'rxjs'; import { CommandLinkRel } from './command.linkrel'; -import { - CommandListResource, - CommandResource, - CommandStatus, - CreateCommand, -} from './command.model'; +import { CommandListResource, CommandResource, CommandStatus, CreateCommand } from './command.model'; @Injectable({ providedIn: 'root' }) export class CommandRepository { constructor(private resourceFactory: ResourceFactory) {} - public createCommand( - resource: Resource, - linkrel: string, - command: CreateCommand, - ): Observable<CommandResource> { + public createCommand(resource: Resource, linkrel: string, command: CreateCommand): Observable<CommandResource> { return this.resourceFactory.from(resource).post(linkrel, command); } @@ -48,14 +39,12 @@ export class CommandRepository { return this.resourceFactory.from(resource).get(CommandLinkRel.SELF); } - public getPendingCommands(resource: Resource, linkRel: string): Observable<CommandListResource> { + public getCommands(resource: Resource, linkRel: string): Observable<CommandListResource> { return this.resourceFactory.from(resource).get(linkRel); } public revokeCommand(resource: CommandResource): Observable<CommandResource> { - return this.resourceFactory - .from(resource) - .patch(CommandLinkRel.REVOKE, { status: CommandStatus.REVOKED }); + return this.resourceFactory.from(resource).patch(CommandLinkRel.REVOKE, { status: CommandStatus.REVOKED }); } public getEffectedResource<T>(command: CommandResource): Observable<T> { diff --git a/alfa-client/libs/command-shared/src/lib/command.service.spec.ts b/alfa-client/libs/command-shared/src/lib/command.service.spec.ts index 4de92f2b517d27c29980f0c12851807c7af76e24..44237dadab3806bd1c1c9bd78f019026e49da405 100644 --- a/alfa-client/libs/command-shared/src/lib/command.service.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/command.service.spec.ts @@ -22,6 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { + LinkRelationName, StateResource, createEmptyStateResource, createErrorStateResource, @@ -32,7 +33,7 @@ import { SnackBarService } from '@alfa-client/ui'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { faker } from '@faker-js/faker'; import { Store } from '@ngrx/store'; -import { Resource } from '@ngxp/rest'; +import { Resource, ResourceUri, getUrl } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; import { createCommand, @@ -42,23 +43,13 @@ import { createCreateCommandProps, } from 'libs/command-shared/test/command'; import { createHttpErrorResponse } from 'libs/tech-shared/test/http'; -import { toResource } from 'libs/tech-shared/test/resource'; +import { createDummyResource, toResource } from 'libs/tech-shared/test/resource'; import { Observable, Subject, of, throwError } from 'rxjs'; import { CommandLinkRel } from './command.linkrel'; import { CommandErrorMessage } from './command.message'; -import { - Command, - CommandListResource, - CommandOrder, - CommandResource, - CreateCommandProps, -} from './command.model'; +import { Command, CommandListResource, CommandOrder, CommandResource, CreateCommandProps } from './command.model'; import { CommandRepository } from './command.repository'; -import { - CommandService, - IntervallHandleWithTickObservable, - startInterval, -} from './command.service'; +import { CommandService, IntervallHandleWithTickObservable, startInterval } from './command.service'; import * as Actions from './+state/command.actions'; import * as Selectors from './+state/command.selectors'; @@ -76,9 +67,7 @@ describe('CommandService', () => { const commandResource: CommandResource = createCommandResource(); const commandStateResource: StateResource<CommandResource> = createStateResource(commandResource); - const commandResourceWithUpdateLink: CommandResource = createCommandResource([ - CommandLinkRel.UPDATE, - ]); + const commandResourceWithUpdateLink: CommandResource = createCommandResource([CommandLinkRel.UPDATE]); beforeEach(() => { store = mock(Store); @@ -87,11 +76,7 @@ describe('CommandService', () => { store.dispatch = jest.fn(); repository = mock(CommandRepository); - service = new CommandService( - useFromMock(repository), - useFromMock(snackbarService), - useFromMock(<any>store), - ); + service = new CommandService(useFromMock(repository), useFromMock(snackbarService), useFromMock(<any>store)); }); describe('create command', () => { @@ -162,17 +147,13 @@ describe('CommandService', () => { beforeEach(() => { repository.revokeCommand.mockReturnValue(cold('a', { a: commandResourceWithUpdateLink })); service.pollCommand = jest.fn(); - (<any>service.pollCommand).mockReturnValue( - cold('a', { a: createStateResource(commandResourceWithUpdateLink, true) }), - ); + (<any>service.pollCommand).mockReturnValue(cold('a', { a: createStateResource(commandResourceWithUpdateLink, true) })); }); it('should return value with loading true', () => { const result = service.revokeCommand(commandResource); - expect(result).toBeObservable( - hot('a', { a: createStateResource(commandResourceWithUpdateLink, true) }), - ); + expect(result).toBeObservable(hot('a', { a: createStateResource(commandResourceWithUpdateLink, true) })); }); }); @@ -189,6 +170,7 @@ describe('CommandService', () => { it.skip('should call handleCommandError', () => { service.handleCommandError = jest.fn(); const commandWithError: CommandResource = createCommandErrorResource(); + service.handleCommand(commandWithError); expect(service.handleCommandError).toHaveBeenCalledWith(commandWithError); @@ -270,9 +252,7 @@ describe('CommandService', () => { it('should return stateResource still loading', () => { const result = service.getAndUpdate(commandResource); - expect(result).toBeObservable( - hot('a', { a: createStateResource(commandResourceWithUpdateLink, true) }), - ); + expect(result).toBeObservable(hot('a', { a: createStateResource(commandResourceWithUpdateLink, true) })); }); describe('command is loaded', () => { @@ -327,20 +307,17 @@ describe('CommandService', () => { }); }); - describe('get pending commands', () => { + describe('get commands', () => { const commandListResource: CommandListResource = createCommandListResource(); beforeEach(() => { - repository.getPendingCommands.mockReturnValue(cold('a', { a: commandListResource })); + repository.getCommands.mockReturnValue(cold('a', { a: commandListResource })); }); it('should call repository', () => { - service.getPendingCommands(commandResource, CommandLinkRel.SELF); + service.getCommands(commandResource, CommandLinkRel.SELF); - expect(repository.getPendingCommands).toHaveBeenLastCalledWith( - commandResource, - CommandLinkRel.SELF, - ); + expect(repository.getCommands).toHaveBeenLastCalledWith(commandResource, CommandLinkRel.SELF); }); }); @@ -380,10 +357,12 @@ describe('CommandService', () => { }); describe('createCommandByProps', () => { - const createCommandProps: CreateCommandProps = { ...createCreateCommandProps(), command }; + const linkRel: LinkRelationName = faker.string.alpha(6); + const resource: Resource = createDummyResource([linkRel]); + const createCommandProps: CreateCommandProps = { ...createCreateCommandProps(), command, resource, linkRel }; beforeEach(() => { - service.getCommandByOrder = jest.fn().mockReturnValue(of(commandStateResource)); + service._getCommand = jest.fn().mockReturnValue(of(commandStateResource)); }); it('should dispatch action', () => { @@ -394,7 +373,7 @@ describe('CommandService', () => { it('should call get command by order', (done) => { service.createCommandByProps(createCommandProps).subscribe(() => { - expect(service.getCommandByOrder).toHaveBeenCalledWith(command.order); + expect(service._getCommand).toHaveBeenCalledWith(command.order, getUrl(resource, linkRel)); done(); }); @@ -411,12 +390,14 @@ describe('CommandService', () => { }); }); - describe('get command by order', () => { + describe('get command', () => { + const uri: ResourceUri = faker.internet.url(); + it('should select from store', (done) => { - const selectorSpy = jest.spyOn(Selectors, 'commandByOrder'); + const selectorSpy = jest.spyOn(Selectors, 'command'); - service.getCommandByOrder(CommandOrder.VORGANG_ANNEHMEN).subscribe(() => { - expect(selectorSpy).toHaveBeenCalledWith(CommandOrder.VORGANG_ANNEHMEN); + service._getCommand(CommandOrder.VORGANG_ANNEHMEN, uri).subscribe(() => { + expect(selectorSpy).toHaveBeenCalledWith(CommandOrder.VORGANG_ANNEHMEN, uri); done(); }); @@ -424,7 +405,7 @@ describe('CommandService', () => { }); it('should return value', (done) => { - service.getCommandByOrder(CommandOrder.VORGANG_ANNEHMEN).subscribe((selected) => { + service._getCommand(CommandOrder.VORGANG_ANNEHMEN, uri).subscribe((selected) => { expect(selected).toBe(commandStateResource); done(); }); @@ -436,7 +417,7 @@ describe('CommandService', () => { store.select.mockReturnValue(of(null)); let success: boolean = true; - service.getCommandByOrder(<CommandOrder>'anyOrder').subscribe({ + service._getCommand(<CommandOrder>'anyOrder', uri).subscribe({ next: (commandStateResource: StateResource<CommandResource>) => { success = commandStateResource !== null; }, diff --git a/alfa-client/libs/command-shared/src/lib/command.service.ts b/alfa-client/libs/command-shared/src/lib/command.service.ts index 8c4a5ec1a7e1506ed20777e6c724822c110aae77..8e035ec94fd8881956a85de10c1841b543178ec3 100644 --- a/alfa-client/libs/command-shared/src/lib/command.service.ts +++ b/alfa-client/libs/command-shared/src/lib/command.service.ts @@ -32,18 +32,12 @@ import { SnackBarService } from '@alfa-client/ui'; import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Resource } from '@ngxp/rest'; +import { getUrl, Resource, ResourceUri } from '@ngxp/rest'; import { Observable, of, Subject, throwError } from 'rxjs'; import { catchError, filter, map, mergeMap, tap } from 'rxjs/operators'; import { CommandEffects } from './+state/command.effects'; import { COMMAND_ERROR_MESSAGES } from './command.message'; -import { - CommandListResource, - CommandOrder, - CommandResource, - CreateCommand, - CreateCommandProps, -} from './command.model'; +import { CommandListResource, CommandOrder, CommandResource, CreateCommand, CreateCommandProps } from './command.model'; import { CommandRepository } from './command.repository'; import { isConcurrentModification, isPending } from './command.util'; @@ -63,11 +57,7 @@ export class CommandService { /** * @deprecated Use createCommandByProps(createCommandProps: CreateCommandProps) instead. */ - public createCommand( - resource: Resource, - linkRel: string, - command: CreateCommand, - ): Observable<StateResource<CommandResource>> { + public createCommand(resource: Resource, linkRel: string, command: CreateCommand): Observable<StateResource<CommandResource>> { return this.handleCommandResponse(this.repository.createCommand(resource, linkRel, command)); } @@ -75,9 +65,7 @@ export class CommandService { return this.handleCommandResponse(this.repository.revokeCommand(resource)); } - private handleCommandResponse( - command$: Observable<CommandResource>, - ): Observable<StateResource<CommandResource>> { + private handleCommandResponse(command$: Observable<CommandResource>): Observable<StateResource<CommandResource>> { return command$.pipe( mergeMap((command) => this.handleCommand(command)), catchError((errorResponse) => this.handleHttpError(errorResponse)), @@ -99,6 +87,7 @@ export class CommandService { return this.startPolling(command); } + // TODO: Pruefen, ob die Funktion noch gebraucht wird handleCommandError(command: CommandResource): Observable<StateResource<CommandResource>> { if (isConcurrentModification(command.errorMessage)) { this.snackBarService.showError(COMMAND_ERROR_MESSAGES[command.errorMessage]); @@ -106,11 +95,10 @@ export class CommandService { } return of(createStateResource(command)); } + // startPolling(commandResource: CommandResource): Observable<StateResource<CommandResource>> { - return isPending(commandResource) ? - this.pollCommand(commandResource) - : of(createStateResource(commandResource)); + return isPending(commandResource) ? this.pollCommand(commandResource) : of(createStateResource(commandResource)); } public pollCommand(commandResource: CommandResource): Observable<StateResource<CommandResource>> { @@ -121,10 +109,7 @@ export class CommandService { ); } - handleInterval( - stateResource: StateResource<CommandResource>, - interval: IntervallHandleWithTickObservable, - ): void { + handleInterval(stateResource: StateResource<CommandResource>, interval: IntervallHandleWithTickObservable): void { if (!stateResource.loading) this.clearInterval(interval.handle); } @@ -142,23 +127,24 @@ export class CommandService { window.clearInterval(handler); } - public getPendingCommands(resource: Resource, linkRel: string): Observable<CommandListResource> { - return this.repository.getPendingCommands(resource, linkRel); + //TODO Pruefen, ob die Funktion noch gebraucht wird + public getCommands(resource: Resource, linkRel: string): Observable<CommandListResource> { + return this.repository.getCommands(resource, linkRel); } - + // + //TODO Pruefen, ob die Funktion noch gebraucht wird public getEffectedResource<T>(command: CommandResource): Observable<T> { return this.repository.getEffectedResource(command); } + // - public createCommandByProps( - createCommandProps: CreateCommandProps, - ): Observable<StateResource<CommandResource>> { + public createCommandByProps(createCommandProps: CreateCommandProps): Observable<StateResource<CommandResource>> { this.store.dispatch(Actions.createCommand(createCommandProps)); - return this.getCommandByOrder(createCommandProps.command.order); + return this._getCommand(createCommandProps.command.order, getUrl(createCommandProps.resource, createCommandProps.linkRel)); } - public getCommandByOrder(order: CommandOrder): Observable<StateResource<CommandResource>> { - return this.store.select(Selectors.commandByOrder(order)).pipe(filter(isNotNil)); + _getCommand(order: CommandOrder, uri: ResourceUri): Observable<StateResource<CommandResource>> { + return this.store.select(Selectors.command(order, uri)).pipe(filter(isNotNil)); } } diff --git a/alfa-client/libs/command-shared/src/lib/command.util.spec.ts b/alfa-client/libs/command-shared/src/lib/command.util.spec.ts index d8dc15892b79797e534a88d774f405e16266b6d2..f3cd21cee6ab8eef273bbc0bd4577b74c18b6e67 100644 --- a/alfa-client/libs/command-shared/src/lib/command.util.spec.ts +++ b/alfa-client/libs/command-shared/src/lib/command.util.spec.ts @@ -21,11 +21,23 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { createCommandErrorResource, createCommandListResource, createCommandResource, } from 'libs/command-shared/test/command'; +import { createStateResource } from '@alfa-client/tech-shared'; +import { getUrl } from '@ngxp/rest'; +import { createCommandErrorResource, createCommandListResource, createCommandResource } from 'libs/command-shared/test/command'; import { CommandLinkRel } from './command.linkrel'; import { CommandErrorMessage } from './command.message'; -import { CommandListResource, CommandResource } from './command.model'; -import { getPendingCommandByOrder, hasCommandError, isConcurrentModification, isDone, isPending, isRevokeable, isSuccessfulDone, notHasCommandError, } from './command.util'; +import { CommandListResource, CommandResource, CommandStatus } from './command.model'; +import { + buildPendingCommandMap, + getPendingCommandByOrder, + hasCommandError, + isConcurrentModification, + isDone, + isPending, + isRevokeable, + isSuccessfulDone, + notHasCommandError, +} from './command.util'; describe('CommandUtil', () => { describe('isRevokeable', () => { @@ -112,20 +124,34 @@ describe('CommandUtil', () => { const command: CommandResource = { ...createCommandResource(), order: anotherOrder }; const listResource: CommandListResource = createCommandListResource([command]); - const pendingCommand: CommandResource = getPendingCommandByOrder(listResource, [ - order, - anotherOrder, - ]); + const pendingCommand: CommandResource = getPendingCommandByOrder(listResource, [order, anotherOrder]); expect(pendingCommand).toBe(command); }); }); + describe('build pending command map', () => { + it('should return map', () => { + const pendingCommand1: CommandResource = { + ...createCommandResource([CommandLinkRel.RELATED_RESOURCE]), + status: CommandStatus.PENDING, + }; + const pendingCommand2: CommandResource = { + ...createCommandResource([CommandLinkRel.RELATED_RESOURCE]), + status: CommandStatus.PENDING, + }; + const commandList: CommandListResource = createCommandListResource([pendingCommand1, pendingCommand2]); + + const map = buildPendingCommandMap(commandList); + + expect(map[getUrl(pendingCommand1, CommandLinkRel.RELATED_RESOURCE)]).toEqual(createStateResource(pendingCommand1)); + expect(map[getUrl(pendingCommand2, CommandLinkRel.RELATED_RESOURCE)]).toEqual(createStateResource(pendingCommand2)); + }); + }); + describe('isConcurrentModification', () => { it('should return true on matching error message', () => { - const doesMatch: boolean = isConcurrentModification( - CommandErrorMessage.CONCURRENT_MODIFICATION, - ); + const doesMatch: boolean = isConcurrentModification(CommandErrorMessage.CONCURRENT_MODIFICATION); expect(doesMatch).toBeTruthy(); }); diff --git a/alfa-client/libs/command-shared/src/lib/command.util.ts b/alfa-client/libs/command-shared/src/lib/command.util.ts index 70e446bee62a1e798e79f117c85e8caae8e54641..820a223daa245a5e15b94e33ca0be22e43edc944 100644 --- a/alfa-client/libs/command-shared/src/lib/command.util.ts +++ b/alfa-client/libs/command-shared/src/lib/command.util.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { createStateResource } from '@alfa-client/tech-shared'; import { getEmbeddedResource, getUrl, hasLink, ResourceUri } from '@ngxp/rest'; import { isEmpty, isNil, isObject } from 'lodash-es'; import { CommandLinkRel, CommandListLinkRel } from './command.linkrel'; @@ -51,21 +52,26 @@ export function notHasCommandError(commandResource: CommandResource): boolean { return !hasCommandError(commandResource); } -export function getPendingCommandByOrder( - pendingCommands: CommandListResource, - commandOrder: any[], -): CommandResource { - const commands: CommandResource[] = getEmbeddedCommandResources(pendingCommands).filter( - (command) => commandOrder.includes(command.order), +export function getPendingCommandByOrder(pendingCommands: CommandListResource, commandOrder: any[]): CommandResource { + const commands: CommandResource[] = getEmbeddedCommandResources(pendingCommands).filter((command: CommandResource) => + commandOrder.includes(command.order), ); return commands.length > 0 ? commands[0] : null; } +export function buildPendingCommandMap(commandList: CommandListResource): any { + let map = {}; + const commands: CommandResource[] = getEmbeddedCommandResources(commandList); + commands.forEach((command: CommandResource) => { + if (hasLink(command, CommandLinkRel.RELATED_RESOURCE)) { + map[getUrl(command, CommandLinkRel.RELATED_RESOURCE)] = createStateResource(command); + } + }); + return map; +} + function getEmbeddedCommandResources(commandListResource: CommandListResource): CommandResource[] { - return getEmbeddedResource<CommandResource[]>( - commandListResource, - CommandListLinkRel.COMMAND_LIST, - ); + return getEmbeddedResource<CommandResource[]>(commandListResource, CommandListLinkRel.COMMAND_LIST); } export function doIfCommandIsDone(commandResource: CommandResource, action: () => void) { diff --git a/alfa-client/libs/command-shared/test/command.ts b/alfa-client/libs/command-shared/test/command.ts index 05fec9de4806f6473cf2d4204ee196ed71db5da0..976d353de258274ba0774b039db2436fcda6af6e 100644 --- a/alfa-client/libs/command-shared/test/command.ts +++ b/alfa-client/libs/command-shared/test/command.ts @@ -36,6 +36,7 @@ import { CreateCommand, CreateCommandProps, CreateCommandPropsWithoutResource, + PendingCommandMap, } from '../src/lib/command.model'; export function createCommand(): Command { @@ -104,3 +105,7 @@ export function createSuccessfullyDoneCommandStateResource(): StateResource<Comm export function createSuccessfullyDoneCommandResource(): CommandResource { return createCommandResource([CommandLinkRel.EFFECTED_RESOURCE]); } + +export function createPendingCommandMap(): PendingCommandMap { + return { [faker.internet.url()]: createCommandStateResource() }; +} diff --git a/alfa-client/libs/postfach-shared/src/lib/postfach.service.spec.ts b/alfa-client/libs/postfach-shared/src/lib/postfach.service.spec.ts index 492502ba27a0198e6c7366785db8e66c0581cf67..5c12de2fd83f0627efe61ab7dd04681173fba0a3 100644 --- a/alfa-client/libs/postfach-shared/src/lib/postfach.service.spec.ts +++ b/alfa-client/libs/postfach-shared/src/lib/postfach.service.spec.ts @@ -31,10 +31,12 @@ import { VorgangService, VorgangWithEingangResource } from '@alfa-client/vorgang import { TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { expect } from '@jest/globals'; +import { getUrl } from '@ngxp/rest'; import { CommandLinkRel } from 'libs/command-shared/src/lib/command.linkrel'; import { createCommandErrorResource, createCommandResource } from 'libs/command-shared/test/command'; import { createVorgangWithEingangResource } from 'libs/vorgang-shared/test/vorgang'; -import { BehaviorSubject, of } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { singleColdCompleted } from '../../../tech-shared/test/marbles'; import { createPostfachFeatures, createPostfachMail, @@ -632,4 +634,35 @@ describe('PostfachService', () => { }); }); }); + + describe('get pending resend command', () => { + const postfachNachricht: PostfachMailResource = createPostfachMailResource(); + + const command: CommandResource = createCommandResource([CommandLinkRel.EFFECTED_RESOURCE]); + const commandStateResource: StateResource<CommandResource> = createStateResource(command); + + beforeEach(() => { + vorgangService.getAndPollPendingCommand.mockReturnValue(of(commandStateResource)); + service._refreshPostfachMailList = jest.fn(); + }); + + it('should call vorgang service', () => { + service.getPendingResendCommand(postfachNachricht); + + expect(vorgangService.getAndPollPendingCommand).toHaveBeenCalledWith(getUrl(postfachNachricht)); + }); + + it('should call refresh list on command successfully done', () => { + service.getPendingResendCommand(postfachNachricht).subscribe(); + + expect(service._refreshPostfachMailList).toHaveBeenCalled(); + }); + + it('should return value', () => { + const pendingResendCommand$: Observable<StateResource<CommandResource>> = + service.getPendingResendCommand(postfachNachricht); + + expect(pendingResendCommand$).toBeObservable(singleColdCompleted(commandStateResource)); + }); + }); }); diff --git a/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts b/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts index b7fbfbe62dbf7c0df54ff24e869acb90b3c2f343..a5dbb53f46bae1c4865d7801284fd7cf6eafb406 100644 --- a/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts +++ b/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts @@ -30,6 +30,7 @@ import { hasCommandError, isDone, isPending, + tapOnCommandSuccessfullyDone, } from '@alfa-client/command-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; import { @@ -46,7 +47,7 @@ import { VorgangResource, VorgangService, VorgangWithEingangResource } from '@al import { inject, Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Params } from '@angular/router'; -import { hasLink, Resource } from '@ngxp/rest'; +import { getUrl, hasLink, Resource } from '@ngxp/rest'; import { isNil, isNull } from 'lodash-es'; import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; import { first, map, take, tap } from 'rxjs/operators'; @@ -75,11 +76,12 @@ export class PostfachService { private readonly postfachFacade = inject(PostfachFacade); private readonly binaryFileService = inject(BinaryFileService); - private readonly isPollSendPostachMail: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); postfachMailList$: BehaviorSubject<StateResource<PostfachMailListResource>> = new BehaviorSubject< StateResource<PostfachMailListResource> >(createEmptyStateResource<PostfachMailListResource>()); + private readonly isPollSendPostachMail: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); + private navigationSubscription: Subscription; private sendPostfachMailSubscription: Subscription; @@ -160,6 +162,14 @@ export class PostfachService { .pipe(map((pendingCommand) => this._pollSendPostfachMailCommand(pendingCommand))); } + public getPendingResendCommand(postfachNachricht: PostfachMailResource): Observable<StateResource<CommandResource>> { + return this.vorgangService.getAndPollPendingCommand(getUrl(postfachNachricht)).pipe( + tapOnCommandSuccessfullyDone((commandStateResource: StateResource<CommandResource>) => { + this._refreshPostfachMailList(commandStateResource); + }), + ); + } + _listenToNavigation(): void { this._unsubscribeToNavigation(); this.navigationSubscription = this.navigationService.urlChanged().subscribe((params) => this._onNavigation(params)); diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.spec.ts b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.spec.ts index 20855cb12108e3adf98ba393635f4d40f96ac861..c531c4853a57517bf34c42989ffafe9a53ad8f6e 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.spec.ts +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.spec.ts @@ -22,11 +22,11 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { CommandResource } from '@alfa-client/command-shared'; -import { PostfachMailLinkRel, PostfachService } from '@alfa-client/postfach-shared'; +import { PostfachMailLinkRel, PostfachMailResource, PostfachService } from '@alfa-client/postfach-shared'; import { createEmptyStateResource, createStateResource, HasLinkPipe, StateResource } from '@alfa-client/tech-shared'; import { existsAsHtmlElement, Mock, mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createCommandResource } from 'libs/command-shared/test/command'; +import { createCommandResource, createCommandStateResource } from 'libs/command-shared/test/command'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; @@ -39,19 +39,23 @@ describe('OutgoingMailErrorContainerComponent', () => { let component: OutgoingMailErrorContainerComponent; let fixture: ComponentFixture<OutgoingMailErrorContainerComponent>; - let postfachService: Mock<PostfachService>; + let service: Mock<PostfachService>; const mailError: string = getDataTestIdOf('outgoing-mail-error'); + const postfachNachricht: PostfachMailResource = createPostfachMailResource(); + + const commandStateResource: StateResource<CommandResource> = createCommandStateResource(); + beforeEach(async () => { - postfachService = mock(PostfachService); + service = mock(PostfachService); await TestBed.configureTestingModule({ declarations: [OutgoingMailErrorContainerComponent, MockComponent(OutgoingMailErrorComponent), HasLinkPipe], providers: [ { provide: PostfachService, - useValue: postfachService, + useValue: service, }, ], }).compileComponents(); @@ -60,6 +64,7 @@ describe('OutgoingMailErrorContainerComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(OutgoingMailErrorContainerComponent); component = fixture.componentInstance; + component.postfachMailResource = postfachNachricht; fixture.detectChanges(); }); @@ -67,17 +72,35 @@ describe('OutgoingMailErrorContainerComponent', () => { expect(component).toBeTruthy(); }); + describe('on init', () => { + beforeEach(() => { + service.getPendingResendCommand.mockReturnValue(of(commandStateResource)); + }); + + it('should call postfach service to get pending resend command', () => { + component.ngOnInit(); + + expect(service.getPendingResendCommand).toHaveBeenCalledWith(postfachNachricht); + }); + + it('should assign response', () => { + component.ngOnInit(); + + expect(component.resendPostfachMailStateResource$).toBeObservable(singleColdCompleted(commandStateResource)); + }); + }); + describe('resendMail', () => { const commandStateResource: StateResource<CommandResource> = createStateResource(createCommandResource()); beforeEach(() => { - postfachService.resendMail.mockReturnValue(of(commandStateResource)); + service.resendMail.mockReturnValue(of(commandStateResource)); }); it('should call postfach service', () => { component.resendMail(); - expect(postfachService.resendMail).toHaveBeenCalled(); + expect(service.resendMail).toHaveBeenCalled(); }); it('should assign service response', () => { diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.ts b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.ts index c0d8d6589d785007b6c620f9bedca94970c0651c..221f5d478b05b50fcff96c18db9ecdf79526cc3d 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.ts +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-list-container/postfach-mail-list/postfach-mail/outgoing-mail/outgoing-mail-error-container/outgoing-mail-error-container.component.ts @@ -41,8 +41,8 @@ export class OutgoingMailErrorContainerComponent implements OnInit { public readonly PostfachMailLinkRel = PostfachMailLinkRel; - ngOnInit() { - this.resendPostfachMailStateResource$ = this.postfachService.getPendingSendPostfachMailCommand(); + ngOnInit(): void { + this.resendPostfachMailStateResource$ = this.postfachService.getPendingResendCommand(this.postfachMailResource); } public resendMail(): void { diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts index 45eb6a8ae7f6c43bec2d6da77fa34e1c44098f1d..e22edf896564d9f90368bb704f730a588316b0e4 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.actions.ts @@ -21,25 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { HttpErrorResponse } from '@angular/common/http'; import { ApiRootResource } from '@alfa-client/api-root-shared'; import { LoadBinaryFileListSuccessProps } from '@alfa-client/binary-file-shared'; -import { - CommandStateResourceProps, - LoadCommandListSuccessProps, -} from '@alfa-client/command-shared'; -import { - ApiErrorAction, - ResourceUriProps, - TypedActionCreator, - TypedActionCreatorWithProps, -} from '@alfa-client/tech-shared'; +import { CommandStateResourceProps, LoadCommandListSuccessProps, UpdateCommandProps } from '@alfa-client/command-shared'; +import { ApiErrorAction, ResourceUriProps, TypedActionCreator, TypedActionCreatorWithProps } from '@alfa-client/tech-shared'; +import { HttpErrorResponse } from '@angular/common/http'; import { createAction, props } from '@ngrx/store'; -import { - AdditionalActions, - VorgangListResource, - VorgangWithEingangResource, -} from '../vorgang.model'; +import { AdditionalActions, VorgangListResource, VorgangWithEingangResource } from '../vorgang.model'; export interface SearchVorgaengeByProps { apiRoot: ApiRootResource; @@ -96,16 +84,16 @@ export const searchVorgaengeBy: TypedActionCreatorWithProps<SearchVorgaengeByPro '[Vorgang] Search VorgangList', props<SearchVorgaengeByProps>(), ); -export const searchVorgaengeBySuccess: TypedActionCreatorWithProps<VorgangListAction> = - createAction('[Vorgang] Search VorgangList Success', props<VorgangListAction>()); +export const searchVorgaengeBySuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction( + '[Vorgang] Search VorgangList Success', + props<VorgangListAction>(), +); export const searchVorgaengeByFailure: TypedActionCreatorWithProps<HttpErrorAction> = createAction( '[Vorgang] Search VorgangList Failure', props<HttpErrorAction>(), ); -export const loadNextPage: TypedActionCreator = createAction( - '[Vorgang] Load next VorgangList page', -); +export const loadNextPage: TypedActionCreator = createAction('[Vorgang] Load next VorgangList page'); export const loadNextPageSuccess: TypedActionCreatorWithProps<VorgangListAction> = createAction( '[Vorgang] Load next VorgangList page Success', props<VorgangListAction>(), @@ -125,9 +113,7 @@ export const searchForPreviewFailure: TypedActionCreatorWithProps<HttpErrorActio props<HttpErrorAction>(), ); -export const clearSearchPreviewList: TypedActionCreator = createAction( - '[Vorgang] Clear search preview list', -); +export const clearSearchPreviewList: TypedActionCreator = createAction('[Vorgang] Clear search preview list'); export const clearSearchString: TypedActionCreator = createAction('[Vorgang] Clear search string'); export const setSearchString: TypedActionCreatorWithProps<StringBasedProps> = createAction( @@ -136,56 +122,70 @@ export const setSearchString: TypedActionCreatorWithProps<StringBasedProps> = cr ); //VorgangWithEingang -export const loadVorgangWithEingang: TypedActionCreatorWithProps<VorgangUriWithContextProps> = - createAction('[Vorgang] Load VorgangWithEingang', props<VorgangUriWithContextProps>()); -export const loadVorgangWithEingangSuccess: TypedActionCreatorWithProps<VorgangWithEingangAction> = - createAction('[Vorgang] Load VorgangWithEingang Success', props<VorgangWithEingangAction>()); -export const loadVorgangWithEingangFailure: TypedActionCreatorWithProps<ApiErrorAction> = - createAction('[Vorgang] Load VorgangWithEingang Failure', props<ApiErrorAction>()); - -export const clearVorgangWithEingang: TypedActionCreator = createAction( - '[Vorgang] Clear VorgangWithEingang', -); -export const setReloadVorgangWithEingang: TypedActionCreator = createAction( - '[Vorgang] Set reload at VorgangWithEingang', -); - -export const loadPendingCommandList: TypedActionCreatorWithProps<VorgangWithEingangAction> = - createAction('[Vorgang] Load pending command list', props<VorgangWithEingangAction>()); -export const loadPendingCommandListSuccess: TypedActionCreatorWithProps<LoadCommandListSuccessProps> = - createAction( - '[Vorgang/API] Load pending command list Success', - props<LoadCommandListSuccessProps>(), - ); -export const loadPendingCommandListFailure: TypedActionCreatorWithProps<ApiErrorAction> = - createAction('[Vorgang/API] Load pending command list Failure', props<ApiErrorAction>()); - -export const loadAttachmentList: TypedActionCreatorWithProps<VorgangWithEingangAction> = - createAction('[Vorgang] Load AttachmentList', props<VorgangWithEingangAction>()); -export const loadAttachmentListSuccess: TypedActionCreatorWithProps<LoadBinaryFileListSuccessProps> = - createAction('[Vorgang] Load AttachmentList Success', props<LoadBinaryFileListSuccessProps>()); +export const loadVorgangWithEingang: TypedActionCreatorWithProps<VorgangUriWithContextProps> = createAction( + '[Vorgang] Load VorgangWithEingang', + props<VorgangUriWithContextProps>(), +); +export const loadVorgangWithEingangSuccess: TypedActionCreatorWithProps<VorgangWithEingangAction> = createAction( + '[Vorgang] Load VorgangWithEingang Success', + props<VorgangWithEingangAction>(), +); +export const loadVorgangWithEingangFailure: TypedActionCreatorWithProps<ApiErrorAction> = createAction( + '[Vorgang] Load VorgangWithEingang Failure', + props<ApiErrorAction>(), +); + +export const clearVorgangWithEingang: TypedActionCreator = createAction('[Vorgang] Clear VorgangWithEingang'); +export const setReloadVorgangWithEingang: TypedActionCreator = createAction('[Vorgang] Set reload at VorgangWithEingang'); + +export const loadPendingCommandList: TypedActionCreatorWithProps<VorgangWithEingangAction> = createAction( + '[Vorgang] Load pending command list', + props<VorgangWithEingangAction>(), +); +export const loadPendingCommandListSuccess: TypedActionCreatorWithProps<LoadCommandListSuccessProps> = createAction( + '[Vorgang/API] Load pending command list Success', + props<LoadCommandListSuccessProps>(), +); +export const loadPendingCommandListFailure: TypedActionCreatorWithProps<ApiErrorAction> = createAction( + '[Vorgang/API] Load pending command list Failure', + props<ApiErrorAction>(), +); + +export const loadAttachmentList: TypedActionCreatorWithProps<VorgangWithEingangAction> = createAction( + '[Vorgang] Load AttachmentList', + props<VorgangWithEingangAction>(), +); +export const loadAttachmentListSuccess: TypedActionCreatorWithProps<LoadBinaryFileListSuccessProps> = createAction( + '[Vorgang] Load AttachmentList Success', + props<LoadBinaryFileListSuccessProps>(), +); export const loadAttachmentListFailure: TypedActionCreatorWithProps<ApiErrorAction> = createAction( '[Vorgang] Load AttachmentList Failure', props<ApiErrorAction>(), ); -export const loadRepresentationList: TypedActionCreatorWithProps<VorgangWithEingangAction> = - createAction('[Vorgang] Load RepresentationList', props<VorgangWithEingangAction>()); -export const loadRepresentationListSuccess: TypedActionCreatorWithProps<LoadBinaryFileListSuccessProps> = - createAction( - '[Vorgang] Load RepresentationList Success', - props<LoadBinaryFileListSuccessProps>(), - ); -export const loadRepresentationListFailure: TypedActionCreatorWithProps<ApiErrorAction> = - createAction('[Vorgang] Load RepresentationList Failure', props<ApiErrorAction>()); +export const loadRepresentationList: TypedActionCreatorWithProps<VorgangWithEingangAction> = createAction( + '[Vorgang] Load RepresentationList', + props<VorgangWithEingangAction>(), +); +export const loadRepresentationListSuccess: TypedActionCreatorWithProps<LoadBinaryFileListSuccessProps> = createAction( + '[Vorgang] Load RepresentationList Success', + props<LoadBinaryFileListSuccessProps>(), +); +export const loadRepresentationListFailure: TypedActionCreatorWithProps<ApiErrorAction> = createAction( + '[Vorgang] Load RepresentationList Failure', + props<ApiErrorAction>(), +); -export const setForwardingSingleCommand: TypedActionCreatorWithProps<CommandStateResourceProps> = - createAction('[Vorgang] Set forward command', props<CommandStateResourceProps>()); -export const setForwardingSingleCommandLoading: TypedActionCreator = createAction( - '[Vorgang] Set forward command loading', +export const setForwardingSingleCommand: TypedActionCreatorWithProps<CommandStateResourceProps> = createAction( + '[Vorgang] Set forward command', + props<CommandStateResourceProps>(), +); +export const setForwardingSingleCommandLoading: TypedActionCreator = createAction('[Vorgang] Set forward command loading'); +export const setSendPostfachNachrichtSingleCommand: TypedActionCreatorWithProps<CommandStateResourceProps> = createAction( + '[Vorgang] Set send postfach nachricht command', + props<CommandStateResourceProps>(), ); -export const setSendPostfachNachrichtSingleCommand: TypedActionCreatorWithProps<CommandStateResourceProps> = - createAction('[Vorgang] Set send postfach nachricht command', props<CommandStateResourceProps>()); export const setSendPostfachNachrichtSingleCommandLoading: TypedActionCreator = createAction( '[Vorgang] Set send postfach nachricht command loading', ); @@ -201,3 +201,13 @@ export const exportVorgangFailure: TypedActionCreatorWithProps<ApiErrorAction> = '[Vorgang] Export Failure', props<ApiErrorAction>(), ); + +export const setPendingCommandLoading: TypedActionCreatorWithProps<ResourceUriProps> = createAction( + '[Vorgang] Set pending command loading', + props<ResourceUriProps>(), +); + +export const updatePendingCommand: TypedActionCreatorWithProps<UpdateCommandProps> = createAction( + '[Vorgang] Update pending command', + props<UpdateCommandProps>(), +); diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts index c2e342d93cc22a74bd18be0d1c135b5e51606c50..70e6360f7622e932ef269e54cfebd67fdae19524 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts @@ -23,7 +23,14 @@ */ import { ApiRootResource } from '@alfa-client/api-root-shared'; import { BinaryFileListResource } from '@alfa-client/binary-file-shared'; -import { CommandListResource, CommandOrder, CommandResource, CreateCommand } from '@alfa-client/command-shared'; +import { + CommandListResource, + CommandOrder, + CommandResource, + CreateCommand, + CreateCommandProps, + PendingCommandMap, +} from '@alfa-client/command-shared'; import { RouteData } from '@alfa-client/navigation-shared'; import { ApiError, @@ -35,11 +42,19 @@ import { } from '@alfa-client/tech-shared'; import { HttpErrorResponse } from '@angular/common/http'; import { UrlSegment } from '@angular/router'; +import { faker } from '@faker-js/faker'; import { Action } from '@ngrx/store'; import { Resource, ResourceUri } from '@ngxp/rest'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createBinaryFileListResource } from 'libs/binary-file-shared/test/binary-file'; -import { createCommand, createCommandListResource, createCommandResource } from 'libs/command-shared/test/command'; +import { + createCommand, + createCommandListResource, + createCommandResource, + createCommandStateResource, + createCreateCommandProps, + createPendingCommandMap, +} from 'libs/command-shared/test/command'; import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; import { createDummyResource } from 'libs/tech-shared/test/resource'; import { @@ -70,11 +85,10 @@ import { import { VorgangListAction } from './vorgang.actions'; import { VorgangState, initialState, reducer } from './vorgang.reducer'; -import { faker } from '@faker-js/faker'; - import * as Storage from '@alfa-client/app-shared'; import * as CommandActions from '@alfa-client/command-shared'; import * as NavigationActions from '@alfa-client/navigation-shared'; +import * as CommandUtil from '../../../../command-shared/src/lib/command.util'; import * as VorgangActions from './vorgang.actions'; import * as Reducer from './vorgang.reducer'; @@ -508,6 +522,52 @@ describe('Vorgang Reducer', () => { expect(state.sendPostfachNachrichtPendingCommand.resource).toBe(sendPostfachNachrichtCommand); }); + + it('should call build pending command map', () => { + const buildPendingCommandMapSpy: jest.SpyInstance = jest.spyOn(CommandUtil, 'buildPendingCommandMap'); + const commandList: CommandListResource = createCommandListResource(); + const action: Action<string> = VorgangActions.loadPendingCommandListSuccess({ commandList }); + + reducer(initialState, action); + + expect(buildPendingCommandMapSpy).toHaveBeenCalledWith(commandList); + }); + + it('should set pending commands map', () => { + const pendingCommandMap: PendingCommandMap = createPendingCommandMap(); + jest.spyOn(CommandUtil, 'buildPendingCommandMap').mockReturnValue(pendingCommandMap); + const action = VorgangActions.loadPendingCommandListSuccess({ commandList: createCommandListResource() }); + + const state: VorgangState = reducer(initialState, action); + + expect(state.pendingCommandMap).toBe(pendingCommandMap); + }); + }); + describe('on "setPendingCommandLoading" action', () => { + it('should set map entry state resource loading by uri', () => { + const resourceUri: ResourceUri = faker.internet.url(); + const action = VorgangActions.setPendingCommandLoading({ resourceUri }); + const pendingCommandMap: PendingCommandMap = { [resourceUri]: createCommandStateResource() }; + const initialStateWithPendingCommandMap: VorgangState = { ...initialState, pendingCommandMap }; + + const state: VorgangState = reducer(initialStateWithPendingCommandMap, action); + + expect(state.pendingCommandMap[resourceUri].loading).toBeTruthy(); + }); + }); + + describe('on "updatePendingCommand" action', () => { + it('should upadte map entry state resource by uri', () => { + const relatedResourceUri: ResourceUri = faker.internet.url(); + const command: CommandResource = createCommandResource(); + const action = VorgangActions.updatePendingCommand({ relatedResourceUri, command }); + const pendingCommandMap: PendingCommandMap = { [relatedResourceUri]: { ...createCommandStateResource(), loading: true } }; + const initialStateWithPendingCommandMap: VorgangState = { ...initialState, pendingCommandMap }; + + const state: VorgangState = reducer(initialStateWithPendingCommandMap, action); + + expect(state.pendingCommandMap[relatedResourceUri]).toEqual(createStateResource(command)); + }); }); }); @@ -606,12 +666,13 @@ describe('Vorgang Reducer', () => { }); describe('createCommandSuccess', () => { + const createCommandProps: CreateCommandProps = createCreateCommandProps(); const command: CommandResource = createCommandResource(); it('should call getStatusCommandMapByCreateCommandSuccess', () => { const spy = jest.spyOn(Reducer, 'getStatusCommandMapByCreateCommandSuccess'); - const action = CommandActions.createCommandSuccess({ command }); + const action = CommandActions.createCommandSuccess({ command, createCommandProps }); reducer(initialState, action); @@ -621,7 +682,7 @@ describe('Vorgang Reducer', () => { it('should call getAssignUserCommandByCreateCommandSuccess', () => { const spy = jest.spyOn(Reducer, 'getAssignUserCommandByCreateCommandSuccess'); - const action = CommandActions.createCommandSuccess({ command }); + const action = CommandActions.createCommandSuccess({ command, createCommandProps }); reducer(initialState, action); @@ -631,7 +692,7 @@ describe('Vorgang Reducer', () => { it('should call getVorgangWithEingangStateResourceByCreateCommandSucces', () => { const spy = jest.spyOn(Reducer, 'getVorgangWithEingangStateResourceByCreateCommandSucces'); - const action = CommandActions.createCommandSuccess({ command }); + const action = CommandActions.createCommandSuccess({ command, createCommandProps }); reducer(initialState, action); diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts index 2601e8b7044b3d484d2556d1e418d1642dffa17f..f9d63437e95446be5017db55593efad8034fa2c0 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts @@ -31,12 +31,16 @@ import { CreateCommand, CreateCommandProps, LoadCommandListSuccessProps, + PendingCommandMap, + UpdateCommandProps, + buildPendingCommandMap, getPendingCommandByOrder, } from '@alfa-client/command-shared'; import { RouteData } from '@alfa-client/navigation-shared'; import { ApiErrorAction, EMPTY_STRING, + ResourceUriProps, StateResource, createEmptyStateResource, createErrorStateResource, @@ -92,6 +96,7 @@ export interface VorgangState { assignUserCommand: StateResource<CommandResource>; forwardPendingCommand: StateResource<CommandResource>; sendPostfachNachrichtPendingCommand: StateResource<CommandResource>; + pendingCommandMap: PendingCommandMap; statusCommandMap: StatusCommandMap; revokeCommand: StateResource<CommandResource>; vorgangExport: StateResource<boolean>; @@ -112,6 +117,7 @@ export const initialState: VorgangState = { assignUserCommand: createEmptyStateResource(), forwardPendingCommand: createEmptyStateResource(), sendPostfachNachrichtPendingCommand: createEmptyStateResource(), + pendingCommandMap: <any>{}, statusCommandMap: <any>{}, revokeCommand: createEmptyStateResource(), vorgangExport: createEmptyStateResource(), @@ -332,6 +338,27 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( sendPostfachNachrichtPendingCommand: createStateResource( getPendingCommandByOrder(props.commandList, [CommandOrder.SEND_POSTFACH_NACHRICHT]), ), + pendingCommandMap: buildPendingCommandMap(props.commandList), + }), + ), + on( + VorgangActions.setPendingCommandLoading, + (state: VorgangState, props: ResourceUriProps): VorgangState => ({ + ...state, + pendingCommandMap: { + ...state.pendingCommandMap, + [props.resourceUri]: { ...state.pendingCommandMap[props.resourceUri], loading: true }, + }, + }), + ), + on( + VorgangActions.updatePendingCommand, + (state: VorgangState, props: UpdateCommandProps): VorgangState => ({ + ...state, + pendingCommandMap: { + ...state.pendingCommandMap, + [props.relatedResourceUri]: createStateResource(props.command), + }, }), ), diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts index bd7c1be9950413ff508ffc6464104492450cc428..e8f4098294ad879795320d5897c5eea28c430b0e 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.spec.ts @@ -24,8 +24,10 @@ import { BinaryFileListResource } from '@alfa-client/binary-file-shared'; import { CommandOrder, CommandResource } from '@alfa-client/command-shared'; import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { faker } from '@faker-js/faker/.'; +import { ResourceUri } from '@ngxp/rest'; import { createBinaryFileListResource } from 'libs/binary-file-shared/test/binary-file'; -import { createCommandResource } from 'libs/command-shared/test/command'; +import { createCommandResource, createCommandStateResource } from 'libs/command-shared/test/command'; import { createVorgangListResource, createVorgangResources, @@ -33,6 +35,7 @@ import { createVorgangWithEingangResource, } from 'libs/vorgang-shared/test/vorgang'; import { + PendingCommandMap, StatusCommandMap, VorgangFilter, VorgangListResource, @@ -49,42 +52,33 @@ import * as VorgangSelectors from './vorgang.selectors'; describe('Vorgang Selectors', () => { let state: VorgangPartialState; - const vorgangList: StateResource<VorgangListResource> = createStateResource( - createVorgangListResource(), - ); + const vorgangList: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); const vorgaenge: VorgangResource[] = createVorgangResources(); - const vorgangStatistic: StateResource<VorgangStatistic> = - createStateResource(createVorgangStatistic()); - const searchPreviewList: StateResource<VorgangListResource> = createStateResource( - createVorgangListResource(), - ); + const vorgangStatistic: StateResource<VorgangStatistic> = createStateResource(createVorgangStatistic()); + const searchPreviewList: StateResource<VorgangListResource> = createStateResource(createVorgangListResource()); const searchString: string = 'searchThisForMe'; const vorgangFilter: VorgangFilter = VorgangFilter.ALLE; const vorgangView: VorgangView = VorgangView.VORGANG_LIST; - const vorgangWithEingang: StateResource<VorgangWithEingangResource> = createStateResource( - createVorgangWithEingangResource(), - ); - const attachmentList: StateResource<BinaryFileListResource> = createStateResource( - createBinaryFileListResource(), - ); - const representationList: StateResource<BinaryFileListResource> = createStateResource( - createBinaryFileListResource(), - ); - const assignUserCommand: StateResource<CommandResource> = - createStateResource(createCommandResource()); - const forwardPendingCommand: StateResource<CommandResource> = - createStateResource(createCommandResource()); - const sendPostfachNachrichtPendingCommand: StateResource<CommandResource> = - createStateResource(createCommandResource()); - - const annehmenCommand: StateResource<CommandResource> = - createStateResource(createCommandResource()); + const vorgangWithEingang: StateResource<VorgangWithEingangResource> = createStateResource(createVorgangWithEingangResource()); + const attachmentList: StateResource<BinaryFileListResource> = createStateResource(createBinaryFileListResource()); + const representationList: StateResource<BinaryFileListResource> = createStateResource(createBinaryFileListResource()); + const assignUserCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); + const forwardPendingCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); + const sendPostfachNachrichtPendingCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); + + const annehmenCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); const statusCommandMap: StatusCommandMap = <any>{ [CommandOrder.VORGANG_ANNEHMEN]: annehmenCommand, }; - const revokeCommand: StateResource<CommandResource> = - createStateResource(createCommandResource()); + + const pendingCommandUri: ResourceUri = faker.internet.url(); + const pendingCommandStateResource: StateResource<CommandResource> = createCommandStateResource(); + const pendingCommandMap: PendingCommandMap = <any>{ + [pendingCommandUri]: pendingCommandStateResource, + }; + + const revokeCommand: StateResource<CommandResource> = createStateResource(createCommandResource()); const vorgangExport: StateResource<boolean> = createStateResource(false); beforeEach(() => { @@ -106,6 +100,7 @@ describe('Vorgang Selectors', () => { forwardPendingCommand, sendPostfachNachrichtPendingCommand, statusCommandMap, + pendingCommandMap, revokeCommand, vorgangExport, }, @@ -113,9 +108,7 @@ describe('Vorgang Selectors', () => { }); it('should return vorgangList', () => { - const result: StateResource<VorgangListResource> = VorgangSelectors.vorgangList.projector( - state.VorgangState, - ); + const result: StateResource<VorgangListResource> = VorgangSelectors.vorgangList.projector(state.VorgangState); expect(result).toBe(vorgangList); }); @@ -127,9 +120,7 @@ describe('Vorgang Selectors', () => { }); it('should return vorgangStatistic', () => { - const result: StateResource<VorgangStatistic> = VorgangSelectors.vorgangStatistic.projector( - state.VorgangState, - ); + const result: StateResource<VorgangStatistic> = VorgangSelectors.vorgangStatistic.projector(state.VorgangState); expect(result).toBe(vorgangStatistic); }); @@ -147,9 +138,7 @@ describe('Vorgang Selectors', () => { }); it('should return searchPreviewList', () => { - const result: StateResource<VorgangListResource> = VorgangSelectors.searchPreviewList.projector( - state.VorgangState, - ); + const result: StateResource<VorgangListResource> = VorgangSelectors.searchPreviewList.projector(state.VorgangState); expect(result).toBe(searchPreviewList); }); @@ -172,17 +161,13 @@ describe('Vorgang Selectors', () => { describe('isVorgangViewSelected', () => { it('should return true if state and view matches', () => { - const result: boolean = VorgangSelectors.isVorgangViewSelected( - VorgangView.VORGANG_LIST, - ).projector(vorgangView); + const result: boolean = VorgangSelectors.isVorgangViewSelected(VorgangView.VORGANG_LIST).projector(vorgangView); expect(result).toBeTruthy(); }); it('should return false if state and view does not match', () => { - const result: boolean = VorgangSelectors.isVorgangViewSelected(VorgangView.NEU).projector( - vorgangView, - ); + const result: boolean = VorgangSelectors.isVorgangViewSelected(VorgangView.NEU).projector(vorgangView); expect(result).toBeFalsy(); }); @@ -190,9 +175,7 @@ describe('Vorgang Selectors', () => { describe('getVorgangViewRoutePath', () => { it('should return /alle/neu', () => { - const result: string = VorgangSelectors.getVorgangViewRoutePath(VorgangView.NEU).projector( - vorgangFilter, - ); + const result: string = VorgangSelectors.getVorgangViewRoutePath(VorgangView.NEU).projector(vorgangFilter); expect(result).toBe('/alle/neu'); }); @@ -200,16 +183,14 @@ describe('Vorgang Selectors', () => { //VorgangWithEingang it('should return vorgangWithEingang', () => { - const result: StateResource<VorgangWithEingangResource> = - VorgangSelectors.vorgangWithEingang.projector(state.VorgangState); + const result: StateResource<VorgangWithEingangResource> = VorgangSelectors.vorgangWithEingang.projector(state.VorgangState); expect(result).toBe(vorgangWithEingang); }); describe('attachmentList', () => { it('should Return attachmentList from state', () => { - const result: StateResource<BinaryFileListResource> = - VorgangSelectors.attachmentList.projector(state.VorgangState); + const result: StateResource<BinaryFileListResource> = VorgangSelectors.attachmentList.projector(state.VorgangState); expect(result).toBe(attachmentList); }); @@ -217,8 +198,7 @@ describe('Vorgang Selectors', () => { describe('representationList', () => { it('should Return representationList from state', () => { - const result: StateResource<BinaryFileListResource> = - VorgangSelectors.representationList.projector(state.VorgangState); + const result: StateResource<BinaryFileListResource> = VorgangSelectors.representationList.projector(state.VorgangState); expect(result).toBe(representationList); }); @@ -226,8 +206,7 @@ describe('Vorgang Selectors', () => { describe('forwardPendingCommand', () => { it('should return command from state', () => { - const result: StateResource<CommandResource> = - VorgangSelectors.forwardPendingCommand.projector(state.VorgangState); + const result: StateResource<CommandResource> = VorgangSelectors.forwardPendingCommand.projector(state.VorgangState); expect(result).toBe(forwardPendingCommand); }); @@ -235,8 +214,9 @@ describe('Vorgang Selectors', () => { describe('send postfach command', () => { it('should return command from state', () => { - const result: StateResource<CommandResource> = - VorgangSelectors.sendPostfachNachrichtPendingCommand.projector(state.VorgangState); + const result: StateResource<CommandResource> = VorgangSelectors.sendPostfachNachrichtPendingCommand.projector( + state.VorgangState, + ); expect(result).toBe(sendPostfachNachrichtPendingCommand); }); @@ -244,9 +224,7 @@ describe('Vorgang Selectors', () => { describe('user assign command', () => { it('should return command from state', () => { - const result: StateResource<CommandResource> = VorgangSelectors.assignUserCommand.projector( - state.VorgangState, - ); + const result: StateResource<CommandResource> = VorgangSelectors.assignUserCommand.projector(state.VorgangState); expect(result).toBe(assignUserCommand); }); @@ -254,9 +232,7 @@ describe('Vorgang Selectors', () => { describe('stausCommandMap', () => { it('should select statusCommandMap from state', () => { - const result: StatusCommandMap = VorgangSelectors.statusCommandMap.projector( - state.VorgangState, - ); + const result: StatusCommandMap = VorgangSelectors.statusCommandMap.projector(state.VorgangState); expect(result).toBe(statusCommandMap); }); @@ -264,9 +240,7 @@ describe('Vorgang Selectors', () => { describe('getStatusCommand', () => { it('should return statusCommand from state by order', () => { - const result: boolean = VorgangSelectors.getStatusCommand( - CommandOrder.VORGANG_ANNEHMEN, - ).projector(statusCommandMap); + const result: boolean = VorgangSelectors.getStatusCommand(CommandOrder.VORGANG_ANNEHMEN).projector(statusCommandMap); expect(result).toBe(annehmenCommand); }); @@ -274,9 +248,7 @@ describe('Vorgang Selectors', () => { describe('revokeCommand', () => { it('should return revokeCommand from state', () => { - const result: StateResource<CommandResource> = VorgangSelectors.revokeCommand.projector( - state.VorgangState, - ); + const result: StateResource<CommandResource> = VorgangSelectors.revokeCommand.projector(state.VorgangState); expect(result).toBe(revokeCommand); }); @@ -284,11 +256,26 @@ describe('Vorgang Selectors', () => { describe('exportCommand', () => { it('should return exportCommand from state', () => { - const result: StateResource<boolean> = VorgangSelectors.vorgangExport.projector( - state.VorgangState, - ); + const result: StateResource<boolean> = VorgangSelectors.vorgangExport.projector(state.VorgangState); expect(result).toBe(vorgangExport); }); }); + + describe('pendingCommandMap', () => { + it('should select statusCommandMap from state', () => { + const result: PendingCommandMap = VorgangSelectors.pendingCommandMap.projector(state.VorgangState); + + expect(result).toBe(pendingCommandMap); + }); + }); + + describe('getPendingCommand', () => { + it('should return statusCommand from state by order', () => { + const commandStateResource: StateResource<CommandResource> = + VorgangSelectors.getPendingCommand(pendingCommandUri).projector(pendingCommandMap); + + expect(commandStateResource).toBe(pendingCommandStateResource); + }); + }); }); diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts index 581b6ead76fbd7af02135c93a4af6900b963b403..0eeceedcce608d9bce4954dbbe850d5ed14c5d8a 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.selectors.ts @@ -25,8 +25,10 @@ import { BinaryFileListResource } from '@alfa-client/binary-file-shared'; import { CommandOrder, CommandResource } from '@alfa-client/command-shared'; import { EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; import { MemoizedSelector, createFeatureSelector, createSelector } from '@ngrx/store'; +import { ResourceUri } from '@ngxp/rest'; import { buildBackButtonUrl, buildVorgangFilterViewRoutePath } from '../vorgang-navigation.util'; import { + PendingCommandMap, StatusCommandMap, VorgangFilter, VorgangListResource, @@ -37,44 +39,36 @@ import { } from '../vorgang.model'; import { VORGANG_FEATURE_KEY, VorgangState } from './vorgang.reducer'; -export const getVorgangState: MemoizedSelector<object, VorgangState> = - createFeatureSelector<VorgangState>(VORGANG_FEATURE_KEY); +export const getVorgangState: MemoizedSelector<object, VorgangState> = createFeatureSelector<VorgangState>(VORGANG_FEATURE_KEY); // VorgangList -export const vorgangList: MemoizedSelector< - VorgangState, - StateResource<VorgangListResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.vorgangList); +export const vorgangList: MemoizedSelector<VorgangState, StateResource<VorgangListResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.vorgangList, +); export const vorgaenge: MemoizedSelector<VorgangState, VorgangResource[]> = createSelector( getVorgangState, (state: VorgangState) => state.vorgaenge, ); -export const vorgangStatistic: MemoizedSelector< - VorgangState, - StateResource<VorgangStatistic> -> = createSelector(getVorgangState, (state: VorgangState) => state.vorgangStatistic); - -export const vorgangFilter = createSelector( +export const vorgangStatistic: MemoizedSelector<VorgangState, StateResource<VorgangStatistic>> = createSelector( getVorgangState, - (state: VorgangState) => state.vorgangFilter, + (state: VorgangState) => state.vorgangStatistic, ); -export const vorgangView = createSelector( + +export const vorgangFilter = createSelector(getVorgangState, (state: VorgangState) => state.vorgangFilter); +export const vorgangView = createSelector(getVorgangState, (state: VorgangState) => state.vorgangView); + +export const searchPreviewList: MemoizedSelector<VorgangState, StateResource<VorgangListResource>> = createSelector( getVorgangState, - (state: VorgangState) => state.vorgangView, + (state: VorgangState) => state.searchPreviewList, ); - -export const searchPreviewList: MemoizedSelector< - VorgangState, - StateResource<VorgangListResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.searchPreviewList); export const searchString: MemoizedSelector<VorgangState, string> = createSelector( getVorgangState, (state: VorgangState) => state.searchString, ); -export const backButtonUrl: MemoizedSelector<VorgangState, string> = createSelector( - getVorgangState, - (state: VorgangState) => buildBackButtonUrl(state), +export const backButtonUrl: MemoizedSelector<VorgangState, string> = createSelector(getVorgangState, (state: VorgangState) => + buildBackButtonUrl(state), ); export const isVorgangViewSelected = (view: VorgangView) => @@ -85,64 +79,59 @@ export const getVorgangViewRoutePath = (view: VorgangView) => ); export const isVorgangFilterSelected = (filter: VorgangFilter) => - createSelector( - vorgangFilter, - (vorgangFilterInState: VorgangFilter) => vorgangFilterInState === filter, - ); + createSelector(vorgangFilter, (vorgangFilterInState: VorgangFilter) => vorgangFilterInState === filter); export const getVorgangFilterRoutePath = (filter: VorgangFilter) => - createSelector( - vorgangView, - searchString, - (vorgangViewInState: VorgangView, searchStringInState: string) => - buildVorgangFilterViewRoutePath(filter, vorgangViewInState, searchStringInState), + createSelector(vorgangView, searchString, (vorgangViewInState: VorgangView, searchStringInState: string) => + buildVorgangFilterViewRoutePath(filter, vorgangViewInState, searchStringInState), ); //VorgangWithEingang -export const vorgangWithEingang: MemoizedSelector< - VorgangState, - StateResource<VorgangWithEingangResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.vorgangWithEingang); -export const attachmentList: MemoizedSelector< - VorgangState, - StateResource<BinaryFileListResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.attachmentList); -export const representationList: MemoizedSelector< - VorgangState, - StateResource<BinaryFileListResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.representationList); -export const forwardPendingCommand: MemoizedSelector< - VorgangState, - StateResource<CommandResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.forwardPendingCommand); -export const sendPostfachNachrichtPendingCommand: MemoizedSelector< - VorgangState, - StateResource<CommandResource> -> = createSelector( +export const vorgangWithEingang: MemoizedSelector<VorgangState, StateResource<VorgangWithEingangResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.vorgangWithEingang, +); +export const attachmentList: MemoizedSelector<VorgangState, StateResource<BinaryFileListResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.attachmentList, +); +export const representationList: MemoizedSelector<VorgangState, StateResource<BinaryFileListResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.representationList, +); +export const forwardPendingCommand: MemoizedSelector<VorgangState, StateResource<CommandResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.forwardPendingCommand, +); +export const sendPostfachNachrichtPendingCommand: MemoizedSelector<VorgangState, StateResource<CommandResource>> = createSelector( getVorgangState, (state: VorgangState) => state.sendPostfachNachrichtPendingCommand, ); -export const assignUserCommand: MemoizedSelector< - VorgangState, - StateResource<CommandResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.assignUserCommand); +export const assignUserCommand: MemoizedSelector<VorgangState, StateResource<CommandResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.assignUserCommand, +); export const statusCommandMap: MemoizedSelector<VorgangState, StatusCommandMap> = createSelector( getVorgangState, (state: VorgangState) => state.statusCommandMap, ); export const getStatusCommand = (order: CommandOrder) => - createSelector( - statusCommandMap, - (statusCommandMapInState: StatusCommandMap) => statusCommandMapInState[order], - ); + createSelector(statusCommandMap, (statusCommandMapInState: StatusCommandMap) => statusCommandMapInState[order]); -export const revokeCommand: MemoizedSelector< - VorgangState, - StateResource<CommandResource> -> = createSelector(getVorgangState, (state: VorgangState) => state.revokeCommand); +export const revokeCommand: MemoizedSelector<VorgangState, StateResource<CommandResource>> = createSelector( + getVorgangState, + (state: VorgangState) => state.revokeCommand, +); export const vorgangExport: MemoizedSelector<VorgangState, StateResource<boolean>> = createSelector( getVorgangState, (state: VorgangState) => state.vorgangExport, ); + +export const pendingCommandMap: MemoizedSelector<VorgangState, PendingCommandMap> = createSelector( + getVorgangState, + (state: VorgangState) => state.pendingCommandMap, +); +export const getPendingCommand = (uri: ResourceUri) => + createSelector(pendingCommandMap, (pendingCommandMapInState: PendingCommandMap) => pendingCommandMapInState[uri]); diff --git a/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.spec.ts b/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.spec.ts index 5e9995262ed38703521d02d10ee078a330163cbd..f272624b26f3fb9eca620c332ea29393be63c4a1 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.spec.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.spec.ts @@ -36,15 +36,21 @@ import { EMPTY_STRING, StateResource, createEmptyStateResource, createStateResou import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { HttpErrorResponse } from '@angular/common/http'; import { faker } from '@faker-js/faker'; +import { Store } from '@ngrx/store'; import { ResourceUri, getUrl } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createBinaryFileListResource } from 'libs/binary-file-shared/test/binary-file'; import { CommandLinkRel } from 'libs/command-shared/src/lib/command.linkrel'; -import { createCommandResource, createCreateCommandPropsWithoutResource } from 'libs/command-shared/test/command'; +import { + createCommandResource, + createCommandStateResource, + createCreateCommandPropsWithoutResource, +} from 'libs/command-shared/test/command'; import { singleColdCompleted } from 'libs/tech-shared/test/marbles'; import { createVorgangWithEingangResource } from 'libs/vorgang-shared/test/vorgang'; import { Observable, of } from 'rxjs'; +import { setPendingCommandLoading, updatePendingCommand } from './+state/vorgang.actions'; import { VorgangFacade } from './+state/vorgang.facade'; import { VorgangWithEingangLinkRel } from './vorgang.linkrel'; import { AdditionalActions, VorgangResource, VorgangWithEingangResource } from './vorgang.model'; @@ -54,11 +60,13 @@ import * as VorgangNavigationUtil from './vorgang-navigation.util'; describe('VorgangService', () => { let service: VorgangService; + + let envConfig: Environment = <any>{ processorNames: ['dummyProcessorName'] }; let navigationService: Mock<NavigationService>; let facade: Mock<VorgangFacade>; let apiRootService: Mock<ApiRootService>; let commandService: Mock<CommandService>; - let envConfig: Environment = <any>{ processorNames: ['dummyProcessorName'] }; + let store: Mock<Store>; beforeEach(() => { navigationService = { ...mock(NavigationService) }; @@ -67,13 +75,15 @@ describe('VorgangService', () => { commandService = mock(CommandService); navigationService.urlChanged = jest.fn(); navigationService.urlChanged.mockReturnValue(of({})); + store = mock(Store); service = new VorgangService( + envConfig, useFromMock(navigationService), useFromMock(facade), useFromMock(apiRootService), useFromMock(commandService), - envConfig, + useFromMock(store), ); }); @@ -581,4 +591,87 @@ describe('VorgangService', () => { expect(service.reloadVorgang).toHaveBeenCalled(); }); }); + + describe('get and poll pending command', () => { + const resourceUri: ResourceUri = faker.internet.url(); + + const command: CommandResource = createCommandResource(); + const commandStateResource: StateResource<CommandResource> = createStateResource(command); + + beforeEach(() => { + service.selectPendingCommand = jest.fn().mockReturnValue(of(commandStateResource)); + service._pollCommand = jest.fn().mockReturnValue(of(commandStateResource)); + }); + + it('should dispatch "set pending loading" action', () => { + service.getAndPollPendingCommand(resourceUri); + + expect(store.dispatch).toHaveBeenCalledWith(setPendingCommandLoading({ resourceUri })); + }); + + it('should call select pending command', () => { + service.getAndPollPendingCommand(resourceUri); + + expect(service.selectPendingCommand).toHaveBeenCalledWith(resourceUri); + }); + + it('should call poll command', () => { + service.getAndPollPendingCommand(resourceUri).subscribe(); + + expect(service._pollCommand).toHaveBeenCalledWith(resourceUri, command); + }); + + it('should return value', () => { + const pendingCommandStateResource$: Observable<StateResource<CommandResource>> = + service.getAndPollPendingCommand(resourceUri); + + expect(pendingCommandStateResource$).toBeObservable(singleColdCompleted(commandStateResource)); + }); + }); + + describe('select pending command', () => { + const resourceUri: ResourceUri = faker.internet.url(); + const commandStateResource: StateResource<CommandResource> = createCommandStateResource(); + + beforeEach(() => { + store.select.mockReturnValue(of(commandStateResource)); + }); + + it('should return selected value from store', () => { + const commandStateResource$: Observable<StateResource<CommandResource>> = service.selectPendingCommand(resourceUri); + + expect(commandStateResource$).toBeObservable(singleColdCompleted(commandStateResource)); + }); + }); + + describe('poll command', () => { + const resourceUri: ResourceUri = faker.internet.url(); + + const command: CommandResource = createCommandResource([CommandLinkRel.EFFECTED_RESOURCE]); + const commandStateResource: StateResource<CommandResource> = createStateResource(command); + + beforeEach(() => { + commandService.pollCommand.mockReturnValue(of(commandStateResource)); + }); + + it('should call command service', () => { + service._pollCommand(resourceUri, command); + + expect(commandService.pollCommand).toHaveBeenCalledWith(command); + }); + + it('should dispatch "update pending command" action on command successfully done', () => { + service._pollCommand(resourceUri, command).subscribe(); + + expect(store.dispatch).toHaveBeenLastCalledWith( + updatePendingCommand({ relatedResourceUri: resourceUri, command: commandStateResource.resource }), + ); + }); + + it('should return value', () => { + const commandStateResource$: Observable<StateResource<CommandResource>> = service._pollCommand(resourceUri, command); + + expect(commandStateResource$).toBeObservable(singleColdCompleted(commandStateResource)); + }); + }); }); diff --git a/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.ts b/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.ts index 7e43262e9757d58e030c4bbe2900aae791255e96..f946fa0aa35da5d677db5aaa8844145e6162774f 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/vorgang.service.ts @@ -36,10 +36,13 @@ import { ENVIRONMENT_CONFIG, Environment } from '@alfa-client/environment-shared import { NavigationService } from '@alfa-client/navigation-shared'; import { StateResource, createEmptyStateResource, doIfLoadingRequired, isNotNull, mapToResource } from '@alfa-client/tech-shared'; import { Inject, Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; import { ResourceUri, hasLink } from '@ngxp/rest'; import { Observable, combineLatest } from 'rxjs'; import { filter, first, map, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { setPendingCommandLoading, updatePendingCommand } from './+state/vorgang.actions'; import { VorgangFacade } from './+state/vorgang.facade'; +import { getPendingCommand } from './+state/vorgang.selectors'; import { buildLinkRelFromPathSegments } from './vorgang-navigation.util'; import { VorgangWithEingangLinkRel } from './vorgang.linkrel'; import { AdditionalActions, VorgangWithEingangResource } from './vorgang.model'; @@ -50,11 +53,12 @@ export class VorgangService { public static readonly VORGANG_WITH_EINGANG_URL: string = 'vorgangWithEingangUrl'; constructor( - private navigationService: NavigationService, - private facade: VorgangFacade, - private apiRootService: ApiRootService, - private commandService: CommandService, @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, + private readonly navigationService: NavigationService, + private readonly facade: VorgangFacade, + private readonly apiRootService: ApiRootService, + private readonly commandService: CommandService, + private readonly store: Store, ) {} public getVorgangWithEingang(): Observable<StateResource<VorgangWithEingangResource>> { @@ -220,4 +224,30 @@ export class VorgangService { public reloadVorgang(commandResource: CommandResource): void { this.facade.loadVorgangWithEingang(getEffectedResourceUrl(commandResource)); } + + public getAndPollPendingCommand(resourceUri: ResourceUri): Observable<StateResource<CommandResource>> { + this.store.dispatch(setPendingCommandLoading({ resourceUri })); + return this.selectPendingCommand(resourceUri).pipe( + mapToResource(), + switchMap((command: CommandResource) => this._pollCommand(resourceUri, command)), + ); + } + + public selectPendingCommand(relatedResourceUri: ResourceUri): Observable<StateResource<CommandResource>> { + return this.store.select(getPendingCommand(relatedResourceUri)); + } + + _pollCommand(resourceUri: ResourceUri, command: CommandResource): Observable<StateResource<CommandResource>> { + return this.commandService + .pollCommand(command) + .pipe( + tapOnCommandSuccessfullyDone((commandStateResource: StateResource<CommandResource>) => + this.updatePendingCommand(resourceUri, commandStateResource.resource), + ), + ); + } + + private updatePendingCommand(relatedResourceUri: ResourceUri, command: CommandResource): void { + this.store.dispatch(updatePendingCommand({ relatedResourceUri, command })); + } }