[AAE-7765] Improved display mandatory form fields (#7531)

* [MNT-22765] Improved display mandatory form fields

* [MNT-22765] added unit tests

* [MNT-22765] fixed test with error icon on rest fail

* Trigger travis

* [MNT-22765] removed underscore from var name

* [AAE-7765] removed underscore from unit test

* [AAE-7765] fixed css lint

* [AAE-7765] fixed e2e error message css class

* [AAE-7765] fixed storybook e2e
This commit is contained in:
Tomasz Gnyp
2022-03-07 19:29:12 +01:00
committed by GitHub
parent e877cd822b
commit 3dc9f7cdfd
67 changed files with 915 additions and 219 deletions

View File

@@ -45,43 +45,39 @@ import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
const typeIntoInput = (targetInput: HTMLInputElement, message: string) => {
expect(targetInput).not.toBeNull('Expected input to set to be valid and not null');
expect(targetInput).toBeTruthy('Expected input to set to be valid and not null');
targetInput.value = message;
targetInput.dispatchEvent(new Event('input'));
};
const typeIntoDate = (targetInput: DebugElement, date: { srcElement: { value: string } }) => {
expect(targetInput).not.toBeNull('Expected input to set to be valid and not null');
expect(targetInput).toBeTruthy('Expected input to set to be valid and not null');
targetInput.triggerEventHandler('change', date);
};
const expectElementToBeHidden = (targetElement: HTMLElement): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.hidden).toBe(true, `${targetElement.id} should be hidden but it is not`);
};
const expectElementToBeVisible = (targetElement: HTMLElement): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.hidden).toBe(false, `${targetElement.id} should be visibile but it is not`);
};
const expectInputElementValueIs = (targetElement: HTMLInputElement, value: string): void => {
expect(targetElement).not.toBeNull();
expect(targetElement).toBeDefined();
expect(targetElement).toBeTruthy();
expect(targetElement.value).toBe(value, `invalid value for ${targetElement.name}`);
};
const expectElementToBeInvalid = (fieldId: string, fixture: ComponentFixture<FormRendererComponent>): void => {
const invalidElementContainer = fixture.nativeElement.querySelector(`#field-${fieldId}-container .adf-invalid`);
expect(invalidElementContainer).not.toBeNull();
expect(invalidElementContainer).toBeDefined();
expect(invalidElementContainer).toBeTruthy();
};
const expectElementToBeValid = (fieldId: string, fixture: ComponentFixture<FormRendererComponent>): void => {
const invalidElementContainer = fixture.nativeElement.querySelector(`#field-${fieldId}-container .adf-invalid`);
expect(invalidElementContainer).toBeNull();
expect(invalidElementContainer).toBeFalsy();
};
describe('Form Renderer Component', () => {
@@ -407,6 +403,12 @@ describe('Form Renderer Component', () => {
const numberInputRequired: HTMLInputElement = fixture.nativeElement.querySelector('#Number0x8cbv');
expectElementToBeVisible(numberInputRequired);
expectElementToBeValid('Number0x8cbv', fixture);
numberInputRequired.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expectElementToBeInvalid('Number0x8cbv', fixture);
typeIntoInput(numberInputRequired, '5');
@@ -444,6 +446,7 @@ describe('Form Renderer Component', () => {
expectElementToBeVisible(numberInputElement);
expectElementToBeValid('Number0him2z', fixture);
numberInputElement.dispatchEvent(new Event('blur'));
typeIntoInput(numberInputElement, '9');
fixture.detectChanges();
await fixture.whenStable();

View File

@@ -1,8 +1,8 @@
<div class="adf-amount-widget__container adf-amount-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly">
<label class="adf-label"
[attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
[attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-form-field class="adf-amount-widget__input" [hideRequiredMarker]="true">
<span matPrefix class="adf-amount-widget__prefix-spacing">{{ currency }} &nbsp;</span>
<input matInput
@@ -17,9 +17,10 @@
[value]="field.value"
[(ngModel)]="field.value"
(ngModelChange)="onFieldChanged(field)"
[disabled]="field.readOnly">
[disabled]="field.readOnly"
(blur)="markAsTouched()">
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()"
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -20,14 +20,16 @@ import { FormFieldModel } from './../core/form-field.model';
import { AmountWidgetComponent, ADF_AMOUNT_SETTINGS } from './amount.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { FormBaseModule } from '../../../form-base.module';
import { FormModel } from '../core';
import { FormFieldTypes } from '../core/form-field-types';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { FormModel } from '../core/form.model';
describe('AmountWidgetComponent', () => {
let widget: AmountWidgetComponent;
let fixture: ComponentFixture<AmountWidgetComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
@@ -39,8 +41,8 @@ describe('AmountWidgetComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(AmountWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
it('should setup currency from field', () => {
@@ -78,6 +80,38 @@ describe('AmountWidgetComponent', () => {
widget.ngOnInit();
expect(widget.placeholder).toBe('1234');
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.AMOUNT,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const amount = fixture.nativeElement.querySelector('input');
expect(element.querySelector('.adf-invalid')).toBeFalsy();
amount.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});
describe('AmountWidgetComponent - rendering', () => {

View File

@@ -1,16 +1,19 @@
<div [ngClass]="field.className"
[class.adf-invalid]="!field.isValid">
[class.adf-invalid]="!field.isValid && isTouched()">
<mat-checkbox
[id]="field.id"
color="primary"
[required]="field.required"
[required]="isRequired()"
[disabled]="field.readOnly || readOnly"
[(ngModel)]="field.value"
(ngModelChange)="onFieldChanged(field)"
[matTooltip]="field.tooltip"
(click)="markAsTouched()"
matTooltipPosition="right"
matTooltipShowDelay="1000">
{{field.name | translate }}
<span *ngIf="field.required" >*</span>
<span class="adf-asterisk" *ngIf="isRequired()" >*</span>
</mat-checkbox>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -21,9 +21,9 @@ import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { CheckboxWidgetComponent } from './checkbox.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { FormBaseModule } from 'core/form/form-base.module';
import { FormBaseModule } from '../../../form-base.module';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateLoaderService } from 'core/services';
import { TranslateLoaderService } from '../../../../services/translate-loader.service';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { CoreTestingModule } from '../../../../testing';
import { MatTooltipModule } from '@angular/material/tooltip';
@@ -69,11 +69,27 @@ describe('CheckboxWidgetComponent', () => {
});
});
it('should be marked as invalid when required', async () => {
it('should be marked as invalid when required after interaction', async () => {
const checkbox = element.querySelector('mat-checkbox');
expect(element.querySelector('.adf-invalid')).toBeFalsy();
checkbox.dispatchEvent(new Event('click'));
checkbox.dispatchEvent(new Event('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).not.toBeNull();
expect(element.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
it('should be checked if boolean true is passed', fakeAsync(() => {

View File

@@ -1,6 +1,6 @@
<div class="{{field.className}}" id="data-time-widget" [class.adf-invalid]="!field.isValid">
<div class="{{field.className}}" id="data-time-widget" [class.adf-invalid]="!field.isValid && isTouched()">
<mat-form-field class="adf-date-time-widget" [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[value]="field.value"
@@ -9,13 +9,14 @@
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000"
(focus)="datetimePicker.open()">
<mat-datetimepicker-toggle matSuffix [for]="datetimePicker" [disabled]="field.readOnly"></mat-datetimepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datetimepicker #datetimePicker type="datetime" [touchUi]="true" [timeInterval]="5" [disabled]="field.readOnly"></mat-datetimepicker>
<input
type="hidden"

View File

@@ -24,6 +24,7 @@ import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FormFieldTypes } from '../core/form-field-types';
describe('DateTimeWidgetComponent', () => {
@@ -106,6 +107,38 @@ describe('DateTimeWidgetComponent', () => {
expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.DATETIME,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const dateTimeInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
dateTimeInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
describe('template check', () => {
it('should show visible date widget', async () => {

View File

@@ -1,17 +1,19 @@
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid">
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid && isTouched()">
<mat-form-field class="adf-date-widget" [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<input matInput
[id]="field.id"
[value]="field.value"
[required]="isRequired()"
[disabled]="field.readOnly"
(change)="onDateChanged($any($event).srcElement.value)"
[placeholder]="field.placeholder">
[placeholder]="field.placeholder"
(blur)="markAsTouched()">
<mat-datepicker-toggle matSuffix [for]="datePicker" [disabled]="field.readOnly" ></mat-datepicker-toggle>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<mat-datepicker #datePicker [touchUi]="true" [startAt]="field.value | adfMomentDate: field.dateDisplayFormat" [disabled]="field.readOnly"></mat-datepicker>
<input
type="hidden"

View File

@@ -23,6 +23,7 @@ import { DateWidgetComponent } from './date.widget';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { TranslateModule } from '@ngx-translate/core';
import { FormFieldTypes } from '../core/form-field-types';
describe('DateWidgetComponent', () => {
@@ -98,6 +99,38 @@ describe('DateWidgetComponent', () => {
expect(widget.onFieldChanged).toHaveBeenCalledWith(field);
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.DATE,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const dateInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
dateInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterix', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
describe('template check', () => {
afterEach(() => {

View File

@@ -1,12 +1,13 @@
<div class="adf-dropdown-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-form-field>
<mat-select class="adf-select"
[id]="field.id"
[(ngModel)]="field.value"
[disabled]="field.readOnly"
(ngModelChange)="onFieldChanged(field)">
(ngModelChange)="onFieldChanged(field)"
(blur)="markAsTouched()">
<mat-option *ngFor="let opt of field.options"
[value]="getOptionValue(opt, field.value)"
[id]="opt.id">{{opt.name}}
@@ -15,6 +16,6 @@
</mat-select>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget class="adf-dropdown-required-message" *ngIf="isInvalidFieldRequired()"
<error-widget class="adf-dropdown-required-message" *ngIf="showRequiredMessage()"
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -130,22 +130,26 @@ describe('DropdownWidgetComponent', () => {
});
});
it('should be able to display label with asterix', async () => {
const label = 'MyLabel123';
widget.field.name = label;
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('label').innerText).toBe(label + '*');
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
it('should be invalid if no default option', async () => {
it('should be invalid if no default option after interaction', async () => {
expect(element.querySelector('.adf-invalid')).toBeFalsy();
const dropdownSelect = element.querySelector('.adf-select');
dropdownSelect.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeDefined();
expect(element.querySelector('.adf-invalid')).not.toBeNull();
expect(element.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be valid if default option', async () => {
@@ -155,7 +159,7 @@ describe('DropdownWidgetComponent', () => {
fixture.detectChanges();
await fixture.whenStable();
expect(element.querySelector('.adf-invalid')).toBeNull();
expect(element.querySelector('.adf-invalid')).toBeFalsy();
});
});

View File

@@ -113,4 +113,7 @@ export class DropdownWidgetComponent extends WidgetComponent implements OnInit {
return this.field.type === 'readonly';
}
showRequiredMessage(): boolean {
return (this.isInvalidFieldRequired() || this.field.value === 'empty') && this.isTouched();
}
}

View File

@@ -1,6 +1,6 @@
<div class="adf-dynamic-table-scrolling {{field.className}}"
[class.adf-invalid]="!isValid()">
<div class="adf-label">{{content.name | translate }}<span *ngIf="isRequired()">*</span></div>
<div class="adf-label">{{content.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></div>
<div *ngIf="!editMode">
<div class="adf-table-container">

View File

@@ -1,9 +1,10 @@
<div class="adf-error-text-container">
<div *ngIf="error?.isActive()" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-error-container">
<div *ngIf="error?.isActive()" [@transitionMessages]="subscriptAnimationState" class="adf-error">
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<div class="adf-error-text">{{error.message | translate:translateParameters}}</div>
<mat-icon class="adf-error-icon">warning</mat-icon>
</div>
<div *ngIf="required" [@transitionMessages]="_subscriptAnimationState">
<div class="adf-error-text">{{required}}</div>
<div *ngIf="required" [@transitionMessages]="subscriptAnimationState" class="adf-error">
<mat-icon class="adf-error-icon">error_outline</mat-icon>
<div class="adf-error-text">{{required}}</div>
</div>
</div>

View File

@@ -1,3 +1,3 @@
.adf-error-text {
width: 85%;
.adf-error {
display: flex;
}

View File

@@ -0,0 +1,73 @@
/*!
* @license
* Copyright 2019 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SimpleChange, SimpleChanges } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { ErrorWidgetComponent } from './error.component';
import { CoreTestingModule } from '../../../../testing';
import { ErrorMessageModel } from '..';
describe('ErrorWidgetComponent', () => {
let widget: ErrorWidgetComponent;
let fixture: ComponentFixture<ErrorWidgetComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(ErrorWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
const errorMessage: string = 'fake-error';
const errorMessageModel: ErrorMessageModel = new ErrorMessageModel({message: errorMessage});
const errorChanges: SimpleChanges = {
error: new SimpleChange(errorMessageModel, errorMessageModel, false)
};
it('should display proper error icon', async () => {
widget.ngOnChanges(errorChanges);
await fixture.whenStable();
fixture.detectChanges();
const errorIcon = element.querySelector('.adf-error-icon').textContent;
expect(errorIcon).toEqual('error_outline');
});
it('should set subscriptAnimationState value', () => {
widget.ngOnChanges(errorChanges);
expect(widget.subscriptAnimationState).toEqual('enter');
});
it('should check proper error message', async () => {
widget.ngOnChanges(errorChanges);
await fixture.whenStable();
fixture.detectChanges();
const requiredErrorText = element.querySelector('.adf-error-text').textContent;
expect(requiredErrorText).toEqual(errorMessage);
});
});

View File

@@ -59,7 +59,7 @@ export class ErrorWidgetComponent extends WidgetComponent implements OnChanges {
translateParameters: any = null;
_subscriptAnimationState: string = '';
subscriptAnimationState: string = '';
constructor(public formService: FormService) {
super(formService);
@@ -68,13 +68,13 @@ export class ErrorWidgetComponent extends WidgetComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges) {
if (changes['required']) {
this.required = changes.required.currentValue;
this._subscriptAnimationState = 'enter';
this.subscriptAnimationState = 'enter';
}
if (changes['error'] && changes['error'].currentValue) {
if (changes.error.currentValue.isActive()) {
this.error = changes.error.currentValue;
this.translateParameters = this.error.getAttributesAsJsonObj();
this._subscriptAnimationState = 'enter';
this.subscriptAnimationState = 'enter';
}
}
}

View File

@@ -1,6 +1,6 @@
<div class="adf-file-viewer-widget {{field.className}}" [class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<adf-viewer [overlayMode]="false" [nodeId]="field.value" [showViewer]="field.value" [allowGoBack]="false"></adf-viewer>
<error-widget [error]="field.validationSummary"></error-widget>

View File

@@ -5,8 +5,12 @@ ul > li > form-field > .adf-focus {
}
}
.mat-form-field-label {
color: var(--theme-colors-mat-grey-dark) !important;
}
.adf {
&-error-text-container {
&-error-container {
height: 20px;
margin-top: -12px;
}
@@ -16,40 +20,35 @@ ul > li > form-field > .adf-focus {
height: 16px;
font-size: var(--theme-caption-font-size);
line-height: 1.33;
float: left;
color: var(--theme-warn-color);
}
&-error-icon {
float: right;
font-size: var(--theme-adf-icon-1-font-size);
color: var(--theme-warn-color);
}
&-label {
color: rgb(186, 186, 186);
color: var(--theme-secondary-text-color);
}
&-asterisk {
padding-left: 2px;
color: var(--theme-warn-color);
}
&-invalid {
.mat-form-field-underline {
background-color: #f44336 !important;
.mat-checkbox-layout {
padding-bottom: 12px;
}
.mat-checkbox {
color: var(--theme-warn-color);
.mat-checkbox-frame {
border-color: var(--theme-warn-color);
}
.mat-form-field-underline {
background-color: var(--theme-warn-color) !important;
}
.mat-select {
&-value {
color: var(--theme-warn-color);
}
&-arrow {
color: var(--theme-warn-color);
color: var(--theme-secondary-text-color) !important;
}
}
@@ -58,20 +57,12 @@ ul > li > form-field > .adf-focus {
}
.mat-form-field-prefix {
color: var(--theme-warn-color);
color: var(--theme-secondary-text-color);
}
.adf-input {
border-color: var(--theme-warn-color);
}
.adf-label {
color: var(--theme-warn-color);
&::after {
background-color: var(--theme-warn-color);
}
}
}
}

View File

@@ -1,11 +1,11 @@
<div class="adf-group-widget {{field.className}}"
[class.is-dirty]="!!field.value"
[class.adf-invalid]="!field.isValid"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly"
id="functional-group-div">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
class="adf-input"
type="text"
@@ -13,6 +13,7 @@
[id]="field.id"
[formControl]="searchTerm"
[placeholder]="field.placeholder"
(blur)="markAsTouched()"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="updateOption($event.option.value)" [displayWith]="getDisplayName">
<mat-option *ngFor="let item of groups$ | async; let i = index"
@@ -25,5 +26,5 @@
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -24,12 +24,14 @@ import { FunctionalGroupWidgetComponent } from './functional-group.widget';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CoreTestingModule, setupTestBed } from '../../../../testing';
import { TranslateModule } from '@ngx-translate/core';
import { FormFieldTypes } from '../core/form-field-types';
describe('FunctionalGroupWidgetComponent', () => {
let fixture: ComponentFixture<FunctionalGroupWidgetComponent>;
let component: FunctionalGroupWidgetComponent;
let formService: FormService;
let getWorkflowGroupsSpy: jasmine.Spy;
let element: HTMLElement;
const groups: GroupModel[] = [
{ id: '1', name: 'group 1' },
{ id: '2', name: 'group 2' }
@@ -49,6 +51,7 @@ describe('FunctionalGroupWidgetComponent', () => {
fixture = TestBed.createComponent(FunctionalGroupWidgetComponent);
component = fixture.componentInstance;
component.field = new FormFieldModel(new FormModel());
element = fixture.nativeElement;
fixture.detectChanges();
});
@@ -148,4 +151,36 @@ describe('FunctionalGroupWidgetComponent', () => {
await typeIntoInput('123');
expect(getWorkflowGroupsSpy).not.toHaveBeenCalled();
});
describe('when is required', () => {
beforeEach(() => {
component.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.FUNCTIONAL_GROUP,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const functionalGroupInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
functionalGroupInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

View File

@@ -1,5 +1,5 @@
<div class="adf-hyperlink-widget {{field.className}}">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk"
*ngIf="isRequired()">*</span></label>
<div [matTooltip]="field.tooltip" matTooltipPosition="above" matTooltipShowDelay="1000">
<a [href]="linkUrl" target="_blank" rel="nofollow">{{linkText}}</a>

View File

@@ -1,7 +1,7 @@
<div class="adf-multiline-text-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<mat-form-field floatPlaceholder="never" [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<textarea matInput class="adf-input"
[matTextareaAutosize]="true"
type="text"
@@ -13,6 +13,7 @@
[disabled]="field.readOnly || readOnly"
[placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000">
</textarea>
@@ -21,6 +22,7 @@
<span>{{field?.value?.length || 0}}/{{field.maxLength}}</span>
</div>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget class="adf-multiline-required-message" *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget class="adf-multiline-required-message" *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}">
</error-widget>
</div>

View File

@@ -15,17 +15,67 @@
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { MultilineTextWidgetComponentComponent } from './multiline-text.widget';
import { CoreTestingModule } from '../../../../testing/core.testing.module';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { FormFieldTypes } from '../core/form-field-types';
describe('MultilineTextWidgetComponentComponent', () => {
let widget: MultilineTextWidgetComponentComponent;
let fixture: ComponentFixture<MultilineTextWidgetComponentComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
TranslateModule.forRoot(),
CoreTestingModule
]
});
beforeEach(() => {
widget = new MultilineTextWidgetComponentComponent(null);
fixture = TestBed.createComponent(MultilineTextWidgetComponentComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
it('should exist', () => {
expect(widget).toBeDefined();
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.MULTILINE_TEXT,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const multilineTextarea = fixture.nativeElement.querySelector('textarea');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
multilineTextarea.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

View File

@@ -1,7 +1,7 @@
<div class="adf-textfield adf-number-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<mat-form-field [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
class="adf-input"
type="text"
@@ -14,9 +14,10 @@
[disabled]="field.readOnly"
[placeholder]="field.placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000">
</mat-form-field>
<error-widget [error]="field.validationSummary" ></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,9 +1,9 @@
<div class="adf-people-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-invalid]="!field.isValid && isTouched()"
[class.adf-readonly]="field.readOnly"
id="people-widget-content">
<mat-form-field>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input #inputValue
matInput
class="adf-input"
@@ -13,6 +13,7 @@
[formControl]="searchTerm"
[placeholder]="field.placeholder"
[matAutocomplete]="auto"
(blur)="markAsTouched()"
[matTooltip]="field.tooltip"
matTooltipPosition="above"
matTooltipShowDelay="1000">
@@ -33,5 +34,5 @@
</mat-autocomplete>
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -155,6 +155,38 @@ describe('PeopleWidgetComponent', () => {
});
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.PEOPLE,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const peopleInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
peopleInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
describe('when template is ready', () => {
const fakeUserResult = [

View File

@@ -1,7 +1,7 @@
<div class="adf-radio-buttons-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly" [id]="field.id">
[class.adf-readonly]="field.readOnly" [id]="field.id">
<div class="adf-radio-button-container">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<mat-radio-group class="adf-radio-group" [(ngModel)]="field.value" [disabled]="field.readOnly">
<mat-radio-button
[matTooltip]="field.tooltip"
@@ -19,5 +19,4 @@
</mat-radio-group>
</div>
<error-widget [error]="field.validationSummary" ></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -1,7 +1,7 @@
<div class="adf-textfield adf-text-widget {{field.className}}"
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
[class.adf-invalid]="!field.isValid && isTouched()" [class.adf-readonly]="field.readOnly">
<mat-form-field [hideRequiredMarker]="true">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<input matInput
class="adf-input"
type="text"
@@ -14,9 +14,10 @@
[textMask]="{mask: mask, isReversed: isMaskReversed}"
[placeholder]="placeholder"
[matTooltip]="field.tooltip"
(blur)="markAsTouched()"
matTooltipPosition="above"
matTooltipShowDelay="1000">
</mat-form-field>
<error-widget [error]="field.validationSummary"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget *ngIf="isInvalidFieldRequired() && isTouched()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
</div>

View File

@@ -93,33 +93,6 @@ describe('TextWidgetComponent', () => {
expect(textWidgetLabel.innerText).toBe('text-name');
});
it('should be able to set a Text Widget as required', async () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'text-id',
name: 'text-name',
value: '',
type: FormFieldTypes.TEXT,
readOnly: false,
required: true
});
fixture.detectChanges();
const textWidgetLabel = element.querySelector('label');
expect(textWidgetLabel.innerText).toBe('text-name*');
expect(widget.field.isValid).toBe(false);
enterValueInTextField(element.querySelector('#text-id'), 'TEXT');
await fixture.whenStable();
fixture.detectChanges();
expect(widget.field.isValid).toBe(true);
enterValueInTextField(element.querySelector('#text-id'), '');
await fixture.whenStable();
fixture.detectChanges();
expect(widget.field.isValid).toBe(false);
});
it('should be able to set a placeholder for Text widget', async () => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'text-id',
@@ -222,6 +195,42 @@ describe('TextWidgetComponent', () => {
});
});
describe('when is required', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), {
id: 'text-id',
name: 'text-name',
value: '',
type: FormFieldTypes.TEXT,
readOnly: false,
required: true
});
});
it('should be marked as invalid after interaction', async () => {
const textInput = fixture.nativeElement.querySelector('input');
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeFalsy();
textInput.dispatchEvent(new Event('blur'));
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.querySelector('.adf-invalid')).toBeTruthy();
});
it('should be able to display label with asterisk', async () => {
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
describe('and no mask is configured on text element', () => {
let inputElement: HTMLInputElement;

View File

@@ -1,7 +1,7 @@
<div class="adf-upload-folder-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
</div>
</div>

View File

@@ -14,3 +14,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { setupTestBed } from '../../../../testing/setup-test-bed';
import { CoreTestingModule } from '../../../../testing';
import { UploadFolderWidgetComponent } from './upload-folder.widget';
import { FormFieldModel } from '../core/form-field.model';
import { FormModel } from '../core/form.model';
import { FormFieldTypes } from '../core/form-field-types';
describe('UploadFolderWidgetComponent', () => {
let widget: UploadFolderWidgetComponent;
let fixture: ComponentFixture<UploadFolderWidgetComponent>;
let element: HTMLElement;
setupTestBed({
imports: [
CoreTestingModule
]
});
beforeEach(() => {
fixture = TestBed.createComponent(UploadFolderWidgetComponent);
widget = fixture.componentInstance;
element = fixture.nativeElement;
});
describe('when is required', () => {
it('should be able to display label with asterisk', async () => {
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
type: FormFieldTypes.UPLOAD,
required: true
});
fixture.detectChanges();
await fixture.whenStable();
const asterisk: HTMLElement = element.querySelector('.adf-asterisk');
expect(asterisk).toBeTruthy();
expect(asterisk.textContent).toEqual('*');
});
});
});

View File

@@ -1,7 +1,7 @@
<div class="adf-upload-widget {{field.className}}"
[class.adf-invalid]="!field.isValid"
[class.adf-readonly]="field.readOnly">
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span *ngIf="isRequired()">*</span></label>
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" *ngIf="isRequired()">*</span></label>
<div class="adf-upload-widget-container">
<div>
<mat-list *ngIf="hasFile">

View File

@@ -56,6 +56,8 @@ export class WidgetComponent implements AfterViewInit {
@Output()
fieldChanged: EventEmitter<FormFieldModel> = new EventEmitter<FormFieldModel>();
touched: boolean = false;
constructor(public formService?: FormService) {
}
@@ -76,6 +78,10 @@ export class WidgetComponent implements AfterViewInit {
return !!this.field.validationSummary;
}
isTouched(): boolean {
return this.touched;
}
hasValue(): boolean {
return this.field &&
this.field.value !== null &&
@@ -83,7 +89,7 @@ export class WidgetComponent implements AfterViewInit {
}
isInvalidFieldRequired() {
return !this.field.isValid && !this.field.validationSummary && this.isRequired();
return !this.field.isValid && (!this.field.validationSummary || !this.field.value) && this.isRequired();
}
ngAfterViewInit() {
@@ -101,4 +107,8 @@ export class WidgetComponent implements AfterViewInit {
event(event: Event): void {
this.formService.formEvents.next(event);
}
markAsTouched() {
this.touched = true;
}
}

View File

@@ -41,7 +41,7 @@
"DOWNLOAD_FILE": "Download",
"REMOVE_FILE": "Remove",
"UPLOAD": "UPLOAD",
"REQUIRED": "*Required",
"REQUIRED": "This is a required field",
"REST_API_FAILED": "The server `{{ hostname }}` is not reachable",
"FILE_NAME": "File Name",
"NO_FILE_ATTACHED": "No file attached",