From e8986ab55b836a6fe5e8763746eb7b06120f9157 Mon Sep 17 00:00:00 2001 From: Unai Zalba Date: Mon, 25 Nov 2024 15:54:11 +0100 Subject: [PATCH] feat/AB#106234_EIS-improve-messages-for-file-download feat: add new interceptor for csApi requests error handling feat: add interceptor in all projects feat: add cs api error builder to be able to build error message based on the returned error status from the interceptor feat: change snackbar to be able to handle line breaks in the given string --- apps/back-office/src/app/app.module.ts | 6 ++ apps/front-office/src/app/app.module.ts | 6 ++ apps/web-widgets/src/app/app.module.ts | 6 ++ libs/shared/src/i18n/en.json | 5 ++ libs/shared/src/i18n/fr.json | 5 ++ libs/shared/src/i18n/test.json | 5 ++ libs/shared/src/index.ts | 7 +- .../src/lib/components/form/form.component.ts | 29 ++++---- .../document-management.service.ts | 68 +++++++++++++------ .../eis-docs-interceptor.service.spec.ts | 22 ++++++ .../eis-docs-interceptor.service.ts | 52 ++++++++++++++ .../form-helper/form-helper.service.ts | 11 ++- .../src/lib/snackbar/snackbar.component.html | 7 +- 13 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.spec.ts create mode 100644 libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.ts diff --git a/apps/back-office/src/app/app.module.ts b/apps/back-office/src/app/app.module.ts index 2be5ebe45d..6d3ee02f52 100644 --- a/apps/back-office/src/app/app.module.ts +++ b/apps/back-office/src/app/app.module.ts @@ -33,6 +33,7 @@ import { AppAbility, FormService, DatePipe, + EISDocsInterceptorService, } from '@oort-front/shared'; import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; @@ -154,6 +155,11 @@ export const httpTranslateLoader = (http: HttpClient) => useClass: AuthInterceptorService, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: EISDocsInterceptorService, + multi: true, + }, { provide: AppAbility, useValue: new AppAbility(), diff --git a/apps/front-office/src/app/app.module.ts b/apps/front-office/src/app/app.module.ts index 323d2da93f..5d3d581e41 100644 --- a/apps/front-office/src/app/app.module.ts +++ b/apps/front-office/src/app/app.module.ts @@ -33,6 +33,7 @@ import { AuthInterceptorService, FormService, DatePipe, + EISDocsInterceptorService, } from '@oort-front/shared'; import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; @@ -150,6 +151,11 @@ export const httpTranslateLoader = (http: HttpClient) => useClass: AuthInterceptorService, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: EISDocsInterceptorService, + multi: true, + }, { provide: AppAbility, useValue: new AppAbility(), diff --git a/apps/web-widgets/src/app/app.module.ts b/apps/web-widgets/src/app/app.module.ts index 4025b42485..4e42d38da9 100644 --- a/apps/web-widgets/src/app/app.module.ts +++ b/apps/web-widgets/src/app/app.module.ts @@ -28,6 +28,7 @@ import { AuthInterceptorService, FormService, DatePipe, + EISDocsInterceptorService, } from '@oort-front/shared'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; @@ -198,6 +199,11 @@ export const getBaseHref = () => { useClass: AuthInterceptorService, multi: true, }, + { + provide: HTTP_INTERCEPTORS, + useClass: EISDocsInterceptorService, + multi: true, + }, { provide: AppAbility, useValue: new AppAbility(), diff --git a/libs/shared/src/i18n/en.json b/libs/shared/src/i18n/en.json index 55e84b8026..deaa52659f 100644 --- a/libs/shared/src/i18n/en.json +++ b/libs/shared/src/i18n/en.json @@ -345,6 +345,11 @@ }, "fetchingGroups": "Fetching groups from service...", "file": { + "csapi": { + "invalid": "File not valid", + "notFound": "File not found", + "unauthorized": "Unauthorized" + }, "download": { "error": "Something went wrong during the download", "ongoing": "Your file export is ongoing. You will receive an email once ready.", diff --git a/libs/shared/src/i18n/fr.json b/libs/shared/src/i18n/fr.json index 4a6f91329e..604edc7d00 100644 --- a/libs/shared/src/i18n/fr.json +++ b/libs/shared/src/i18n/fr.json @@ -345,6 +345,11 @@ }, "fetchingGroups": "Fetching groups from service...", "file": { + "csapi": { + "invalid": "**************************", + "notFound": "**************************", + "unauthorized": "**************************" + }, "download": { "error": "Un problème est survenu lors du téléchargement", "ongoing": "Votre téléchargement est en cours. Vous recevrez bientôt un e-mail contenant votre fichier.", diff --git a/libs/shared/src/i18n/test.json b/libs/shared/src/i18n/test.json index bc4742b2eb..5f971448e5 100644 --- a/libs/shared/src/i18n/test.json +++ b/libs/shared/src/i18n/test.json @@ -345,6 +345,11 @@ }, "fetchingGroups": "******", "file": { + "csapi": { + "invalid": "******", + "notFound": "******", + "unauthorized": "******" + }, "download": { "error": "******", "ongoing": "******", diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 0cfa0bb495..578643e909 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -19,6 +19,7 @@ export * from './lib/services/date-translate/date-translate.service'; export * from './lib/services/document-management/document-management.service'; export * from './lib/services/download/download.service'; export * from './lib/services/editor/editor.service'; +export * from './lib/services/eis-docs-interceptor/eis-docs-interceptor.service'; export * from './lib/services/form-builder/form-builder.service'; export * from './lib/services/form/form.service'; export * from './lib/services/grid-data-formatter/grid-data-formatter.service'; @@ -27,8 +28,8 @@ export * from './lib/services/grid/grid.service'; export * from './lib/services/html-parser/html-parser.service'; export * from './lib/services/kendo-translation/kendo-translation.service'; export * from './lib/services/map/map-layers.service'; -export * from './lib/services/reference-data/reference-data.service'; export * from './lib/services/query-builder/query-builder.service'; +export * from './lib/services/reference-data/reference-data.service'; export * from './lib/services/rest/rest.service'; export * from './lib/services/workflow/workflow.service'; @@ -66,6 +67,7 @@ export * from './lib/models/workflow.model'; // === COMPONENTS === export * from './lib/components/access/public-api'; +export * from './lib/components/action-buttons/public-api'; export * from './lib/components/aggregation/edit-aggregation-modal/edit-aggregation-modal.component'; export * from './lib/components/applications-summary/public-api'; export * from './lib/components/confirm-modal/public-api'; @@ -85,6 +87,7 @@ export * from './lib/components/layout/public-api'; export * from './lib/components/mapping/public-api'; export * from './lib/components/navbar/public-api'; export * from './lib/components/payload-modal/payload-modal.component'; +export * from './lib/components/query-builder/public-api'; export * from './lib/components/record-history/public-api'; export * from './lib/components/record-modal/public-api'; export * from './lib/components/role-summary/public-api'; @@ -106,8 +109,6 @@ export * from './lib/components/widgets/map-settings/public-api'; export * from './lib/components/widgets/map/public-api'; export * from './lib/components/widgets/summary-card-settings/public-api'; export * from './lib/components/workflow-stepper/public-api'; -export * from './lib/components/query-builder/public-api'; -export * from './lib/components/action-buttons/public-api'; // Export of controls export * from './lib/components/applications-archive/public-api'; diff --git a/libs/shared/src/lib/components/form/form.component.ts b/libs/shared/src/lib/components/form/form.component.ts index 5b0d624b04..b1b1788adc 100644 --- a/libs/shared/src/lib/components/form/form.component.ts +++ b/libs/shared/src/lib/components/form/form.component.ts @@ -1,4 +1,5 @@ -import { Apollo } from 'apollo-angular'; +import { Dialog } from '@angular/cdk/dialog'; +import { HttpErrorResponse } from '@angular/common/http'; import { Component, ElementRef, @@ -9,25 +10,25 @@ import { Output, ViewChild, } from '@angular/core'; -import { Dialog } from '@angular/cdk/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { SnackbarService, UILayoutService } from '@oort-front/ui'; +import { Apollo } from 'apollo-angular'; +import { isNil } from 'lodash'; +import { BehaviorSubject, takeUntil } from 'rxjs'; import { SurveyModel } from 'survey-core'; -import { ADD_RECORD, EDIT_RECORD } from './graphql/mutations'; import { Form } from '../../models/form.model'; import { AddRecordMutationResponse, EditRecordMutationResponse, Record as RecordModel, } from '../../models/record.model'; -import { BehaviorSubject, takeUntil } from 'rxjs'; -import addCustomFunctions from '../../utils/custom-functions'; import { AuthService } from '../../services/auth/auth.service'; import { FormBuilderService } from '../../services/form-builder/form-builder.service'; +import { FormHelpersService } from '../../services/form-helper/form-helper.service'; +import addCustomFunctions from '../../utils/custom-functions'; import { RecordHistoryComponent } from '../record-history/record-history.component'; -import { TranslateService } from '@ngx-translate/core'; import { UnsubscribeComponent } from '../utils/unsubscribe/unsubscribe.component'; -import { FormHelpersService } from '../../services/form-helper/form-helper.service'; -import { SnackbarService, UILayoutService } from '@oort-front/ui'; -import { isNil } from 'lodash'; +import { ADD_RECORD, EDIT_RECORD } from './graphql/mutations'; /** * This component is used to display forms @@ -272,11 +273,11 @@ export class FormComponent ); } catch (errors) { /** If there is any upload errors, save them for display */ - const uploadErrors = (errors as { question: string; file: File }[]).map( - (error) => { - return `${error.question}: ${error.file.name}`; - } - ); + const uploadErrors = ( + errors as { question: string; file: File; error: HttpErrorResponse }[] + ).map((error) => { + return `- Error ${error.error.status}, ${error.error.message} for ${error.question}: ${error.file.name}`; + }); this.snackBar.openSnackBar( this.translate.instant('models.form.notifications.savingFailed') + (!isNil(uploadErrors) ? '\n' + uploadErrors?.join('\n') : ''), diff --git a/libs/shared/src/lib/services/document-management/document-management.service.ts b/libs/shared/src/lib/services/document-management/document-management.service.ts index e011959291..6752f1a918 100644 --- a/libs/shared/src/lib/services/document-management/document-management.service.ts +++ b/libs/shared/src/lib/services/document-management/document-management.service.ts @@ -1,23 +1,23 @@ import { DOCUMENT } from '@angular/common'; -import { HttpHeaders } from '@angular/common/http'; +import { HttpHeaders, HttpStatusCode } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; +import { InMemoryCache } from '@apollo/client'; +import { setContext } from '@apollo/client/link/context'; import { TranslateService } from '@ngx-translate/core'; import { SnackbarService } from '@oort-front/ui'; +import { Apollo } from 'apollo-angular'; +import { HttpLink } from 'apollo-angular/http'; import { isNil, set } from 'lodash'; +import { catchError, firstValueFrom, throwError } from 'rxjs'; import { Question } from 'survey-core'; import { SnackbarSpinnerComponent } from '../../components/snackbar-spinner/snackbar-spinner.component'; import { RestService } from '../rest/rest.service'; -import { Apollo } from 'apollo-angular'; import { DriveQueryResponse, GET_DRIVE_ID, GET_OCCURRENCE_BY_ID, OccurrenceQueryResponse, } from './graphql/queries'; -import { firstValueFrom } from 'rxjs'; -import { InMemoryCache } from '@apollo/client'; -import { HttpLink } from 'apollo-angular/http'; -import { setContext } from '@apollo/client/link/context'; /** * Available properties from the CS API Documentation @@ -160,6 +160,11 @@ export class DocumentManagementService { const url = `${this.environment.csApiUrl}/documents/drives/${file.content.driveId}/items/${file.content.itemId}/content`; this.restService .get(url, { ...options, responseType: 'blob', headers }) + .pipe( + catchError((err) => { + return throwError(() => this.buildCSApiError(err)); + }) + ) .subscribe({ next: (res) => { const blob = new Blob([res]); @@ -170,13 +175,12 @@ export class DocumentManagementService { snackBarSpinner.instance.loading = false; snackBarRef.instance.triggerSnackBar(SNACKBAR_DURATION); }, - error: () => { - snackBarSpinner.instance.message = this.translate.instant( - 'common.notifications.file.download.error' + error: async (error) => { + snackBarRef.instance.dismiss(); + this.snackBar.openSnackBar( + `Error ${error.status}, ${error.message}:\n${file.name}`, + { error: true } ); - snackBarSpinner.instance.loading = false; - snackBarSpinner.instance.error = true; - snackBarRef.instance.triggerSnackBar(SNACKBAR_DURATION); }, }); } @@ -197,6 +201,30 @@ export class DocumentManagementService { link.remove(); } + /** + * Update the given error instance from the eis-docs-interceptor to be displayed with correct message based on it's status + * + * @param error Error formatted from the eis-docs-interceptor file + * @returns Error instance with correct messages per status, or default if different status + */ + private buildCSApiError(error: any) { + const parseError = JSON.parse(error.message || '{}'); + if (parseError.status === HttpStatusCode.Unauthorized) { + parseError.message = this.translate.instant( + 'common.notifications.file.csapi.unauthorized' + ); + } else if (parseError.status === HttpStatusCode.NotFound) { + parseError.message = this.translate.instant( + 'common.notifications.file.csapi.notFound' + ); + } else if (parseError.status === HttpStatusCode.BadRequest) { + parseError.message = this.translate.instant( + 'common.notifications.file.csapi.invalid' + ); + } + return parseError; + } + /** * Uploads a file * @@ -270,6 +298,11 @@ export class DocumentManagementService { headers, } ) + .pipe( + catchError((err) => { + return throwError(() => this.buildCSApiError(err)); + }) + ) .subscribe({ next: (data) => { const { itemId, driveId } = data; @@ -283,14 +316,9 @@ export class DocumentManagementService { itemId, }); }, - error: () => { - snackBarSpinner.instance.message = this.translate.instant( - 'common.notifications.file.upload.error' - ); - snackBarSpinner.instance.loading = false; - snackBarSpinner.instance.error = true; - snackBarRef.instance.triggerSnackBar(SNACKBAR_DURATION); - reject(null); + error: (error) => { + snackBarRef.instance.dismiss(); + reject(error); }, }); }); diff --git a/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.spec.ts b/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.spec.ts new file mode 100644 index 0000000000..16db901126 --- /dev/null +++ b/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.spec.ts @@ -0,0 +1,22 @@ +import { TestBed } from '@angular/core/testing'; +import { EISDocsInterceptorService } from './eis-docs-interceptor.service'; +import { ApolloTestingModule } from 'apollo-angular/testing'; +import { HttpClientModule } from '@angular/common/http'; + +describe('EISDocsInterceptorService', () => { + let service: EISDocsInterceptorService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: 'environment', useValue: { csApiUrl: 'csApiUrl' } }, + ], + imports: [ApolloTestingModule, HttpClientModule], + }); + service = TestBed.inject(EISDocsInterceptorService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.ts b/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.ts new file mode 100644 index 0000000000..cff006e7f0 --- /dev/null +++ b/libs/shared/src/lib/services/eis-docs-interceptor/eis-docs-interceptor.service.ts @@ -0,0 +1,52 @@ +import { + HttpErrorResponse, + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +/** + * Shared EIS docs interceptor service + */ +@Injectable({ + providedIn: 'root', +}) +export class EISDocsInterceptorService implements HttpInterceptor { + /** + * Shared EIS docs interceptor service + * + * @param environment environment + */ + constructor(@Inject('environment') private environment: any) {} + + /** + * Intercept request to format given errors from CS API accordingly to our frontend + * + * @param request http request + * @param next next interceptor in chain + * @returns request with format error if needed + */ + intercept( + request: HttpRequest, + next: HttpHandler + ): Observable> { + if (request.url.startsWith(this.environment.csApiUrl)) { + return next.handle(request).pipe( + catchError((err) => { + return throwError( + () => + new HttpErrorResponse({ + ...err, + error: JSON.stringify(err), + }) + ); + }) + ); + } + return next.handle(request); + } +} diff --git a/libs/shared/src/lib/services/form-helper/form-helper.service.ts b/libs/shared/src/lib/services/form-helper/form-helper.service.ts index aa27418156..704f28f4fb 100644 --- a/libs/shared/src/lib/services/form-helper/form-helper.service.ts +++ b/libs/shared/src/lib/services/form-helper/form-helper.service.ts @@ -1,4 +1,5 @@ import { DialogRef } from '@angular/cdk/dialog'; +import { HttpErrorResponse } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { SnackbarService } from '@oort-front/ui'; @@ -125,7 +126,11 @@ export class FormHelpersService { const data = survey.data; const questionsToUpload = Object.keys(temporaryFilesStorage); - const failedFilesToUpload: { question: string; file: File }[] = []; + const failedFilesToUpload: { + question: string; + file: File; + error: HttpErrorResponse; + }[] = []; // Is using document management system const useDocumentManagement = !!this.environment.csApiUrl; for (const name of questionsToUpload) { @@ -176,8 +181,8 @@ export class FormHelpersService { }); }); } - } catch (error) { - failedFilesToUpload.push({ question: name, file }); + } catch (error: any) { + failedFilesToUpload.push({ question: name, file, error }); } } } else { diff --git a/libs/ui/src/lib/snackbar/snackbar.component.html b/libs/ui/src/lib/snackbar/snackbar.component.html index 90715f5d9f..efaf22609f 100644 --- a/libs/ui/src/lib/snackbar/snackbar.component.html +++ b/libs/ui/src/lib/snackbar/snackbar.component.html @@ -23,9 +23,10 @@ >
-

- {{ message }} -

+