diff --git a/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.spec.ts b/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.spec.ts index a3ac259d1be..c18781df6a0 100644 --- a/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.spec.ts +++ b/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.spec.ts @@ -1,11 +1,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { I18nTestingModule, RoutingService } from '@spartacus/core'; +import { + GlobalMessageEntities, + GlobalMessageService, + GlobalMessageType, + HttpErrorModel, + I18nTestingModule, + RoutingService, + Translatable, + TranslationService, +} from '@spartacus/core'; import { CustomerTicketingFacade, TicketStarter, } from '@spartacus/customer-ticketing/root'; import { LaunchDialogService } from '@spartacus/storefront'; -import { EMPTY, of } from 'rxjs'; +import { EMPTY, Observable, of, throwError } from 'rxjs'; import { CustomerTicketingCreateDialogComponent } from './customer-ticketing-create-dialog.component'; import createSpy = jasmine.createSpy; @@ -54,10 +63,25 @@ class MockCustomerTicketingFacade implements Partial { ); } +class MockGlobalMessageService implements Partial { + get(): Observable { + return of({}); + } + add(_: string | Translatable, __: GlobalMessageType, ___?: number): void {} + remove(_: GlobalMessageType, __?: number): void {} +} + +class MockTranslationService { + translate(): Observable { + return of('translated string'); + } +} + describe('CustomerTicketingCreateDialogComponent', () => { let component: CustomerTicketingCreateDialogComponent; let fixture: ComponentFixture; let customerTicketingFacade: CustomerTicketingFacade; + let globalMessageService: GlobalMessageService; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -70,9 +94,14 @@ describe('CustomerTicketingCreateDialogComponent', () => { useClass: MockCustomerTicketingFacade, }, { provide: RoutingService, useClass: MockRoutingService }, + { provide: GlobalMessageService, useClass: MockGlobalMessageService }, + { provide: TranslationService, useClass: MockTranslationService }, ], }).compileComponents(); customerTicketingFacade = TestBed.inject(CustomerTicketingFacade); + globalMessageService = TestBed.inject(GlobalMessageService); + + spyOn(globalMessageService, 'add').and.callThrough(); }); beforeEach(() => { @@ -109,5 +138,41 @@ describe('CustomerTicketingCreateDialogComponent', () => { component.createTicketRequest(); expect(customerTicketingFacade.createTicket).not.toHaveBeenCalled(); }); + + it('should handle HttpErrorModel error correctly when creating a ticket', () => { + const expectedErrorMessage = 'mock-error-message'; + const error = new HttpErrorModel(); + error.details = [{ message: expectedErrorMessage }]; + customerTicketingFacade.createTicket = createSpy().and.returnValue( + throwError(error) + ); + component.form.get('message')?.setValue(mockTicketStarter.message); + component.form.get('subject')?.setValue(mockTicketStarter.subject); + component.form.get('ticketCategory')?.setValue(mockCategories); + component.form.get('associatedTo')?.setValue(mockTicketAssociatedObjects); + component.createTicketRequest(); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { raw: expectedErrorMessage }, + GlobalMessageType.MSG_TYPE_ERROR + ); + }); + + it('should handle other error correctly when creating a ticket', () => { + const expectedErrorMessage = 'error'; + customerTicketingFacade.createTicket = createSpy().and.returnValue( + throwError(expectedErrorMessage) + ); + component.form.get('message')?.setValue(mockTicketStarter.message); + component.form.get('subject')?.setValue(mockTicketStarter.subject); + component.form.get('ticketCategory')?.setValue(mockCategories); + component.form.get('associatedTo')?.setValue(mockTicketAssociatedObjects); + component.createTicketRequest(); + + expect(globalMessageService.add).toHaveBeenCalledWith( + { raw: 'translated string' }, + GlobalMessageType.MSG_TYPE_ERROR + ); + }); }); }); diff --git a/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.ts b/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.ts index 5d8e02e751c..0017b3c0853 100644 --- a/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.ts +++ b/feature-libs/customer-ticketing/components/list/customer-ticketing-create/customer-ticketing-create-dialog/customer-ticketing-create-dialog.component.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { AssociatedObject, @@ -14,8 +14,15 @@ import { TicketStarter, } from '@spartacus/customer-ticketing/root'; import { FormUtils } from '@spartacus/storefront'; -import { Observable, Subscription } from 'rxjs'; +import { Observable, Subscription, of } from 'rxjs'; import { CustomerTicketingDialogComponent } from '../../../shared/customer-ticketing-dialog/customer-ticketing-dialog.component'; +import { + GlobalMessageService, + GlobalMessageType, + HttpErrorModel, + TranslationService, +} from '@spartacus/core'; +import { catchError, first } from 'rxjs/operators'; @Component({ selector: 'cx-customer-ticketing-create-dialog', templateUrl: './customer-ticketing-create-dialog.component.html', @@ -27,7 +34,12 @@ export class CustomerTicketingCreateDialogComponent ticketCategories$: Observable = this.customerTicketingFacade.getTicketCategories(); ticketAssociatedObjects$: Observable = - this.customerTicketingFacade.getTicketAssociatedObjects(); + this.customerTicketingFacade.getTicketAssociatedObjects().pipe( + catchError((error: any) => { + this.handleError(error); + return of([]); + }) + ); subscription: Subscription; @Input() @@ -38,6 +50,10 @@ export class CustomerTicketingCreateDialogComponent attachment: File; + protected globalMessage = inject(GlobalMessageService); + + protected translationService = inject(TranslationService); + protected getCreateTicketPayload(form: FormGroup): TicketStarter { return { message: form?.get('message')?.value, @@ -108,13 +124,37 @@ export class CustomerTicketingCreateDialogComponent complete: () => { this.onComplete(); }, - error: () => { - this.onError(); + error: (error: any) => { + this.handleError(error); }, }); } } + protected handleError(error: any): void { + if (error instanceof HttpErrorModel) { + (error.details ?? []).forEach((err) => { + if (err.message) { + this.globalMessage.add( + { raw: err.message }, + GlobalMessageType.MSG_TYPE_ERROR + ); + } + }); + } else { + this.translationService + .translate('httpHandlers.unknownError') + .pipe(first()) + .subscribe((text) => { + this.globalMessage.add( + { raw: text }, + GlobalMessageType.MSG_TYPE_ERROR + ); + }); + } + this.onError(); + } + protected onComplete(): void { this.close('Ticket created successfully'); } diff --git a/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.spec.ts b/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.spec.ts index be98bb39a72..9d95e5eb972 100644 --- a/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.spec.ts +++ b/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.spec.ts @@ -16,7 +16,7 @@ import { TicketReopenedEvent, TicketStarter, } from '@spartacus/customer-ticketing/root'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { take } from 'rxjs/operators'; import { CustomerTicketingConnector } from '../connectors'; import { CustomerTicketingService } from './customer-ticketing.service'; @@ -346,6 +346,27 @@ describe('CustomerTicketingService', () => { done(); }); }); + + it('should handle error response', () => { + const errorResponse = { + loading: false, + data: null, + error: 'Some error message', + }; + + spyOn(service, 'getTicketAssociatedObjectsState').and.returnValue( + throwError(errorResponse.error) + ); + + service.getTicketAssociatedObjects().subscribe( + () => { + fail('Should not reach here'); + }, + (error) => { + expect(error).toEqual(errorResponse.error); + } + ); + }); }); describe('createTicketEvent', () => { diff --git a/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.ts b/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.ts index b05bb2eb41c..27b531c274e 100644 --- a/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.ts +++ b/feature-libs/customer-ticketing/core/facade/customer-ticketing.service.ts @@ -10,6 +10,7 @@ import { CommandService, CommandStrategy, EventService, + HttpErrorModel, Query, QueryNotifier, QueryService, @@ -40,8 +41,9 @@ import { TicketStarter, UploadAttachmentSuccessEvent, } from '@spartacus/customer-ticketing/root'; -import { combineLatest, Observable } from 'rxjs'; +import { combineLatest, Observable, of, throwError } from 'rxjs'; import { + concatMap, distinctUntilChanged, map, switchMap, @@ -295,6 +297,9 @@ export class CustomerTicketingService implements CustomerTicketingFacade { } getTicketAssociatedObjects(): Observable { return this.getTicketAssociatedObjectsState().pipe( + concatMap((state) => + state?.error ? throwError(state.error as HttpErrorModel) : of(state) + ), map((state) => state.data ?? []) ); }