mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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:
@@ -1,10 +1,10 @@
|
||||
<div class="adf-attach-file-widget-container">
|
||||
<div class="adf-attach-widget {{field.className}}" [class.adf-invalid]="!field.isValid"
|
||||
<div class="adf-attach-widget {{field.className}}"
|
||||
[class.adf-readonly]="field.readOnly">
|
||||
<label class="adf-label" [attr.for]="field.id">{{field.name}}
|
||||
<span *ngIf="isRequired()">*</span>
|
||||
<span class="adf-asterisk" *ngIf="isRequired()">*</span>
|
||||
</label>
|
||||
<div class="adf-attach-widget-container">
|
||||
<div class="adf-attach-widget-container" (focusout)="markAsTouched()">
|
||||
<div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible()">
|
||||
<button (click)="openSelectDialog()" mat-raised-button color="primary" [id]="field.id"
|
||||
[matTooltip]="field.tooltip" matTooltipPosition="above" matTooltipShowDelay="1000">
|
||||
@@ -36,5 +36,5 @@
|
||||
</div>
|
||||
|
||||
<error-widget [error]="field.validationSummary"></error-widget>
|
||||
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
<error-widget *ngIf="!field.isValid && isTouched() && !isSelected()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
</div>
|
||||
|
@@ -73,6 +73,7 @@
|
||||
border-bottom: none;
|
||||
background: var(--theme-colors-mat-grey);
|
||||
min-height: 27px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.adf-label {
|
||||
width: 32px;
|
||||
|
@@ -201,6 +201,24 @@ describe('AttachFileCloudWidgetComponent', () => {
|
||||
expect(contentNodeSelectorPanelService.customModels).toEqual([]);
|
||||
});
|
||||
|
||||
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('*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upload widget with displayable ContentModel properties', () => {
|
||||
|
||||
it('should display CM Properties if the file contains value', async () => {
|
||||
|
@@ -255,6 +255,10 @@ export class AttachFileCloudWidgetComponent extends UploadCloudWidgetComponent i
|
||||
return alias && VALID_ALIAS.includes(alias);
|
||||
}
|
||||
|
||||
isSelected(): boolean {
|
||||
return this.hasFile;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.contentNodeSelectorPanelService.customModels = [];
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<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
|
||||
<label class="adf-label" [attr.for]="field.id">{{ field.name | translate }}<span class="adf-asterisk"
|
||||
*ngIf="isRequired()">*</span></label>
|
||||
<div class="adf-cloud-upload-widget-container">
|
||||
<div>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<div class="{{field.className}}" id="data-widget" [class.adf-invalid]="!field.isValid">
|
||||
<mat-form-field class="adf-date-widget">
|
||||
<label class="adf-label" [attr.for]="field.id">{{field.name | translate }} ({{field.dateDisplayFormat}})<span *ngIf="isRequired()">*</span></label>
|
||||
<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 class="adf-asterisk"
|
||||
*ngIf="isRequired()">*</span></label>
|
||||
<input matInput
|
||||
[id]="field.id"
|
||||
[value]="field.value"
|
||||
@@ -9,12 +10,13 @@
|
||||
(change)="onDateChanged($any($event).srcElement.value)"
|
||||
[placeholder]="field.placeholder"
|
||||
[matTooltip]="field.tooltip"
|
||||
(blur)="markAsTouched()"
|
||||
matTooltipPosition="above"
|
||||
matTooltipShowDelay="1000">
|
||||
<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"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="!field.isValid" [class.adf-readonly]="field.readOnly">
|
||||
[class.adf-invalid]="(!field.isValid && isTouched()) || isRestApiFailed" [class.adf-readonly]="field.readOnly">
|
||||
<div class="adf-dropdown-widget-top-labels">
|
||||
<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>
|
||||
@@ -13,7 +13,9 @@
|
||||
[compareWith]="compareDropdownValues"
|
||||
(ngModelChange)="selectionChangedForField(field)"
|
||||
[matTooltip]="field.tooltip"
|
||||
[required]="isRequired()"
|
||||
panelClass="adf-select-filter"
|
||||
(blur)="markAsTouched()"
|
||||
matTooltipPosition="above"
|
||||
matTooltipShowDelay="1000"
|
||||
[multiple]="field.hasMultipleValues">
|
||||
@@ -27,7 +29,7 @@
|
||||
</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>
|
||||
<error-widget class="adf-dropdown-failed-message" *ngIf="isRestApiFailed"
|
||||
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"></error-widget>
|
||||
|
@@ -16,11 +16,11 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-dropdown-required-message .adf-error-text-container {
|
||||
&-dropdown-required-message .adf-error-container {
|
||||
margin-top: 1px !important;
|
||||
}
|
||||
|
||||
&-dropdown-failed-message .adf-error-text-container {
|
||||
&-dropdown-failed-message .adf-error-container {
|
||||
margin-top: 1px !important;
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
|
||||
import { FormFieldModel, FormModel, FormService, setupTestBed, FormFieldEvent } from '@alfresco/adf-core';
|
||||
import { FormFieldModel, FormModel, FormService, setupTestBed, FormFieldEvent, FormFieldTypes } from '@alfresco/adf-core';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -160,6 +160,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
it('should show error message if the restUrl failed to fetch options', async () => {
|
||||
const jsonDataSpy = spyOn(formCloudService, 'getRestWidgetData').and.returnValue(throwError('Failed to fetch options'));
|
||||
const errorIcon: string = 'error_outline';
|
||||
widget.field.restUrl = 'https://fake-rest-url';
|
||||
widget.field.optionType = 'rest';
|
||||
widget.field.restIdProperty = 'name';
|
||||
@@ -173,11 +174,10 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
|
||||
|
||||
expect(jsonDataSpy).toHaveBeenCalled();
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
expect(failedErrorMsgElement.nativeElement.innerText.trim()).toBe('FORM.FIELD.REST_API_FAILED');
|
||||
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
|
||||
});
|
||||
|
||||
it('should preselect dropdown widget value when Json (rest call) passed', async () => {
|
||||
@@ -234,6 +234,41 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
|
||||
type: FormFieldTypes.DROPDOWN,
|
||||
required: true
|
||||
});
|
||||
});
|
||||
|
||||
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 invalid if no default option after interaction', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
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')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -391,6 +426,7 @@ 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]);
|
||||
|
||||
@@ -415,7 +451,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
expect(widget.isRestApiFailed).toBe(true);
|
||||
expect(widget.field.options.length).toBe(0);
|
||||
expect(failedErrorMsgElement2.nativeElement.innerText.trim()).toBe('FORM.FIELD.REST_API_FAILED');
|
||||
expect(failedErrorMsgElement2.nativeElement.textContent.trim()).toBe(errorIcon + 'FORM.FIELD.REST_API_FAILED');
|
||||
|
||||
jsonDataSpy.and.returnValue(of(mockSecondRestDropdownOptions));
|
||||
selectParentOption('IT');
|
||||
|
@@ -310,4 +310,8 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
return this.field?.restUrl;
|
||||
}
|
||||
}
|
||||
|
||||
showRequiredMessage(): boolean {
|
||||
return (this.isInvalidFieldRequired() || this.field.value === 'empty') && this.isTouched() && !this.isRestApiFailed;
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,22 @@
|
||||
<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
|
||||
[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>
|
||||
<adf-cloud-group [mode]="mode"
|
||||
[title]="title"
|
||||
[readOnly]="field.readOnly"
|
||||
[roles]="roles"
|
||||
[searchGroupsControl]="search"
|
||||
[required]="isRequired()"
|
||||
(changedGroups)="onChangedGroup($event)"
|
||||
[preSelectGroups]="preSelectGroup"
|
||||
(blur)="markAsTouched()"
|
||||
[matTooltip]="field.tooltip"
|
||||
[matTooltipPosition]="'above'"
|
||||
[matTooltipShowDelay]="1000">
|
||||
</adf-cloud-group>
|
||||
<error-widget [error]="field.validationSummary"></error-widget>
|
||||
<error-widget class="adf-dropdown-required-message" *ngIf="isInvalidFieldRequired()"
|
||||
<error-widget class="adf-dropdown-required-message" *ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
</div>
|
||||
|
||||
|
@@ -0,0 +1,83 @@
|
||||
/*!
|
||||
* @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 { FormFieldModel, FormFieldTypes, FormModel, setupTestBed } from '@alfresco/adf-core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { GroupCloudWidgetComponent } from './group-cloud.widget';
|
||||
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('GroupCloudWidgetComponent', () => {
|
||||
let fixture: ComponentFixture<GroupCloudWidgetComponent>;
|
||||
let widget: GroupCloudWidgetComponent;
|
||||
let element: HTMLElement;
|
||||
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
ProcessServiceCloudTestingModule
|
||||
],
|
||||
declarations: [
|
||||
GroupCloudWidgetComponent
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GroupCloudWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
|
||||
type: FormFieldTypes.GROUP,
|
||||
required: true
|
||||
});
|
||||
});
|
||||
|
||||
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 invalid if no default option after interaction', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeFalsy();
|
||||
|
||||
const cloudGroupInput = element.querySelector('adf-cloud-group');
|
||||
cloudGroupInput.dispatchEvent(new Event('blur'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,6 +1,6 @@
|
||||
<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>
|
||||
<adf-cloud-people
|
||||
[preSelectUsers]="preSelectUsers"
|
||||
[validate]="true"
|
||||
@@ -8,13 +8,17 @@
|
||||
[title]="title"
|
||||
[readOnly]="field.readOnly"
|
||||
[searchUserCtrl]="search"
|
||||
[required]="isRequired()"
|
||||
(changedUsers)="onChangedUser($event)"
|
||||
[roles]="roles"
|
||||
[mode]="mode"
|
||||
(blur)="markAsTouched()"
|
||||
[matTooltip]="field.tooltip"
|
||||
matTooltipPosition="above"
|
||||
matTooltipShowDelay="1000">
|
||||
</adf-cloud-people>
|
||||
<error-widget [error]="field.validationSummary"></error-widget>
|
||||
<error-widget class="adf-dropdown-required-message" *ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
|
||||
</div>
|
||||
|
||||
|
@@ -15,15 +15,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CoreTestingModule, FormFieldModel, FormModel, IdentityUserService, setupTestBed } from '@alfresco/adf-core';
|
||||
import { FormFieldModel, FormFieldTypes, FormModel, IdentityUserService, setupTestBed } from '@alfresco/adf-core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { PeopleCloudWidgetComponent } from './people-cloud.widget';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
|
||||
|
||||
describe('PeopleCloudWidgetComponent', () => {
|
||||
let fixture: ComponentFixture<PeopleCloudWidgetComponent>;
|
||||
let widget: PeopleCloudWidgetComponent;
|
||||
let element: HTMLElement;
|
||||
let identityUserService: IdentityUserService;
|
||||
const currentUser = { id: 'id', username: 'user' };
|
||||
const fakeUser = { id: 'fake-id', username: 'fake' };
|
||||
@@ -31,7 +33,7 @@ describe('PeopleCloudWidgetComponent', () => {
|
||||
setupTestBed({
|
||||
imports: [
|
||||
TranslateModule.forRoot(),
|
||||
CoreTestingModule
|
||||
ProcessServiceCloudTestingModule
|
||||
],
|
||||
declarations: [
|
||||
PeopleCloudWidgetComponent
|
||||
@@ -45,6 +47,7 @@ describe('PeopleCloudWidgetComponent', () => {
|
||||
identityUserService = TestBed.inject(IdentityUserService);
|
||||
fixture = TestBed.createComponent(PeopleCloudWidgetComponent);
|
||||
widget = fixture.componentInstance;
|
||||
element = fixture.nativeElement;
|
||||
spyOn(identityUserService, 'getCurrentUserInfo').and.returnValue(fakeUser);
|
||||
});
|
||||
|
||||
@@ -61,4 +64,39 @@ describe('PeopleCloudWidgetComponent', () => {
|
||||
expect(widget.preSelectUsers).toEqual([currentUser]);
|
||||
expect(identityUserService.getCurrentUserInfo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel( new FormModel({ taskId: '<id>' }), {
|
||||
type: FormFieldTypes.PEOPLE,
|
||||
required: true
|
||||
});
|
||||
});
|
||||
|
||||
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 invalid if no default option after interaction', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeFalsy();
|
||||
|
||||
const cloudPeopleInput = element.querySelector('adf-cloud-people');
|
||||
cloudPeopleInput.dispatchEvent(new Event('blur'));
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-invalid')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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>
|
||||
<ng-template #properties [ngTemplateOutlet]="properties" let-properties="properties" [ngTemplateOutletContext]="{ properties: field.params?.propertiesViewerOptions }">
|
||||
<adf-properties-viewer-wrapper *ngIf="field.value" [nodeId]="field.value"
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="adf-radio-buttons-widget-cloud {{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>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<form>
|
||||
<mat-form-field class="adf-cloud-group">
|
||||
<mat-form-field class="adf-cloud-group" [class.adf-invalid]="hasError() && isDirty()">
|
||||
<mat-label *ngIf="!isReadonly()"
|
||||
id="adf-group-cloud-title-id">{{ (title || 'ADF_CLOUD_GROUPS.SEARCH-GROUP') | translate }}</mat-label>
|
||||
<mat-chip-list #groupChipList [disabled]="isReadonly() || isValidationLoading()" data-automation-id="adf-cloud-group-chip-list">
|
||||
@@ -20,8 +20,9 @@
|
||||
[formControl]="searchGroupsControl"
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="groupChipList"
|
||||
[required]="required"
|
||||
(focus)="setFocus(true)"
|
||||
(blur)="setFocus(false)"
|
||||
(blur)="setFocus(false); markAsTouched()"
|
||||
class="adf-group-input"
|
||||
data-automation-id="adf-cloud-group-search-input" #groupInput>
|
||||
</mat-chip-list>
|
||||
@@ -58,23 +59,32 @@
|
||||
mode="indeterminate">
|
||||
</mat-progress-bar>
|
||||
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : validateGroupsMessage } }}</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('pattern')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('maxlength')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('minlength')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('searchTypingError') && !this.isFocused" data-automation-id="invalid-groups-typing-error">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }}</mat-error>
|
||||
<div class="adf-error-container">
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : validateGroupsMessage } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('pattern')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('maxlength')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('minlength')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="(searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()"
|
||||
[@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('searchTypingError') && !this.isFocused"
|
||||
data-automation-id="invalid-groups-typing-error" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate : { groupName : searchedValue } }}</div>
|
||||
</mat-error>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -219,6 +219,26 @@ describe('GroupCloudComponent', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display proper error icon', (done) => {
|
||||
findGroupsByNameSpy.and.returnValue(of([]));
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = getElement<HTMLInputElement>('input');
|
||||
input.focus();
|
||||
input.value = 'ZZZ';
|
||||
input.dispatchEvent(new Event('keyup'));
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
input.blur();
|
||||
fixture.detectChanges();
|
||||
const errorIcon = element.querySelector('.adf-error-icon').textContent;
|
||||
expect(errorIcon).toEqual('error_outline');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when application name defined', () => {
|
||||
|
@@ -83,6 +83,11 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
readOnly = false;
|
||||
|
||||
/** Mark this field as required
|
||||
*/
|
||||
@Input()
|
||||
required = false;
|
||||
|
||||
/** FormControl to list of group */
|
||||
@Input()
|
||||
groupChipsCtrl = new FormControl({ value: '', disabled: false });
|
||||
@@ -121,9 +126,10 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
invalidGroups: IdentityGroupModel[] = [];
|
||||
|
||||
searchGroups$ = new BehaviorSubject<IdentityGroupModel[]>(this.searchGroups);
|
||||
_subscriptAnimationState = 'enter';
|
||||
subscriptAnimationState: string = 'enter';
|
||||
clientId: string;
|
||||
isFocused: boolean;
|
||||
touched: boolean = false;
|
||||
|
||||
validateGroupsMessage: string;
|
||||
searchedValue = '';
|
||||
@@ -472,6 +478,22 @@ export class GroupCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.isValidationEnabled() && this.validationLoading;
|
||||
}
|
||||
|
||||
markAsTouched(): void {
|
||||
this.touched = true;
|
||||
}
|
||||
|
||||
isTouched(): boolean {
|
||||
return this.touched;
|
||||
}
|
||||
|
||||
isSelected(): boolean {
|
||||
return this.selectedGroups && !!this.selectedGroups.length;
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.isTouched() && !this.isSelected();
|
||||
}
|
||||
|
||||
setFocus(isFocused: boolean) {
|
||||
this.isFocused = isFocused;
|
||||
}
|
||||
|
@@ -302,7 +302,7 @@
|
||||
"INVALID_PATTERN": "Must match the pattern {{pattern}}",
|
||||
"INVALID_MIN_LENGTH": "Must be at least {{requiredLength}} characters",
|
||||
"INVALID_MAX_LENGTH": "Length exceeded, {{requiredLength}} characters max",
|
||||
"REQUIRED": "Field required"
|
||||
"REQUIRED": "This is a required field"
|
||||
}
|
||||
},
|
||||
"ADF_CLOUD_TASK_HEADER": {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<form>
|
||||
<mat-form-field [floatLabel]="'auto'" class="adf-people-cloud">
|
||||
<mat-form-field [floatLabel]="'auto'" class="adf-people-cloud" [class.adf-invalid]="hasError() && isDirty()">
|
||||
<mat-label *ngIf="!isReadonly()" id="adf-people-cloud-title-id">{{ title | translate }}</mat-label>
|
||||
<mat-chip-list #userMultipleChipList [disabled]="isReadonly() || isValidationLoading()" data-automation-id="adf-cloud-people-chip-list">
|
||||
<mat-chip
|
||||
@@ -20,8 +20,9 @@
|
||||
[formControl]="searchUserCtrl"
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="userMultipleChipList"
|
||||
[required]="required"
|
||||
(focus)="setFocus(true)"
|
||||
(blur)="setFocus(false)"
|
||||
(blur)="setFocus(false); markAsTouched()"
|
||||
class="adf-cloud-input"
|
||||
data-automation-id="adf-people-cloud-search-input" #userInput>
|
||||
</mat-chip-list>
|
||||
@@ -53,23 +54,32 @@
|
||||
mode="indeterminate">
|
||||
</mat-progress-bar>
|
||||
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : validateUsersMessage } }}</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('pattern')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('maxlength')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('minlength')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }} </mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('searchTypingError') && !this.isFocused" data-automation-id="invalid-users-typing-error">
|
||||
<mat-icon class="adf-start-task-cloud-error-icon">warning</mat-icon>
|
||||
{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : searchedValue } }}</mat-error>
|
||||
<div class="adf-error-container">
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : validateUsersMessage } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('pattern')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('maxlength')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('minlength')" [@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="(searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()"
|
||||
[@transitionMessages]="subscriptAnimationState" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('searchTypingError') && !this.isFocused"
|
||||
[@transitionMessages]="subscriptAnimationState" data-automation-id="invalid-users-typing-error" class="adf-error">
|
||||
<mat-icon class="adf-error-icon">error_outline</mat-icon>
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate : { userName : searchedValue } }}</div>
|
||||
</mat-error>
|
||||
</div>
|
||||
</form>
|
||||
|
@@ -327,6 +327,26 @@ describe('PeopleCloudComponent', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display proper error icon', (done) => {
|
||||
findUsersByNameSpy.and.returnValue(of([]));
|
||||
fixture.detectChanges();
|
||||
|
||||
const input = getElement<HTMLInputElement>('input');
|
||||
input.focus();
|
||||
input.value = 'ZZZ';
|
||||
input.dispatchEvent(new Event('keyup'));
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
input.blur();
|
||||
fixture.detectChanges();
|
||||
const errorIcon = element.querySelector('.adf-error-icon').textContent;
|
||||
expect(errorIcon).toEqual('error_outline');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when application name defined', () => {
|
||||
|
@@ -84,6 +84,11 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
readOnly: boolean = false;
|
||||
|
||||
/** Mark this field as required
|
||||
*/
|
||||
@Input()
|
||||
required = false;
|
||||
|
||||
/** Array of users to be pre-selected. All users in the
|
||||
* array are pre-selected in multi selection mode, but only the first user
|
||||
* is pre-selected in single selection mode.
|
||||
@@ -137,9 +142,10 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
invalidUsers: IdentityUserModel[] = [];
|
||||
|
||||
searchUsers$ = new BehaviorSubject<IdentityUserModel[]>(this._searchUsers);
|
||||
_subscriptAnimationState: string = 'enter';
|
||||
subscriptAnimationState: string = 'enter';
|
||||
clientId: string;
|
||||
isFocused: boolean;
|
||||
touched: boolean = false;
|
||||
|
||||
validateUsersMessage: string;
|
||||
searchedValue = '';
|
||||
@@ -535,6 +541,22 @@ export class PeopleCloudComponent implements OnInit, OnChanges, OnDestroy {
|
||||
return this.isValidationEnabled() && this.validationLoading;
|
||||
}
|
||||
|
||||
markAsTouched(): void {
|
||||
this.touched = true;
|
||||
}
|
||||
|
||||
isTouched(): boolean {
|
||||
return this.touched;
|
||||
}
|
||||
|
||||
isSelected(): boolean {
|
||||
return this.selectedUsers && !!this.selectedUsers.length;
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.isTouched() && !this.isSelected();
|
||||
}
|
||||
|
||||
setFocus(isFocused: boolean) {
|
||||
this.isFocused = isFocused;
|
||||
}
|
||||
|
@@ -25,7 +25,11 @@ import {
|
||||
AppConfigServiceMock,
|
||||
TranslationService,
|
||||
TranslationMock,
|
||||
CoreModule
|
||||
CoreModule,
|
||||
IdentityUserService,
|
||||
IdentityUserServiceMock,
|
||||
IdentityGroupService,
|
||||
IdentityGroupServiceMock
|
||||
} from '@alfresco/adf-core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ProcessServicesCloudModule } from '../process-services-cloud.module';
|
||||
@@ -43,7 +47,9 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
providers: [
|
||||
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
|
||||
{ provide: AppConfigService, useClass: AppConfigServiceMock },
|
||||
{ provide: TranslationService, useClass: TranslationMock }
|
||||
{ provide: TranslationService, useClass: TranslationMock },
|
||||
{ provide: IdentityUserService, useClass: IdentityUserServiceMock },
|
||||
{ provide: IdentityGroupService, useClass: IdentityGroupServiceMock }
|
||||
],
|
||||
exports: [
|
||||
NoopAnimationsModule,
|
||||
|
Reference in New Issue
Block a user