diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a41cb3fd7d..b82a902936 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,5 +1,11 @@ ## RELEASE NOTES +### Version 7.1.36-rc1 +**EXUI-2104** Event History Summary - ExUI Changes +**EXUI-2668** when Update Referral event completed (incorrect) +**EXUI-2595** Spinner on 'Manage Support'n'Request Support' events +**EXUI-2743** Check Application Task is supposed to AutoClose + ### Version 7.1.36 **EXUI-1562** accessibility issue @@ -47,8 +53,7 @@ Taken by PR **EXUI-2389** PED and Media Viewer ### Version 7.0.75-exui-2315 -**EXUI-2315** etrieve current user language selection - +**EXUI-2315** Retrieve current user language selection ### Version 7.0.75-exui-2515 **EXUI-2515** case-link-issue diff --git a/package.json b/package.json index b352105dd2..d70eca0dfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.1.36", + "version": "7.1.36-rc2", "engines": { "node": ">=18.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/package.json b/projects/ccd-case-ui-toolkit/package.json index d7ee4fb19b..a655d2eed4 100644 --- a/projects/ccd-case-ui-toolkit/package.json +++ b/projects/ccd-case-ui-toolkit/package.json @@ -1,6 +1,6 @@ { "name": "@hmcts/ccd-case-ui-toolkit", - "version": "7.1.36", + "version": "7.1.36-rc2", "engines": { "node": ">=18.19.0" }, diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.spec.ts index 32525da16f..8571636f43 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.spec.ts @@ -1,4 +1,5 @@ -import { CaseEditUtils, convertNonASCIICharacter } from "./case-edit.utils"; +import { SessionStorageService } from "../../../services"; +import { CaseEditUtils, convertNonASCIICharacter, removeTaskFromClientContext } from "./case-edit.utils"; describe('CaseEditUtils', () => { const caseUtils: CaseEditUtils = new CaseEditUtils(); @@ -61,4 +62,34 @@ describe('CaseEditUtils', () => { }); }); + describe('removeTaskFromClientContext', () => { + const fullMockClientContext = { client_context: { user_task: 'task', user_language: 'language' } }; + const partialMockClientContext = { client_context: { user_language: 'language' } }; + + let mockSessionStorageService; + + beforeEach(() => { + mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', ['getItem', 'removeItem', 'setItem']); + }) + + it('should correctly remove task from client context', () => { + mockSessionStorageService.getItem.and.returnValue(JSON.stringify(fullMockClientContext)); + removeTaskFromClientContext(mockSessionStorageService); + expect(mockSessionStorageService.setItem).toHaveBeenCalledWith('clientContext', JSON.stringify(partialMockClientContext)); + }); + + it('should do nothing if there is no session storage service', () => { + removeTaskFromClientContext(null); + mockSessionStorageService.getItem.and.returnValue(null); + removeTaskFromClientContext(mockSessionStorageService); + expect(mockSessionStorageService.setItem).not.toHaveBeenCalled(); + }); + + it('should do nothing if there is no user_task', () => { + mockSessionStorageService.getItem.and.returnValue(JSON.stringify(partialMockClientContext)); + removeTaskFromClientContext(mockSessionStorageService); + expect(mockSessionStorageService.setItem).not.toHaveBeenCalled(); + }); + }); + }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.ts index c5c42061d6..88c7e7239d 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit-utils/case-edit.utils.ts @@ -1,3 +1,5 @@ +import { SessionStorageService } from "../../../services"; +import { CaseEditComponent } from "../case-edit/case-edit.component"; export function convertNonASCIICharacter(character: string): string { if (character === '£') { @@ -36,3 +38,15 @@ export class CaseEditUtils { return rawString; } } + +export function removeTaskFromClientContext(sessionStorageService: SessionStorageService): void { + if (!sessionStorageService) { + return; + } + const clientContextString = sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); + const clientContext = clientContextString ? JSON.parse(clientContextString) : null; + if (clientContext?.client_context?.user_task) { + delete clientContext.client_context.user_task; + sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(clientContext)); + } +} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts index 43b38d3b6b..ae1fe03185 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.spec.ts @@ -11,7 +11,7 @@ import { CaseEventTrigger } from '../../../domain/case-view/case-event-trigger.m import { CaseField } from '../../../domain/definition/case-field.model'; import { createCaseEventTrigger } from '../../../fixture/shared.test.fixture'; import { FieldsFilterPipe } from '../../../pipes/complex/fields-filter.pipe'; -import { AlertService, FieldsPurger, FieldsUtils, LoadingService, SessionStorageService, WindowService } from '../../../services'; +import { AlertService, FieldsPurger, FieldsUtils, LoadingService, ReadCookieService, SessionStorageService, WindowService } from '../../../services'; import { FormErrorService, FormValueService } from '../../../services/form'; import { PaletteUtilsModule } from '../../palette'; import { Confirmation, Wizard, WizardPage, WizardPageField } from '../domain'; @@ -227,6 +227,7 @@ describe('CaseEditComponent', () => { let mockSessionStorageService: jasmine.SpyObj; let mockWorkAllocationService: jasmine.SpyObj; let mockAlertService: jasmine.SpyObj; + let mockCookieService: jasmine.SpyObj; let mockabstractConfig: jasmine.SpyObj; const validPageListCaseFieldsService = new ValidPageListCaseFieldsService(fieldsUtils); @@ -289,6 +290,7 @@ describe('CaseEditComponent', () => { mockSessionStorageService = createSpyObj('SessionStorageService', ['getItem', 'removeItem', 'setItem']); mockWorkAllocationService = createSpyObj('WorkAllocationService', ['assignAndCompleteTask', 'completeTask']); mockAlertService = createSpyObj('AlertService', ['error', 'setPreserveAlerts']); + mockCookieService = createSpyObj('ReadCookieService', ['getCookie']); mockabstractConfig = createSpyObj('AbstractAppConfig', ['logMessage']); spyOn(validPageListCaseFieldsService, 'deleteNonValidatedFields'); spyOn(validPageListCaseFieldsService, 'validPageListCaseFields'); @@ -342,6 +344,7 @@ describe('CaseEditComponent', () => { { provide: WorkAllocationService, useValue: mockWorkAllocationService}, { provide: SessionStorageService, useValue: mockSessionStorageService}, { provide: AlertService, useValue: mockAlertService }, + { provide: ReadCookieService, useValue: mockCookieService }, WindowService, { provide: LoadingService, loadingServiceMock }, { provide: ValidPageListCaseFieldsService, useValue: validPageListCaseFieldsService}, @@ -1242,8 +1245,8 @@ describe('CaseEditComponent', () => { expect(validPageListCaseFieldsService.validPageListCaseFields).toHaveBeenCalled(); expect(formValueService.removeUnnecessaryFields).toHaveBeenCalled(); // check that tasks removed from session storage once event has been completed - expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('clientContext'); - expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith('taskEventCompletionInfo'); + expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT); + expect(mockSessionStorageService.removeItem).toHaveBeenCalledWith(CaseEditComponent.TASK_EVENT_COMPLETION_INFO); }); it('should submit the case for a Case Flags submission', () => { @@ -1526,7 +1529,7 @@ describe('CaseEditComponent', () => { }); it('should return true when there is a task present that matches the current case when there is no event in session storage', () => { - const mockTask = {id: '123', case_id: '123456789'}; + const mockTask = {id: '123', case_id: '123456789', description: 'test test test [testEvent]'}; expect(component.taskExistsForThisEvent(mockTask as Task, null, mockEventDetails)).toBe(true); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts index 30565c51d4..38e01bea12 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-edit/case-edit.component.ts @@ -19,12 +19,14 @@ import { EventDetails, Task, TaskEventCompletionInfo } from '../../../domain/wor import { AlertService, FieldsPurger, FieldsUtils, FormErrorService, FormValueService, LoadingService, + ReadCookieService, SessionStorageService, WindowService } from '../../../services'; import { Confirmation, Wizard, WizardPage } from '../domain'; import { EventCompletionParams } from '../domain/event-completion-params.model'; import { CaseNotifier, WizardFactoryService, WorkAllocationService } from '../services'; import { ValidPageListCaseFieldsService } from '../services/valid-page-list-caseFields.service'; +import { removeTaskFromClientContext } from '../case-edit-utils/case-edit.utils'; @Component({ selector: 'ccd-case-edit', @@ -35,6 +37,8 @@ import { ValidPageListCaseFieldsService } from '../services/valid-page-list-case export class CaseEditComponent implements OnInit, OnDestroy { public static readonly ORIGIN_QUERY_PARAM = 'origin'; public static readonly ALERT_MESSAGE = 'Page is being refreshed so you will be redirected to the first page of this event.'; + public static readonly CLIENT_CONTEXT = 'clientContext'; + public static readonly TASK_EVENT_COMPLETION_INFO = 'taskEventCompletionInfo'; @Input() public eventTrigger: CaseEventTrigger; @@ -106,7 +110,8 @@ export class CaseEditComponent implements OnInit, OnDestroy { private readonly validPageListCaseFieldsService: ValidPageListCaseFieldsService, private readonly workAllocationService: WorkAllocationService, private readonly alertService: AlertService, - private readonly abstractConfig: AbstractAppConfig + private readonly abstractConfig: AbstractAppConfig, + private readonly cookieService: ReadCookieService ) {} public ngOnInit(): void { @@ -238,12 +243,12 @@ export class CaseEditComponent implements OnInit, OnDestroy { this.isSubmitting = true; // We have to run the event completion checks if task in session storage // and if the task is in session storage, then is it associated to the case - const clientContextStr = this.sessionStorageService.getItem('clientContext'); + const clientContextStr = this.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); const taskInSessionStorage = userTask ? userTask.task_data : null; let taskEventCompletionInfo: TaskEventCompletionInfo; let userInfo: UserInfo; - const taskEventCompletionStr = this.sessionStorageService.getItem('taskEventCompletionInfo'); + const taskEventCompletionStr = this.sessionStorageService.getItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO); const userInfoStr = this.sessionStorageService.getItem('userDetails'); const assignNeeded = this.sessionStorageService.getItem('assignNeeded'); if (taskEventCompletionStr) { @@ -272,7 +277,7 @@ export class CaseEditComponent implements OnInit, OnDestroy { userId, taskId: taskInSessionStorage.id, createdTimestamp: Date.now()}; - this.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); + this.sessionStorageService.setItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO, JSON.stringify(taskEventCompletionInfo)); this.isEventCompletionChecksRequired = true; } else { // Task not in session storage, proceed to submit @@ -460,8 +465,9 @@ export class CaseEditComponent implements OnInit, OnDestroy { }),finalize(() => { this.loadingService.unregister(loadingSpinnerToken); // on event completion ensure the previous event clientContext/taskEventCompletionInfo removed - this.sessionStorageService.removeItem('clientContext'); - this.sessionStorageService.removeItem('taskEventCompletionInfo') + // Note - Not removeTaskFromClientContext because could interfere with other logic + this.sessionStorageService.removeItem(CaseEditComponent.CLIENT_CONTEXT); + this.sessionStorageService.removeItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO) this.isSubmitting = false; })) .subscribe( @@ -492,7 +498,7 @@ export class CaseEditComponent implements OnInit, OnDestroy { } private postCompleteTaskIfRequired(): Observable { - const clientContextStr = this.sessionStorageService.getItem('clientContext'); + const clientContextStr = this.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); const [task, taskToBeCompleted] = userTask ? [userTask.task_data, userTask.complete_task] : [null, false]; const assignNeeded = this.sessionStorageService.getItem('assignNeeded') === 'true'; @@ -537,7 +543,14 @@ export class CaseEditComponent implements OnInit, OnDestroy { } if (!taskEventCompletionInfo) { // if no task event present then there is no task to complete from previous event present - return true; + // EXUI-2668 - Add additional logic to confirm the task is relevant to the event + if (this.taskIsForEvent(taskInSessionStorage, eventDetails)) { + return true; + } else { + // client context still needed for language + removeTaskFromClientContext(this.sessionStorageService); + return false; + } } else { if (taskEventCompletionInfo.taskId !== taskInSessionStorage.id) { return true; @@ -546,8 +559,8 @@ export class CaseEditComponent implements OnInit, OnDestroy { || this.eventMoreThanDayAgo(taskEventCompletionInfo.createdTimestamp) ) { // if the session storage not related to event, ignore it and remove - this.sessionStorageService.removeItem('clientContext'); - this.sessionStorageService.removeItem('taskEventCompletionInfo'); + removeTaskFromClientContext(this.sessionStorageService); + this.sessionStorageService.removeItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO); return false; } if (eventDetails.assignNeeded === 'false' && eventDetails.userId !== taskInSessionStorage.assignee) { @@ -593,4 +606,13 @@ export class CaseEditComponent implements OnInit, OnDestroy { } return false; } + + private taskIsForEvent(task: Task, eventDetails: EventDetails): boolean { + // EXUI-2668 - Ensure description for task includes event ID + // Note - This is a failsafe for an edge case that may never occur again + // Description may not include eventId in some cases which may mean task not completed (however this will be easy to check) + // In instances of the above taskEventCompletionInfo will be created to block this check from occurring + this.abstractConfig.logMessage(`checking taskIsForEvent: task ID ${task.id}, task description ${task.description}, event name ${eventDetails.eventId}`); + return task.case_id === eventDetails.caseId && (task.description?.includes(eventDetails.eventId)); + } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts index c2c6a76cae..4d22301080 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.spec.ts @@ -2,6 +2,7 @@ import { Component, Input, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { MockRpxTranslatePipe } from '../../../../../test/mock-rpx-translate.pipe'; +import { CaseEditComponent } from '../../../case-edit'; import { EventCompletionStateMachineContext } from '../../../domain'; import { CaseEventCompletionTaskCancelledComponent } from './case-event-completion-task-cancelled.component'; @@ -46,7 +47,7 @@ describe('TaskCancelledComponent', () => { it('should emit event can be completed true when clicked on continue button', () => { spyOn(component.notifyEventCompletionCancelled, 'emit'); component.onContinue(); - expect(component.context.sessionStorageService.removeItem).toHaveBeenCalledWith('clientContext') + expect(component.context.sessionStorageService.removeItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT); expect(component.notifyEventCompletionCancelled.emit).toHaveBeenCalledWith(true); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts index c2f5840dbb..de31081a8f 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-cancelled/case-event-completion-task-cancelled.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { COMPONENT_PORTAL_INJECTION_TOKEN, CaseEventCompletionComponent } from '../../case-event-completion.component'; import { EventCompletionStateMachineContext } from '../../../domain'; +import { CaseEditComponent } from '../../../case-edit'; @Component({ selector: 'app-case-event-completion-task-cancelled', @@ -20,7 +20,7 @@ export class CaseEventCompletionTaskCancelledComponent implements OnInit { public onContinue(): void { // Removes task to complete so event completes without task - this.context.sessionStorageService.removeItem('clientContext'); + this.context.sessionStorageService.removeItem(CaseEditComponent.CLIENT_CONTEXT); // may be able to remove this call below since it is now unneccesary this.notifyEventCompletionCancelled.emit(true); } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts index 1b55aafa0e..ad267025f8 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/case-event-completion/components/case-event-completion-task-reassigned/case-event-completion-task-reassigned.component.ts @@ -7,6 +7,7 @@ import { import { EventCompletionStateMachineContext } from '../../../domain'; import { CaseworkerService } from '../../../services/case-worker.service'; import { JudicialworkerService } from '../../../services/judicial-worker.service'; +import { CaseEditComponent } from '../../../case-edit'; @Component({ selector: 'app-case-event-completion-task-reassigned', @@ -77,7 +78,7 @@ export class CaseEventCompletionTaskReassignedComponent implements OnInit, OnDes public onContinue(): void { // Get task details - const clientContextStr = this.sessionStorageService.getItem('clientContext'); + const clientContextStr = this.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); const task = userTask ? userTask.task_data : null; // not complete_task not utilised here as related to event completion diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts index 299d0d8743..64eb15236a 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/cases.service.ts @@ -22,6 +22,7 @@ import { CaseAccessUtils } from '../case-access-utils'; import { WizardPage } from '../domain'; import { WizardPageFieldToCaseFieldMapper } from './wizard-page-field-to-case-field.mapper'; import { CaseEditUtils } from '../case-edit-utils/case-edit.utils'; +import { CaseEditComponent } from '../case-edit'; @Injectable() export class CasesService { @@ -395,7 +396,7 @@ export class CasesService { } private addClientContextHeader(headers: HttpHeaders): HttpHeaders { - const clientContextDetails = this.sessionStorageService.getItem('clientContext'); + const clientContextDetails = this.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); if (clientContextDetails) { const caseEditUtils = new CaseEditUtils(); // below changes non-ASCII characters @@ -414,8 +415,7 @@ export class CasesService { const clientContextString = window.atob(headers.get('Client-Context')); // below reverts non-ASCII characters const editedClientContextString = caseEditUtils.convertHTMLEntities(clientContextString); - this.sessionStorageService.setItem('clientContext', editedClientContextString); + this.sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, editedClientContextString); } } - } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts index 99d1114dbb..2a38ddfe7c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-editor/services/event-completion-state-machine.service.ts @@ -6,6 +6,7 @@ import { FieldsUtils } from '../../../services'; import { EventCompletionStateMachineContext } from '../domain/event-completion-state-machine-context.model'; import { EventCompletionStates } from '../domain/event-completion-states.enum.model'; import { EventCompletionTaskStates } from '../domain/event-completion-task-states.model'; +import { CaseEditComponent } from '../case-edit'; const EVENT_COMPLETION_STATE_MACHINE = 'EVENT COMPLETION STATE MACHINE'; @@ -139,7 +140,7 @@ export class EventCompletionStateMachineService { public entryActionForStateCompleteEventAndTask(state: State, context: EventCompletionStateMachineContext): void { // Trigger final state to complete processing of state machine state.trigger(EventCompletionStates.Final); - const clientContextStr = context.sessionStorageService.getItem('clientContext'); + const clientContextStr = context.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); if (userTask?.task_data) { context.sessionStorageService.setItem('assignNeeded', 'false'); @@ -163,7 +164,7 @@ export class EventCompletionStateMachineService { public entryActionForStateTaskUnassigned(state: State, context: EventCompletionStateMachineContext): void { // Trigger final state to complete processing of state machine state.trigger(EventCompletionStates.Final); - const clientContextStr = context.sessionStorageService.getItem('clientContext'); + const clientContextStr = context.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); if (userTask?.task_data) { context.sessionStorageService.setItem('assignNeeded', 'true'); @@ -233,7 +234,7 @@ export class EventCompletionStateMachineService { } public taskPresentInSessionStorage(context: EventCompletionStateMachineContext): boolean { - const clientContextStr = context.sessionStorageService.getItem('clientContext'); + const clientContextStr = context.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); const userTask = FieldsUtils.getUserTaskFromClientContext(clientContextStr); return !!userTask.task_data; } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.spec.ts index 2ba22edcf1..2fb497b9a7 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.spec.ts @@ -6,7 +6,7 @@ import { Observable, of } from 'rxjs'; import { CaseEventData, CaseEventTrigger, CaseField, CaseView, FieldType, HttpError } from '../../../domain'; import { createCaseEventTrigger } from '../../../fixture'; import { CaseReferencePipe } from '../../../pipes'; -import { ActivityPollingService, AlertService, FieldsUtils, SessionStorageService } from '../../../services'; +import { ActivityPollingService, AlertService, FieldsUtils, LoadingService, SessionStorageService } from '../../../services'; import { CaseNotifier, CasesService } from '../../case-editor'; import { CaseEventTriggerComponent } from './case-event-trigger.component'; import createSpyObj = jasmine.createSpyObj; @@ -130,6 +130,7 @@ describe('CaseEventTriggerComponent', () => { let alertService: any; let caseNotifier: any; let casesService: any; + let loadingService: any; let sessionStorageService: any; let casesReferencePipe: any; let activityPollingService: any; @@ -137,6 +138,7 @@ describe('CaseEventTriggerComponent', () => { beforeEach(waitForAsync(() => { caseNotifier = createSpyObj('caseService', ['announceCase']); casesService = createSpyObj('casesService', ['createEvent', 'validateCase']); + loadingService = new LoadingService(); casesService.createEvent.and.returnValue(of(true)); casesService.validateCase.and.returnValue(of(true)); @@ -178,7 +180,8 @@ describe('CaseEventTriggerComponent', () => { { provide: AlertService, useValue: alertService }, { provide: CaseReferencePipe, useValue: casesReferencePipe }, { provide: ActivityPollingService, useValue: activityPollingService }, - { provide: SessionStorageService, useValue: sessionStorageService } + { provide: SessionStorageService, useValue: sessionStorageService }, + { provide: LoadingService, useValue: loadingService } ] }) .compileComponents(); @@ -372,4 +375,12 @@ describe('CaseEventTriggerComponent', () => { component.cancel(); expect(router.navigate).toHaveBeenCalledWith(['cases', 'case-details', '1111-2222-3333-4444'], { fragment: 'Linked cases' }); }); + + it('should call unregisterStoredSpinner if there is a stored spinnter', () => { + spyOn(loadingService, 'hasSharedSpinner').and.returnValue(true); + spyOn(loadingService, 'unregisterSharedSpinner'); + component.ngOnInit(); + expect(loadingService.hasSharedSpinner).toBeTruthy(); + expect(loadingService.unregisterSharedSpinner).toHaveBeenCalled(); + }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts index 0824f61469..bd1a4376a0 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-event-trigger/case-event-trigger.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { Observable, Subscription, of } from 'rxjs'; import { Activity, CaseEventData, CaseEventTrigger, CaseView, DisplayMode } from '../../../domain'; import { CaseReferencePipe } from '../../../pipes'; -import { ActivityPollingService, AlertService, EventStatusService, FieldsUtils, SessionStorageService } from '../../../services'; +import { ActivityPollingService, AlertService, EventStatusService, FieldsUtils, LoadingService, SessionStorageService } from '../../../services'; import { CaseNotifier, CasesService } from '../../case-editor'; import { Constants } from '../../../commons/constants'; @@ -31,11 +31,15 @@ export class CaseEventTriggerComponent implements OnInit, OnDestroy { private readonly route: ActivatedRoute, private readonly caseReferencePipe: CaseReferencePipe, private readonly activityPollingService: ActivityPollingService, - private readonly sessionStorageService: SessionStorageService + private readonly sessionStorageService: SessionStorageService, + private readonly loadingService: LoadingService ) { } public ngOnInit(): void { + if (this.loadingService.hasSharedSpinner()){ + this.loadingService.unregisterSharedSpinner(); + } if (this.route.snapshot.data.case) { this.caseDetails = this.route.snapshot.data.case; } else { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.spec.ts index 78a38f6161..b715c9370d 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.spec.ts @@ -40,6 +40,7 @@ import { FormValueService, HttpErrorService, HttpService, + LoadingService, NavigationNotifierService, NavigationOrigin, ProfileNotifier, @@ -706,6 +707,7 @@ describe('CaseFullAccessViewComponent', () => { { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterMockService }, { provide: SessionStorageService, useValue: sessionStorageMockService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate']) }, + LoadingService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) @@ -1316,7 +1318,8 @@ describe('CaseFullAccessViewComponent - prependedTabs', () => { { provide: MatDialogConfig, useValue: DIALOG_CONFIG }, { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate']) }, - DeleteOrCancelDialogComponent + DeleteOrCancelDialogComponent, + LoadingService ], teardown: { destroyAfterEach: false } }) @@ -1676,6 +1679,7 @@ describe('CaseFullAccessViewComponent - ends with caseID', () => { PageValidationService, CaseFieldService, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate', 'getTranslation$']) }, + LoadingService ], teardown: { destroyAfterEach: false } }) @@ -1805,7 +1809,8 @@ describe('CaseFullAccessViewComponent - Overview with prepended Tabs', () => { { provide: MatDialogConfig, useValue: DIALOG_CONFIG }, { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate', 'getTranslation$']) }, - DeleteOrCancelDialogComponent + DeleteOrCancelDialogComponent, + LoadingService ], teardown: { destroyAfterEach: false } }) @@ -2097,7 +2102,8 @@ describe('CaseFullAccessViewComponent - get default hrefMarkdownLinkContent', () { provide: MatDialogConfig, useValue: DIALOG_CONFIG }, { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate']) }, - DeleteOrCancelDialogComponent + DeleteOrCancelDialogComponent, + LoadingService ], teardown: { destroyAfterEach: false } }) @@ -2266,7 +2272,8 @@ describe('CaseFullAccessViewComponent - findPreSelectedActiveTab', () => { { provide: MatDialogConfig, useValue: DIALOG_CONFIG }, { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate']) }, - DeleteOrCancelDialogComponent + DeleteOrCancelDialogComponent, + LoadingService ], teardown: { destroyAfterEach: false } }).compileComponents(); @@ -2731,7 +2738,8 @@ xdescribe('CaseFullAccessViewComponent - print and event selector disabled', () { provide: ConvertHrefToRouterService, useValue: convertHrefToRouterMockService }, { provide: SessionStorageService, useValue: sessionStorageMockService }, { provide: RpxTranslationService, useValue: createSpyObj('RpxTranslationService', ['translate']) }, - DeleteOrCancelDialogComponent + DeleteOrCancelDialogComponent, + LoadingService ] }) .compileComponents(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.ts index 0f3fe11226..806aaa34ac 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/case-full-access-view/case-full-access-view.component.ts @@ -25,6 +25,7 @@ import { DraftService, ErrorNotifierService, FieldsUtils, + LoadingService, NavigationNotifierService, NavigationOrigin, OrderService, @@ -96,7 +97,8 @@ export class CaseFullAccessViewComponent implements OnInit, OnDestroy, OnChanges private readonly location: Location, private readonly crf: ChangeDetectorRef, private readonly sessionStorageService: SessionStorageService, - private readonly rpxTranslationPipe: RpxTranslatePipe + private readonly rpxTranslationPipe: RpxTranslatePipe, + private readonly loadingService: LoadingService ) { } @@ -194,6 +196,8 @@ export class CaseFullAccessViewComponent implements OnInit, OnDestroy, OnChanges } public async applyTrigger(trigger: CaseViewTrigger): Promise { + const spinner = this.loadingService.register(); + this.loadingService.addSharedSpinner(spinner); this.errorNotifierService.announceError(null); const theQueryParams: Params = {}; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.spec.ts index cd93040fa4..6f77c1b9db 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.spec.ts @@ -4,7 +4,7 @@ import { AbstractAppConfig } from '../../../../app.config'; import { CaseEventTrigger, HttpError, Profile } from '../../../domain'; import { createAProfile } from '../../../domain/profile/profile.test.fixture'; import { createCaseEventTrigger } from '../../../fixture'; -import { ErrorNotifierService, HttpService, ProfileNotifier, ProfileService } from '../../../services'; +import { ErrorNotifierService, HttpService, LoadingService, ProfileNotifier, ProfileService } from '../../../services'; import { CaseResolver } from './case.resolver'; import { EventTriggerResolver } from './event-trigger.resolver'; @@ -31,6 +31,7 @@ describe('EventTriggerResolver', () => { let casesService: any; let alertService: any; let orderService: any; + let loadingService: any; let route: any; @@ -73,6 +74,7 @@ describe('EventTriggerResolver', () => { casesService = createSpyObj('casesService', ['getEventTrigger']); alertService = createSpyObj('alertService', ['error', 'setPreserveAlerts']); orderService = createSpyObj('orderService', ['sort']); + loadingService = createSpyObj('loadingService', ['sort']); errorNotifier = createSpyObj('errorNotifierService', ['announceError']); profileService = createSpyObj('profileService', ['get']); profileNotifier = new ProfileNotifier(); @@ -86,7 +88,7 @@ describe('EventTriggerResolver', () => { httpService = createSpyObj('httpService', ['get']); httpService.get.and.returnValue(of(MOCK_PROFILE)); - eventTriggerResolver = new EventTriggerResolver(casesService, alertService, profileService, profileNotifier, router, appConfig, errorNotifier); + eventTriggerResolver = new EventTriggerResolver(casesService, alertService, profileService, profileNotifier, router, appConfig, errorNotifier, loadingService); route = { firstChild: { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.ts index 9228c09379..90347e1996 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/case-viewer/services/event-trigger.resolver.ts @@ -10,6 +10,7 @@ import { ProfileService } from '../../../services/profile/profile.service'; import { CasesService } from '../../case-editor/services/cases.service'; import { AbstractAppConfig } from '../../../../app.config'; import { ErrorNotifierService } from '../../../services/error/error-notifier.service'; +import { LoadingService } from '../../../services'; @Injectable() export class EventTriggerResolver implements Resolve { @@ -27,6 +28,7 @@ export class EventTriggerResolver implements Resolve { private router: Router, private appConfig: AbstractAppConfig, private errorNotifier: ErrorNotifierService, + private readonly loadingService: LoadingService ) {} public resolve(route: ActivatedRouteSnapshot): Promise { @@ -62,8 +64,8 @@ export class EventTriggerResolver implements Resolve { return this.casesService .getEventTrigger(caseTypeId, eventTriggerId, cid, ignoreWarning) .pipe( - map(eventTrigger => this.cachedEventTrigger = eventTrigger), - catchError(error => { + map((eventTrigger) => this.cachedEventTrigger = eventTrigger), + catchError((error) => { error.details = { eventId: eventTriggerId, ...error.details }; this.alertService.setPreserveAlerts(true); this.alertService.error(error.message); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts index 8636750f2a..991721c7d4 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.spec.ts @@ -4,7 +4,7 @@ import { of } from 'rxjs'; import { TaskPayload } from '../../../domain/work-allocation/TaskPayload'; import { UserInfo } from '../../../domain/user/user-info.model'; import { ReadCookieService, SessionStorageService } from '../../../services'; -import { WorkAllocationService } from '../../case-editor'; +import { CaseEditComponent, WorkAllocationService } from '../../case-editor'; import { EventStartGuard } from './event-start.guard'; import { AbstractAppConfig } from '../../../../app.config'; @@ -76,7 +76,7 @@ describe('EventStartGuard', () => { result$.subscribe(result => { expect(result).toEqual(false); // check client context is set correctly - expect(sessionStorageService.setItem).toHaveBeenCalledWith('clientContext', JSON.stringify(mockClientContext)); + expect(sessionStorageService.setItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(mockClientContext)); }); }); @@ -90,7 +90,7 @@ describe('EventStartGuard', () => { result$.subscribe(result => { expect(result).toEqual(false); // check client context is set correctly - expect(sessionStorageService.setItem).toHaveBeenCalledWith('clientContext', JSON.stringify(mockClientContext)); + expect(sessionStorageService.setItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(mockClientContext)); }); }); @@ -153,16 +153,18 @@ describe('EventStartGuard', () => { describe('checkTaskInEventNotRequired', () => { const caseId = '1234567890'; + const eventId = 'eventId'; + const userId = 'testUser'; it('should return true if there are no tasks in the payload', () => { const mockEmptyPayload: TaskPayload = { task_required_for_event: false, tasks: [] }; - expect(guard.checkTaskInEventNotRequired(mockEmptyPayload, caseId, null)).toBe(true); + expect(guard.checkTaskInEventNotRequired(mockEmptyPayload, caseId, null, null, null)).toBe(true); }); it('should return true if there are no tasks assigned to the user', () => { const mockPayload: TaskPayload = {task_required_for_event: false, tasks}; sessionStorageService.getItem.and.returnValue(JSON.stringify(getExampleUserInfo())); - expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null)).toBe(true); + expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null, null, null)).toBe(true); }); it('should return true and navigate to event trigger if one task is assigned to user', () => { @@ -182,8 +184,8 @@ describe('EventStartGuard', () => { const mockPayload: TaskPayload = {task_required_for_event: false, tasks}; sessionStorageService.getItem.and.returnValue(JSON.stringify(getExampleUserInfo())); mockCookieService.getCookie.and.returnValue(mockLanguage); - expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null)).toBe(true); - expect(sessionStorageService.setItem).toHaveBeenCalledWith('clientContext', JSON.stringify(clientContext)); + expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null, null, null)).toBe(true); + expect(sessionStorageService.setItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(clientContext)); }); it('should return false with error navigation if there are more than 1 tasks assigned to the user', () => { @@ -191,11 +193,12 @@ describe('EventStartGuard', () => { tasks.push(tasks[0]); const mockPayload: TaskPayload = {task_required_for_event: false, tasks}; sessionStorageService.getItem.and.returnValue(JSON.stringify(getExampleUserInfo())); - expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null)).toBe(false); + expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, null, null, null)).toBe(false); expect(router.navigate).toHaveBeenCalledWith([`/cases/case-details/${caseId}/multiple-tasks-exist`]); }); it('should return true and navigate to event trigger if navigated to via task next steps', () => { + let baseTime = new Date(2025, 3, 2); const mockLanguage = 'cy'; const clientContext = { client_context: { @@ -208,13 +211,23 @@ describe('EventStartGuard', () => { } } } + const mockTaskEventCompletionInfo = { + caseId, + eventId, + userId, + taskId: '0d22d838-b25a-11eb-a18c-f2d58a9b7bc6', + createdTimestamp: baseTime.getTime() + } tasks[0].assignee = '1'; tasks.push(tasks[0]); const mockPayload: TaskPayload = {task_required_for_event: false, tasks}; sessionStorageService.getItem.and.returnValue(JSON.stringify(getExampleUserInfo())); mockCookieService.getCookie.and.returnValue(mockLanguage); - expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, '0d22d838-b25a-11eb-a18c-f2d58a9b7bc6')).toBe(true); - expect(sessionStorageService.setItem).toHaveBeenCalledWith('clientContext', JSON.stringify(clientContext)); + // mock the time so we get the correct timestamp + jasmine.clock().mockDate(baseTime); + expect(guard.checkTaskInEventNotRequired(mockPayload, caseId, '0d22d838-b25a-11eb-a18c-f2d58a9b7bc6', eventId, userId)).toBe(true); + expect(sessionStorageService.setItem).toHaveBeenCalledWith(CaseEditComponent.TASK_EVENT_COMPLETION_INFO, JSON.stringify(mockTaskEventCompletionInfo)); + expect(sessionStorageService.setItem).toHaveBeenCalledWith(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(clientContext)); }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts index 2472f22c03..0b059149b6 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/event-guard/event-start.guard.ts @@ -4,14 +4,14 @@ import { Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { AbstractAppConfig } from '../../../../app.config'; -import { TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; +import { Task, TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; import { TaskPayload } from '../../../domain/work-allocation/TaskPayload'; import { ReadCookieService, SessionStorageService } from '../../../services'; -import { WorkAllocationService } from '../../case-editor'; +import { CaseEditComponent, WorkAllocationService } from '../../case-editor'; +import { removeTaskFromClientContext } from '../../case-editor/case-edit-utils/case-edit.utils'; @Injectable() export class EventStartGuard implements CanActivate { - public static readonly CLIENT_CONTEXT = 'clientContext'; constructor(private readonly workAllocationService: WorkAllocationService, private readonly router: Router, @@ -33,7 +33,7 @@ export class EventStartGuard implements CanActivate { const caseInfoStr = this.sessionStorageService.getItem('caseInfo'); const languageCookie = this.cookieService.getCookie('exui-preferred-language'); const currentLanguage = !!languageCookie && languageCookie !== '' ? languageCookie : 'en'; - const preClientContext = this.sessionStorageService.getItem(EventStartGuard.CLIENT_CONTEXT); + const preClientContext = this.sessionStorageService.getItem(CaseEditComponent.CLIENT_CONTEXT); if (!preClientContext) { // creates client context for language if not already existing const storeClientContext = { @@ -43,7 +43,7 @@ export class EventStartGuard implements CanActivate { } } }; - this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); + this.sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); } else { const clientContextObj = JSON.parse(preClientContext); if (!clientContextObj?.client_context?.user_language) { @@ -56,7 +56,7 @@ export class EventStartGuard implements CanActivate { } } } - this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(clientContextAddLanguage)); + this.sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(clientContextAddLanguage)); } } if (caseInfoStr) { @@ -75,7 +75,7 @@ export class EventStartGuard implements CanActivate { return of(false); } - public checkTaskInEventNotRequired(payload: TaskPayload, caseId: string, taskId: string): boolean { + public checkTaskInEventNotRequired(payload: TaskPayload, caseId: string, taskId: string, eventId: string, userId: string): boolean { if (!payload || !payload.tasks) { return true; } @@ -104,56 +104,18 @@ export class EventStartGuard implements CanActivate { } else { task = tasksAssignedToUser[0]; } - const currentLanguage = this.cookieService.getCookie('exui-preferred-language'); - // if one task assigned to user, allow user to complete event - const storeClientContext = { - client_context: { - user_task: { - task_data: task, - complete_task: true - }, - user_language: { - language: currentLanguage - } - } - }; - this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); + this.setClientContextStorage(task, caseId, eventId, userId); return true; } } - private removeTaskFromSessionStorage(): void { - this.sessionStorageService.removeItem(EventStartGuard.CLIENT_CONTEXT); - } - private checkForTasks(payload: TaskPayload, caseId: string, eventId: string, taskId: string, userId: string): Observable { if (taskId && payload?.tasks?.length > 0) { const task = payload.tasks.find((t) => t.id == taskId); if (task) { - // Store task to session - const taskEventCompletionInfo: TaskEventCompletionInfo = { - caseId: caseId, - eventId: eventId, - userId: userId, - taskId: task.id, - createdTimestamp: Date.now() - }; - const currentLanguage = this.cookieService.getCookie('exui-preferred-language'); - const storeClientContext = { - client_context: { - user_task: { - task_data: task, - complete_task: true - }, - user_language: { - language: currentLanguage - } - } - }; - this.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); - this.sessionStorageService.setItem(EventStartGuard.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); + this.setClientContextStorage(task, caseId, eventId, userId); } else { - this.removeTaskFromSessionStorage(); + removeTaskFromClientContext(this.sessionStorageService); } } if (payload.task_required_for_event) { @@ -167,7 +129,33 @@ export class EventStartGuard implements CanActivate { this.router.navigate([`/cases/case-details/${caseId}/event-start`], { queryParams: { caseId, eventId, taskId } }); return of(false); } else { - return of(this.checkTaskInEventNotRequired(payload, caseId, taskId)); + return of(this.checkTaskInEventNotRequired(payload, caseId, taskId, eventId, userId)); } } + + // EXUI-2743 - Make taskEventCompletionInfo always available in session storage with client context + private setClientContextStorage(task: Task, caseId: string, eventId: string, userId: string): void { + // Store task to session + const taskEventCompletionInfo: TaskEventCompletionInfo = { + caseId: caseId, + eventId: eventId, + userId: userId, + taskId: task.id, + createdTimestamp: Date.now() + }; + const currentLanguage = this.cookieService.getCookie('exui-preferred-language'); + const storeClientContext = { + client_context: { + user_task: { + task_data: task, + complete_task: true + }, + user_language: { + language: currentLanguage + } + } + }; + this.sessionStorageService.setItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO, JSON.stringify(taskEventCompletionInfo)); + this.sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(storeClientContext)); + } } diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts index 5b95b4cf08..10e7474326 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/event-start/services/event-start-state-machine.service.ts @@ -4,6 +4,7 @@ import { State, StateMachine } from '@edium/fsm'; import { EventStartStateMachineContext, EventStartStates } from '../models'; import { TaskEventCompletionInfo } from '../../../domain/work-allocation/Task'; import { UserInfo } from '../../../domain/user/user-info.model'; +import { CaseEditComponent } from '../../case-editor'; const EVENT_STATE_MACHINE = 'EVENT STATE MACHINE'; @@ -199,7 +200,6 @@ export class EventStartStateMachineService { } } }; - context.sessionStorageService.setItem('clientContext', JSON.stringify(clientContext)); let userInfo: UserInfo; const userInfoStr = context.sessionStorageService.getItem('userDetails'); if (userInfoStr) { @@ -212,7 +212,9 @@ export class EventStartStateMachineService { userId: userInfo.id ? userInfo.id : userInfo.uid, taskId: task.id, createdTimestamp: Date.now()}; - context.sessionStorageService.setItem('taskEventCompletionInfo', JSON.stringify(taskEventCompletionInfo)); + context.sessionStorageService.setItem(CaseEditComponent.TASK_EVENT_COMPLETION_INFO, JSON.stringify(taskEventCompletionInfo)); + // EXUI-2668 - Only add client context when taskEventCompletionInfo is set - stops auto completing incorrect tasks + context.sessionStorageService.setItem(CaseEditComponent.CLIENT_CONTEXT, JSON.stringify(clientContext)); // Allow user to perform the event context.router.navigate([`/cases/case-details/${context.caseId}/trigger/${context.eventId}`], { relativeTo: context.route }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.html b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.html index a73525fd97..5c3b1263b9 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.html +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.html @@ -18,7 +18,8 @@
{{event.event_name | rpxTranslate}} + *ngIf="event.state_id !== 'Draft' && !isUserExternal" class="text-16 event-link" attr.aria-label="{{getAriaLabelforLink(event)}}">{{event.event_name | rpxTranslate}} + {{event.event_name | rpxTranslate}}
{{event.event_name}} diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.spec.ts index f36a4ea626..e401813345 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.spec.ts @@ -9,8 +9,10 @@ import { MockRpxTranslatePipe } from '../../../../test/mock-rpx-translate.pipe'; import { DatePipe } from '../../utils'; import { EventLogTableComponent } from './event-log-table.component'; import createSpyObj = jasmine.createSpyObj; +import { SessionStorageService } from '../../../../services'; describe('EventLogTableComponent', () => { + const mockSessionStorageService = jasmine.createSpyObj('SessionStorageService', ['getItem']); const EVENTS: CaseViewEvent[] = [ { @@ -57,6 +59,13 @@ describe('EventLogTableComponent', () => { const $TABLE_ROW_LINKS_TIMELINE = By.css('table>tbody>tr>td>div#case-timeline>a'); const $TABLE_ROW_LINKS_STANDALONE = By.css('table>tbody>tr>td>div:not(.tooltip)>a'); const $TABLE_COLUMNS = By.css('table>tbody>tr>td'); + const $TABLE_ROW_LINKS_TIMELINE_EXTERNAL = By.css('table>tbody>tr>td>div>span.event-link'); + + const USER = { + roles: [ + 'caseworker' + ] + }; const COL_EVENT = 0; const COL_DATE = 1; @@ -76,12 +85,15 @@ describe('EventLogTableComponent', () => { DatePipe, MockRpxTranslatePipe ], - providers: [FormatTranslatorService] + providers: [FormatTranslatorService, + { provide: SessionStorageService, useValue: mockSessionStorageService } + ] }) .compileComponents(); fixture = TestBed.createComponent(EventLogTableComponent); component = fixture.componentInstance; + mockSessionStorageService.getItem.and.returnValue(JSON.stringify(USER)); component.events = EVENTS; component.selected = SELECTED_EVENT; @@ -199,10 +211,8 @@ describe('EventLogTableComponent', () => { }); describe('Case timeline use', () => { - let caseHistoryHandler; beforeEach(waitForAsync(() => { - caseHistoryHandler = createSpyObj('caseHistoryHandler', ['apply']); TestBed @@ -213,12 +223,16 @@ describe('EventLogTableComponent', () => { DatePipe, MockRpxTranslatePipe ], - providers: [FormatTranslatorService] + providers: [FormatTranslatorService, + { provide: SessionStorageService, useValue: mockSessionStorageService } + + ] }) .compileComponents(); fixture = TestBed.createComponent(EventLogTableComponent); component = fixture.componentInstance; + mockSessionStorageService.getItem.and.returnValue(JSON.stringify(USER)); component.events = EVENTS; component.selected = SELECTED_EVENT; @@ -238,4 +252,48 @@ describe('EventLogTableComponent', () => { expect(caseHistoryHandler.apply).toHaveBeenCalledWith(4); }); }); + + describe('External user', () => { + beforeEach(waitForAsync(() => { + TestBed + .configureTestingModule({ + imports: [RouterTestingModule], + declarations: [ + EventLogTableComponent, + DatePipe, + MockRpxTranslatePipe + ], + providers: [FormatTranslatorService, + { provide: SessionStorageService, useValue: mockSessionStorageService } + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EventLogTableComponent); + component = fixture.componentInstance; + const EXTERNAL_USER = { + roles: [ + 'pui-case-manager' + ] + }; + mockSessionStorageService.getItem.and.returnValue(JSON.stringify(EXTERNAL_USER)); + + component.events = EVENTS; + component.selected = SELECTED_EVENT; + component.isPartOfCaseTimeline = false; + + de = fixture.debugElement; + fixture.detectChanges(); + })); + + it('should not render hyperlink for each row and link to event id', () => { + const links = de.queryAll($TABLE_ROW_LINKS_TIMELINE_EXTERNAL); + expect(component.isUserExternal).toBeTruthy(); + expect(links.length).toBe(2); + expect(links[0].nativeElement.getAttribute('href')).toBeNull(); + expect(links[1].nativeElement.getAttribute('href')).toBeNull(); + expect(links[0].nativeElement.textContent).toContain('Update a case'); + expect(links[1].nativeElement.textContent).toContain('Update a case'); + }); + }); }); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.ts index 08420b3178..aaa7c5a5af 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/components/palette/history/event-log/event-log-table.component.ts @@ -1,6 +1,7 @@ import { formatDate } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { CaseViewEvent } from '../../../../domain'; +import { SessionStorageService } from '../../../../services'; @Component({ selector: 'ccd-event-log-table', @@ -8,7 +9,6 @@ import { CaseViewEvent } from '../../../../domain'; styleUrls: ['./event-log-table.scss'] }) export class EventLogTableComponent implements OnInit { - @Input() public events: CaseViewEvent[]; @@ -23,8 +23,15 @@ export class EventLogTableComponent implements OnInit { public isPartOfCaseTimeline = false; + public isUserExternal: boolean; + + constructor( + private readonly sessionStorage: SessionStorageService + ){} + public ngOnInit(): void { this.isPartOfCaseTimeline = this.onCaseHistory.observers.length > 0; + this.isUserExternal = JSON.parse(this.sessionStorage.getItem('userDetails')).roles.includes('pui-case-manager'); } public select(event: CaseViewEvent): void { diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts index be72e165aa..02b0742b4e 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/domain/work-allocation/Task.ts @@ -8,6 +8,7 @@ export interface Task { case_type_id?: string; created_date: string; due_date?: string; + description?: string; execution_type?: string; id: string; jurisdiction: string; diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/case-file-view/case-file-view.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/case-file-view/case-file-view.service.spec.ts index 3b7753e16f..1be724cb01 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/case-file-view/case-file-view.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/case-file-view/case-file-view.service.spec.ts @@ -9,6 +9,7 @@ import { HttpErrorService, HttpService } from '../http'; import { CaseFileViewService } from './case-file-view.service'; import createSpyObj = jasmine.createSpyObj; +import { LoadingService } from '../loading'; describe('Case File View service', () => { const categoriesAndDocumentsUrl = '/categoriesAndDocuments'; @@ -93,7 +94,8 @@ describe('Case File View service', () => { HttpService, HttpErrorService, { provide: AbstractAppConfig, useValue: appConfig }, - { provide: AuthService, useValue: authService } + { provide: AuthService, useValue: authService }, + LoadingService ] }); // Note: TestBed.get() is deprecated in favour of TestBed.inject() (type-safe) from Angular 9 diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.spec.ts index aded1ec649..0fa1c6412c 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.spec.ts @@ -2,6 +2,7 @@ import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { HttpError } from '../../domain/http/http-error.model'; import { AuthService } from '../auth/auth.service'; import { HttpErrorService } from './http-error.service'; +import { LoadingService } from '../loading'; describe('HttpErrorService', () => { const CURRENT_URL = 'http://core-case-data.common-components.reform'; @@ -64,11 +65,13 @@ describe('HttpErrorService', () => { let authService: any; let errorService: HttpErrorService; + let loadingService: LoadingService; beforeEach(() => { authService = jasmine.createSpyObj('authService', ['signIn']); + loadingService = new LoadingService(); - errorService = new HttpErrorService(authService); + errorService = new HttpErrorService(authService, loadingService); jasmine.clock().uninstall(); jasmine.clock().install(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts index 5eb7405839..1654013566 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http-error.service.ts @@ -3,12 +3,15 @@ import { Injectable } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { HttpError } from '../../domain/http/http-error.model'; import { AuthService } from '../auth/auth.service'; +import { LoadingService } from '../loading'; @Injectable() export class HttpErrorService { - constructor(private readonly authService: AuthService) {} + constructor(private readonly authService: AuthService, + private readonly loadingService: LoadingService + ) {} private static readonly CONTENT_TYPE = 'Content-Type'; private static readonly JSON = 'json'; @@ -54,6 +57,9 @@ export class HttpErrorService { public handle(error: HttpErrorResponse | any, redirectIfNotAuthorised = true): Observable { console.error('Handling error in http error service.'); console.error(error); + if (this.loadingService.hasSharedSpinner()){ + this.loadingService.unregisterSharedSpinner(); + } const httpError: HttpError = HttpErrorService.convertToHttpError(error); if (redirectIfNotAuthorised && httpError.status === 401) { this.authService.signIn(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http.service.spec.ts index f8761ba871..2a8c3ea2de 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/http/http.service.spec.ts @@ -8,6 +8,7 @@ import { HttpService, OptionsType } from './http.service'; import createSpyObj = jasmine.createSpyObj; import any = jasmine.any; +import { LoadingService } from '../loading'; describe('HttpService', () => { const URL = 'http://ccd.reform/'; @@ -37,11 +38,10 @@ describe('HttpService', () => { }); const EXPECTED_RESPONSE = of(new HttpResponse()); let httpService: HttpService; - let httpMock: jasmine.SpyObj; let httpErrorService: jasmine.SpyObj; let catchObservable: jasmine.SpyObj>; - const realHttpErrorService = new HttpErrorService(null); + const realHttpErrorService = new HttpErrorService(null, new LoadingService()); beforeEach(waitForAsync(() => { catchObservable = createSpyObj>('observable', ['pipe']); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.spec.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.spec.ts index e797a2f97b..d03d72b5f1 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.spec.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.spec.ts @@ -57,6 +57,13 @@ describe('LoadingService', () => { }, 1); })); + it('should delete stored spinner token and return observable of false', waitForAsync(() => { + const token = loadingService.register(); + loadingService.addSharedSpinner(token); + loadingService.unregisterSharedSpinner(); + expect(loadingService.hasSharedSpinner()).toBeFalsy(); + })); + afterEach(() => { if (subscription) { subscription.unsubscribe(); diff --git a/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.ts b/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.ts index 453c5632e9..9533bfd787 100644 --- a/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.ts +++ b/projects/ccd-case-ui-toolkit/src/lib/shared/services/loading/loading.service.ts @@ -6,6 +6,7 @@ import { distinctUntilChanged } from 'rxjs/operators'; export class LoadingService { private readonly registered = new Map(); private readonly loading = new BehaviorSubject(false); + private readonly sharedSpinners = []; public get isLoading(): Observable { return this.loading.pipe(distinctUntilChanged()); @@ -23,6 +24,20 @@ export class LoadingService { this.loading.next(this.registered.size > 0); } + public addSharedSpinner(spinnerId: string){ + this.sharedSpinners.push(spinnerId); + } + + public hasSharedSpinner(): boolean { + return this.sharedSpinners.length > 0; + } + + public unregisterSharedSpinner(): void { + this.registered.delete(this.sharedSpinners[0]); + this.sharedSpinners.shift(); + this.loading.next(this.registered.size > 0); + } + private generateToken(): string { const timestamp = window.performance.now(); return `toolkit-loading-${timestamp}`; // format: [source-library]-[unique incrementing number]