mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-05-12 17:04:57 +00:00
[MNT-24657] Add multiple selection support for people widget (#10458)
* [MNT-24657] Add multiple selection support for people widget [ci:force] * [MNT-24657] cr fixes
This commit is contained in:
parent
60a9565c71
commit
2b0100466a
@ -4,22 +4,37 @@
|
||||
id="people-widget-content">
|
||||
<mat-form-field>
|
||||
<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"
|
||||
data-automation-id="adf-people-search-input"
|
||||
type="text"
|
||||
[id]="field.id"
|
||||
[formControl]="searchTerm"
|
||||
[placeholder]="field.placeholder"
|
||||
[matAutocomplete]="auto"
|
||||
(blur)="markAsTouched()"
|
||||
[title]="field.tooltip">
|
||||
<mat-chip-grid #chipGrid [attr.aria-label]="'ADF_PROCESS_LIST.START_PROCESS.FORM.LABEL.SELECTED_PEOPLE' | translate">
|
||||
<mat-chip-row
|
||||
*ngFor="let user of selectedUsers"
|
||||
(removed)="onRemove(user)"
|
||||
[disabled]="field.readOnly"
|
||||
[attr.data-automation-id]="'adf-people-widget-chip-' + user.id">
|
||||
{{ getDisplayName(user) }}
|
||||
<button matChipRemove [attr.aria-label]="'remove ' + user.firstName">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</mat-chip-row>
|
||||
<input #inputValue
|
||||
matInput
|
||||
class="adf-input"
|
||||
[matChipInputFor]="chipGrid"
|
||||
data-automation-id="adf-people-search-input"
|
||||
type="text"
|
||||
[disabled]="!multiSelect && selectedUsers.length > 0 || field.readOnly"
|
||||
[id]="field.id"
|
||||
[formControl]="searchTerm"
|
||||
[placeholder]="selectedUsers.length > 0 ? '' : field.placeholder"
|
||||
[matAutocomplete]="auto"
|
||||
(blur)="markAsTouched()"
|
||||
[title]="field.tooltip">
|
||||
</mat-chip-grid>
|
||||
|
||||
<mat-autocomplete class="adf-people-widget-list"
|
||||
#auto="matAutocomplete"
|
||||
(optionSelected)="onItemSelect($event.option.value)"
|
||||
[displayWith]="getDisplayName">
|
||||
<mat-option *ngFor="let user of users$ | async; let i = index" [value]="user">
|
||||
<mat-option *ngFor="let user of users$ | async; let i = index" [value]="user" [disabled]="isUserAlreadySelected(user)">
|
||||
<div class="adf-people-widget-row" id="adf-people-widget-user-{{i}}">
|
||||
<div [outerHTML]="user | usernameInitials:'adf-people-widget-pic'"></div>
|
||||
<div *ngIf="user.pictureId" class="adf-people-widget-image-row">
|
||||
|
@ -23,20 +23,28 @@ import { PeopleWidgetComponent } from './people.widget';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PeopleProcessService } from '../../../services/people-process.service';
|
||||
import { LightUserRepresentation } from '@alfresco/js-api';
|
||||
import { MatChipHarness } from '@angular/material/chips/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
|
||||
describe('PeopleWidgetComponent', () => {
|
||||
let widget: PeopleWidgetComponent;
|
||||
let fixture: ComponentFixture<PeopleWidgetComponent>;
|
||||
let element: HTMLElement;
|
||||
let loader: HarnessLoader;
|
||||
let translationService: TranslateService;
|
||||
let peopleProcessService: PeopleProcessService;
|
||||
|
||||
const getChipById = async (id: string) =>
|
||||
loader.getHarness(MatChipHarness.with({ selector: `[data-automation-id="adf-people-widget-chip-${id}"]` }));
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CoreTestingModule]
|
||||
});
|
||||
fixture = TestBed.createComponent(PeopleWidgetComponent);
|
||||
peopleProcessService = TestBed.inject(PeopleProcessService);
|
||||
loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
|
||||
translationService = TestBed.inject(TranslateService);
|
||||
spyOn(translationService, 'instant').and.callFake((key) => key);
|
||||
@ -48,26 +56,28 @@ describe('PeopleWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should return empty display name for missing model', () => {
|
||||
expect(widget.getDisplayName(null)).toBe('');
|
||||
});
|
||||
describe('display name', () => {
|
||||
it('should return empty display name for missing model', () => {
|
||||
expect(widget.getDisplayName(null)).toBe('');
|
||||
});
|
||||
|
||||
it('should return full name for a given model', () => {
|
||||
const model = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
};
|
||||
expect(widget.getDisplayName(model)).toBe('John Doe');
|
||||
});
|
||||
it('should return full name for a given model', () => {
|
||||
const model = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
};
|
||||
expect(widget.getDisplayName(model)).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('should skip first name for display name', () => {
|
||||
const model = { firstName: null, lastName: 'Doe' };
|
||||
expect(widget.getDisplayName(model)).toBe('Doe');
|
||||
});
|
||||
it('should skip first name for display name', () => {
|
||||
const model = { firstName: null, lastName: 'Doe' };
|
||||
expect(widget.getDisplayName(model)).toBe('Doe');
|
||||
});
|
||||
|
||||
it('should skip last name for display name', () => {
|
||||
const model = { firstName: 'John', lastName: null };
|
||||
expect(widget.getDisplayName(model)).toBe('John');
|
||||
it('should skip last name for display name', () => {
|
||||
const model = { firstName: 'John', lastName: null };
|
||||
expect(widget.getDisplayName(model)).toBe('John');
|
||||
});
|
||||
});
|
||||
|
||||
it('should init value from the field', async () => {
|
||||
@ -83,7 +93,33 @@ describe('PeopleWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect((element.querySelector('input') as HTMLInputElement).value).toBe('John Doe');
|
||||
const chip = await getChipById('people-id');
|
||||
expect(await chip.getText()).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('should show correct number of chips if multiple users provided', async () => {
|
||||
widget.field.readOnly = false;
|
||||
widget.field.params.multiple = true;
|
||||
widget.field.value = [
|
||||
{
|
||||
id: 'people-id-1',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
},
|
||||
{
|
||||
id: 'people-id-2',
|
||||
firstName: 'Rick',
|
||||
lastName: 'Grimes'
|
||||
}
|
||||
];
|
||||
widget.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const chips = await loader.getAllHarnesses(MatChipHarness);
|
||||
expect(chips.length).toBe(2);
|
||||
expect(await chips[0].getText()).toBe('John Doe');
|
||||
expect(await chips[1].getText()).toBe('Rick Grimes');
|
||||
});
|
||||
|
||||
it('should show the readonly value when the form is readonly', async () => {
|
||||
@ -101,10 +137,30 @@ describe('PeopleWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect((element.querySelector('input') as HTMLInputElement).value).toBe('John Doe');
|
||||
const chip = await getChipById('people-id');
|
||||
expect(await chip.getText()).toBe('John Doe');
|
||||
expect(await chip.isDisabled()).toBe(true);
|
||||
expect((element.querySelector('input') as HTMLInputElement).disabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display the cancel button in the chip', async () => {
|
||||
widget.field.value = {
|
||||
id: 'people-id',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
};
|
||||
|
||||
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of(null));
|
||||
|
||||
widget.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const chip = await getChipById('people-id');
|
||||
const cancelIcon = await chip.getRemoveButton();
|
||||
expect(cancelIcon).toBeDefined();
|
||||
});
|
||||
|
||||
it('should require form field to setup values on init', () => {
|
||||
widget.field.value = null;
|
||||
widget.ngOnInit();
|
||||
@ -138,13 +194,63 @@ describe('PeopleWidgetComponent', () => {
|
||||
email: 'john@test.com'
|
||||
};
|
||||
widget.ngOnInit();
|
||||
|
||||
const involvedUser = fixture.debugElement.nativeElement.querySelector('input[data-automation-id="adf-people-search-input"]');
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(involvedUser.value).toBe('John Doe');
|
||||
const chip = await getChipById('people-id');
|
||||
expect(await chip.getText()).toBe('John Doe');
|
||||
});
|
||||
|
||||
it('should add user to selectedUsers when multiSelect is false and user is not already selected', () => {
|
||||
const user: LightUserRepresentation = { id: 1, firstName: 'John', lastName: 'Doe' };
|
||||
widget.multiSelect = false;
|
||||
widget.onItemSelect(user);
|
||||
expect(widget.selectedUsers).toContain(user);
|
||||
expect(widget.field.value).toEqual(widget.selectedUsers[0]);
|
||||
});
|
||||
|
||||
it('should not add user to selectedUsers when multiSelect is true and user is already selected', () => {
|
||||
const user: LightUserRepresentation = { id: 1, firstName: 'John', lastName: 'Doe' };
|
||||
widget.multiSelect = true;
|
||||
widget.selectedUsers = [user];
|
||||
widget.onItemSelect(user);
|
||||
expect(widget.selectedUsers.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should clear the input value after selection', () => {
|
||||
const user: LightUserRepresentation = { id: 1, firstName: 'John', lastName: 'Doe' };
|
||||
widget.input.nativeElement.value = 'test';
|
||||
widget.onItemSelect(user);
|
||||
expect(widget.input.nativeElement.value).toBe('');
|
||||
});
|
||||
|
||||
it('should reset the search term after selection', () => {
|
||||
spyOn(peopleProcessService, 'getWorkflowUsers').and.returnValue(of(null));
|
||||
const user: LightUserRepresentation = { id: 1, firstName: 'John', lastName: 'Doe' };
|
||||
widget.searchTerm.setValue('test');
|
||||
widget.onItemSelect(user);
|
||||
expect(widget.searchTerm.value).toBe('');
|
||||
});
|
||||
|
||||
it('should remove user from selectedUsers if user exists', () => {
|
||||
const users: LightUserRepresentation[] = [
|
||||
{ id: 1, firstName: 'John', lastName: 'Doe' },
|
||||
{ id: 2, firstName: 'Jane', lastName: 'Doe' }
|
||||
];
|
||||
|
||||
widget.selectedUsers = [...users];
|
||||
widget.onRemove(users[0]);
|
||||
|
||||
expect(widget.selectedUsers).not.toContain(users[0]);
|
||||
expect(widget.field.value).toEqual([users[1]]);
|
||||
});
|
||||
|
||||
it('should not change selectedUsers if user does not exist', () => {
|
||||
const selectedUser: LightUserRepresentation = { id: 1, firstName: 'John', lastName: 'Doe' };
|
||||
const anotherUser: LightUserRepresentation = { id: 2, firstName: 'Jane', lastName: 'Doe' };
|
||||
widget.selectedUsers = [selectedUser];
|
||||
widget.onRemove(anotherUser);
|
||||
expect(widget.selectedUsers).toEqual([selectedUser]);
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
@ -274,16 +380,12 @@ describe('PeopleWidgetComponent', () => {
|
||||
|
||||
it('should emit peopleSelected if option is valid', async () => {
|
||||
const selectEmitSpy = spyOn(widget.peopleSelected, 'emit');
|
||||
const peopleHTMLElement = element.querySelector<HTMLInputElement>('input');
|
||||
peopleHTMLElement.focus();
|
||||
peopleHTMLElement.value = 'Test01 Test01';
|
||||
peopleHTMLElement.dispatchEvent(new Event('keyup'));
|
||||
peopleHTMLElement.dispatchEvent(new Event('input'));
|
||||
widget.onItemSelect(fakeUserResult[0]);
|
||||
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(selectEmitSpy).toHaveBeenCalledWith(1001);
|
||||
expect(selectEmitSpy).toHaveBeenCalledWith(fakeUserResult[0].id);
|
||||
});
|
||||
|
||||
it('should display tooltip when tooltip is set', async () => {
|
||||
|
@ -20,8 +20,10 @@
|
||||
import { ErrorWidgetComponent, FormService, InitialUsernamePipe, WidgetComponent } from '@alfresco/adf-core';
|
||||
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
import { PeopleProcessService } from '../../../services/people-process.service';
|
||||
import { LightUserRepresentation } from '@alfresco/js-api';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@ -38,6 +40,8 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
TranslateModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatChipsModule,
|
||||
MatIconModule,
|
||||
ReactiveFormsModule,
|
||||
MatAutocompleteModule,
|
||||
InitialUsernamePipe,
|
||||
@ -66,20 +70,19 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
|
||||
@Output()
|
||||
peopleSelected: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
selectedUsers: LightUserRepresentation[] = [];
|
||||
multiSelect = false;
|
||||
groupId: number;
|
||||
value: any;
|
||||
|
||||
searchTerm = new UntypedFormControl();
|
||||
searchTerms$: Observable<any> = this.searchTerm.valueChanges;
|
||||
|
||||
users$ = this.searchTerms$.pipe(
|
||||
tap((searchInput) => {
|
||||
if (typeof searchInput === 'string') {
|
||||
this.onItemSelect();
|
||||
}
|
||||
}),
|
||||
users$: Observable<LightUserRepresentation[]> = this.searchTerms$.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((searchTerm) => {
|
||||
if (!searchTerm) {
|
||||
return of([]);
|
||||
}
|
||||
const value = searchTerm.email ? this.getDisplayName(searchTerm) : searchTerm;
|
||||
return this.peopleProcessService.getWorkflowUsers(undefined, value, this.groupId).pipe(catchError(() => of([])));
|
||||
}),
|
||||
@ -97,16 +100,16 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
if (this.field) {
|
||||
if (this.field.value) {
|
||||
this.searchTerm.setValue(this.field.value);
|
||||
}
|
||||
if (this.field.readOnly) {
|
||||
this.searchTerm.disable();
|
||||
Array.isArray(this.field.value) ? this.selectedUsers.push(...this.field.value) : this.selectedUsers.push(this.field.value);
|
||||
}
|
||||
const params = this.field.params;
|
||||
if (params?.restrictWithGroup) {
|
||||
const restrictWithGroup = params.restrictWithGroup;
|
||||
this.groupId = restrictWithGroup.id;
|
||||
}
|
||||
if (params?.multiple) {
|
||||
this.multiSelect = params.multiple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,11 +129,7 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
|
||||
isValidUser(users: LightUserRepresentation[], name: string): boolean {
|
||||
if (users) {
|
||||
return !!users.find((user) => {
|
||||
const selectedUser = this.getDisplayName(user).toLocaleLowerCase() === name.toLocaleLowerCase();
|
||||
if (selectedUser) {
|
||||
this.peopleSelected.emit(user?.id || undefined);
|
||||
}
|
||||
return selectedUser;
|
||||
return this.getDisplayName(user).toLocaleLowerCase() === name.toLocaleLowerCase();
|
||||
});
|
||||
}
|
||||
return false;
|
||||
@ -144,11 +143,36 @@ export class PeopleWidgetComponent extends WidgetComponent implements OnInit {
|
||||
return '';
|
||||
}
|
||||
|
||||
onItemSelect(item?: LightUserRepresentation) {
|
||||
if (item) {
|
||||
this.field.value = item;
|
||||
} else {
|
||||
this.field.value = null;
|
||||
onRemove(user: LightUserRepresentation) {
|
||||
const index = this.selectedUsers.indexOf(user);
|
||||
if (index >= 0) {
|
||||
this.selectedUsers.splice(index, 1);
|
||||
this.field.value = this.selectedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
onItemSelect(user: LightUserRepresentation) {
|
||||
if (this.multiSelect) {
|
||||
if (!this.isUserAlreadySelected(user)) {
|
||||
this.selectedUsers.push(user);
|
||||
}
|
||||
this.field.value = this.selectedUsers;
|
||||
} else {
|
||||
this.selectedUsers = [user];
|
||||
this.field.value = user;
|
||||
}
|
||||
|
||||
this.peopleSelected.emit(user?.id || undefined);
|
||||
this.input.nativeElement.value = '';
|
||||
this.searchTerm.setValue('');
|
||||
}
|
||||
|
||||
isUserAlreadySelected(user: LightUserRepresentation): boolean {
|
||||
if (this.selectedUsers?.length > 0) {
|
||||
const result = this.selectedUsers.find((selectedUser) => selectedUser.id === user.id);
|
||||
|
||||
return !!result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -281,7 +281,8 @@
|
||||
"LABEL": {
|
||||
"SELECT_APPLICATION": "Select Application",
|
||||
"TYPE": "Select Process",
|
||||
"NAME": "Process Name"
|
||||
"NAME": "Process Name",
|
||||
"SELECTED_PEOPLE": "Selected people"
|
||||
},
|
||||
"TYPE_PLACEHOLDER": "Choose one...",
|
||||
"ACTION": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user