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 @@ @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 }} + } -
- {{ 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 }} + + }
- + -
-
-
{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}
+ @if (dateError) { + warning -
-
+ {{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}} + + }
this.dateError + }; + private readonly destroyRef = inject(DestroyRef); ngOnInit() {