diff --git a/lib/content-services/src/lib/_mixins.scss b/lib/content-services/src/lib/_mixins.scss
new file mode 100644
index 0000000000..6f96c96896
--- /dev/null
+++ b/lib/content-services/src/lib/_mixins.scss
@@ -0,0 +1,7 @@
+@mixin adf-error-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ vertical-align: text-bottom;
+}
diff --git a/lib/content-services/src/lib/dialogs/folder/folder.dialog.html b/lib/content-services/src/lib/dialogs/folder/folder.dialog.html
index b8f8db1817..007bed5daf 100644
--- a/lib/content-services/src/lib/dialogs/folder/folder.dialog.html
+++ b/lib/content-services/src/lib/dialogs/folder/folder.dialog.html
@@ -16,20 +16,10 @@
/>
@if (form.controls['name'].dirty && form.controls['name'].invalid) {
- @if (form.controls['name'].hasError('required')) {
-
- {{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
-
- }
-
- @else if (form.controls['name'].hasError('message')) {
-
- {{ form.controls['name'].errors?.message | translate }}
-
- }
+ @if (form.controls['name'].errors?.required) {{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}} @else if (form.controls['name'].errors?.message) {{{ form.controls['name'].errors?.message | translate }}}
}
-
diff --git a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.html b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.html
index de01b394e4..3366ce3999 100644
--- a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.html
+++ b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.html
@@ -24,8 +24,14 @@
type="text"
(keypress)="cleanErrorMsg()"
[(ngModel)]="newTagName"
+ [errorStateMatcher]="tagErrorStateMatcher"
/>
- {{ errorMsg }}
+ @if (errorMsg) {
+
+ error_outline
+ {{ errorMsg }}
+
+ }
diff --git a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.scss b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.scss
index d00861d15c..9818f8c39b 100644
--- a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.scss
+++ b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.scss
@@ -1,3 +1,5 @@
+@use '../../_mixins' as mixins;
+
.adf-tag-node-actions-list {
.adf-full-width {
width: 100%;
@@ -18,3 +20,7 @@
height: 20px;
}
}
+
+.adf-full-width .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.ts b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.ts
index 15cb130ffe..fae841390b 100644
--- a/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.ts
+++ b/lib/content-services/src/lib/tag/tag-actions/tag-actions.component.ts
@@ -16,6 +16,8 @@
*/
import { IconModule, TranslationService } from '@alfresco/adf-core';
+import { ErrorStateMatcher } from '@angular/material/core';
+import { MatIconModule } from '@angular/material/icon';
import { Component, DestroyRef, EventEmitter, inject, Input, OnChanges, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TagService } from '../services/tag.service';
import { TagPaging } from '@alfresco/js-api';
@@ -35,7 +37,17 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'adf-tag-node-actions-list',
- imports: [CommonModule, MatListModule, IconModule, MatFormFieldModule, MatInputModule, TranslatePipe, FormsModule, MatButtonModule],
+ imports: [
+ CommonModule,
+ MatListModule,
+ IconModule,
+ MatIconModule,
+ MatFormFieldModule,
+ MatInputModule,
+ TranslatePipe,
+ FormsModule,
+ MatButtonModule
+ ],
templateUrl: './tag-actions.component.html',
styleUrls: ['./tag-actions.component.scss'],
encapsulation: ViewEncapsulation.None,
@@ -66,6 +78,10 @@ export class TagActionsComponent implements OnChanges, OnInit {
errorMsg: string;
disableAddTag: boolean = true;
+ tagErrorStateMatcher: ErrorStateMatcher = {
+ isErrorState: () => !!this.errorMsg
+ };
+
private readonly destroyRef = inject(DestroyRef);
ngOnInit() {
diff --git a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
index f680d4119f..d302aa80c1 100644
--- a/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
+++ b/lib/core/src/lib/card-view/components/card-view-dateitem/card-view-dateitem.component.html
@@ -84,7 +84,7 @@
/>
@if (cardViewDateTimeControl.hasError('matDatepickerParse')) {
- {{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}
+ {{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}
}
diff --git a/lib/core/src/lib/form/components/form-renderer.component.scss b/lib/core/src/lib/form/components/form-renderer.component.scss
index b1b04cbc0e..76ad886573 100644
--- a/lib/core/src/lib/form/components/form-renderer.component.scss
+++ b/lib/core/src/lib/form/components/form-renderer.component.scss
@@ -39,6 +39,10 @@
}
.adf-container-widget {
+ .adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
+ margin-bottom: 35px;
+ }
+
.adf-grid-list {
display: grid;
diff --git a/lib/core/src/lib/form/components/form-renderer.component.spec.ts b/lib/core/src/lib/form/components/form-renderer.component.spec.ts
index f454290ae0..0b14768fb6 100644
--- a/lib/core/src/lib/form/components/form-renderer.component.spec.ts
+++ b/lib/core/src/lib/form/components/form-renderer.component.spec.ts
@@ -392,8 +392,8 @@ describe('Form Renderer Component', () => {
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeHidden(testingUtils, 'Number2');
- const errorWidgetText = testingUtils.getByCSS('#field-Number1-container error-widget .adf-error-text').nativeElement;
- expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
+ const errorText = testingUtils.getByCSS('#field-Number1-container .adf-error-text').nativeElement;
+ expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
});
@@ -569,16 +569,16 @@ describe('Form Renderer Component', () => {
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0x8cbv');
- let errorWidgetText = testingUtils.getByCSS('#field-Number0x8cbv-container error-widget .adf-error-text').nativeElement;
- expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
+ let errorText = testingUtils.getByCSS('#field-Number0x8cbv-container .adf-error-text').nativeElement;
+ expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
typeIntoInput(testingUtils, '#Number0x8cbv', '?');
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0x8cbv');
- errorWidgetText = testingUtils.getByCSS('#field-Number0x8cbv-container error-widget .adf-error-text').nativeElement;
- expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
+ errorText = testingUtils.getByCSS('#field-Number0x8cbv-container .adf-error-text').nativeElement;
+ expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.INVALID_NUMBER`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without mandatory field');
typeIntoInput(testingUtils, '#Number0x8cbv', '-5');
@@ -600,8 +600,8 @@ describe('Form Renderer Component', () => {
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0him2z');
- let errorWidgetText = testingUtils.getByCSS('#field-Number0him2z-container error-widget .adf-error-text').nativeElement;
- expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_LESS_THAN`);
+ let errorText = testingUtils.getByCSS('#field-Number0him2z-container .adf-error-text').nativeElement;
+ expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_LESS_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without valid field');
typeIntoInput(testingUtils, '#Number0him2z', '10');
@@ -621,8 +621,8 @@ describe('Form Renderer Component', () => {
await fixture.whenStable();
expectElementToBeInvalid(testingUtils, 'Number0him2z');
- errorWidgetText = testingUtils.getByCSS('#field-Number0him2z-container error-widget .adf-error-text').nativeElement;
- expect(errorWidgetText.textContent).toBe(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
+ errorText = testingUtils.getByCSS('#field-Number0him2z-container .adf-error-text').nativeElement;
+ expect(errorText.textContent).toContain(`FORM.FIELD.VALIDATOR.NOT_GREATER_THAN`);
expect(formRendererComponent.formDefinition.isValid).toBe(false, 'Form should not be valid without valid field');
});
diff --git a/lib/core/src/lib/form/components/widgets/amount/amount.widget.html b/lib/core/src/lib/form/components/widgets/amount/amount.widget.html
index 6727eb3d12..00fefd8e34 100644
--- a/lib/core/src/lib/form/components/widgets/amount/amount.widget.html
+++ b/lib/core/src/lib/form/components/widgets/amount/amount.widget.html
@@ -27,16 +27,17 @@
[(ngModel)]="amountWidgetValue"
(ngModelChange)="onFieldChangedAmountWidget()"
[disabled]="field.readOnly"
+ [errorStateMatcher]="errorStateMatcher"
(focus)="amountWidgetOnFocus()"
(blur)="amountWidgetOnBlur()"
/>
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/amount/amount.widget.scss b/lib/core/src/lib/form/components/widgets/amount/amount.widget.scss
index ba702027da..ccb713274f 100644
--- a/lib/core/src/lib/form/components/widgets/amount/amount.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/amount/amount.widget.scss
@@ -1,5 +1,6 @@
/* stylelint-disable selector-class-pattern */
@use '../../../../styles/mat-selectors' as ms;
+@use '../../../../styles/mixins' as mixins;
.adf {
&-amount-widget {
@@ -25,3 +26,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/core/src/lib/form/components/widgets/amount/amount.widget.spec.ts b/lib/core/src/lib/form/components/widgets/amount/amount.widget.spec.ts
index b4c20e00a9..ddbe8d483f 100644
--- a/lib/core/src/lib/form/components/widgets/amount/amount.widget.spec.ts
+++ b/lib/core/src/lib/form/components/widgets/amount/amount.widget.spec.ts
@@ -335,8 +335,9 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('gdfgdf');
expect(widget.field.isValid).toBe(false);
- const errorWidget = testingUtils.getByCSS('error-widget .adf-error-text').nativeElement;
- expect(errorWidget.textContent).toBe('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
+ const formField = await testingUtils.getMatFormField();
+ const errors = await formField.getTextErrors();
+ expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
});
it('[C309693] - Should be possible to set the Advanced Properties for Amount Widget', async () => {
@@ -372,13 +373,14 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('8');
expect(widget.field.isValid).toBe(false);
- let errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
- expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
+ const formField = await testingUtils.getMatFormField();
+ let errors = await formField.getTextErrors();
+ expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
await input.setValue('99');
expect(widget.field.isValid).toBe(false);
- errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
- expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
+ errors = await formField.getTextErrors();
+ expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.NOT_GREATER_THAN');
await input.setValue('80');
expect(widget.field.isValid).toBe(true);
@@ -388,8 +390,8 @@ describe('AmountWidgetComponent - rendering', () => {
await input.setValue('incorrect format');
expect(widget.field.isValid).toBe(false);
- errorMessage = testingUtils.getByCSS('.adf-error-text').nativeElement;
- expect(errorMessage.textContent.trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
+ errors = await formField.getTextErrors();
+ expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
});
describe('when form model has left labels', () => {
diff --git a/lib/core/src/lib/form/components/widgets/amount/amount.widget.ts b/lib/core/src/lib/form/components/widgets/amount/amount.widget.ts
index 3ba65caae7..39d37a53a5 100644
--- a/lib/core/src/lib/form/components/widgets/amount/amount.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/amount/amount.widget.ts
@@ -19,11 +19,12 @@
import { CurrencyPipe, NgIf } from '@angular/common';
import { Component, OnInit, ViewEncapsulation, InjectionToken, inject, DestroyRef } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } 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 { TranslatePipe } from '@ngx-translate/core';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { filter, isObservable, Observable } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -52,7 +53,7 @@ export const ADF_AMOUNT_SETTINGS = new InjectionToken = {};
valueAsNumber: number;
get placeholder(): string {
@@ -117,6 +120,8 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
}
this.subscribeToFieldChanges();
this.setInitialValues();
+ this.initErrorStateMatcher();
+ this.updateTranslateParameters();
}
}
@@ -138,6 +143,7 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
}
}
this.markAsTouched();
+ this.updateTranslateParameters();
}
amountWidgetOnFocus(): void {
@@ -156,6 +162,8 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
onFieldChangedAmountWidget(): void {
this.field.value = this.amountWidgetValue;
super.onFieldChanged(this.field);
+ this.markAsTouched();
+ this.updateTranslateParameters();
}
setInitialValues(): void {
@@ -180,6 +188,7 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
} else if (!this.isInputInFocus) {
this.amountWidgetValue = ev.field.value;
}
+ this.updateTranslateParameters();
});
}
@@ -192,4 +201,19 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
this.enableDisplayBasedOnLocale = data?.enableDisplayBasedOnLocale ?? false;
this.showReadonlyPlaceholder = data?.showReadonlyPlaceholder;
}
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
+ };
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field?.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
+ }
}
diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html
index b01b539e8c..d1c5a7b33b 100644
--- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html
+++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.html
@@ -37,9 +37,12 @@
[touchUi]="true"
[timeInterval]="5"
[disabled]="field.readOnly" />
+ @if (datetimeInputControl.invalid && datetimeInputControl.touched && field.validationSummary?.message) {
+
+ error_outline
+ {{ field.validationSummary.message | translate:translateParameters }}
+
+ }
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.scss b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.scss
index 1427ba136d..1889eb81f7 100644
--- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.scss
@@ -1,5 +1,6 @@
/* stylelint-disable selector-class-pattern */
@use '../../../../styles/mat-selectors' as ms;
+@use '../../../../styles/mixins' as mixins;
.adf {
&-date-time-widget {
@@ -23,6 +24,10 @@
}
}
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
+
#{ms.$mat-datetimepicker-toggle} {
color: var(--mat-sys-on-surface);
}
diff --git a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts
index 67a6ff90ad..e34d0a4341 100644
--- a/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/date-time/date-time.widget.ts
@@ -22,12 +22,12 @@ import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angul
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { DatetimeAdapter, MAT_DATETIME_FORMATS, MatDatetimepickerModule } from '@mat-datetimepicker/core';
import { TranslatePipe } from '@ngx-translate/core';
import { ADF_DATE_FORMATS, ADF_DATETIME_FORMATS, AdfDateFnsAdapter, AdfDateTimeFnsAdapter, DateFnsUtils } from '../../../../common';
import { FormService } from '../../../services/form.service';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { ErrorMessageModel } from '../core/error-message.model';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -46,13 +46,14 @@ import { ReactiveFormWidget } from '../reactive-widget.interface';
host: {
'(click)': 'event($event)'
},
- imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, ErrorWidgetComponent],
+ imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, MatDatetimepickerModule, ReactiveFormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class DateTimeWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
minDate: Date;
maxDate: Date;
datetimeInputControl: FormControl = new FormControl(null);
+ translateParameters: Record = {};
public readonly formService = inject(FormService);
private readonly destroyRef = inject(DestroyRef);
@@ -116,6 +117,15 @@ export class DateTimeWidgetComponent extends WidgetComponent implements OnInit,
this.resetErrors();
this.field.markAsValid();
}
+ this.updateTranslateParameters();
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
}
private handleErrors(errors: ValidationErrors): void {
diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.html b/lib/core/src/lib/form/components/widgets/date/date.widget.html
index d3da060cae..f36c6ae24e 100644
--- a/lib/core/src/lib/form/components/widgets/date/date.widget.html
+++ b/lib/core/src/lib/form/components/widgets/date/date.widget.html
@@ -21,11 +21,12 @@
+ @if (dateInputControl.invalid && dateInputControl.touched) {
+
+ error_outline
+ @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 } }}}
+
+ }
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.scss b/lib/core/src/lib/form/components/widgets/date/date.widget.scss
index 0b1838d10c..6f1b1afe88 100644
--- a/lib/core/src/lib/form/components/widgets/date/date.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/date/date.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../../styles/mixins' as mixins;
+
.adf {
&-widget {
&-container {
@@ -7,3 +9,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/core/src/lib/form/components/widgets/date/date.widget.ts b/lib/core/src/lib/form/components/widgets/date/date.widget.ts
index f42b0ae940..499dd3f704 100644
--- a/lib/core/src/lib/form/components/widgets/date/date.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/date/date.widget.ts
@@ -17,17 +17,16 @@
/* eslint-disable @angular-eslint/component-selector */
-import { NgIf } from '@angular/common';
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { FormControl, ReactiveFormsModule, ValidationErrors, Validators } from '@angular/forms';
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { TranslatePipe } from '@ngx-translate/core';
import { ADF_DATE_FORMATS, AdfDateFnsAdapter, DateFnsUtils, DEFAULT_DATE_FORMAT } from '../../../../common';
import { FormService } from '../../../services/form.service';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { ErrorMessageModel } from '../core/error-message.model';
import { parseISO } from 'date-fns';
@@ -53,7 +52,7 @@ import { ReactiveFormWidget } from '../reactive-widget.interface';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
- imports: [MatFormFieldModule, TranslatePipe, MatInputModule, MatDatepickerModule, ReactiveFormsModule, ErrorWidgetComponent, NgIf],
+ imports: [MatFormFieldModule, TranslatePipe, MatInputModule, MatDatepickerModule, ReactiveFormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class DateWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
@@ -118,6 +117,16 @@ export class DateWidgetComponent extends WidgetComponent implements OnInit, Reac
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);
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html
index f51f1e4c6d..e2c5be4965 100644
--- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html
+++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.html
@@ -18,19 +18,19 @@
[id]="field.id"
[required]="field.required && field.isVisible"
[(ngModel)]="field.value"
- (ngModelChange)="onFieldChanged(field)"
+ (ngModelChange)="onDecimalFieldChanged()"
[disabled]="field.readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
- (blur)="markAsTouched()" />
+ [errorStateMatcher]="errorStateMatcher"
+ (blur)="onBlur()" />
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss
index fcb68154ab..c4fb5c38e4 100644
--- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss
+++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.scss
@@ -1,3 +1,5 @@
+@use '../../../../styles/mixins' as mixins;
+
.adf {
&-decimal-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts
index 8a2c1cd51e..681061e69f 100644
--- a/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts
+++ b/lib/core/src/lib/form/components/widgets/decimal/decimal.component.ts
@@ -16,12 +16,13 @@
*/
import { NgIf } from '@angular/common';
-import { Component, ViewEncapsulation } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { Component, OnInit, ViewEncapsulation } from '@angular/core';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } 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 { TranslatePipe } from '@ngx-translate/core';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
@Component({
@@ -39,7 +40,39 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
- imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent],
+ imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
-export class DecimalWidgetComponent extends WidgetComponent {}
+export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
+ errorStateMatcher: ErrorStateMatcher;
+ translateParameters: Record = {};
+
+ ngOnInit(): void {
+ this.initErrorStateMatcher();
+ }
+
+ onBlur(): void {
+ this.markAsTouched();
+ this.updateTranslateParameters();
+ }
+
+ onDecimalFieldChanged(): void {
+ this.onFieldChanged(this.field);
+ this.updateTranslateParameters();
+ }
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !this.field.isValid && this.isTouched()
+ };
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
+ }
+}
diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html
index dc653751d9..cea1673327 100644
--- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html
+++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.html
@@ -7,6 +7,7 @@
0"
[floatLabel]="field.placeholder ? 'always' : null"
>
@if(field.name || field.required) {
@@ -20,24 +21,24 @@
rows="3"
[id]="field.id"
[required]="field.required"
- [ngModel]="displayValue"
- (ngModelChange)="onValueChange($event)"
+ [(ngModel)]="field.value"
+ (ngModelChange)="onMultilineTextFieldChanged()"
[disabled]="field.readOnly || readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
- (blur)="markAsTouched()"
+ [errorStateMatcher]="errorStateMatcher"
+ (blur)="onBlur()"
>
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ @if (field.maxLength > 0) {{{ field?.value?.length || 0 }}/{{ field.maxLength }}}
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ } @else if (field.maxLength > 0) {
+ {{ field?.value?.length || 0 }}/{{ field.maxLength }}
+ }
- 0" class="adf-multiline-word-counter">
- {{ displayValue?.length || 0 }}/{{ field.maxLength }}
-
-
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss
index c89911f0bf..435d152e6e 100644
--- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.scss
@@ -1,9 +1,12 @@
+@use '../../../../styles/mixins' as mixins;
+
.adf {
&-multiline-text-widget {
width: 100%;
display: flex;
align-items: flex-start;
flex-direction: column;
+ position: relative;
.adf-label {
top: 20px;
@@ -16,23 +19,30 @@
}
}
- &-multiline-word-counter:has(.adf-multiline-word-counter-value) {
+ &-multiline-counter {
float: right;
- margin-top: -20px;
- min-height: 24px;
- min-width: 1px;
- font-size: var(--mat-sys-body-small-size);
+ color: var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6));
+ }
+
+ &-multiline-counter-block {
+ display: block;
+ color: var(--mat-sys-on-surface-variant, rgba(0, 0, 0, 0.6));
+ }
+
+ &-multiline-hint {
+ margin-top: 7px;
line-height: 14px;
- overflow: hidden;
- transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2);
- opacity: 1;
- padding-top: 5px;
- text-align: right;
- padding-right: 2px;
- padding-left: 0;
}
&-multiline-required-message {
display: flex;
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
+
+.adf-container-widget .adf-multiline-text-widget .adf-form-field-input.adf-has-counter {
+ margin-bottom: 44px;
+}
diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts
index 47d1f6ca14..01dfa19e00 100644
--- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts
+++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.spec.ts
@@ -24,7 +24,6 @@ import { MultilineTextWidgetComponentComponent } from './multiline-text.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
-import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
import { of, Subject } from 'rxjs';
describe('MultilineTextWidgetComponentComponent', () => {
@@ -330,92 +329,4 @@ describe('MultilineTextWidgetComponentComponent - ADF_CUSTOM_MESSAGE', () => {
expect(widget.field.validationSummary.message).toBe('FORM.FIELD.VALIDATOR.INVALID_VALUE');
});
});
-
- describe('typed value formatting', () => {
- describe('when flag is on', () => {
- beforeEach(() => {
- TestBed.resetTestingModule();
- TestBed.configureTestingModule({
- imports: [MultilineTextWidgetComponentComponent],
- providers: [{ provide: ADF_TYPED_VALUE_FORMATTING_ENABLED, useValue: true }]
- });
- fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
- widget = fixture.componentInstance;
- testingUtils = new UnitTestingUtils(fixture.debugElement);
- });
-
- it('should return formatted name for a People value in read-only mode', () => {
- widget.field = new FormFieldModel(new FormModel(), {
- id: 'people-field',
- type: FormFieldTypes.PEOPLE,
- value: [{ firstName: 'Alice', lastName: 'Brown' }],
- readOnly: true
- });
- fixture.detectChanges();
-
- expect(widget.displayValue).toBe('Alice Brown');
- });
-
- it('should not return [object Object] for a complex field value', () => {
- widget.field = new FormFieldModel(new FormModel(), {
- id: 'people-field',
- type: FormFieldTypes.PEOPLE,
- value: [{ firstName: 'Alice', lastName: 'Brown' }],
- readOnly: true
- });
- fixture.detectChanges();
-
- expect(String(widget.displayValue)).not.toContain('[object Object]');
- });
-
- it('should pass through plain string values unchanged', () => {
- widget.field = new FormFieldModel(new FormModel(), {
- id: 'multiline-id',
- type: FormFieldTypes.MULTILINE_TEXT,
- value: 'plain text',
- readOnly: true
- });
- fixture.detectChanges();
-
- expect(widget.displayValue).toBe('plain text');
- });
-
- it('should not JSON-stringify a Date value for an unregistered type', () => {
- const date = new Date('2026-06-02T14:30:00.000Z');
- widget.field = new FormFieldModel(new FormModel(), {
- id: 'date-id',
- type: FormFieldTypes.MULTILINE_TEXT,
- value: date,
- readOnly: true
- });
- fixture.detectChanges();
-
- expect(String(widget.displayValue)).toBe(String(date));
- expect(String(widget.displayValue)).not.toContain('"');
- });
- });
-
- describe('when flag is off', () => {
- beforeEach(() => {
- TestBed.resetTestingModule();
- TestBed.configureTestingModule({
- imports: [MultilineTextWidgetComponentComponent]
- });
- fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
- widget = fixture.componentInstance;
- });
-
- it('should not format complex field values (default behaviour preserved)', () => {
- widget.field = new FormFieldModel(new FormModel(), {
- id: 'people-field',
- type: FormFieldTypes.PEOPLE,
- value: [{ firstName: 'Alice', lastName: 'Brown' }],
- readOnly: true
- });
- fixture.detectChanges();
-
- expect(widget.displayValue).not.toBe('Alice Brown');
- });
- });
- });
});
diff --git a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts
index 015641b81e..93295ad963 100644
--- a/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/multiline-text/multiline-text.widget.ts
@@ -17,14 +17,17 @@
/* eslint-disable @angular-eslint/component-selector */
-import { NgIf } from '@angular/common';
-import { Component, ViewEncapsulation } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } 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 { TranslatePipe } from '@ngx-translate/core';
-import { ErrorWidgetComponent } from '../error/error.component';
-import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
+import { isObservable } from 'rxjs';
+import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
+import { WidgetComponent } from '../widget.component';
@Component({
selector: 'multiline-text-widget',
@@ -41,7 +44,55 @@ import { FormattableTextWidgetComponent } from '../core/formattable-text.widget'
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
- imports: [MatFormFieldModule, NgIf, TranslatePipe, MatInputModule, FormsModule, ErrorWidgetComponent],
+ imports: [MatFormFieldModule, TranslatePipe, MatInputModule, FormsModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
-export class MultilineTextWidgetComponentComponent extends FormattableTextWidgetComponent {}
+export class MultilineTextWidgetComponentComponent extends WidgetComponent implements OnInit {
+ private readonly destroyRef = inject(DestroyRef);
+ private readonly enableCustomMessage = inject(ADF_CUSTOM_MESSAGE, { optional: true });
+
+ errorStateMatcher: ErrorStateMatcher;
+ translateParameters: Record = {};
+
+ ngOnInit(): void {
+ this.initErrorStateMatcher();
+ if (this.enableCustomMessage != null) {
+ if (isObservable(this.enableCustomMessage)) {
+ this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
+ if (this.field) {
+ this.field.enableCustomValidationMessage = enabled ?? false;
+ }
+ });
+ } else {
+ this.field.enableCustomValidationMessage = this.enableCustomMessage;
+ }
+ } else {
+ this.field.enableCustomValidationMessage = false;
+ }
+ }
+
+ onBlur(): void {
+ this.markAsTouched();
+ this.updateTranslateParameters();
+ }
+
+ onMultilineTextFieldChanged(): void {
+ this.onFieldChanged(this.field);
+ this.updateTranslateParameters();
+ }
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !this.field.isValid && this.isTouched()
+ };
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
+ }
+}
diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.html b/lib/core/src/lib/form/components/widgets/number/number.widget.html
index 8f5749b960..11999ea2b1 100644
--- a/lib/core/src/lib/form/components/widgets/number/number.widget.html
+++ b/lib/core/src/lib/form/components/widgets/number/number.widget.html
@@ -27,14 +27,15 @@
[disabled]="field.readOnly"
[placeholder]="field.placeholder"
[title]="field.tooltip"
- (blur)="markAsTouched()">
+ [errorStateMatcher]="errorStateMatcher"
+ (blur)="onBlur()">
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
-
diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.scss b/lib/core/src/lib/form/components/widgets/number/number.widget.scss
index e2e277d3a5..c1d7cacca0 100644
--- a/lib/core/src/lib/form/components/widgets/number/number.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/number/number.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../../styles/mixins' as mixins;
+
.adf {
&-number-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/core/src/lib/form/components/widgets/number/number.widget.ts b/lib/core/src/lib/form/components/widgets/number/number.widget.ts
index 8cabe2a308..fd27622097 100644
--- a/lib/core/src/lib/form/components/widgets/number/number.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/number/number.widget.ts
@@ -19,12 +19,13 @@
import { NgIf } from '@angular/common';
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } 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 { TranslatePipe } from '@ngx-translate/core';
import { DecimalNumberPipe } from '../../../../pipes';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
@Component({
@@ -42,12 +43,14 @@ import { WidgetComponent } from '../widget.component';
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
- imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent],
+ imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, MatIconModule],
providers: [DecimalNumberPipe],
encapsulation: ViewEncapsulation.None
})
export class NumberWidgetComponent extends WidgetComponent implements OnInit {
displayValue: number;
+ errorStateMatcher: ErrorStateMatcher;
+ translateParameters: Record = {};
private readonly decimalNumberPipe = inject(DecimalNumberPipe);
@@ -57,6 +60,12 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
} else {
this.displayValue = this.field.value;
}
+ this.initErrorStateMatcher();
+ }
+
+ onBlur(): void {
+ this.markAsTouched();
+ this.updateTranslateParameters();
}
protected onNumberChange(value: string) {
@@ -65,5 +74,21 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
}
this.onFieldChanged(this.field);
+ this.updateTranslateParameters();
+ }
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
+ };
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
}
}
diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.html b/lib/core/src/lib/form/components/widgets/text/text.widget.html
index 5312d8451d..ab5da84c10 100644
--- a/lib/core/src/lib/form/components/widgets/text/text.widget.html
+++ b/lib/core/src/lib/form/components/widgets/text/text.widget.html
@@ -18,21 +18,23 @@
type="text"
[id]="field.id"
[required]="field.required"
- [ngModel]="displayValue"
- (ngModelChange)="onValueChange($event)"
+ [value]="field.value"
+ [(ngModel)]="field.value"
+ (ngModelChange)="onTextFieldChanged()"
[disabled]="field.readOnly || readOnly"
[textMask]="{mask: mask, isReversed: isMaskReversed}"
[placeholder]="placeholder"
[title]="field.tooltip"
- (blur)="markAsTouched()">
+ [errorStateMatcher]="errorStateMatcher"
+ (blur)="onBlur()">
+ @if (!fieldStatusTemplate && (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched()))) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
-
-
-
-
+
diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.scss b/lib/core/src/lib/form/components/widgets/text/text.widget.scss
index 5585f3a788..ff4d7496ce 100644
--- a/lib/core/src/lib/form/components/widgets/text/text.widget.scss
+++ b/lib/core/src/lib/form/components/widgets/text/text.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../../styles/mixins' as mixins;
+
.adf {
&-text-widget {
width: 100%;
@@ -9,3 +11,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts b/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts
index f7f8b57578..0a99fe52f8 100644
--- a/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts
+++ b/lib/core/src/lib/form/components/widgets/text/text.widget.spec.ts
@@ -34,7 +34,6 @@ describe('TextWidgetComponent', () => {
let loader: HarnessLoader;
let widget: TextWidgetComponent;
let fixture: ComponentFixture;
- let errorWidget: HTMLElement;
let testingUtils: UnitTestingUtils;
beforeEach(() => {
@@ -126,20 +125,21 @@ describe('TextWidgetComponent', () => {
await testingUtils.fillMatInput('TEXT');
- errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
- expect(errorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.AT_LEAST_LONG');
+ const formField = await testingUtils.getMatFormField();
+ let errors = await formField.getTextErrors();
+ expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.AT_LEAST_LONG');
expect(widget.field.isValid).toBe(false);
await testingUtils.fillMatInput('TEXT VALUE');
- errorWidget = testingUtils.getByCSS('.adf-error-text')?.nativeElement;
+ errors = await formField.getTextErrors();
expect(widget.field.isValid).toBe(true);
await testingUtils.fillMatInput('TEXT VALUE TOO LONG');
expect(widget.field.isValid).toBe(false);
- errorWidget = testingUtils.getByCSS('.adf-error-text').nativeElement;
- expect(errorWidget.innerHTML).toBe('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
+ errors = await formField.getTextErrors();
+ expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.NO_LONGER_THAN');
});
it('should be able to set regex pattern property for Text widget', async () => {
diff --git a/lib/core/src/lib/form/components/widgets/text/text.widget.ts b/lib/core/src/lib/form/components/widgets/text/text.widget.ts
index 2ca8ae9aaf..c78c530c82 100644
--- a/lib/core/src/lib/form/components/widgets/text/text.widget.ts
+++ b/lib/core/src/lib/form/components/widgets/text/text.widget.ts
@@ -19,14 +19,16 @@
import { NgIf, NgTemplateOutlet } from '@angular/common';
import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } 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 { TranslatePipe } from '@ngx-translate/core';
-import { ErrorWidgetComponent } from '../error/error.component';
import { WidgetComponent } from '../widget.component';
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
import { InputMaskDirective } from './text-mask.component';
+import { IconModule } from '../../../../icon/icon.module';
type FieldStatusTemplate = TemplateRef<{ $implicit: WidgetComponent }>;
const FIELD_STATUS_TEMPLATE = new InjectionToken('FIELD_STATUS_TEMPLATE');
@@ -61,7 +63,7 @@ export class FieldStatusTemplateDirective {
'(invalid)': 'event($event)',
'(select)': 'event($event)'
},
- imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, InputMaskDirective, NgTemplateOutlet],
+ imports: [NgIf, TranslatePipe, MatFormFieldModule, MatInputModule, FormsModule, InputMaskDirective, NgTemplateOutlet, IconModule, MatIconModule],
encapsulation: ViewEncapsulation.None
})
export class TextWidgetComponent extends FormattableTextWidgetComponent {
@@ -69,6 +71,8 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
placeholder: string;
isMaskReversed: boolean;
fieldStatusTemplate = inject(FIELD_STATUS_TEMPLATE, { optional: true });
+ errorStateMatcher: ErrorStateMatcher;
+ translateParameters: Record = {};
override ngOnInit() {
super.ngOnInit();
@@ -81,5 +85,31 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
: this.field.placeholder;
this.isMaskReversed = this.field.params['inputMaskReversed'] ? this.field.params['inputMaskReversed'] : false;
}
+ this.initErrorStateMatcher();
+ }
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !this.fieldStatusTemplate && (!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched()))
+ };
+ }
+
+ private updateTranslateParameters(): void {
+ if (this.field.validationSummary?.isActive()) {
+ this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
+ } else {
+ this.translateParameters = {};
+ }
+ }
+
+ onBlur(): void {
+ this.markAsTouched();
+ this.updateTranslateParameters();
+ }
+
+ onTextFieldChanged(): void {
+ this.onFieldChanged(this.field);
+ this.updateTranslateParameters();
}
}
diff --git a/lib/core/src/lib/styles/_mixins.scss b/lib/core/src/lib/styles/_mixins.scss
index 5a6eb1ae8c..29b1c10ee5 100644
--- a/lib/core/src/lib/styles/_mixins.scss
+++ b/lib/core/src/lib/styles/_mixins.scss
@@ -44,3 +44,11 @@
height: 100%;
overflow: hidden;
}
+
+@mixin adf-error-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ vertical-align: text-bottom;
+}
diff --git a/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html
index 0a18e20c4f..f2cb8566f3 100644
--- a/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html
+++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.html
@@ -10,9 +10,10 @@
type="password"
placeholder="{{ 'ADF_VIEWER.PDF_DIALOG.PLACEHOLDER' | translate }}"
[formControl]="passwordFormControl" />
+ @if (isError()) {
+ {{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}
+ }
-
- {{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}
diff --git a/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts
index 50509956b5..e853a66557 100644
--- a/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts
+++ b/lib/core/viewer/pdf/src/lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog.ts
@@ -15,7 +15,6 @@
* limitations under the License.
*/
-import { NgIf } from '@angular/common';
import { Component, OnInit, ViewEncapsulation, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
@@ -31,7 +30,7 @@ declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_
selector: 'adf-pdf-viewer-password-dialog',
templateUrl: './pdf-viewer-password-dialog.html',
styleUrls: ['./pdf-viewer-password-dialog.scss'],
- imports: [MatDialogModule, IconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslatePipe, NgIf, MatButtonModule],
+ imports: [MatDialogModule, IconModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule, TranslatePipe, MatButtonModule],
encapsulation: ViewEncapsulation.None
})
export class PdfPasswordDialogComponent implements OnInit {
diff --git a/lib/process-services-cloud/src/lib/_mixins.scss b/lib/process-services-cloud/src/lib/_mixins.scss
new file mode 100644
index 0000000000..6f96c96896
--- /dev/null
+++ b/lib/process-services-cloud/src/lib/_mixins.scss
@@ -0,0 +1,7 @@
+@mixin adf-error-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ vertical-align: text-bottom;
+}
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html
index 813816989f..1cc7d8e98b 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.html
@@ -35,9 +35,13 @@
/>
+ @if (dateInputControl.invalid && dateInputControl.touched) {
+
+ error_outline
+ @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 } }}}
+
+ }
-
-
-
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.scss
index 39b34007b2..54aa93609a 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.scss
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts
index 83b99fd3f8..61365ec971 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/date/date-cloud.widget.ts
@@ -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);
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html
index 9fe4680574..39a356ac8d 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.html
@@ -23,6 +23,7 @@
[id]="field.id"
[formControl]="propertyControl"
[required]="field.required"
+ [errorStateMatcher]="propertyErrorStateMatcher"
/>
@@ -30,13 +31,9 @@
{{ field.params.externalPropertyLabel }}
+ @if (propertyLoadFailed && !previewState) {
+ error_outline{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }}
+ }
-
-
-
-
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss
index 5575d651d3..9f6ffc3f39 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts
index 0459b3af07..6f99fd4b80 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.spec.ts
@@ -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', () => {
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts
index c6bdcd0f6f..dacb696ec1 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/display-external-property/display-external-property.widget.ts
@@ -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 });
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
index c84fca1b7c..eedec326ff 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html
@@ -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 @@
}
+ @if ((dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) || (!previewState && !field.readOnly && (isRestApiFailed || variableOptionsFailed))) {
+
+ error_outline
+ @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 }}}
+
+ }
-
-
- @if(showRequiredMessage) {
-
- }
- @if(!previewState && isRestApiFailed) {
-
- }
- @if(!previewState && variableOptionsFailed) {
-
- }
-
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.scss b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.scss
index fd2d2dc27a..884f8451be 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.scss
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts
index b3c2984a4c..163fbfb1c8 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts
@@ -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 => 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 () => {
diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts
index 02795e73e6..2ddf550ba0 100644
--- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts
+++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts
@@ -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(undefined);
+ dropdownErrorStateMatcher: ErrorStateMatcher = {
+ isErrorState: (control) =>
+ (control?.touched && control?.invalid) || (!this.previewState && (this.isRestApiFailed || this.variableOptionsFailed))
+ };
+
list$ = new BehaviorSubject([]);
filter$ = new BehaviorSubject('');
@@ -101,10 +98,6 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
private readonly debounceSetValue = new Subject();
- get showRequiredMessage(): boolean {
- return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
- }
-
get isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
diff --git a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss
index c984214200..fba17a0610 100644
--- a/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss
+++ b/lib/process-services-cloud/src/lib/group/components/group-cloud.component.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss
index c482ea98cd..9cabe0edfc 100644
--- a/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss
+++ b/lib/process-services-cloud/src/lib/people/components/people-cloud.component.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.html b/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.html
index 8d3c9fb7cf..e1a2318811 100644
--- a/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.html
+++ b/lib/process-services-cloud/src/lib/process/process-filters/components/edit-process-filter/edit-process-filter-cloud.component.html
@@ -73,7 +73,7 @@
placeholder="{{processFilterProperty.label | translate}}"
[attr.data-automation-id]="'adf-cloud-edit-process-property-' + processFilterProperty.key"/>
-
+
{{processFilterProperty.label | translate}}
-
-
- {{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}
-
+ @if (hasError(processFilterProperty)) {
+
+ error_outline
+ {{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}
-
+ }
@if (processDefinition.hasError('required')) {
-
+
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_DEFINITION_REQUIRED' | translate }}
}
diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.html
index 65c21b9404..f5fb7e9777 100644
--- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.html
+++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.html
@@ -61,6 +61,7 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
{{taskFilterProperty.label | translate}}
@@ -75,12 +76,10 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
-
-
- {{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}
-
-
-
+
+ warning
+ {{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}
+
diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.scss b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.scss
index 7b8a84044d..19ca07495c 100644
--- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.scss
+++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-service-task-filter/edit-service-task-filter-cloud.component.scss
@@ -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;
+}
diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.html b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.html
index 32a1ec39fe..4522f2bab0 100644
--- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.html
+++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.html
@@ -61,6 +61,7 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
{{taskFilterProperty.label | translate}}
@@ -75,12 +76,12 @@
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
-
-
- {{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}
-
+ @if (hasError(taskFilterProperty)) {
+
+ error_outline
+ {{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}
-
+ }
diff --git a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.scss b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.scss
index 7b8a84044d..19ca07495c 100644
--- a/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.scss
+++ b/lib/process-services-cloud/src/lib/task/task-filters/components/edit-task-filters/edit-task-filter/edit-task-filter-cloud.component.scss
@@ -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;
+}
diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html
index 3860689906..55a0f27e0c 100644
--- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html
+++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.html
@@ -6,13 +6,17 @@
-
+
{{opt.name}}
{{field.value}}
+ @if (!isReadOnlyField && dropdownControl.touched && (field.validationSummary?.message || dropdownControl.hasError('required'))) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.scss b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.scss
index ded8916a3c..a57dc1c4a1 100644
--- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.scss
+++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/mixins' as mixins;
+
.adf {
&-dropdown-widget {
width: 100%;
@@ -12,3 +14,7 @@
}
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.spec.ts b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.spec.ts
index 9c37576010..c31cf14fae 100644
--- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.spec.ts
+++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.spec.ts
@@ -117,7 +117,7 @@ describe('DropdownWidgetComponent', () => {
widget.ngOnInit();
fixture.detectChanges();
- expect(element.querySelector('.adf-dropdown-required-message')).toBeNull();
+ expect(element.querySelector('[data-automation-id="adf-dropdown-error"]')).toBeNull();
});
it('should NOT preserve empty option when loading fields', () => {
diff --git a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts
index cc250dd712..5be21cb1b9 100644
--- a/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts
+++ b/lib/process-services/src/lib/form/widgets/dropdown/dropdown.widget.ts
@@ -18,19 +18,12 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
-import {
- FormService,
- FormFieldOption,
- WidgetComponent,
- ErrorWidgetComponent,
- ErrorMessageModel,
- FormFieldModel,
- ReactiveFormWidget
-} from '@alfresco/adf-core';
+import { FormService, FormFieldOption, WidgetComponent, ErrorMessageModel, FormFieldModel, ReactiveFormWidget } from '@alfresco/adf-core';
import { ProcessDefinitionService } from '../../services/process-definition.service';
import { TaskFormService } from '../../services/task-form.service';
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
import { MatSelectModule } from '@angular/material/select';
import { AbstractControl, FormControl, ReactiveFormsModule, ValidationErrors, ValidatorFn } from '@angular/forms';
import { filter } from 'rxjs/operators';
@@ -39,7 +32,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'dropdown-widget',
- imports: [CommonModule, TranslatePipe, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, ErrorWidgetComponent],
+ imports: [CommonModule, TranslatePipe, MatFormFieldModule, MatSelectModule, ReactiveFormsModule, MatIconModule],
templateUrl: './dropdown.widget.html',
styleUrls: ['./dropdown.widget.scss'],
host: {
diff --git a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.html b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.html
index 3368c00588..0962a2aef8 100644
--- a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.html
+++ b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.html
@@ -48,7 +48,12 @@
{{item.name}}
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
diff --git a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.scss b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.scss
index 654b166375..2fc80b0161 100644
--- a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.scss
+++ b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/mixins' as mixins;
+
.adf {
&-group-widget {
width: 100%;
@@ -14,3 +16,7 @@
}
}
}
+
+.adf-group-widget-field .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.ts b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.ts
index 6a3202c9fb..0381e8d728 100644
--- a/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.ts
+++ b/lib/process-services/src/lib/form/widgets/functional-group/functional-group.widget.ts
@@ -18,7 +18,7 @@
/* eslint-disable @angular-eslint/component-selector */
import { Component, ElementRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
-import { ErrorWidgetComponent, GroupModel, WidgetComponent } from '@alfresco/adf-core';
+import { GroupModel, WidgetComponent } from '@alfresco/adf-core';
import { catchError, debounceTime, distinctUntilChanged, switchMap, tap } from 'rxjs/operators';
import { merge, of } from 'rxjs';
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
@@ -40,7 +40,6 @@ import { MatIconModule } from '@angular/material/icon';
MatInputModule,
ReactiveFormsModule,
MatAutocompleteModule,
- ErrorWidgetComponent,
MatChipsModule,
MatIconModule
],
diff --git a/lib/process-services/src/lib/form/widgets/people/people.widget.html b/lib/process-services/src/lib/form/widgets/people/people.widget.html
index 7e68ed773c..662abf7051 100644
--- a/lib/process-services/src/lib/form/widgets/people/people.widget.html
+++ b/lib/process-services/src/lib/form/widgets/people/people.widget.html
@@ -5,7 +5,8 @@
-
+
+ @if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
diff --git a/lib/process-services/src/lib/form/widgets/people/people.widget.scss b/lib/process-services/src/lib/form/widgets/people/people.widget.scss
index 866a331716..51d69b059c 100644
--- a/lib/process-services/src/lib/form/widgets/people/people.widget.scss
+++ b/lib/process-services/src/lib/form/widgets/people/people.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/mixins' as mixins;
+
.adf {
&-people-widget {
width: 100%;
@@ -59,3 +61,7 @@
padding-left: 10px;
}
}
+
+.adf-people-widget-field .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/process-services/src/lib/form/widgets/people/people.widget.spec.ts b/lib/process-services/src/lib/form/widgets/people/people.widget.spec.ts
index f4bb920809..c44660bc5d 100644
--- a/lib/process-services/src/lib/form/widgets/people/people.widget.spec.ts
+++ b/lib/process-services/src/lib/form/widgets/people/people.widget.spec.ts
@@ -368,17 +368,13 @@ describe('PeopleWidgetComponent', () => {
});
it('should show an error message if the user is invalid', fakeAsync(() => {
- const peopleHTMLElement = element.querySelector('input');
- peopleHTMLElement.focus();
- peopleHTMLElement.value = 'K';
- peopleHTMLElement.dispatchEvent(new Event('keyup'));
- peopleHTMLElement.dispatchEvent(new Event('input'));
-
+ widget.searchTerm.setValue('K');
tick(300);
fixture.detectChanges();
- expect(element.querySelector('.adf-error-text')).not.toBeNull();
- expect(element.querySelector('.adf-error-text').textContent).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
+ const errorText = element.querySelector('.adf-error-text');
+ expect(errorText).not.toBeNull();
+ expect(errorText.textContent).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
}));
it('should show the people if the typed result match', fakeAsync(() => {
diff --git a/lib/process-services/src/lib/form/widgets/people/people.widget.ts b/lib/process-services/src/lib/form/widgets/people/people.widget.ts
index f45b4bab2a..bde65a2eb3 100644
--- a/lib/process-services/src/lib/form/widgets/people/people.widget.ts
+++ b/lib/process-services/src/lib/form/widgets/people/people.widget.ts
@@ -17,9 +17,10 @@
/* eslint-disable @angular-eslint/component-selector */
-import { ErrorWidgetComponent, InitialUsernamePipe, WidgetComponent } from '@alfresco/adf-core';
+import { InitialUsernamePipe, WidgetComponent } from '@alfresco/adf-core';
import { Component, ElementRef, EventEmitter, inject, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
-import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule, UntypedFormControl, FormGroupDirective, NgForm } from '@angular/forms';
+import { ErrorStateMatcher } from '@angular/material/core';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { Observable, of } from 'rxjs';
@@ -41,10 +42,10 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
MatInputModule,
MatChipsModule,
MatIconModule,
+ FormsModule,
ReactiveFormsModule,
MatAutocompleteModule,
- InitialUsernamePipe,
- ErrorWidgetComponent
+ InitialUsernamePipe
],
templateUrl: './people.widget.html',
styleUrls: ['./people.widget.scss'],
@@ -93,8 +94,10 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
})
);
public peopleProcessService = inject(PeopleProcessService);
+ errorStateMatcher: ErrorStateMatcher;
ngOnInit() {
+ this.initErrorStateMatcher();
if (this.field) {
if (this.field.value) {
Array.isArray(this.field.value) ? this.selectedUsers.push(...this.field.value) : this.selectedUsers.push(this.field.value);
@@ -163,6 +166,13 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
this.searchTerm.setValue('');
}
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
+ };
+ }
+
isUserAlreadySelected(user: LightUserRepresentation): boolean {
if (this.selectedUsers?.length > 0) {
const result = this.selectedUsers.find((selectedUser) => selectedUser.id === user.id);
diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html
index 233d9f6100..c4160a0040 100644
--- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html
+++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.html
@@ -4,7 +4,7 @@
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly"
id="typehead-div">
-
+
{{item.name}}
+ @if (field.validationSummary?.message || isInvalidFieldRequired()) {
+
+ error_outline
+ @if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}
+
+ }
-
-
-
diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.scss b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.scss
index 3dc1d40173..2aca8c4e20 100644
--- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.scss
+++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.scss
@@ -1,3 +1,5 @@
+@use '../../../styles/mixins' as mixins;
+
.adf {
&-typeahead-widget-container {
position: relative;
@@ -8,3 +10,7 @@
width: 100%;
}
}
+
+.adf-form-field-input .adf-error-icon {
+ @include mixins.adf-error-icon;
+}
diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.spec.ts b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.spec.ts
index 17a9ef79f3..5c932b7e44 100644
--- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.spec.ts
+++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.spec.ts
@@ -23,6 +23,9 @@ import { TypeaheadWidgetComponent } from './typeahead.widget';
import { TranslateService } from '@ngx-translate/core';
import { TaskFormService } from '../../services/task-form.service';
import { ProcessDefinitionService } from '../../services/process-definition.service';
+import { HarnessLoader } from '@angular/cdk/testing';
+import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
+import { MatFormFieldHarness } from '@angular/material/form-field/testing';
describe('TypeaheadWidgetComponent', () => {
let widget: TypeaheadWidgetComponent;
@@ -181,6 +184,7 @@ describe('TypeaheadWidgetComponent', () => {
let typeaheadWidgetComponent: TypeaheadWidgetComponent;
let fixture: ComponentFixture;
let element: HTMLElement;
+ let loader: HarnessLoader;
let stubProcessDefinitionService;
const fakeOptionList: FormFieldOption[] = [
{
@@ -198,6 +202,7 @@ describe('TypeaheadWidgetComponent', () => {
fixture = TestBed.createComponent(TypeaheadWidgetComponent);
typeaheadWidgetComponent = fixture.componentInstance;
element = fixture.nativeElement;
+ loader = TestbedHarnessEnvironment.loader(fixture);
});
afterEach(() => {
@@ -295,15 +300,16 @@ describe('TypeaheadWidgetComponent', () => {
typeaheadWidgetComponent.value = 'Fake Name';
typeaheadWidgetComponent.field.value = 'Fake Name';
typeaheadWidgetComponent.field.options = fakeOptionList;
- expect(element.querySelector('.adf-error-text')).toBeNull();
const keyboardEvent = new KeyboardEvent('keypress');
typeaheadWidgetComponent.onKeyUp(keyboardEvent);
fixture.detectChanges();
await fixture.whenStable();
- expect(element.querySelector('.adf-error-text')).not.toBeNull();
- expect(element.querySelector('.adf-error-text').textContent).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
+ const formField = await loader.getHarness(MatFormFieldHarness);
+ const errors = await formField.getTextErrors();
+ expect(errors.length).toBeGreaterThan(0);
+ expect(errors[0]).toContain('FORM.FIELD.VALIDATOR.INVALID_VALUE');
});
});
diff --git a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.ts b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.ts
index 95de622eba..b9d7cc2c64 100644
--- a/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.ts
+++ b/lib/process-services/src/lib/form/widgets/typeahead/typeahead.widget.ts
@@ -17,21 +17,23 @@
/* eslint-disable @angular-eslint/component-selector */
-import { FormFieldOption, WidgetComponent, ErrorWidgetComponent } from '@alfresco/adf-core';
+import { FormFieldOption, WidgetComponent } from '@alfresco/adf-core';
import { ENTER, ESCAPE } from '@angular/cdk/keycodes';
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
+import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
+import { ErrorStateMatcher } from '@angular/material/core';
import { TaskFormService } from '../../services/task-form.service';
import { ProcessDefinitionService } from '../../services/process-definition.service';
import { CommonModule } from '@angular/common';
import { TranslatePipe } from '@ngx-translate/core';
import { MatFormFieldModule } from '@angular/material/form-field';
-import { FormsModule } from '@angular/forms';
+import { MatIconModule } from '@angular/material/icon';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'typeahead-widget',
- imports: [CommonModule, TranslatePipe, MatFormFieldModule, FormsModule, MatAutocompleteModule, ErrorWidgetComponent, MatInputModule],
+ imports: [CommonModule, TranslatePipe, MatFormFieldModule, FormsModule, MatAutocompleteModule, MatInputModule, MatIconModule],
templateUrl: './typeahead.widget.html',
styleUrls: ['./typeahead.widget.scss'],
host: {
@@ -52,11 +54,13 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
value: string;
oldValue: string;
options: FormFieldOption[] = [];
+ errorStateMatcher: ErrorStateMatcher;
private readonly taskFormService = inject(TaskFormService);
private readonly processDefinitionService = inject(ProcessDefinitionService);
ngOnInit() {
+ this.initErrorStateMatcher();
if (this.field.form.taskId && this.field.restUrl) {
this.getValuesByTaskId();
} else if (this.field.form.processDefinitionId && this.field.restUrl) {
@@ -153,4 +157,11 @@ export class TypeaheadWidgetComponent extends WidgetComponent implements OnInit
isReadOnlyType(): boolean {
return this.field.type === 'readonly';
}
+
+ private initErrorStateMatcher(): void {
+ this.errorStateMatcher = {
+ isErrorState: (_control: UntypedFormControl | null, _form: FormGroupDirective | NgForm | null): boolean =>
+ !!this.field.validationSummary?.message || this.isInvalidFieldRequired()
+ };
+ }
}
diff --git a/lib/process-services/src/lib/styles/_mixins.scss b/lib/process-services/src/lib/styles/_mixins.scss
new file mode 100644
index 0000000000..6f96c96896
--- /dev/null
+++ b/lib/process-services/src/lib/styles/_mixins.scss
@@ -0,0 +1,7 @@
+@mixin adf-error-icon {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ vertical-align: text-bottom;
+}
diff --git a/lib/process-services/src/lib/task-list/components/start-task/start-task.component.html b/lib/process-services/src/lib/task-list/components/start-task/start-task.component.html
index 5ce70712cd..277bb0556c 100644
--- a/lib/process-services/src/lib/task-list/components/start-task/start-task.component.html
+++ b/lib/process-services/src/lib/task-list/components/start-task/start-task.component.html
@@ -11,12 +11,12 @@
matInput
id="name_id"
formControlName="name">
-
- {{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.REQUIRED' | translate }}
-
-
- {{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MAXIMUM_LENGTH' | translate : { characters : maxTaskNameLength } }}
-
+ @if (nameController.invalid) {
+
+ @if (nameController.hasError('required') || nameController.hasError('whitespace')) {{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.REQUIRED' | translate }}} @else if (nameController.hasError('maxlength')) {{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MAXIMUM_LENGTH' | translate : { characters : maxTaskNameLength } }}}
+
+ }
@@ -28,18 +28,21 @@
id="description_id"
formControlName="description">
-
- {{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MESSAGE' | translate }}
-
+ @if (descriptionController.hasError('whitespace')) {
+
+ {{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MESSAGE' | translate }}
+
+ }
|