diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e4ae8bc4a..f452e9d23c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2795,6 +2795,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2813,6 +2818,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2832,6 +2842,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2849,6 +2864,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2866,6 +2886,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2885,6 +2910,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -2906,6 +2936,11 @@ "tslib": "^1.9.3" }, "dependencies": { + "@sentry/types": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.3.tgz", + "integrity": "sha512-BpA+9FherWgYlkMD/82bGFh/gAqZNlZX5UE8vWLKyyzNyOEEz3v9ScxE8dOSWE4v5iXJR1O3jjxaTcRQxPVgCA==" + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index f6a28770bf..4eceef5026 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -80,8 +80,9 @@ "@ng-select/ng-option-highlight": "0.0.5", "@ng-select/ng-select": "^4.0.4", "@ngx-formly/core": "^5.10.19", - "@sentry/angular": "^6.2.3", - "@sentry/tracing": "^6.2.3", + "@sentry/angular": "6.2.3", + "@sentry/tracing": "6.2.3", + "@sentry/types": "^6.2.3", "@uirouter/angular": "^8.0.0", "@uirouter/core": "^6.0.7", "@uirouter/rx": "^0.6.5", diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 002260ed63..e8ab4dae5f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -27,7 +27,10 @@ //++ import { - APP_INITIALIZER, ApplicationRef, Injector, NgModule, + APP_INITIALIZER, + ApplicationRef, + Injector, + NgModule, } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { OpContextMenuTrigger } from 'core-app/shared/components/op-context-menu/handlers/op-context-menu-trigger.directive'; diff --git a/frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths.ts b/frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths.ts index f00f8a7214..fd8a68c69f 100644 --- a/frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths.ts +++ b/frontend/src/app/core/apiv3/endpoints/work_packages/api-v3-work-packages-paths.ts @@ -157,7 +157,7 @@ export class APIV3WorkPackagesPaths extends CachableAPIV3Collection { return this .halResourceService - .getAllPaginated( + .getAllPaginated( this.path, ids.length, { diff --git a/frontend/src/app/core/errors/sentry/sentry-reporter.ts b/frontend/src/app/core/errors/sentry/sentry-reporter.ts index cd84b84714..eb15fd4378 100644 --- a/frontend/src/app/core/errors/sentry/sentry-reporter.ts +++ b/frontend/src/app/core/errors/sentry/sentry-reporter.ts @@ -27,9 +27,15 @@ //++ import { - Event as SentryEvent, Hub, Scope, Severity, + Event as SentryEvent, + Hub, + Scope, + Severity, } from '@sentry/types'; import { environment } from '../../../../environments/environment'; +import { EventHint } from '@sentry/angular'; +import { HttpErrorResponse } from '@angular/common/http'; +import { debugLog } from 'core-app/shared/helpers/debug_output'; export type ScopeCallback = (scope:Scope) => void; export type MessageSeverity = 'fatal'|'error'|'warning'|'log'|'info'|'debug'; @@ -55,7 +61,7 @@ export interface ErrorReporter extends CaptureInterface { interface QueuedMessage { type:'captureMessage'|'captureException'; - args:any[]; + args:unknown[]; } export class SentryReporter implements ErrorReporter { @@ -82,7 +88,7 @@ export class SentryReporter implements ErrorReporter { const version = sentryElement.dataset.version || 'unknown'; const traceFactor = parseFloat(sentryElement.dataset.tracingFactor || '0.0'); - import('./sentry-dependency').then((imported) => { + void import('./sentry-dependency').then((imported) => { const sentry = imported.Sentry; sentry.init({ dsn, @@ -114,10 +120,10 @@ export class SentryReporter implements ErrorReporter { 'Non-Error exception captured with keys: $embedded, $halType, $links, $loaded', ], - beforeSend: (event) => this.filterEvent(event), + beforeSend: (event, hint) => SentryReporter.filterEvent(event, hint), }); - this.sentryLoaded(sentry as any); + this.sentryLoaded(sentry as unknown as Hub); }); } @@ -127,13 +133,15 @@ export class SentryReporter implements ErrorReporter { // Send all messages from before sentry got loaded this.messageStack.forEach((item) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access this[item.type].bind(this).apply(item.args); }); } public captureMessage(msg:string, severity:MessageSeverity = 'info'):void { if (!this.client) { - return this.handleOfflineMessage('captureMessage', [msg, severity]); + this.handleOfflineMessage('captureMessage', [msg, severity]); + return; } this.client.withScope((scope:Scope) => { @@ -145,11 +153,12 @@ export class SentryReporter implements ErrorReporter { public captureException(err:Error|string):void { if (!this.client || !err) { this.handleOfflineMessage('captureException', [err]); - throw err; + throw (err as Error); } if (typeof err === 'string') { - return this.captureMessage(err, 'error'); + this.captureMessage(err, 'error'); + return; } this.client.withScope((scope:Scope) => { @@ -172,11 +181,11 @@ export class SentryReporter implements ErrorReporter { * @param type * @param args */ - private handleOfflineMessage(type:'captureMessage'|'captureException', args:any[]) { + private handleOfflineMessage(type:'captureMessage'|'captureException', args:unknown[]) { if (this.sentryConfigured) { this.messageStack.push({ type, args }); } else { - console.log('[ErrorReporter] Would queue sentry message %O %O, but is not configured.', type, args); + debugLog('[ErrorReporter] Would queue sentry message %O %O, but is not configured.', type, args); } } @@ -199,8 +208,16 @@ export class SentryReporter implements ErrorReporter { * it from being sent. * * @param event + * @param hint */ - private filterEvent(event:SentryEvent):SentryEvent|null { + private static filterEvent(event:SentryEvent, hint:EventHint|undefined):SentryEvent|null { + // avoid duplicate requests on thrown angular errors, they + // are handled by the hal error handler + // https://github.com/getsentry/sentry-javascript/issues/2532#issuecomment-875428325 + if (hint?.originalException instanceof HttpErrorResponse) { + return null; + } + const unsupportedBrowser = document.body.classList.contains('-unsupported-browser'); if (unsupportedBrowser) { console.warn('Browser is not supported, skipping sentry reporting completely.'); diff --git a/frontend/src/app/features/hal/services/hal-aware-error-handler.ts b/frontend/src/app/features/hal/services/hal-aware-error-handler.ts index a9be076628..ac1d788a38 100644 --- a/frontend/src/app/features/hal/services/hal-aware-error-handler.ts +++ b/frontend/src/app/features/hal/services/hal-aware-error-handler.ts @@ -1,7 +1,16 @@ -import { ErrorHandler, Injectable } from '@angular/core'; +import { + ErrorHandler, + Injectable, +} from '@angular/core'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; +import { HalError } from 'core-app/features/hal/services/hal-error'; +import { HttpErrorResponse } from '@angular/common/http'; + +interface RejectedPromise { + rejection:unknown; +} @Injectable() export class HalAwareErrorHandler extends ErrorHandler { @@ -13,17 +22,31 @@ export class HalAwareErrorHandler extends ErrorHandler { super(); } - public handleError(error:unknown) { + public handleError(error:unknown):void { let message:string = this.text.internal_error; - if (error instanceof ErrorResource) { + // Angular wraps our errors into uncaught promises if + // no one catches the error explictly. Unwrap the error in that case + if ((error as RejectedPromise)?.rejection instanceof HalError) { + this.handleError((error as RejectedPromise).rejection); + return; + } + + if (error instanceof HalError) { + console.error('Returned HTTP HAL error resource %O', error.message); + message = error.httpError?.status >= 500 ? `${message} ${error.message}` : error.message; + HalAwareErrorHandler.captureHttpError(error.httpError); + } else if (error instanceof ErrorResource) { console.error('Returned error resource %O', error); message += ` ${error.errorMessages.join('\n')}`; } else if (error instanceof HalResource) { console.error('Returned hal resource %O', error); message += `Resource returned ${error.name}`; } else if (error instanceof Error) { - window.ErrorReporter.captureException(error); + HalAwareErrorHandler.reportError(error); + } else if (error instanceof HttpErrorResponse) { + HalAwareErrorHandler.captureHttpError(error); + message = error.message; } else if (typeof error === 'string') { window.ErrorReporter.captureMessage(error); message = error; @@ -31,4 +54,19 @@ export class HalAwareErrorHandler extends ErrorHandler { super.handleError(message); } + + private static reportError(error:Error):void { + window.ErrorReporter.captureException(error); + } + + /** + * Report any 5xx errors to sentry, if configured. + * @param httpError + * @private + */ + private static captureHttpError(httpError:HttpErrorResponse):void { + if (httpError.status >= 500) { + HalAwareErrorHandler.reportError(httpError); + } + } } diff --git a/frontend/src/app/features/hal/services/hal-error.ts b/frontend/src/app/features/hal/services/hal-error.ts new file mode 100644 index 0000000000..c744bc7c74 --- /dev/null +++ b/frontend/src/app/features/hal/services/hal-error.ts @@ -0,0 +1,22 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; + +export class HalError extends Error { + readonly name = 'HALError'; + + get message():string { + return this.resource.message || this.httpError?.message || 'Unknown error'; + } + + get errorIdentifier():string { + return this.resource.errorIdentifier; + } + + constructor( + readonly httpError:HttpErrorResponse, + readonly resource:ErrorResource, + ) { + super(); + Object.setPrototypeOf(this, HalError.prototype); + } +} diff --git a/frontend/src/app/features/hal/services/hal-resource-notification.service.ts b/frontend/src/app/features/hal/services/hal-resource-notification.service.ts index e98b0802be..31f09250d6 100644 --- a/frontend/src/app/features/hal/services/hal-resource-notification.service.ts +++ b/frontend/src/app/features/hal/services/hal-resource-notification.service.ts @@ -37,6 +37,7 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decorator'; import { SchemaCacheService } from 'core-app/core/schemas/schema-cache.service'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; +import { HalError } from 'core-app/features/hal/services/hal-error'; @Injectable() export class HalResourceNotificationService { @@ -78,8 +79,8 @@ export class HalResourceNotificationService { // Some transformation may already have returned the error as a HAL resource, // which we will forward to handleErrorResponse - if (response instanceof ErrorResource) { - return this.handleErrorResponse(response, resource); + if (response instanceof HalError) { + return this.handleErrorResponse(response.resource, resource); } const errorBody = this.retrieveError(response); @@ -108,7 +109,7 @@ export class HalResourceNotificationService { public retrieveErrorMessage(response:unknown):string { const error = this.retrieveError(response); - if (error instanceof ErrorResource) { + if (error instanceof ErrorResource || error instanceof HalError) { return error.message; } @@ -142,6 +143,10 @@ export class HalResourceNotificationService { } protected handleErrorResponse(errorResource:any, resource?:HalResource) { + if (errorResource instanceof HalError && resource) { + return this.showError(errorResource.resource, resource); + } + if (!(errorResource instanceof ErrorResource)) { return this.showGeneralError(errorResource); } @@ -150,7 +155,7 @@ export class HalResourceNotificationService { return this.showError(errorResource, resource); } - this.showApiErrorMessages(errorResource); + return this.showApiErrorMessages(errorResource); } public showError(errorResource:any, resource:HalResource) { diff --git a/frontend/src/app/features/hal/services/hal-resource.service.ts b/frontend/src/app/features/hal/services/hal-resource.service.ts index cbcf49d360..919f1ff7d5 100644 --- a/frontend/src/app/features/hal/services/hal-resource.service.ts +++ b/frontend/src/app/features/hal/services/hal-resource.service.ts @@ -26,10 +26,23 @@ // See docs/COPYRIGHT.rdoc for more details. //++ -import { Injectable, Injector } from '@angular/core'; -import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http'; -import { catchError, map } from 'rxjs/operators'; -import { Observable, throwError } from 'rxjs'; +import { + Injectable, + Injector, +} from '@angular/core'; +import { + HttpClient, + HttpErrorResponse, + HttpParams, +} from '@angular/common/http'; +import { + catchError, + map, +} from 'rxjs/operators'; +import { + Observable, + throwError, +} from 'rxjs'; import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; import * as Pako from 'pako'; @@ -41,10 +54,17 @@ import { HTTPClientParamMap, HTTPSupportedMethods, } from 'core-app/features/hal/http/http.interfaces'; -import { HalLink, HalLinkInterface } from 'core-app/features/hal/hal-link/hal-link'; +import { + HalLink, + HalLinkInterface, +} from 'core-app/features/hal/hal-link/hal-link'; import { URLParamsEncoder } from 'core-app/features/hal/services/url-params-encoder'; -import { HalResource, HalResourceClass } from 'core-app/features/hal/resources/hal-resource'; +import { + HalResource, + HalResourceClass, +} from 'core-app/features/hal/resources/hal-resource'; import { initializeHalProperties } from '../helpers/hal-resource-builder'; +import { HalError } from 'core-app/features/hal/services/hal-error'; export interface HalResourceFactoryConfigInterface { cls?:any; @@ -58,18 +78,20 @@ export class HalResourceService { */ private config:{ [typeName:string]:HalResourceFactoryConfigInterface } = {}; - constructor(readonly injector:Injector, - readonly http:HttpClient) { + constructor( + readonly injector:Injector, + readonly http:HttpClient, + ) { } /** * Perform a HTTP request and return a HalResource promise. */ - public request(method:HTTPSupportedMethods, href:string, data?:any, headers:HTTPClientHeaders = {}):Observable { + public request(method:HTTPSupportedMethods, href:string, data?:unknown, headers:HTTPClientHeaders = {}):Observable { // HttpClient requires us to create HttpParams instead of passing data for get // so forward to that method instead. if (method === 'get') { - return this.get(href, data, headers); + return this.get(href, data as HTTPClientParamMap|undefined, headers); } const config:HTTPClientOptions = { @@ -79,20 +101,19 @@ export class HalResourceService { responseType: 'json', }; - return this._request(method, href, config); + return this.performRequest(method, href, config); } - private _request(method:HTTPSupportedMethods, href:string, config:HTTPClientOptions):Observable { - return this.http.request(method, href, config) + private performRequest(method:HTTPSupportedMethods, href:string, config:HTTPClientOptions):Observable { + return this.http.request(method, href, config) .pipe( - map((response:any) => this.createHalResource(response)), + map((response:unknown) => this.createHalResource(response)), catchError((error:HttpErrorResponse) => { whenDebugging(() => console.error(`Failed to ${method} ${href}: ${error.name}`)); const resource = this.createHalResource(error.error); - resource.httpError = error; - return throwError(resource); + return throwError(new HalError(error, resource)); }), - ) as any; + ); } /** @@ -111,7 +132,7 @@ export class HalResourceService { responseType: 'json', }; - return this._request('get', href, config); + return this.performRequest('get', href, config); } /** @@ -124,20 +145,27 @@ export class HalResourceService { * @param headers * @return {Promise} */ - public async getAllPaginated(href:string, expected:number, params:any = {}, headers:HTTPClientHeaders = {}) { + public async getAllPaginated( + href:string, + expected:number, + params:Record = {}, + headers:HTTPClientHeaders = {}, + ):Promise { // Total number retrieved let retrieved = 0; // Current offset page let page = 1; // Accumulated results - const allResults:T = [] as any; + const allResults:T[] = []; // If possible, request all at once. - params.pageSize = expected; + const requestParams = { ...params }; + requestParams.pageSize = expected; while (retrieved < expected) { - params.offset = page; + requestParams.offset = page; - const promise = this.request('get', href, this.toEprops(params), headers).toPromise(); + const promise = this.request('get', href, this.toEprops(requestParams), headers).toPromise(); + // eslint-disable-next-line no-await-in-loop const results = await promise; if (results.count === 0) { diff --git a/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.ts b/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.ts index 9bc48c1fd4..f53c2534ef 100644 --- a/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.ts +++ b/frontend/src/app/features/work-packages/components/work-package-comment/work-package-comment.component.ts @@ -50,6 +50,7 @@ import { CommentService } from 'core-app/features/work-packages/components/wp-ac import { WorkPackagesActivityService } from 'core-app/features/work-packages/components/wp-single-view-tabs/activity-panel/wp-activity.service'; import { WorkPackageResource } from 'core-app/features/hal/resources/work-package-resource'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; +import { HalError } from 'core-app/features/hal/services/hal-error'; @Component({ selector: 'work-package-comment', @@ -146,13 +147,13 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler this.inFlight = true; await this.onSubmit(); const indicator = this.loadingIndicator.wpDetails; - return indicator.promise = this.commentService.createComment(this.workPackage, this.commentValue) + indicator.promise = this.commentService.createComment(this.workPackage, this.commentValue) .then(() => { this.active = false; this.NotificationsService.addSuccess(this.I18n.t('js.work_packages.comment_added')); - this.wpLinkedActivities.require(this.workPackage, true); - this + void this.wpLinkedActivities.require(this.workPackage, true); + void this .apiV3Service .work_packages .id(this.workPackage.id!) @@ -163,12 +164,14 @@ export class WorkPackageCommentComponent extends WorkPackageCommentFieldHandler }) .catch((error:any) => { this.inFlight = false; - if (error instanceof ErrorResource) { - this.workPackageNotificationService.showError(error, this.workPackage); + if (error instanceof HalError) { + this.workPackageNotificationService.showError(error.resource, this.workPackage); } else { this.NotificationsService.addError(this.I18n.t('js.work_packages.comment_send_failed')); } }); + + return indicator.promise; } scrollToBottom():void { diff --git a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts index c3487f896e..4ad392d48d 100644 --- a/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts +++ b/frontend/src/app/features/work-packages/components/wp-new/wp-create.component.ts @@ -48,6 +48,7 @@ import { APIV3Service } from 'core-app/core/apiv3/api-v3.service'; import { HalSource } from 'core-app/features/hal/resources/hal-resource'; import { OpTitleService } from 'core-app/core/html/op-title.service'; import { WorkPackageCreateService } from './wp-create.service'; +import { HalError } from 'core-app/features/hal/services/hal-error'; @Directive() export class WorkPackageCreateComponent extends UntilDestroyedMixin implements OnInit { @@ -152,8 +153,8 @@ export class WorkPackageCreateComponent extends UntilDestroyedMixin implements O }); } }) - .catch((error:any) => { - if (error.errorIdentifier === 'urn:openproject-org:api:v3:errors:MissingPermission') { + .catch((error:unknown) => { + if (error instanceof HalError && error.errorIdentifier === 'urn:openproject-org:api:v3:errors:MissingPermission') { this.apiV3Service.root.get().subscribe((root:RootResource) => { if (!root.user) { // Not logged in diff --git a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts index 9f7a331c02..eb5a63c1d9 100644 --- a/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts +++ b/frontend/src/app/shared/components/fields/edit/edit-form/edit-form.ts @@ -41,6 +41,7 @@ import { InjectField } from 'core-app/shared/helpers/angular/inject-field.decora import { HalResourceNotificationService } from 'core-app/features/hal/services/hal-resource-notification.service'; import { ErrorResource } from 'core-app/features/hal/resources/error-resource'; import isNewResource from 'core-app/features/hal/helpers/is-new-resource'; +import { HalError } from 'core-app/features/hal/services/hal-error'; export const activeFieldContainerClassName = 'inline-edit--active-field'; export const activeFieldClassName = 'inline-edit--field'; @@ -193,8 +194,8 @@ export abstract class EditForm { .catch((error:ErrorResource|unknown) => { this.halNotification.handleRawError(error, this.resource); - if (error instanceof ErrorResource) { - this.handleSubmissionErrors(error); + if (error instanceof HalError) { + this.handleSubmissionErrors(error.resource); reject(); } @@ -226,12 +227,12 @@ export abstract class EditForm { }); } - protected handleSubmissionErrors(error:any) { + protected handleSubmissionErrors(error:ErrorResource):void { // Process single API errors this.handleErroneousAttributes(error); } - protected handleErroneousAttributes(error:any) { + protected handleErroneousAttributes(error:ErrorResource):void { // Get attributes withe errors const erroneousAttributes = error.getInvolvedAttributes(); @@ -241,7 +242,7 @@ export abstract class EditForm { return; } - return this.setErrorsForFields(erroneousAttributes); + this.setErrorsForFields(erroneousAttributes); } private setErrorsForFields(erroneousFields:string[]) { diff --git a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts index 7289e1aa37..e71086ced7 100644 --- a/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts +++ b/frontend/src/app/shared/components/work-package-graphs/configuration/wp-graph-configuration.service.ts @@ -21,7 +21,7 @@ export class WpGraphConfigurationService { private _forms:{ [id:string]:QueryFormResource } = {}; - private _formsPromise:Promise|null; + private _formsPromise:Promise|null; constructor( readonly I18n:I18nService,