mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
Improvement/AAE-43974 Refactoring form fields (#11945)
* AAE-43974 Refactoring form fields with mat-error * AAE-43974 Removed adf-error redundant class from all available places * AAE-43974 Followed code flow syntax * AAE-43974 Refactored based on review comments * AAE-43974 Added dummy test lable * AAE-43974 Added dummy error message for amount field * AAE-43974 Refactored multiline widget based on reviewer's comment * AAE-43974 Error icon style added by mixins * AAE-43974 Missing Conditions added * AAE-43974 Added Missing conditions * AAE-43974 Consistent conditions added and dummy error message added * AAE-43974 Added subscriptsizing dynamic for testing * AAE-43974 Reverted Subscriptsizing * AAE-43974 Added spacing between form fields in mobile view * AAE-43974 Dummy error message changed * AAE-43974 Changed error icon allignment for testing * AAE-43974 Added dummy error message * AAE-43974 Added form field gap in mobile view and dummy erro message * AAE-43974 Refactored based on review coemments and changed the error icon allignment * AAE-43974 Fixed the mixin path * AAE-43974 Removed dummy error message for amount widget * AAE-43974 Added adf-error-text class to mat-error for e2e fix * AAE-43974 adf-error-text class added inside mat-error * AAE-43974 Whitespace issue resolved * AAE-43974 Whiespace issue fix * AAE-43974 Added context pushing logic in text widget * AAE-43974 Added margin-bottom for regression fix * AAE-43974 Trying with different margin-bottom * AAE-43974 Testing regression issue * AAE-43974 Testing for regression failure * AAE-43974 Testing Regression failures * AAE-43974 Test Regression failure * AAE-43974 Testing multiline Regression failure * AAE-43974 Testing fix regarding regression failure related to multiline-text widget * AAE-43974 Test Regression failure for multiline-text widget * AAE-43974 Testing regression failure for multiline-text * AAE-43974 Testing regression failure for multi-line * AAE-43974 Testing regression failure * AAE-43974 Testing Regression failure * AAE-43974 Testing regression failure * AAE-43974 Removed the commented code * [ci:force] re-trigger CI --------- Co-authored-by: Ehsan Rezaei <ehsan.rezaei@hyland.com>
This commit is contained in:
co-authored by
Ehsan Rezaei
parent
23f0392c12
commit
b0dc85422c
@@ -0,0 +1,7 @@
|
||||
@mixin adf-error-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
@@ -16,20 +16,10 @@
|
||||
/>
|
||||
@if (form.controls['name'].dirty && form.controls['name'].invalid) {
|
||||
<mat-error>
|
||||
@if (form.controls['name'].hasError('required')) {
|
||||
<span>
|
||||
{{ 'CORE.FOLDER_DIALOG.FOLDER_NAME.ERRORS.REQUIRED' | translate }}
|
||||
</span>
|
||||
}
|
||||
|
||||
@else if (form.controls['name'].hasError('message')) {
|
||||
<span>
|
||||
{{ form.controls['name'].errors?.message | translate }}
|
||||
</span>
|
||||
}
|
||||
<span class="adf-error-text"
|
||||
>@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 }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="adf-full-width adf-folder-dialog-form-field">
|
||||
|
||||
@@ -24,8 +24,14 @@
|
||||
type="text"
|
||||
(keypress)="cleanErrorMsg()"
|
||||
[(ngModel)]="newTagName"
|
||||
[errorStateMatcher]="tagErrorStateMatcher"
|
||||
/>
|
||||
<mat-hint data-automation-id="errorMessage" *ngIf="error" [ngStyle]="{ color: 'red' }" align="start">{{ errorMsg }} </mat-hint>
|
||||
@if (errorMsg) {
|
||||
<mat-error data-automation-id="errorMessage">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text">{{ errorMsg }}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@
|
||||
/>
|
||||
@if (cardViewDateTimeControl.hasError('matDatepickerParse')) {
|
||||
<mat-error>
|
||||
{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}
|
||||
<span class="adf-error-text">{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate }}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -27,16 +27,17 @@
|
||||
[(ngModel)]="amountWidgetValue"
|
||||
(ngModelChange)="onFieldChangedAmountWidget()"
|
||||
[disabled]="field.readOnly"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
(focus)="amountWidgetOnFocus()"
|
||||
(blur)="amountWidgetOnBlur()"
|
||||
/>
|
||||
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<Observable<AmountWidgetSet
|
||||
'(invalid)': 'event($event)',
|
||||
'(select)': 'event($event)'
|
||||
},
|
||||
imports: [MatFormFieldModule, MatInputModule, FormsModule, ErrorWidgetComponent, TranslatePipe, NgIf],
|
||||
imports: [MatFormFieldModule, MatInputModule, FormsModule, TranslatePipe, NgIf, MatIconModule],
|
||||
providers: [CurrencyPipe],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
@@ -69,11 +70,13 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
currencyDisplay: string | boolean = 'symbol';
|
||||
decimalProperty: string;
|
||||
enableDisplayBasedOnLocale: boolean;
|
||||
errorStateMatcher: ErrorStateMatcher;
|
||||
isInputInFocus = false;
|
||||
locale: string;
|
||||
notShowDecimalDigits = '1.0-0';
|
||||
showDecimalDigits = '1.2-2';
|
||||
showReadonlyPlaceholder: boolean;
|
||||
translateParameters: Record<string, string> = {};
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,12 @@
|
||||
[touchUi]="true"
|
||||
[timeInterval]="5"
|
||||
[disabled]="field.readOnly" />
|
||||
@if (datetimeInputControl.invalid && datetimeInputControl.touched && field.validationSummary?.message) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text">{{ field.validationSummary.message | translate:translateParameters }}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget *ngIf="datetimeInputControl.invalid && datetimeInputControl.touched" [error]="field.validationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Date> = new FormControl<Date>(null);
|
||||
translateParameters: Record<string, string> = {};
|
||||
|
||||
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 {
|
||||
|
||||
@@ -21,11 +21,12 @@
|
||||
<mat-datepicker #datePicker
|
||||
[startAt]="startAt"
|
||||
[disabled]="field.readOnly" />
|
||||
@if (dateInputControl.invalid && dateInputControl.touched) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget
|
||||
*ngIf="dateInputControl.invalid && dateInputControl.touched"
|
||||
[error]="field.validationSummary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-14
@@ -7,6 +7,7 @@
|
||||
<mat-form-field
|
||||
floatPlaceholder="never"
|
||||
class="adf-form-field-input"
|
||||
[class.adf-has-counter]="field.maxLength > 0"
|
||||
[floatLabel]="field.placeholder ? 'always' : null"
|
||||
>
|
||||
@if(field.name || field.required) {
|
||||
@@ -20,24 +21,24 @@
|
||||
rows="3"
|
||||
[id]="field.id"
|
||||
[required]="field.required"
|
||||
[ngModel]="displayValue"
|
||||
(ngModelChange)="onValueChange($event)"
|
||||
[(ngModel)]="field.value"
|
||||
(ngModelChange)="onMultilineTextFieldChanged()"
|
||||
[disabled]="field.readOnly || readOnly"
|
||||
[placeholder]="field.placeholder"
|
||||
[title]="field.tooltip"
|
||||
(blur)="markAsTouched()"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
(blur)="onBlur()"
|
||||
>
|
||||
</textarea>
|
||||
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
|
||||
<mat-error>
|
||||
@if (field.maxLength > 0) {<span class="adf-multiline-counter-block">{{ field?.value?.length || 0 }}/{{ field.maxLength }}</span>}
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
} @else if (field.maxLength > 0) {
|
||||
<mat-hint class="adf-multiline-hint">{{ field?.value?.length || 0 }}/{{ field.maxLength }}</mat-hint>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div *ngIf="field.maxLength > 0" class="adf-multiline-word-counter">
|
||||
<span class="adf-multiline-word-counter-value">{{ displayValue?.length || 0 }}/{{ field.maxLength }}</span>
|
||||
</div>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
class="adf-multiline-required-message"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+22
-12
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate:translateParameters }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<ng-container *ngTemplateOutlet="fieldStatusTemplate ?? defaultErrorMessageTemplate; context: { $implicit: this }" />
|
||||
<ng-template #defaultErrorMessageTemplate>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngTemplateOutlet="fieldStatusTemplate ?? null; context: { $implicit: this }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ describe('TextWidgetComponent', () => {
|
||||
let loader: HarnessLoader;
|
||||
let widget: TextWidgetComponent;
|
||||
let fixture: ComponentFixture<TextWidgetComponent>;
|
||||
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 () => {
|
||||
|
||||
@@ -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<FieldStatusTemplate>('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<string, string> = {};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+3
-2
@@ -10,9 +10,10 @@
|
||||
type="password"
|
||||
placeholder="{{ 'ADF_VIEWER.PDF_DIALOG.PLACEHOLDER' | translate }}"
|
||||
[formControl]="passwordFormControl" />
|
||||
@if (isError()) {
|
||||
<mat-error data-automation-id='adf-password-dialog-error'><span class="adf-error-text">{{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}</span></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-error *ngIf="isError()" data-automation-id='adf-password-dialog-error'>{{ 'ADF_VIEWER.PDF_DIALOG.ERROR' | translate }}</mat-error>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
||||
+1
-2
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@mixin adf-error-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
+7
-3
@@ -35,9 +35,13 @@
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" />
|
||||
<mat-datepicker #datePicker [startAt]="startAt" [disabled]="field.readOnly" />
|
||||
@if (dateInputControl.invalid && dateInputControl.touched) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (dateInputControl.hasError('required')) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (dateInputControl.hasError('matDatepickerParse')) {{{ 'FORM.FIELD.VALIDATOR.INVALID_DATE_FORMAT' | translate: { format: field.dateDisplayFormat || field.defaultDateTimeFormat } }}} @else if (dateInputControl.hasError('matDatepickerMin')) {{{ 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN' | translate: { minValue: formattedMinDate } }}} @else if (dateInputControl.hasError('matDatepickerMax')) {{{ 'FORM.FIELD.VALIDATOR.NOT_GREATER_THAN' | translate: { maxValue: formattedMaxDate } }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget *ngIf="dateInputControl.invalid && dateInputControl.touched" [error]="field.validationSummary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
@use '../../../../mat-selectors' as ms;
|
||||
@use '../../../../mixins' as mixins;
|
||||
|
||||
.adf {
|
||||
// eslint-disable-selector-class-pattern
|
||||
@@ -29,3 +30,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-form-field-input .adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
+12
-2
@@ -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);
|
||||
|
||||
+4
-7
@@ -23,6 +23,7 @@
|
||||
[id]="field.id"
|
||||
[formControl]="propertyControl"
|
||||
[required]="field.required"
|
||||
[errorStateMatcher]="propertyErrorStateMatcher"
|
||||
/>
|
||||
|
||||
<ng-container *ngIf="previewState">
|
||||
@@ -30,13 +31,9 @@
|
||||
{{ field.params.externalPropertyLabel }}
|
||||
</span>
|
||||
</ng-container>
|
||||
@if (propertyLoadFailed && !previewState) {
|
||||
<mat-error><mat-icon class="adf-error-icon">error_outline</mat-icon><span class="adf-error-text">{{ 'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate }}</span></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="adf-error-messages-container"
|
||||
[ngClass]="!previewState ? 'adf-error-messages-container-visible' : 'adf-error-messages-container-hidden'"
|
||||
>
|
||||
<error-widget *ngIf="propertyLoadFailed" [required]="'FORM.FIELD.EXTERNAL_PROPERTY_LOAD_FAILED' | translate" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+6
@@ -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;
|
||||
}
|
||||
|
||||
+9
-6
@@ -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', () => {
|
||||
|
||||
+7
-1
@@ -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 });
|
||||
|
||||
+8
-21
@@ -25,6 +25,7 @@
|
||||
panelClass="adf-select-filter"
|
||||
[multiple]="field.hasMultipleValues"
|
||||
[required]="field.required"
|
||||
[errorStateMatcher]="dropdownErrorStateMatcher"
|
||||
#select
|
||||
(keydown.escape)="select.close()"
|
||||
>
|
||||
@@ -47,27 +48,13 @@
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if ((dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) || (!previewState && !field.readOnly && (isRestApiFailed || variableOptionsFailed))) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (dropdownControl.hasError('required') && !isRestApiFailed && !variableOptionsFailed) {{{ 'FORM.FIELD.REQUIRED' | translate }}} @else if (isRestApiFailed) {{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}} @else if (variableOptionsFailed) {{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="adf-error-messages-container"
|
||||
[ngClass]="!field.readOnly ? 'adf-error-messages-container-visible' : 'adf-error-messages-container-hidden'"
|
||||
>
|
||||
@if(showRequiredMessage) {
|
||||
<error-widget class="adf-dropdown-required-message" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
}
|
||||
@if(!previewState && isRestApiFailed) {
|
||||
<error-widget
|
||||
class="adf-dropdown-failed-message"
|
||||
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"
|
||||
/>
|
||||
}
|
||||
@if(!previewState && variableOptionsFailed) {
|
||||
<error-widget
|
||||
class="adf-dropdown-failed-message"
|
||||
required="{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
+80
-77
@@ -45,7 +45,7 @@ import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatSelectHarness } from '@angular/material/select/testing';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
|
||||
import { FormUtilsService } from '../../../services/form-utils.service';
|
||||
|
||||
describe('DropdownCloudWidgetComponent', () => {
|
||||
@@ -234,8 +234,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
describe('when failed on loading options from restUrl', () => {
|
||||
let getRestWidgetDataSpy: jasmine.Spy;
|
||||
const getErrorMessageElement = (): DebugElement => fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
const errorIcon: string = 'error_outline';
|
||||
const getFormFieldHarness = (): Promise<MatFormFieldHarness> => loader.getHarness(MatFormFieldHarness);
|
||||
|
||||
beforeEach(() => {
|
||||
getRestWidgetDataSpy = spyOn(formCloudService, 'getRestWidgetData').and.returnValue(throwError('Failed to fetch options'));
|
||||
@@ -243,23 +242,21 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.field.optionType = 'rest';
|
||||
});
|
||||
|
||||
it('should show error message when widget is NOT readonly', () => {
|
||||
it('should show error message when widget is NOT readonly', async () => {
|
||||
widget.field.readOnly = false;
|
||||
|
||||
widget.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const errorMessageElement = getErrorMessageElement();
|
||||
const formField = await getFormFieldHarness();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(getRestWidgetDataSpy).toHaveBeenCalled();
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
expect(errorMessageElement.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
|
||||
|
||||
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
|
||||
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-visible')).toBe(true);
|
||||
expect(errors[0]).toContain('FORM.FIELD.REST_API_FAILED');
|
||||
});
|
||||
|
||||
it('should NOT show required message when REST API failed', () => {
|
||||
it('should NOT show required message when REST API failed', async () => {
|
||||
widget.field.readOnly = false;
|
||||
widget.field.required = true;
|
||||
|
||||
@@ -268,10 +265,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
const restApiFailedElement = getErrorMessageElement();
|
||||
expect(restApiFailedElement).toBeTruthy();
|
||||
const formField = await getFormFieldHarness();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors[0]).not.toContain('FORM.FIELD.REQUIRED');
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should NOT show error message when widget is readonly', async () => {
|
||||
@@ -284,11 +281,12 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
|
||||
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
|
||||
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-hidden')).toBe(true);
|
||||
const formField = await getFormFieldHarness();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should NOT show REST API failed error message when form is in preview state', () => {
|
||||
it('should NOT show REST API failed error message when form is in preview state', async () => {
|
||||
widget.field.readOnly = false;
|
||||
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
|
||||
|
||||
@@ -299,8 +297,9 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
|
||||
const failedErrorMsgElement = getErrorMessageElement();
|
||||
expect(failedErrorMsgElement).toBeNull();
|
||||
const formField = await getFormFieldHarness();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,10 +313,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.field.isVisible = true;
|
||||
});
|
||||
|
||||
it('should show required message for rest type when required and value is empty after touch', fakeAsync(() => {
|
||||
it('should show required message for rest type when required and value is empty after touch', async () => {
|
||||
widget.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(false);
|
||||
@@ -326,10 +325,11 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.dropdownControl.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeTruthy();
|
||||
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
|
||||
}));
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
|
||||
});
|
||||
});
|
||||
|
||||
it('should preselect dropdown widget value when Json (rest call) passed', async () => {
|
||||
@@ -398,25 +398,27 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.touched = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not display required error when selecting a valid option for a required dropdown', fakeAsync(async () => {
|
||||
it('should not display required error when selecting a valid option for a required dropdown', async () => {
|
||||
widget.field.required = true;
|
||||
widget.field.options = [{ id: 'empty', name: 'Choose empty' }, ...fakeOptionList];
|
||||
|
||||
widget.ngOnInit();
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
await fixture.whenStable();
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.open();
|
||||
|
||||
widget.touched = true;
|
||||
await dropdown.clickOptions({ selector: '[id="opt_1"]' });
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
}));
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should not have a value when switching from an available option to the None option', async () => {
|
||||
widget.field.options = [{ id: 'empty', name: 'This is a mock none option' }, ...fakeOptionList];
|
||||
@@ -455,7 +457,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(asterisk.textContent).toEqual('*');
|
||||
});
|
||||
|
||||
it('should display a required error when dropdown is required and has no value after an interaction', () => {
|
||||
it('should display a required error when dropdown is required and has no value after an interaction', async () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeFalsy();
|
||||
@@ -467,11 +469,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeTruthy();
|
||||
|
||||
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
|
||||
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-visible')).toBe(true);
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
|
||||
});
|
||||
|
||||
it('should NOT display a required error when dropdown is readonly', () => {
|
||||
@@ -815,7 +816,6 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
it('should reset previous child options if the rest url failed for a linked dropdown', async () => {
|
||||
const jsonDataSpy = spyOn(formCloudService, 'getRestWidgetData').and.returnValue(of(mockRestDropdownOptions));
|
||||
const errorIcon: string = 'error_outline';
|
||||
const mockParentDropdown = { id: 'parentDropdown', value: 'mock-value', validate: () => true };
|
||||
spyOn(widget.field.form, 'getFormFields').and.returnValue([mockParentDropdown]);
|
||||
|
||||
@@ -828,27 +828,28 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
selectParentOption('UK');
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.open();
|
||||
const failedErrorMsgElement1 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors1 = await formField.getTextErrors();
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(false);
|
||||
expect(widget.field.options.length).toBe(2);
|
||||
expect(failedErrorMsgElement1).toBeNull();
|
||||
expect(errors1.length).toBe(0);
|
||||
|
||||
jsonDataSpy.and.returnValue(throwError('Failed to fetch options'));
|
||||
selectParentOption('GR');
|
||||
const failedErrorMsgElement2 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
const errors2 = await formField.getTextErrors();
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toBe(0);
|
||||
expect(failedErrorMsgElement2.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
|
||||
expect(errors2[0]).toContain('FORM.FIELD.REST_API_FAILED');
|
||||
|
||||
jsonDataSpy.and.returnValue(of(mockSecondRestDropdownOptions));
|
||||
selectParentOption('IT');
|
||||
const failedErrorMsgElement3 = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
const errors3 = await formField.getTextErrors();
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(false);
|
||||
expect(widget.field.options.length).toBe(2);
|
||||
expect(failedErrorMsgElement3).toBeNull();
|
||||
expect(errors3.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('Rest - On parent value changes (chain)', () => {
|
||||
@@ -1067,8 +1068,6 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
});
|
||||
|
||||
describe('variable options', () => {
|
||||
const errorIcon: string = 'error_outline';
|
||||
|
||||
const getVariableDropdownWidget = (
|
||||
variableName: string,
|
||||
optionsPath: string,
|
||||
@@ -1090,9 +1089,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
const checkDropdownVariableOptionsFailed = () => {
|
||||
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED'));
|
||||
const checkDropdownVariableOptionsFailed = async () => {
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain('FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED');
|
||||
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
};
|
||||
@@ -1258,7 +1258,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(await allOptions[2].getText()).toEqual('Parrot');
|
||||
});
|
||||
|
||||
it('should return empty array and display error when path is incorrect', () => {
|
||||
it('should return empty array and display error when path is incorrect', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.wrongPath.players',
|
||||
@@ -1268,10 +1268,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
checkDropdownVariableOptionsFailed();
|
||||
await checkDropdownVariableOptionsFailed();
|
||||
});
|
||||
|
||||
it('should NOT show required message when variable options failed', () => {
|
||||
it('should NOT show required message when variable options failed', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.wrongPath.players',
|
||||
@@ -1283,13 +1283,13 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.variableOptionsFailed).toBe(true);
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
const variableFailedElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
expect(variableFailedElement).toBeTruthy();
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors[0]).not.toContain('FORM.FIELD.REQUIRED');
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should show required message for variable type when options load successfully and required and value is empty after touch', fakeAsync(() => {
|
||||
it('should show required message for variable type when options load successfully and required and value is empty after touch', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.people.players',
|
||||
@@ -1302,7 +1302,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.field.isVisible = true;
|
||||
widget.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.variableOptionsFailed).toBe(false);
|
||||
@@ -1311,12 +1311,13 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.dropdownControl.markAsTouched();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeTruthy();
|
||||
expect(requiredErrorElement.nativeElement.innerText).toEqual('FORM.FIELD.REQUIRED');
|
||||
}));
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect(errors[0]).toContain('FORM.FIELD.REQUIRED');
|
||||
});
|
||||
|
||||
it('should return empty array and display error when id is incorrect', () => {
|
||||
it('should return empty array and display error when id is incorrect', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.people.players',
|
||||
@@ -1326,10 +1327,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
checkDropdownVariableOptionsFailed();
|
||||
await checkDropdownVariableOptionsFailed();
|
||||
});
|
||||
|
||||
it('should return empty array and display error when label is incorrect', () => {
|
||||
it('should return empty array and display error when label is incorrect', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.people.players',
|
||||
@@ -1339,10 +1340,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
checkDropdownVariableOptionsFailed();
|
||||
await checkDropdownVariableOptionsFailed();
|
||||
});
|
||||
|
||||
it('should return empty array and display error when variable is NOT found', () => {
|
||||
it('should return empty array and display error when variable is NOT found', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.wrong-variable-id',
|
||||
'response.people.players',
|
||||
@@ -1352,17 +1353,17 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
);
|
||||
fixture.detectChanges();
|
||||
|
||||
checkDropdownVariableOptionsFailed();
|
||||
await checkDropdownVariableOptionsFailed();
|
||||
});
|
||||
|
||||
it('should return empty array and display error if there are NO process and form variables', () => {
|
||||
it('should return empty array and display error if there are NO process and form variables', async () => {
|
||||
widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'playerId', 'playerFullName', [], []);
|
||||
fixture.detectChanges();
|
||||
|
||||
checkDropdownVariableOptionsFailed();
|
||||
await checkDropdownVariableOptionsFailed();
|
||||
});
|
||||
|
||||
it('should NOT display variable options failed error when form is in the preview state', () => {
|
||||
it('should NOT display variable options failed error when form is in the preview state', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.json-variable',
|
||||
'response.wrongPath.players',
|
||||
@@ -1373,11 +1374,12 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
spyOn(formCloudService, 'getPreviewState').and.returnValue(true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
expect(failedErrorMsgElement).toBeNull();
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should NOT display errors if field is readonly', () => {
|
||||
it('should NOT display errors if field is readonly', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
'variables.wrong-variable-id',
|
||||
'response.wrongPath.players',
|
||||
@@ -1388,8 +1390,9 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
widget.field.readOnly = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const errorsMessagesElement = fixture.debugElement.query(By.css('.adf-error-messages-container'));
|
||||
expect(errorsMessagesElement.nativeElement.classList.contains('adf-error-messages-container-hidden')).toBe(true);
|
||||
const formField = await loader.getHarness(MatFormFieldHarness);
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should update options when form variable changes', async () => {
|
||||
|
||||
+9
-16
@@ -18,7 +18,6 @@
|
||||
import {
|
||||
AppConfigService,
|
||||
ErrorMessageModel,
|
||||
ErrorWidgetComponent,
|
||||
FormFieldEvent,
|
||||
FormFieldModel,
|
||||
FormFieldOption,
|
||||
@@ -31,11 +30,13 @@ import {
|
||||
SelectFilterInputComponent,
|
||||
WidgetComponent
|
||||
} from '@alfresco/adf-core';
|
||||
import { AsyncPipe, NgClass } from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, isObservable, Subject } from 'rxjs';
|
||||
@@ -62,16 +63,7 @@ export const DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE = 100;
|
||||
'(click)': 'event($event)'
|
||||
},
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
imports: [
|
||||
NgClass,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
ErrorWidgetComponent,
|
||||
TranslatePipe,
|
||||
SelectFilterInputComponent
|
||||
]
|
||||
imports: [AsyncPipe, ReactiveFormsModule, MatFormFieldModule, MatSelectModule, TranslatePipe, SelectFilterInputComponent, MatIconModule]
|
||||
})
|
||||
export class DropdownCloudWidgetComponent extends WidgetComponent implements OnInit, ReactiveFormWidget {
|
||||
public formService = inject(FormService);
|
||||
@@ -92,6 +84,11 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
restApiHostName: string;
|
||||
dropdownControl = new FormControl<FormFieldOption | FormFieldOption[]>(undefined);
|
||||
|
||||
dropdownErrorStateMatcher: ErrorStateMatcher = {
|
||||
isErrorState: (control) =>
|
||||
(control?.touched && control?.invalid) || (!this.previewState && (this.isRestApiFailed || this.variableOptionsFailed))
|
||||
};
|
||||
|
||||
list$ = new BehaviorSubject<FormFieldOption[]>([]);
|
||||
filter$ = new BehaviorSubject<string>('');
|
||||
|
||||
@@ -101,10 +98,6 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
|
||||
private readonly debounceSetValue = new Subject<void>();
|
||||
|
||||
get showRequiredMessage(): boolean {
|
||||
return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
|
||||
}
|
||||
|
||||
get isReadOnlyType(): boolean {
|
||||
return this.field.type === 'readonly';
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../mixins' as mixins;
|
||||
|
||||
.adf {
|
||||
&-cloud-group-list {
|
||||
margin: 5px 0;
|
||||
@@ -42,3 +44,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-error-messages-container .adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../mixins' as mixins;
|
||||
|
||||
.adf {
|
||||
&-people-cloud {
|
||||
width: 100%;
|
||||
@@ -46,3 +48,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-error-messages-container .adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
+6
-6
@@ -73,7 +73,7 @@
|
||||
placeholder="{{processFilterProperty.label | translate}}"
|
||||
[attr.data-automation-id]="'adf-cloud-edit-process-property-' + processFilterProperty.key"/>
|
||||
</mat-form-field>
|
||||
<mat-form-field [floatLabel]="'auto'" *ngIf="processFilterProperty.type === 'date'" [attr.data-automation-id]="processFilterProperty.key">
|
||||
<mat-form-field [floatLabel]="'auto'" class="adf-form-field-input" *ngIf="processFilterProperty.type === 'date'" [attr.data-automation-id]="processFilterProperty.key">
|
||||
<mat-label>{{processFilterProperty.label | translate}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
@@ -87,12 +87,12 @@
|
||||
matSuffix [for]="dateController"
|
||||
[attr.data-automation-id]="'adf-cloud-edit-process-property-date-toggle-' + processFilterProperty.key" />
|
||||
<mat-datepicker #dateController [attr.data-automation-id]="'adf-cloud-edit-process-property-date-picker-' + processFilterProperty.key" />
|
||||
<div class="adf-edit-process-filter-date-error-container">
|
||||
<mat-error *ngIf="hasError(processFilterProperty)">
|
||||
<div class="adf-error-text">{{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}</div>
|
||||
<mat-icon class="adf-error-icon" adf-icon="warning" />
|
||||
@if (hasError(processFilterProperty)) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text">{{'ADF_CLOUD_EDIT_PROCESS_FILTER.ERROR.DATE' | translate}}</span>
|
||||
</mat-error>
|
||||
</div>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<adf-cloud-date-range-filter *ngIf="processFilterProperty.type === 'date-range'"
|
||||
|
||||
+5
@@ -1,4 +1,5 @@
|
||||
@use '../../../../flex' as flex;
|
||||
@use '../../../../mixins' as mixins;
|
||||
|
||||
.adf-edit-process-filter-date-error-container {
|
||||
position: absolute;
|
||||
@@ -62,3 +63,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-form-field-input .adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
</button>
|
||||
</div>
|
||||
@if (processDefinition.hasError('required')) {
|
||||
<mat-error class="adf-error-pb">
|
||||
<mat-error>
|
||||
{{ 'ADF_CLOUD_PROCESS_LIST.ADF_CLOUD_START_PROCESS.ERROR.PROCESS_DEFINITION_REQUIRED' | translate }}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
+5
-6
@@ -61,6 +61,7 @@
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
|
||||
</mat-form-field>
|
||||
<mat-form-field [floatLabel]="'auto'"
|
||||
class="adf-form-field-input"
|
||||
*ngIf="taskFilterProperty.type === 'date'"
|
||||
[attr.data-automation-id]="taskFilterProperty.key">
|
||||
<mat-label>{{taskFilterProperty.label | translate}}</mat-label>
|
||||
@@ -75,12 +76,10 @@
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
|
||||
<mat-datepicker #dateController
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-picker-' + taskFilterProperty.key" />
|
||||
<div class="adf-edit-task-filter-date-error-container">
|
||||
<mat-error *ngIf="hasError(taskFilterProperty)">
|
||||
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
|
||||
<mat-icon class="adf-error-icon" adf-icon="warning" />
|
||||
</mat-error>
|
||||
</div>
|
||||
<mat-error *ngIf="hasError(taskFilterProperty)">
|
||||
<mat-icon class="adf-error-icon">warning</mat-icon>
|
||||
<span class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</span>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<div class="adf-edit-task-filter-checkbox"
|
||||
*ngIf="taskFilterProperty.type === 'checkbox'">
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
+6
-5
@@ -61,6 +61,7 @@
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-' + taskFilterProperty.key" />
|
||||
</mat-form-field>
|
||||
<mat-form-field [floatLabel]="'auto'"
|
||||
class="adf-form-field-input"
|
||||
*ngIf="taskFilterProperty.type === 'date'"
|
||||
[attr.data-automation-id]="taskFilterProperty.key">
|
||||
<mat-label>{{taskFilterProperty.label | translate}}</mat-label>
|
||||
@@ -75,12 +76,12 @@
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-toggle-' + taskFilterProperty.key" />
|
||||
<mat-datepicker #dateController
|
||||
[attr.data-automation-id]="'adf-cloud-edit-task-property-date-picker-' + taskFilterProperty.key" />
|
||||
<div class="adf-edit-task-filter-date-error-container">
|
||||
<mat-error *ngIf="hasError(taskFilterProperty)">
|
||||
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
|
||||
<mat-icon class="adf-error-icon" adf-icon="warning" />
|
||||
@if (hasError(taskFilterProperty)) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</span>
|
||||
</mat-error>
|
||||
</div>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="adf-edit-task-filter-checkbox"
|
||||
*ngIf="taskFilterProperty.type === 'checkbox'">
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
<label class="adf-label" [attr.for]="field.id"
|
||||
>{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label
|
||||
>
|
||||
<mat-form-field>
|
||||
<mat-form-field class="adf-form-field-input">
|
||||
<mat-select class="adf-select" [id]="field.id" [formControl]="dropdownControl">
|
||||
<mat-option *ngFor="let opt of field.options" [value]="opt" [id]="opt.id">{{opt.name}}</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="dropdownControl.disabled" [value]="field.value">{{field.value}}</mat-option>
|
||||
</mat-select>
|
||||
@if (!isReadOnlyField && dropdownControl.touched && (field.validationSummary?.message || dropdownControl.hasError('required'))) {
|
||||
<mat-error data-automation-id="adf-dropdown-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<ng-container *ngIf="!isReadOnlyField && dropdownControl.touched">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+7
-2
@@ -48,7 +48,12 @@
|
||||
<span id="adf-group-label-name">{{item.name}}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
</div>
|
||||
|
||||
+6
@@ -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;
|
||||
}
|
||||
|
||||
+1
-2
@@ -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
|
||||
],
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label>
|
||||
<mat-form-field
|
||||
class="adf-people-widget-field">
|
||||
<mat-chip-grid #chipGrid [attr.aria-label]="'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.SELECTED_PEOPLE' | translate">
|
||||
<mat-chip-grid #chipGrid [attr.aria-label]="'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.SELECTED_PEOPLE' | translate"
|
||||
[ngModel]="selectedUsers" [errorStateMatcher]="errorStateMatcher">
|
||||
<mat-chip-row
|
||||
*ngFor="let user of selectedUsers"
|
||||
(removed)="onRemove(user)"
|
||||
@@ -47,7 +48,12 @@
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
@if (field.validationSummary?.message || (isInvalidFieldRequired() && isTouched())) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -368,17 +368,13 @@ describe('PeopleWidgetComponent', () => {
|
||||
});
|
||||
|
||||
it('should show an error message if the user is invalid', fakeAsync(() => {
|
||||
const peopleHTMLElement = element.querySelector<HTMLInputElement>('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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[class.adf-invalid]="!field.isValid"
|
||||
[class.adf-readonly]="field.readOnly"
|
||||
id="typehead-div">
|
||||
<mat-form-field>
|
||||
<mat-form-field class="adf-form-field-input">
|
||||
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}</label>
|
||||
<input matInput class="adf-input"
|
||||
type="text"
|
||||
@@ -15,15 +15,20 @@
|
||||
[disabled]="field.readOnly"
|
||||
data-automation-id="adf-typeahed-search-input"
|
||||
placeholder="{{field.placeholder}}"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
[matAutocomplete]="auto">
|
||||
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onItemSelect($event.option.value)">
|
||||
<mat-option *ngFor="let item of options; let i = index" id="adf-typeahed-widget-user-{{i}}" [value]="item">
|
||||
<span id="adf-typeahed-label-name">{{item.name}}</span>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
@if (field.validationSummary?.message || isInvalidFieldRequired()) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<span class="adf-error-text"
|
||||
>@if (field.validationSummary?.message) {{{ field.validationSummary.message | translate }}} @else {{{ 'FORM.FIELD.REQUIRED' | translate }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<TypeaheadWidgetComponent>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@mixin adf-error-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
+18
-15
@@ -11,12 +11,12 @@
|
||||
matInput
|
||||
id="name_id"
|
||||
formControlName="name">
|
||||
<mat-error *ngIf="nameController.hasError('required') || nameController.hasError('whitespace')">
|
||||
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.REQUIRED' | translate }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="nameController.hasError('maxlength')">
|
||||
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MAXIMUM_LENGTH' | translate : { characters : maxTaskNameLength } }}
|
||||
</mat-error>
|
||||
@if (nameController.invalid) {
|
||||
<mat-error>
|
||||
<span class="adf-error-text"
|
||||
>@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 } }}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="adf-task-description">
|
||||
@@ -28,18 +28,21 @@
|
||||
id="description_id"
|
||||
formControlName="description">
|
||||
</textarea>
|
||||
<mat-error *ngIf="descriptionController.hasError('whitespace')">
|
||||
{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MESSAGE' | translate }}
|
||||
</mat-error>
|
||||
@if (descriptionController.hasError('whitespace')) {
|
||||
<mat-error>
|
||||
<span class="adf-error-text">{{ 'ADF_TASK_LIST.START_TASK.FORM.ERROR.MESSAGE' | translate }}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="adf-input-row">
|
||||
<mat-form-field>
|
||||
<mat-form-field class="adf-form-field-input">
|
||||
<input
|
||||
matInput
|
||||
(keyup)="onDateChanged($any($event).srcElement.value)"
|
||||
(dateInput)="onDateChanged($any($event).value)"
|
||||
[matDatepicker]="taskDatePicker"
|
||||
[errorStateMatcher]="dateErrorStateMatcher"
|
||||
placeholder="{{'ADF_TASK_LIST.START_TASK.FORM.LABEL.DATE'|translate}}"
|
||||
id="date_id">
|
||||
<mat-datepicker-toggle
|
||||
@@ -48,12 +51,12 @@
|
||||
<mat-datepicker
|
||||
#taskDatePicker
|
||||
[touchUi]="true" />
|
||||
<div class="adf-error-text-container">
|
||||
<div *ngIf="dateError">
|
||||
<div class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</div>
|
||||
@if (dateError) {
|
||||
<mat-error>
|
||||
<mat-icon class="adf-error-icon">warning</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<span class="adf-error-text">{{'ADF_TASK_LIST.START_TASK.FORM.ERROR.DATE'|translate}}</span>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<people-widget
|
||||
(peopleSelected)="setAssigneeId($event)"
|
||||
|
||||
+5
-5
@@ -1,4 +1,5 @@
|
||||
@use '../../../styles/flex' as flex;
|
||||
@use '../../../styles/mixins' as mixins;
|
||||
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.adf-new-task-heading {
|
||||
@@ -112,11 +113,6 @@ adf-start-task {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&-error-icon {
|
||||
font: var(--mat-sys-body-medium);
|
||||
color: var(--mat-sys-error);
|
||||
}
|
||||
|
||||
&-label {
|
||||
color: var(--mat-sys-outline);
|
||||
}
|
||||
@@ -140,3 +136,7 @@ adf-start-task {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.adf-form-field-input .adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
+5
-1
@@ -17,7 +17,7 @@
|
||||
|
||||
import { ADF_DATE_FORMATS, AdfDateFnsAdapter, DateFnsUtils, FormFieldModel, FormModel } from '@alfresco/adf-core';
|
||||
import { Component, DestroyRef, EventEmitter, inject, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
|
||||
import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core';
|
||||
import { DateAdapter, ErrorStateMatcher, MAT_DATE_FORMATS } from '@angular/material/core';
|
||||
import { EMPTY, Observable } from 'rxjs';
|
||||
import { Form } from '../../models/form.model';
|
||||
import { TaskListService } from '../../services/tasklist.service';
|
||||
@@ -96,6 +96,10 @@ export class StartTaskComponent implements OnInit {
|
||||
maxTaskNameLength: number = MAX_LENGTH;
|
||||
loading = false;
|
||||
|
||||
dateErrorStateMatcher: ErrorStateMatcher = {
|
||||
isErrorState: () => this.dateError
|
||||
};
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
Reference in New Issue
Block a user