Improvement/AAE-43974 Refactoring form fields (#11945)

* AAE-43974 Refactoring form fields with mat-error

* AAE-43974 Removed adf-error redundant class from all available places

* AAE-43974 Followed code flow syntax

* AAE-43974 Refactored based on review comments

* AAE-43974 Added dummy test lable

* AAE-43974 Added dummy error message for amount field

* AAE-43974 Refactored multiline widget based on reviewer's comment

* AAE-43974 Error icon style added by mixins

* AAE-43974 Missing Conditions added

* AAE-43974 Added Missing conditions

* AAE-43974 Consistent conditions added and dummy error message added

* AAE-43974 Added subscriptsizing dynamic for testing

* AAE-43974 Reverted Subscriptsizing

* AAE-43974 Added spacing between form fields in mobile view

* AAE-43974 Dummy error message changed

* AAE-43974 Changed error icon allignment for testing

* AAE-43974 Added dummy error message

* AAE-43974 Added form field gap in mobile view and dummy erro message

* AAE-43974 Refactored based on review coemments and changed the error icon allignment

* AAE-43974 Fixed the mixin path

* AAE-43974 Removed dummy error message for amount widget

* AAE-43974 Added adf-error-text class to mat-error for e2e fix

* AAE-43974 adf-error-text class added inside mat-error

* AAE-43974 Whitespace issue resolved

* AAE-43974 Whiespace issue fix

* AAE-43974 Added context pushing logic in text widget

* AAE-43974 Added margin-bottom for regression fix

* AAE-43974 Trying with different margin-bottom

* AAE-43974 Testing regression issue

* AAE-43974 Testing for regression failure

* AAE-43974 Testing Regression failures

* AAE-43974 Test Regression failure

* AAE-43974 Testing multiline Regression failure

* AAE-43974 Testing fix regarding regression failure related to multiline-text widget

* AAE-43974 Test Regression failure for multiline-text widget

* AAE-43974 Testing regression failure for multiline-text

* AAE-43974 Testing regression failure for multi-line

* AAE-43974 Testing regression failure

* AAE-43974 Testing Regression failure

* AAE-43974 Testing regression failure

* AAE-43974 Removed the commented code

* [ci:force] re-trigger CI

---------

Co-authored-by: Ehsan Rezaei <ehsan.rezaei@hyland.com>
This commit is contained in:
Anamika Dey
2026-07-08 18:32:46 +00:00
committed by GitHub
co-authored by Ehsan Rezaei
parent 23f0392c12
commit b0dc85422c
75 changed files with 756 additions and 445 deletions
@@ -0,0 +1,7 @@
@mixin adf-error-icon {
font-size: 16px;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: text-bottom;
}
@@ -35,9 +35,13 @@
/>
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" />
<mat-datepicker #datePicker [startAt]="startAt" [disabled]="field.readOnly" />
@if (dateInputControl.invalid && dateInputControl.touched) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}}</span>
</mat-error>
}
</mat-form-field>
<div class="adf-error-messages-container">
<error-widget *ngIf="dateInputControl.invalid && dateInputControl.touched" [error]="field.validationSummary" />
</div>
</div>
</div>
@@ -1,5 +1,6 @@
/* stylelint-disable selector-class-pattern */
@use '../../../../mat-selectors' as ms;
@use '../../../../mixins' as mixins;
.adf {
// eslint-disable-selector-class-pattern
@@ -29,3 +30,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -25,7 +25,6 @@ import {
DateFnsUtils,
DEFAULT_DATE_FORMAT,
ErrorMessageModel,
ErrorWidgetComponent,
FormService,
WidgetComponent,
ReactiveFormWidget
@@ -36,12 +35,13 @@ import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '
import { NgIf } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'date-widget',
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent],
imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, MatIconModule],
providers: [
{ provide: MAT_DATE_FORMATS, useValue: ADF_DATE_FORMATS },
{ provide: DateAdapter, useClass: AdfDateFnsAdapter }
@@ -125,6 +125,16 @@ export class DateCloudWidgetComponent extends WidgetComponent implements OnInit,
this.onFieldChanged(this.field);
}
get formattedMinDate(): string {
const min = this.dateInputControl.errors?.matDatepickerMin?.min;
return min ? DateFnsUtils.formatDate(min, this.field.dateDisplayFormat).toLocaleUpperCase() : '';
}
get formattedMaxDate(): string {
const max = this.dateInputControl.errors?.matDatepickerMax?.max;
return max ? DateFnsUtils.formatDate(max, this.field.dateDisplayFormat).toLocaleUpperCase() : '';
}
private validateField(): void {
if (this.dateInputControl.invalid) {
this.handleErrors(this.dateInputControl.errors);
@@ -23,6 +23,7 @@
[id]="field.id"
[formControl]="propertyControl"
[required]="field.required"
[errorStateMatcher]="propertyErrorStateMatcher"
/>
<ng-container *ngIf="previewState">
@@ -30,13 +31,9 @@
{{ field.params.externalPropertyLabel }}
</span>
</ng-container>
@if (propertyLoadFailed && !previewState) {
<mat-error><mat-icon class="adf-error-icon">error_outline</mat-icon><span class="adf-error-text">{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }}</span></mat-error>
}
</mat-form-field>
<div
class="adf-error-messages-container"
[ngClass]="!previewState ? 'adf-error-messages-container-visible' : 'adf-error-messages-container-hidden'"
>
<error-widget *ngIf="propertyLoadFailed" [required]="'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate" />
</div>
</div>
</div>
@@ -1,3 +1,5 @@
@use '../../../../mixins' as mixins;
.adf {
&-display-external-property-widget {
width: 100%;
@@ -11,3 +13,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -20,6 +20,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatInputHarness } from '@angular/material/input/testing';
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
import { DisplayExternalPropertyWidgetComponent } from './display-external-property.widget';
import { FormCloudService } from '../../../services/form-cloud.service';
import { By } from '@angular/platform-browser';
@@ -104,9 +105,10 @@ describe('DisplayExternalPropertyWidgetComponent', () => {
fixture.detectChanges();
});
it('should display the error message', () => {
const errorElement = element.querySelector('error-widget');
expect(errorElement.textContent.trim()).toContain('FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED');
it('should display the error message', async () => {
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors[0].trim()).toContain('FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED');
});
});
@@ -123,9 +125,10 @@ describe('DisplayExternalPropertyWidgetComponent', () => {
fixture.detectChanges();
});
it('should NOT display the error message', () => {
const errorElement = element.querySelector('error-widget');
expect(errorElement).toBeFalsy();
it('should NOT display the error message', async () => {
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should display external property name', () => {
@@ -21,14 +21,16 @@ import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { FormCloudService } from '../../../services/form-cloud.service';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { isObservable } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
standalone: true,
imports: [CommonModule, TranslatePipe, ReactiveFormsModule, MatFormFieldModule, MatInputModule, FormBaseModule],
imports: [CommonModule, TranslatePipe, ReactiveFormsModule, MatFormFieldModule, MatInputModule, FormBaseModule, MatIconModule],
selector: 'adf-cloud-display-external-property',
templateUrl: './display-external-property.widget.html',
styleUrls: ['./display-external-property.widget.scss'],
@@ -51,6 +53,10 @@ export class DisplayExternalPropertyWidgetComponent extends WidgetComponent impl
previewState = false;
propertyControl: FormControl;
propertyErrorStateMatcher: ErrorStateMatcher = {
isErrorState: () => this.propertyLoadFailed && !this.previewState
};
private readonly formCloudService = inject(FormCloudService);
private readonly formatter = inject(FormFieldValueFormatterService);
private readonly formattingEnabledToken = inject(ADF_TYPED_VALUE_FORMATTING_ENABLED, { optional: true });
@@ -25,6 +25,7 @@
panelClass="adf-select-filter"
[multiple]="field.hasMultipleValues"
[required]="field.required"
[errorStateMatcher]="dropdownErrorStateMatcher"
#select
(keydown.escape)="select.close()"
>
@@ -47,27 +48,13 @@
</mat-option>
}
</mat-select>
@if ((dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) || (!previewState && !field.readOnly && (isRestApiFailed || variableOptionsFailed))) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text"
>@if (dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (isRestApiFailed) {{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}} @else if (variableOptionsFailed) {{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}}</span>
</mat-error>
}
</mat-form-field>
<div
class="adf-error-messages-container"
[ngClass]="!field.readOnly ? 'adf-error-messages-container-visible' : 'adf-error-messages-container-hidden'"
>
@if(showRequiredMessage) {
<error-widget class="adf-dropdown-required-message" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
}
@if(!previewState && isRestApiFailed) {
<error-widget
class="adf-dropdown-failed-message"
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"
/>
}
@if(!previewState && variableOptionsFailed) {
<error-widget
class="adf-dropdown-failed-message"
required="{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}"
/>
}
</div>
</div>
</div>
@@ -1,4 +1,5 @@
@use '../../../../mat-selectors' as ms;
@use '../../../../mixins' as mixins;
.adf {
&-dropdown-widget {
@@ -34,3 +35,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -45,7 +45,7 @@ import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatSelectHarness } from '@angular/material/select/testing';
import { DebugElement } from '@angular/core';
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
import { FormUtilsService } from '../../../services/form-utils.service';
describe('DropdownCloudWidgetComponent', () => {
@@ -234,8 +234,7 @@ describe('DropdownCloudWidgetComponent', () => {
describe('when failed on loading options from restUrl', () => {
let getRestWidgetDataSpy: jasmine.Spy;
const getErrorMessageElement = (): DebugElement => fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
const errorIcon: string = 'error_outline';
const getFormFieldHarness = (): Promise<MatFormFieldHarness> => loader.getHarness(MatFormFieldHarness);
beforeEach(() => {
getRestWidgetDataSpy = spyOn(formCloudService, 'getRestWidgetData').and.returnValue(throwError('Failed to fetch options'));
@@ -243,23 +242,21 @@ describe('DropdownCloudWidgetComponent', () => {
widget.field.optionType = 'rest';
});
it('should show error message when widget is NOT readonly', () => {
it('should show error message when widget is NOT readonly', async () => {
widget.field.readOnly = false;
widget.ngOnInit();
fixture.detectChanges();
const errorMessageElement = getErrorMessageElement();
const formField = await getFormFieldHarness();
const errors = await formField.getTextErrors();
expect(getRestWidgetDataSpy).toHaveBeenCalled();
expect(widget.isRestApiFailed).toBe(true);
expect(widget.field.options.length).toEqual(0);
expect(errorMessageElement.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-visible')).toBe(true);
expect(errors[0]).toContain('FORM.FIELD.REST_API_FAILED');
});
it('should NOT show required message when REST API failed', () => {
it('should NOT show required message when REST API failed', async () => {
widget.field.readOnly = false;
widget.field.required = true;
@@ -268,10 +265,10 @@ describe('DropdownCloudWidgetComponent', () => {
fixture.detectChanges();
expect(widget.isRestApiFailed).toBe(true);
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeFalsy();
const restApiFailedElement = getErrorMessageElement();
expect(restApiFailedElement).toBeTruthy();
const formField = await getFormFieldHarness();
const errors = await formField.getTextErrors();
expect(errors[0]).not.toContain('FORM.FIELD.REQUIRED');
expect(errors.length).toBeGreaterThan(0);
});
it('should NOT show error message when widget is readonly', async () => {
@@ -284,11 +281,12 @@ describe('DropdownCloudWidgetComponent', () => {
expect(widget.isRestApiFailed).toBe(true);
expect(widget.field.options.length).toEqual(0);
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-hidden')).toBe(true);
const formField = await getFormFieldHarness();
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should NOT show REST API failed error message when form is in preview state', () => {
it('should NOT show REST API failed error message when form is in preview state', async () => {
widget.field.readOnly = false;
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
@@ -299,8 +297,9 @@ describe('DropdownCloudWidgetComponent', () => {
expect(widget.isRestApiFailed).toBe(true);
expect(widget.field.options.length).toEqual(0);
const failedErrorMsgElement = getErrorMessageElement();
expect(failedErrorMsgElement).toBeNull();
const formField = await getFormFieldHarness();
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
});
@@ -314,10 +313,10 @@ describe('DropdownCloudWidgetComponent', () => {
widget.field.isVisible = true;
});
it('should show required message for rest type when required and value is empty after touch', fakeAsync(() => {
it('should show required message for rest type when required and value is empty after touch', async () => {
widget.ngOnInit();
fixture.detectChanges();
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
await fixture.whenStable();
fixture.detectChanges();
expect(widget.isRestApiFailed).toBe(false);
@@ -326,10 +325,11 @@ describe('DropdownCloudWidgetComponent', () => {
widget.dropdownControl.markAsTouched();
fixture.detectChanges();
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeTruthy();
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
}));
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
});
});
it('should preselect dropdown widget value when Json (rest call) passed', async () => {
@@ -398,25 +398,27 @@ describe('DropdownCloudWidgetComponent', () => {
widget.touched = true;
fixture.detectChanges();
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeFalsy();
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should not display required error when selecting a valid option for a required dropdown', fakeAsync(async () => {
it('should not display required error when selecting a valid option for a required dropdown', async () => {
widget.field.required = true;
widget.field.options = [{ id: 'empty', name: 'Choose empty' }, ...fakeOptionList];
widget.ngOnInit();
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
await fixture.whenStable();
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
await dropdown.open();
widget.touched = true;
await dropdown.clickOptions({ selector: '[id="opt_1"]' });
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeFalsy();
}));
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should not have a value when switching from an available option to the None option', async () => {
widget.field.options = [{ id: 'empty', name: 'This is a mock none option' }, ...fakeOptionList];
@@ -455,7 +457,7 @@ describe('DropdownCloudWidgetComponent', () => {
expect(asterisk.textContent).toEqual('*');
});
it('should display a required error when dropdown is required and has no value after an interaction', () => {
it('should display a required error when dropdown is required and has no value after an interaction', async () => {
fixture.detectChanges();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
@@ -467,11 +469,10 @@ describe('DropdownCloudWidgetComponent', () => {
expect(element.querySelector('.adf-invalid')).toBeTruthy();
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-visible')).toBe(true);
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
});
it('should NOT display a required error when dropdown is readonly', () => {
@@ -815,7 +816,6 @@ describe('DropdownCloudWidgetComponent', () => {
it('should reset previous child options if the rest url failed for a linked dropdown', async () => {
const jsonDataSpy = spyOn(formCloudService, 'getRestWidgetData').and.returnValue(of(mockRestDropdownOptions));
const errorIcon: string = 'error_outline';
const mockParentDropdown = { id: 'parentDropdown', value: 'mock-value', validate: () => true };
spyOn(widget.field.form, 'getFormFields').and.returnValue([mockParentDropdown]);
@@ -828,27 +828,28 @@ describe('DropdownCloudWidgetComponent', () => {
selectParentOption('UK');
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
await dropdown.open();
const failedErrorMsgElement1 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
const formField = await loader.getHarness(MatFormFieldHarness);
const errors1 = await formField.getTextErrors();
expect(widget.isRestApiFailed).toBe(false);
expect(widget.field.options.length).toBe(2);
expect(failedErrorMsgElement1).toBeNull();
expect(errors1.length).toBe(0);
jsonDataSpy.and.returnValue(throwError('Failed to fetch options'));
selectParentOption('GR');
const failedErrorMsgElement2 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
const errors2 = await formField.getTextErrors();
expect(widget.isRestApiFailed).toBe(true);
expect(widget.field.options.length).toBe(0);
expect(failedErrorMsgElement2.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
expect(errors2[0]).toContain('FORM.FIELD.REST_API_FAILED');
jsonDataSpy.and.returnValue(of(mockSecondRestDropdownOptions));
selectParentOption('IT');
const failedErrorMsgElement3 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
const errors3 = await formField.getTextErrors();
expect(widget.isRestApiFailed).toBe(false);
expect(widget.field.options.length).toBe(2);
expect(failedErrorMsgElement3).toBeNull();
expect(errors3.length).toBe(0);
});
describe('Rest - On parent value changes (chain)', () => {
@@ -1067,8 +1068,6 @@ describe('DropdownCloudWidgetComponent', () => {
});
describe('variable options', () => {
const errorIcon: string = 'error_outline';
const getVariableDropdownWidget = (
variableName: string,
optionsPath: string,
@@ -1090,9 +1089,10 @@ describe('DropdownCloudWidgetComponent', () => {
}
});
const checkDropdownVariableOptionsFailed = () => {
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED'));
const checkDropdownVariableOptionsFailed = async () => {
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors[0]).toContain('FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED');
expect(widget.field.options.length).toEqual(0);
};
@@ -1258,7 +1258,7 @@ describe('DropdownCloudWidgetComponent', () => {
expect(await allOptions[2].getText()).toEqual('Parrot');
});
it('should return empty array and display error when path is incorrect', () => {
it('should return empty array and display error when path is incorrect', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.wrongPath.players',
@@ -1268,10 +1268,10 @@ describe('DropdownCloudWidgetComponent', () => {
);
fixture.detectChanges();
checkDropdownVariableOptionsFailed();
await checkDropdownVariableOptionsFailed();
});
it('should NOT show required message when variable options failed', () => {
it('should NOT show required message when variable options failed', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.wrongPath.players',
@@ -1283,13 +1283,13 @@ describe('DropdownCloudWidgetComponent', () => {
fixture.detectChanges();
expect(widget.variableOptionsFailed).toBe(true);
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeFalsy();
const variableFailedElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
expect(variableFailedElement).toBeTruthy();
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors[0]).not.toContain('FORM.FIELD.REQUIRED');
expect(errors.length).toBeGreaterThan(0);
});
it('should show required message for variable type when options load successfully and required and value is empty after touch', fakeAsync(() => {
it('should show required message for variable type when options load successfully and required and value is empty after touch', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.people.players',
@@ -1302,7 +1302,7 @@ describe('DropdownCloudWidgetComponent', () => {
widget.field.isVisible = true;
widget.ngOnInit();
fixture.detectChanges();
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
await fixture.whenStable();
fixture.detectChanges();
expect(widget.variableOptionsFailed).toBe(false);
@@ -1311,12 +1311,13 @@ describe('DropdownCloudWidgetComponent', () => {
widget.dropdownControl.markAsTouched();
fixture.detectChanges();
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
expect(requiredErrorElement).toBeTruthy();
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
}));
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBeGreaterThan(0);
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
});
it('should return empty array and display error when id is incorrect', () => {
it('should return empty array and display error when id is incorrect', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.people.players',
@@ -1326,10 +1327,10 @@ describe('DropdownCloudWidgetComponent', () => {
);
fixture.detectChanges();
checkDropdownVariableOptionsFailed();
await checkDropdownVariableOptionsFailed();
});
it('should return empty array and display error when label is incorrect', () => {
it('should return empty array and display error when label is incorrect', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.people.players',
@@ -1339,10 +1340,10 @@ describe('DropdownCloudWidgetComponent', () => {
);
fixture.detectChanges();
checkDropdownVariableOptionsFailed();
await checkDropdownVariableOptionsFailed();
});
it('should return empty array and display error when variable is NOT found', () => {
it('should return empty array and display error when variable is NOT found', async () => {
widget.field = getVariableDropdownWidget(
'variables.wrong-variable-id',
'response.people.players',
@@ -1352,17 +1353,17 @@ describe('DropdownCloudWidgetComponent', () => {
);
fixture.detectChanges();
checkDropdownVariableOptionsFailed();
await checkDropdownVariableOptionsFailed();
});
it('should return empty array and display error if there are NO process and form variables', () => {
it('should return empty array and display error if there are NO process and form variables', async () => {
widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'playerId', 'playerFullName', [], []);
fixture.detectChanges();
checkDropdownVariableOptionsFailed();
await checkDropdownVariableOptionsFailed();
});
it('should NOT display variable options failed error when form is in the preview state', () => {
it('should NOT display variable options failed error when form is in the preview state', async () => {
widget.field = getVariableDropdownWidget(
'variables.json-variable',
'response.wrongPath.players',
@@ -1373,11 +1374,12 @@ describe('DropdownCloudWidgetComponent', () => {
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
fixture.detectChanges();
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
expect(failedErrorMsgElement).toBeNull();
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should NOT display errors if field is readonly', () => {
it('should NOT display errors if field is readonly', async () => {
widget.field = getVariableDropdownWidget(
'variables.wrong-variable-id',
'response.wrongPath.players',
@@ -1388,8 +1390,9 @@ describe('DropdownCloudWidgetComponent', () => {
widget.field.readOnly = true;
fixture.detectChanges();
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-hidden')).toBe(true);
const formField = await loader.getHarness(MatFormFieldHarness);
const errors = await formField.getTextErrors();
expect(errors.length).toBe(0);
});
it('should update options when form variable changes', async () => {
@@ -18,7 +18,6 @@
import {
AppConfigService,
ErrorMessageModel,
ErrorWidgetComponent,
FormFieldEvent,
FormFieldModel,
FormFieldOption,
@@ -31,11 +30,13 @@ import {
SelectFilterInputComponent,
WidgetComponent
} from '@alfresco/adf-core';
import { AsyncPipe, NgClass } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { TranslatePipe } from '@ngx-translate/core';
import { BehaviorSubject, isObservable, Subject } from 'rxjs';
@@ -62,16 +63,7 @@ export const DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE = 100;
'(click)': 'event($event)'
},
encapsulation: ViewEncapsulation.None,
imports: [
NgClass,
AsyncPipe,
ReactiveFormsModule,
MatFormFieldModule,
MatSelectModule,
ErrorWidgetComponent,
TranslatePipe,
SelectFilterInputComponent
]
imports: [AsyncPipe, ReactiveFormsModule, MatFormFieldModule, MatSelectModule, TranslatePipe, SelectFilterInputComponent, MatIconModule]
})
export class DropdownCloudWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
public formService = inject(FormService);
@@ -92,6 +84,11 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
restApiHostName: string;
dropdownControl = new FormControl<FormFieldOption | FormFieldOption[]>(undefined);
dropdownErrorStateMatcher: ErrorStateMatcher = {
isErrorState: (control) =>
(control?.touched && control?.invalid) || (!this.previewState && (this.isRestApiFailed || this.variableOptionsFailed))
};
list$ = new BehaviorSubject<FormFieldOption[]>([]);
filter$ = new BehaviorSubject<string>('');
@@ -101,10 +98,6 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
private readonly debounceSetValue = new Subject<void>();
get showRequiredMessage(): boolean {
return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
}
get isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
@@ -1,3 +1,5 @@
@use '../../mixins' as mixins;
.adf {
&-cloud-group-list {
margin: 5px 0;
@@ -42,3 +44,7 @@
}
}
}
.adf-error-messages-container .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -1,3 +1,5 @@
@use '../../mixins' as mixins;
.adf {
&-people-cloud {
width: 100%;
@@ -46,3 +48,7 @@
}
}
}
.adf-error-messages-container .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -73,7 +73,7 @@
placeholder="{{processFilterProperty.label | translate}}"
[attr.data-automation-id]="'adf-cloud-edit-process-property-' + processFilterProperty.key"/>
</mat-form-field>
<mat-form-field [floatLabel]="'auto'" *ngIf="processFilterProperty.type === 'date'" [attr.data-automation-id]="processFilterProperty.key">
<mat-form-field [floatLabel]="'auto'" class="adf-form-field-input" *ngIf="processFilterProperty.type === 'date'" [attr.data-automation-id]="processFilterProperty.key">
<mat-label>{{processFilterProperty.label | translate}}</mat-label>
<input
matInput
@@ -87,12 +87,12 @@
matSuffix [for]="dateController"
[attr.data-automation-id]="'adf-cloud-edit-process-property-date-toggle-' + processFilterProperty.key" />
<mat-datepicker #dateController [attr.data-automation-id]="'adf-cloud-edit-process-property-date-picker-' + processFilterProperty.key" />
<div class="adf-edit-process-filter-date-error-container">
<mat-error *ngIf="hasError(processFilterProperty)">
<div class="adf-error-text">{{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}</div>
<mat-icon class="adf-error-icon" adf-icon="warning" />
@if (hasError(processFilterProperty)) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text">{{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}</span>
</mat-error>
</div>
}
</mat-form-field>
<adf-cloud-date-range-filter *ngIf="processFilterProperty.type === 'date-range'"
@@ -1,4 +1,5 @@
@use '../../../../flex' as flex;
@use '../../../../mixins' as mixins;
.adf-edit-process-filter-date-error-container {
position: absolute;
@@ -62,3 +63,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -53,7 +53,7 @@
</button>
</div>
@if (processDefinition.hasError('required')) {
<mat-error class="adf-error-pb">
<mat-error>
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_DEFINITION_REQUIRED' | translate }}
</mat-error>
}
@@ -61,6 +61,7 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
</mat-form-field>
<mat-form-field [floatLabel]="'auto'"
class="adf-form-field-input"
*ngIf="taskFilterProperty.type === 'date'"
[attr.data-automation-id]="taskFilterProperty.key">
<mat-label>{{taskFilterProperty.label | translate}}</mat-label>
@@ -75,12 +76,10 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
<mat-datepicker #dateController
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-picker-' + taskFilterProperty.key" />
<div class="adf-edit-task-filter-date-error-container">
<mat-error *ngIf="hasError(taskFilterProperty)">
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
<mat-icon class="adf-error-icon" adf-icon="warning" />
</mat-error>
</div>
<mat-error *ngIf="hasError(taskFilterProperty)">
<mat-icon class="adf-error-icon">warning</mat-icon>
<span class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</span>
</mat-error>
</mat-form-field>
<div class="adf-edit-task-filter-checkbox"
*ngIf="taskFilterProperty.type === 'checkbox'">
@@ -1,4 +1,5 @@
@use '../../../../../flex' as flex;
@use '../../../../../mixins' as mixins;
.adf-edit-task-filter-checkbox {
padding-top: 10px;
@@ -71,3 +72,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}
@@ -61,6 +61,7 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
</mat-form-field>
<mat-form-field [floatLabel]="'auto'"
class="adf-form-field-input"
*ngIf="taskFilterProperty.type === 'date'"
[attr.data-automation-id]="taskFilterProperty.key">
<mat-label>{{taskFilterProperty.label | translate}}</mat-label>
@@ -75,12 +76,12 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
<mat-datepicker #dateController
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-picker-' + taskFilterProperty.key" />
<div class="adf-edit-task-filter-date-error-container">
<mat-error *ngIf="hasError(taskFilterProperty)">
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
<mat-icon class="adf-error-icon" adf-icon="warning" />
@if (hasError(taskFilterProperty)) {
<mat-error>
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<span class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</span>
</mat-error>
</div>
}
</mat-form-field>
<div class="adf-edit-task-filter-checkbox"
*ngIf="taskFilterProperty.type === 'checkbox'">
@@ -1,4 +1,5 @@
@use '../../../../../flex' as flex;
@use '../../../../../mixins' as mixins;
.adf-edit-task-filter-checkbox {
padding-top: 10px;
@@ -71,3 +72,7 @@
}
}
}
.adf-form-field-input .adf-error-icon {
@include mixins.adf-error-icon;
}