diff --git a/lib/core/form/components/widgets/functional-group/functional-group.widget.html b/lib/core/form/components/widgets/functional-group/functional-group.widget.html index 6ace4cb2aa..9e044c6b75 100644 --- a/lib/core/form/components/widgets/functional-group/functional-group.widget.html +++ b/lib/core/form/components/widgets/functional-group/functional-group.widget.html @@ -1,6 +1,9 @@
+ [class.is-dirty]="!!field.value" + [class.adf-invalid]="!field.isValid" + [class.adf-readonly]="field.readOnly" + id="functional-group-div"> + - - + + [value]="item"> {{item.name}} diff --git a/lib/core/form/components/widgets/functional-group/functional-group.widget.spec.ts b/lib/core/form/components/widgets/functional-group/functional-group.widget.spec.ts index cb179b0711..76c56b998d 100644 --- a/lib/core/form/components/widgets/functional-group/functional-group.widget.spec.ts +++ b/lib/core/form/components/widgets/functional-group/functional-group.widget.spec.ts @@ -15,23 +15,25 @@ * limitations under the License. */ -import { ElementRef } from '@angular/core'; -import { Observable } from 'rxjs'; +import { of, timer } from 'rxjs'; import { FormService } from '../../../services/form.service'; import { FormFieldModel } from '../core/form-field.model'; import { FormModel } from '../core/form.model'; import { GroupModel } from '../core/group.model'; import { FunctionalGroupWidgetComponent } from './functional-group.widget'; -import { AlfrescoApiService } from '../../../../services'; -import { TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CoreTestingModule, setupTestBed } from '../../../../testing'; import { TranslateModule } from '@ngx-translate/core'; describe('FunctionalGroupWidgetComponent', () => { + let fixture: ComponentFixture; + let component: FunctionalGroupWidgetComponent; let formService: FormService; - let elementRef: ElementRef; - let widget: FunctionalGroupWidgetComponent; - let alfrescoApiService: AlfrescoApiService; + let getWorkflowGroupsSpy: jasmine.Spy; + const groups: GroupModel[] = [ + { id: '1', name: 'group 1' }, + { id: '2', name: 'group 2' } + ]; setupTestBed({ imports: [ @@ -41,170 +43,109 @@ describe('FunctionalGroupWidgetComponent', () => { }); beforeEach(() => { - alfrescoApiService = TestBed.inject(AlfrescoApiService); + formService = TestBed.inject(FormService); + getWorkflowGroupsSpy = spyOn(formService, 'getWorkflowGroups').and.returnValue(of([])); - formService = new FormService(null, alfrescoApiService, null); - elementRef = new ElementRef(null); - widget = new FunctionalGroupWidgetComponent(formService, elementRef); - widget.field = new FormFieldModel(new FormModel()); + fixture = TestBed.createComponent(FunctionalGroupWidgetComponent); + component = fixture.componentInstance; + component.field = new FormFieldModel(new FormModel()); + fixture.detectChanges(); }); - it('should setup text from underlying field on init', () => { + afterEach(() => { + getWorkflowGroupsSpy.calls.reset(); + fixture.destroy(); + }); + + async function typeIntoInput(text: string) { + component.searchTerm.setValue(text); + fixture.detectChanges(); + + await timer(300).toPromise(); + await fixture.whenStable(); + fixture.detectChanges(); + + const input = fixture.nativeElement.querySelector('input'); + input.focus(); + input.dispatchEvent(new Event('focusin')); + input.dispatchEvent(new Event('input')); + + await fixture.whenStable(); + fixture.detectChanges(); + } + + it('should setup text from underlying field on init', async () => { const group: GroupModel = { name: 'group-1'}; - widget.field.value = group; + component.field.value = group; + component.ngOnInit(); - spyOn(formService, 'getWorkflowGroups').and.returnValue( - new Observable((observer) => { - observer.next(null); - observer.complete(); - }) - ); - - widget.ngOnInit(); - expect(formService.getWorkflowGroups).toHaveBeenCalled(); - expect(widget.value).toBe(group.name); + expect(component.searchTerm.value).toEqual(group.name); }); it('should not setup text on init', () => { - widget.field.value = null; - widget.ngOnInit(); - expect(widget.value).toBeUndefined(); - }); - - it('should require form field to setup values on init', () => { - widget.field = null; - widget.ngOnInit(); - - expect(widget.value).toBeUndefined(); - expect(widget.groupId).toBeUndefined(); + component.field.value = null; + component.ngOnInit(); + expect(component.searchTerm.value).toBeNull(); }); it('should setup group restriction', () => { - widget.ngOnInit(); - expect(widget.groupId).toBeUndefined(); + component.ngOnInit(); + expect(component.groupId).toBeUndefined(); - widget.field.params = { restrictWithGroup: { id: '' } }; - widget.ngOnInit(); - expect(widget.groupId).toBe(''); - }); - - it('should prevent default behaviour on option item click', () => { - const event = jasmine.createSpyObj('event', ['preventDefault']); - widget.onItemClick(null, event); - expect(event.preventDefault).toHaveBeenCalled(); - }); - - it('should update values on item click', () => { - const item: GroupModel = { name: 'group-1' }; - - widget.onItemClick(item, null); - expect(widget.field.value).toBe(item); - expect(widget.value).toBe(item.name); + component.field.params = { restrictWithGroup: { id: '' } }; + component.ngOnInit(); + expect(component.groupId).toBe(''); }); it('should update form on value flush', () => { - spyOn(widget.field, 'updateForm').and.callThrough(); - widget.flushValue(); - expect(widget.field.updateForm).toHaveBeenCalled(); + spyOn(component.field, 'updateForm').and.callThrough(); + component.updateOption(); + expect(component.field.updateForm).toHaveBeenCalled(); }); it('should flush selected value', () => { - const groups: GroupModel[] = [ - { id: '1', name: 'group 1' }, - { id: '2', name: 'group 2' } - ]; + getWorkflowGroupsSpy.and.returnValue(of(groups)); - widget.groups = groups; - widget.value = 'group 2'; - widget.flushValue(); + component.updateOption(groups[1]); - expect(widget.value).toBe(groups[1].name); - expect(widget.field.value).toBe(groups[1]); + expect(component.field.value).toBe(groups[1]); }); - it('should be case insensitive when flushing value', () => { - const groups: GroupModel[] = [ - { id: '1', name: 'group 1' }, - { id: '2', name: 'gRoUp 2' } - ]; + it('should fetch groups and show popup on key up', async () => { + component.groupId = 'parentGroup'; + getWorkflowGroupsSpy.and.returnValue(of(groups)); - widget.groups = groups; - widget.value = 'GROUP 2'; - widget.flushValue(); + await typeIntoInput('group'); - expect(widget.value).toBe(groups[1].name); - expect(widget.field.value).toBe(groups[1]); + const options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]')); + expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']); + expect(getWorkflowGroupsSpy).toHaveBeenCalledWith('group', 'parentGroup'); }); - it('should fetch groups and show popup on key up', () => { - const groups: GroupModel[] = [{}, {}]; - spyOn(formService, 'getWorkflowGroups').and.returnValue( - new Observable((observer) => { - observer.next(groups); - observer.complete(); - }) - ); + it('should hide popup when fetching empty group list', async () => { + component.groupId = 'parentGroup'; + getWorkflowGroupsSpy.and.returnValues(of(groups), of([])); - const keyboardEvent = new KeyboardEvent('keypress'); - widget.value = 'group'; - widget.onKeyUp(keyboardEvent); + await typeIntoInput('group'); - expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', undefined); - expect(widget.groups).toBe(groups); + let options: HTMLElement[] = Array.from(document.querySelectorAll('[id="adf-group-label-name"]')); + expect(options.map(option => option.innerText)).toEqual(['group 1', 'group 2']); + + await typeIntoInput('unknown-group'); + + options = Array.from(document.querySelectorAll('[id="adf-group-label-name"]')); + expect(options).toEqual([]); + expect(getWorkflowGroupsSpy).toHaveBeenCalledTimes(2); }); - it('should fetch groups with a group filter', () => { - const groups: GroupModel[] = [{}, {}]; - spyOn(formService, 'getWorkflowGroups').and.returnValue( - new Observable((observer) => { - observer.next(groups); - observer.complete(); - }) - ); - - const keyboardEvent = new KeyboardEvent('keypress'); - widget.groupId = 'parentGroup'; - widget.value = 'group'; - widget.onKeyUp(keyboardEvent); - - expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', 'parentGroup'); - expect(widget.groups).toBe(groups); + it('should not fetch groups when value is missing', async () => { + await typeIntoInput(''); + expect(getWorkflowGroupsSpy).not.toHaveBeenCalled(); }); - it('should hide popup when fetching empty group list', () => { - spyOn(formService, 'getWorkflowGroups').and.returnValue( - new Observable((observer) => { - observer.next(null); - observer.complete(); - }) - ); - - const keyboardEvent = new KeyboardEvent('keypress'); - widget.value = 'group'; - widget.onKeyUp(keyboardEvent); - - expect(formService.getWorkflowGroups).toHaveBeenCalledWith('group', undefined); - expect(widget.groups.length).toBe(0); - }); - - it('should not fetch groups when value is missing', () => { - spyOn(formService, 'getWorkflowGroups').and.stub(); - - const keyboardEvent = new KeyboardEvent('keypress'); - widget.value = null; - widget.onKeyUp(keyboardEvent); - - expect(formService.getWorkflowGroups).not.toHaveBeenCalled(); - }); - - it('should not fetch groups when value violates constraints', () => { - spyOn(formService, 'getWorkflowGroups').and.stub(); - - const keyboardEvent = new KeyboardEvent('keypress'); - widget.minTermLength = 4; - widget.value = '123'; - widget.onKeyUp(keyboardEvent); - - expect(formService.getWorkflowGroups).not.toHaveBeenCalled(); + it('should not fetch groups when value violates constraints', async () => { + component.minTermLength = 4; + await typeIntoInput('123'); + expect(getWorkflowGroupsSpy).not.toHaveBeenCalled(); }); }); diff --git a/lib/core/form/components/widgets/functional-group/functional-group.widget.ts b/lib/core/form/components/widgets/functional-group/functional-group.widget.ts index d564038131..61e2aa8f0b 100644 --- a/lib/core/form/components/widgets/functional-group/functional-group.widget.ts +++ b/lib/core/form/components/widgets/functional-group/functional-group.widget.ts @@ -17,11 +17,13 @@ /* tslint:disable:component-selector */ -import { ENTER, ESCAPE } from '@angular/cdk/keycodes'; import { Component, ElementRef, OnInit, ViewEncapsulation } from '@angular/core'; import { FormService } from '../../../services/form.service'; import { GroupModel } from './../core/group.model'; import { WidgetComponent } from './../widget.component'; +import { catchError, debounceTime, filter, switchMap, tap } from 'rxjs/operators'; +import { merge, of } from 'rxjs'; +import { FormControl } from '@angular/forms'; @Component({ selector: 'functional-group-widget', @@ -42,11 +44,21 @@ import { WidgetComponent } from './../widget.component'; }) export class FunctionalGroupWidgetComponent extends WidgetComponent implements OnInit { - value: string; - oldValue: string; - groups: GroupModel[] = []; minTermLength: number = 1; groupId: string; + searchTerm = new FormControl(); + groups$ = merge(this.searchTerm.valueChanges).pipe( + tap((search: GroupModel | string) => { + const isValid = typeof search !== 'string'; + const empty = search === ''; + this.updateOption( isValid ? search as GroupModel : null ); + this.validateGroup(isValid, empty); + }), + filter((group: string | GroupModel) => typeof group === 'string' && group.length >= this.minTermLength), + debounceTime(300), + switchMap((searchTerm: string) => this.formService.getWorkflowGroups(searchTerm, this.groupId) + .pipe(catchError(() => of([])))) + ); constructor(public formService: FormService, public elementRef: ElementRef) { @@ -55,65 +67,52 @@ export class FunctionalGroupWidgetComponent extends WidgetComponent implements O ngOnInit() { if (this.field) { - const group = this.field.value; - if (group) { - this.value = group.name; + + if (this.field.readOnly) { + this.searchTerm.disable(); } const params = this.field.params; - if (params && params['restrictWithGroup']) { - const restrictWithGroup = params['restrictWithGroup']; + if (params && params.restrictWithGroup) { + const restrictWithGroup = params.restrictWithGroup; this.groupId = restrictWithGroup.id; } - // Load auto-completion for previously saved value - if (this.value) { - this.formService - .getWorkflowGroups(this.value, this.groupId) - .subscribe(groups => this.groups = groups || []); + if (this.field.value?.name) { + this.searchTerm.setValue(this.field.value.name); } } } - onKeyUp(event: KeyboardEvent) { - if (this.value && this.value.length >= this.minTermLength && this.oldValue !== this.value) { - if (event.keyCode !== ESCAPE && event.keyCode !== ENTER) { - this.oldValue = this.value; - this.formService - .getWorkflowGroups(this.value, this.groupId) - .subscribe(groups => this.groups = groups || []); - } - } - } - - flushValue() { - const option = this.groups.find((item) => item.name.toLocaleLowerCase() === this.value.toLocaleLowerCase()); - + updateOption(option?: GroupModel) { if (option) { this.field.value = option; - this.value = option.name; } else { this.field.value = null; - this.value = null; } this.field.updateForm(); } - onItemClick(item: GroupModel, event: Event) { - if (item) { - this.field.value = item; - this.value = item.name; - } - if (event) { - event.preventDefault(); + validateGroup(valid: boolean, empty: boolean) { + const isEmpty = !this.field.required && (empty || valid); + const hasValue = this.field.required && valid; + + if (hasValue || isEmpty) { + this.field.validationSummary.message = ''; + this.field.validate(); + this.field.form.validateForm(); + } else { + this.field.validationSummary.message = 'FORM.FIELD.VALIDATOR.INVALID_VALUE'; + this.field.markAsInvalid(); + this.field.form.markAsInvalid(); } } - onItemSelect(item: GroupModel) { - if (item) { - this.field.value = item; - this.value = item.name; + getDisplayName(model: GroupModel | string) { + if (model) { + return typeof model === 'string' ? model : model.name; } + return ''; } } diff --git a/lib/core/form/components/widgets/people/people.widget.html b/lib/core/form/components/widgets/people/people.widget.html index 41ecddb29d..d7ad337211 100644 --- a/lib/core/form/components/widgets/people/people.widget.html +++ b/lib/core/form/components/widgets/people/people.widget.html @@ -11,7 +11,7 @@ type="text" [id]="field.id" [formControl]="searchTerm" - placeholder="{{field.placeholder}}" + [placeholder]="field.placeholder" [matAutocomplete]="auto" [matTooltip]="field.tooltip" matTooltipPosition="above" diff --git a/lib/core/form/components/widgets/people/people.widget.spec.ts b/lib/core/form/components/widgets/people/people.widget.spec.ts index 5ad02e5b20..28c80c69b5 100644 --- a/lib/core/form/components/widgets/people/people.widget.spec.ts +++ b/lib/core/form/components/widgets/people/people.widget.spec.ts @@ -180,10 +180,11 @@ describe('PeopleWidgetComponent', () => { element = fixture.nativeElement; }); + afterEach(() => { + fixture.destroy(); + }); + afterAll(() => { - if (fixture) { - fixture.destroy(); - } TestBed.resetTestingModule(); }); @@ -219,17 +220,18 @@ describe('PeopleWidgetComponent', () => { expect(fixture.debugElement.query(By.css('#adf-people-widget-user-1'))).not.toBeNull(); }); - it('should hide result list if input is empty', () => { + it('should hide result list if input is empty', async () => { const peopleHTMLElement: HTMLInputElement = element.querySelector('input'); peopleHTMLElement.focus(); peopleHTMLElement.value = ''; peopleHTMLElement.dispatchEvent(new Event('keyup')); + peopleHTMLElement.dispatchEvent(new Event('focusin')); peopleHTMLElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); - fixture.whenStable().then(() => { - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).toBeNull(); - }); + await fixture.whenStable(); + + expect(fixture.debugElement.query(By.css('#adf-people-widget-user-0'))).toBeNull(); }); it('should display two options if we tap one letter', async () => { diff --git a/lib/core/form/components/widgets/people/people.widget.ts b/lib/core/form/components/widgets/people/people.widget.ts index 8b2938859e..4b1901ad54 100644 --- a/lib/core/form/components/widgets/people/people.widget.ts +++ b/lib/core/form/components/widgets/people/people.widget.ts @@ -55,29 +55,25 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit { input: ElementRef; @Output() - peopleSelected: EventEmitter; + peopleSelected: EventEmitter = new EventEmitter(); groupId: string; value: any; searchTerm = new FormControl(); - errorMsg = ''; searchTerms$: Observable = this.searchTerm.valueChanges; users$ = this.searchTerms$.pipe( - tap(() => { - this.errorMsg = ''; + tap((searchInput) => { + if (typeof searchInput === 'string') { + this.onItemSelect(); + } }), distinctUntilChanged(), switchMap((searchTerm) => { const value = searchTerm.email ? this.getDisplayName(searchTerm) : searchTerm; return this.formService.getWorkflowUsers(value, this.groupId) - .pipe( - catchError((err) => { - this.errorMsg = err.message; - return of(); - }) - ); + .pipe(catchError(() => of([]))); }), map((list: UserProcessModel[]) => { const value = this.searchTerm.value.email ? this.getDisplayName(this.searchTerm.value) : this.searchTerm.value; @@ -88,7 +84,6 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit { constructor(public formService: FormService, public peopleProcessService: PeopleProcessService) { super(formService); - this.peopleSelected = new EventEmitter(); } ngOnInit() { @@ -122,13 +117,13 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit { isValidUser(users: UserProcessModel[], name: string): boolean { if (users) { - return users.find((user) => { + return !!users.find((user) => { const selectedUser = this.getDisplayName(user).toLocaleLowerCase() === name.toLocaleLowerCase(); if (selectedUser) { this.peopleSelected.emit(user && user.id || undefined); } return selectedUser; - }) ? true : false; + }); } return false; } @@ -141,9 +136,11 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit { return ''; } - onItemSelect(item: UserProcessModel) { + onItemSelect(item?: UserProcessModel) { if (item) { this.field.value = item; + } else { + this.field.value = null; } } }