AAE-36869 Handles error messages with wrong JSON format (#11078)

* Handles error messages with wrong JSON format

Handles cases where the error message from the backend is not a valid JSON.

If the message is not a valid JSON, it displays the error as a string.
This prevents the application from crashing when receiving error messages with wrong JSON format.

Fixes AAE-36869

* SonarCloud solution

* Displays the error message from the response

The error message is now extracted from the response body,
providing more context when process start fails.

This change ensures that the user sees the specific error
message returned by the service when a process instance
cannot be started, improving the user experience.

* Refactors start process template to use `@if` blocks

Migrates the start process component template from `*ngIf` directives to the new Angular `@if` syntax for improved readability and performance.
This change enhances the structure and efficiency of conditional rendering within the template.

* prettier
This commit is contained in:
Alexander Puschkin
2025-08-05 09:20:17 +02:00
committed by GitHub
parent bfd29139a1
commit 8ef0aee768
4 changed files with 162 additions and 146 deletions

View File

@@ -1,147 +1,154 @@
<mat-card appearance="outlined" class="adf-start-process" *ngIf="processDefinitionLoaded; else spinner">
@if (processDefinitionLoaded) {
<mat-card appearance="outlined" class="adf-start-process">
<mat-card-content>
@if (showTitle) {
<mat-card-title class="adf-title">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.TITLE' | translate }}
</mat-card-title>
}
<mat-card-content>
<mat-card-title
*ngIf="showTitle"
class="adf-title">
{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.TITLE' | translate}}
</mat-card-title>
@if (errorMessageId) {
<mat-card-subtitle class="adf-error-message" id="error-message">
{{ errorMessageId | translate }}
</mat-card-subtitle>
}
<mat-card-subtitle id="error-message" *ngIf="errorMessageId">
{{ errorMessageId | translate }}
</mat-card-subtitle>
@if (!isProcessDefinitionsEmpty) {
<div>
<form [formGroup]="processForm" class="adf-select-process-form">
@if (showSelectProcessDropdown) {
<mat-form-field
class="adf-process-input-container"
floatLabel="always"
data-automation-id="adf-select-cloud-process-dropdown"
>
<mat-label class="adf-start-process-input-label">{{
'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.TYPE' | translate
}}</mat-label>
<input matInput formControlName="processDefinition" [matAutocomplete]="auto" id="processDefinitionName" />
<div *ngIf="!isProcessDefinitionsEmpty; else emptyProcessDefinitionsList">
<form [formGroup]="processForm" class="adf-select-process-form">
<mat-form-field
class="adf-process-input-container"
floatLabel="always"
*ngIf="showSelectProcessDropdown"
data-automation-id="adf-select-cloud-process-dropdown"
>
<mat-label class="adf-start-process-input-label">{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.TYPE' | translate }}</mat-label>
<input
matInput
formControlName="processDefinition"
[matAutocomplete]="auto"
id="processDefinitionName"
>
<div class="adf-process-input-autocomplete">
<mat-autocomplete
#auto="matAutocomplete"
id="processDefinitionOptions"
[displayWith]="displayProcessNameOnDropdown"
(optionSelected)="setProcessDefinitionOnForm($event.option.value)" >
<mat-option
*ngFor="let processDef of filteredProcesses"
[value]="getProcessDefinitionValue(processDef)"
(click)="processDefinitionSelectionChanged(processDef)">
<div class="adf-process-input-autocomplete">
<mat-autocomplete
#auto="matAutocomplete"
id="processDefinitionOptions"
[displayWith]="displayProcessNameOnDropdown"
(optionSelected)="setProcessDefinitionOnForm($event.option.value)"
>
<mat-option
*ngFor="let processDef of filteredProcesses"
[value]="getProcessDefinitionValue(processDef)"
(click)="processDefinitionSelectionChanged(processDef)"
>
{{ getProcessDefinitionValue(processDef) }}
</mat-option>
</mat-autocomplete>
</mat-option>
</mat-autocomplete>
<button
id="adf-select-process-dropdown"
title="{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.SELECT_PROCESS_DROPDOWN' | translate}}"
mat-icon-button
(click)="displayDropdown($event)">
<mat-icon>arrow_drop_down</mat-icon>
</button>
<button
id="adf-select-process-dropdown"
title="{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.SELECT_PROCESS_DROPDOWN' | translate }}"
mat-icon-button
(click)="displayDropdown($event)"
>
<mat-icon>arrow_drop_down</mat-icon>
</button>
</div>
@if (processDefinition.hasError('required')) {
<mat-error class="adf-error-pb">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_DEFINITION_REQUIRED' | translate }}
</mat-error>
}
</mat-form-field>
}
</div>
<mat-error
*ngIf="processDefinition.hasError('required')"
class="adf-error-pb">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_DEFINITION_REQUIRED' | translate }}
</mat-error>
</mat-form-field>
<adf-inplace-form-input [control]="processInstanceName">
<ng-container label>
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.NAME' | translate }}
</ng-container>
<adf-inplace-form-input [control]="processInstanceName">
<ng-container label>
{{'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.FORM.LABEL.NAME' | translate}}
</ng-container>
<ng-container error>
@if (processInstanceName.hasError('required')) {
<span>
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_NAME_REQUIRED' | translate }}
</span>
}
@if (processInstanceName.hasError('maxlength')) {
<span id="adf-start-process-maxlength-error">
{{
'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.MAXIMUM_LENGTH'
| translate: { characters: maxNameLength }
}}
</span>
}
@if (processInstanceName.hasError('pattern')) {
<span>
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.SPACE_VALIDATOR' | translate }}
</span>
}
</ng-container>
</adf-inplace-form-input>
</form>
<ng-container error>
<span *ngIf="processInstanceName.hasError('required')">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_NAME_REQUIRED' | translate }}
</span>
<span *ngIf="processInstanceName.hasError('maxlength')" id="adf-start-process-maxlength-error">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.MAXIMUM_LENGTH' | translate : { characters : maxNameLength } }}
</span>
<span *ngIf="processInstanceName.hasError('pattern')">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.SPACE_VALIDATOR' | translate }}
</span>
</ng-container>
</adf-inplace-form-input>
</form>
<ng-container *ngIf="hasForm else taskFormCloudButtons">
<adf-cloud-form
#startForm
[appName]="appName"
[appVersion]="processDefinitionCurrent.appVersion"
[data]="resolvedValues"
[formId]="processDefinitionCurrent.formKey"
[displayModeConfigurations]="displayModeConfigurations"
[showSaveButton]="showSaveButton"
[showCompleteButton]="showCompleteButton"
[showRefreshButton]="false"
[showValidationIcon]="false"
[showTitle]="false"
(formContentClicked)="onFormContentClicked($event)"
(formLoaded)="onFormLoaded($event)"
(executeOutcome)="onCustomOutcomeClicked($event.outcome.name)"
>
<adf-cloud-form-custom-outcomes>
<ng-template [ngTemplateOutlet]="taskFormCloudButtons" />
</adf-cloud-form-custom-outcomes>
</adf-cloud-form>
</ng-container>
</div>
</mat-card-content>
</mat-card>
@if (hasForm) {
<adf-cloud-form
#startForm
[appName]="appName"
[appVersion]="processDefinitionCurrent.appVersion"
[data]="resolvedValues"
[formId]="processDefinitionCurrent.formKey"
[displayModeConfigurations]="displayModeConfigurations"
[showSaveButton]="showSaveButton"
[showCompleteButton]="showCompleteButton"
[showRefreshButton]="false"
[showValidationIcon]="false"
[showTitle]="false"
(formContentClicked)="onFormContentClicked($event)"
(formLoaded)="onFormLoaded($event)"
(executeOutcome)="onCustomOutcomeClicked($event.outcome.name)"
>
<adf-cloud-form-custom-outcomes>
<ng-template [ngTemplateOutlet]="taskFormCloudButtons" />
</adf-cloud-form-custom-outcomes>
</adf-cloud-form>
} @else {
<ng-template [ngTemplateOutlet]="taskFormCloudButtons" />
}
</div>
} @else {
@if (processDefinitionLoaded) {
<mat-card-content>
<mat-card-subtitle class="error-message" id="no-process-message">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.NO_PROCESS_DEFINITIONS' | translate | uppercase }}
</mat-card-subtitle>
</mat-card-content>
}
}
</mat-card-content>
</mat-card>
} @else {
<div class="adf-loading-container">
<mat-progress-spinner class="adf-loading" color="primary" mode="indeterminate" />
</div>
}
<ng-template #taskFormCloudButtons>
<div class="adf-start-process-cloud-actions">
<button
*ngIf="showCancelButton"
mat-button
(click)="cancelStartProcess()"
id="cancel_process"
>
{{ cancelButtonLabel }}
</button>
<button
*ngIf="showStartProcessButton$ | async"
color="primary"
mat-raised-button
[disabled]="disableStartButton || !isProcessFormValid"
(click)="startProcess()"
data-automation-id="btn-start"
id="button-start"
class="adf-btn-start"
>
@if (showCancelButton) {
<button mat-button (click)="cancelStartProcess()" id="cancel_process">
{{ cancelButtonLabel }}
</button>
}
@if (showStartProcessButton$ | async) {
<button
color="primary"
mat-raised-button
[disabled]="disableStartButton || !isProcessFormValid"
(click)="startProcess()"
data-automation-id="btn-start"
id="button-start"
class="adf-btn-start"
>
{{ startProcessButtonLabel }}
</button>
</div>
</ng-template>
<ng-template #emptyProcessDefinitionsList>
<mat-card-content *ngIf="processDefinitionLoaded">
<mat-card-subtitle class="error-message" id="no-process-message">
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.NO_PROCESS_DEFINITIONS' | translate | uppercase}}
</mat-card-subtitle>
</mat-card-content>
</ng-template>
<ng-template #spinner>
<div class="adf-loading-container">
<mat-progress-spinner
class="adf-loading"
color="primary"
mode="indeterminate" />
</button>
}
</div>
</ng-template>

View File

@@ -19,6 +19,10 @@
padding-bottom: 1.25em;
}
&-error-message {
padding-left: 0.5em;
}
&-process-input-container {
margin: 0 7px;
}

View File

@@ -877,7 +877,13 @@ describe('StartProcessCloudComponent', () => {
it('should throw error event when process cannot be started', async () => {
const errorSpy = spyOn(component.error, 'emit');
const error = { message: 'My error' };
const error = {
response: {
body: {
message: 'My error'
}
}
};
startProcessSpy = startProcessSpy.and.returnValue(throwError(error));
component.startProcess();
await fixture.whenStable();
@@ -888,14 +894,21 @@ describe('StartProcessCloudComponent', () => {
getDefinitionsSpy.and.returnValue(of(fakeProcessDefinitions));
const change = new SimpleChange('myApp', 'myApp1', true);
component.ngOnChanges({ appName: change });
startProcessSpy = startProcessSpy.and.returnValue(throwError({}));
const error = {
response: {
body: {
message: 'Process start failed'
}
}
};
startProcessSpy = startProcessSpy.and.returnValue(throwError(error));
component.startProcess();
fixture.detectChanges();
await fixture.whenStable();
const errorEl = fixture.nativeElement.querySelector('#error-message');
expect(errorEl.innerText.trim()).toBe('ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.START');
expect(errorEl.innerText.trim()).toBe('Process start failed');
});
it('should emit start event when start select a process and add a name', (done) => {

View File

@@ -70,6 +70,7 @@ const PROCESS_DEFINITION_IDENTIFIER_REG_EXP = new RegExp('%{processdefinition}',
@Component({
selector: 'adf-cloud-start-process',
standalone: true,
imports: [
CommonModule,
TranslatePipe,
@@ -456,8 +457,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
this.isProcessStarting = false;
},
error: (err) => {
this.errorMessageId = 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.START';
this.unifyErrorResponse(err);
this.errorMessageId = err?.response?.body?.message || 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.START_PROCESS';
this.error.emit(err);
this.isProcessStarting = false;
}
@@ -483,14 +483,6 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
}
}
private unifyErrorResponse(err: any) {
if (!err?.response?.body?.entry && err?.response?.body?.message) {
err.response.body = {
entry: JSON.parse(err.response.body.message)
};
}
}
cancelStartProcess() {
this.cancel.emit();
}