mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-10-01 14:41:32 +00:00
AAE-38287 Cannot select dropdown options when selection type is multiple (#11220)
* AAE-38287 Fix multiselect dropdown * update tests * update * update * update * update * update
This commit is contained in:
@@ -36,9 +36,15 @@ import { FormFieldModel } from '../widgets/core/form-field.model';
|
||||
import { FieldStylePipe } from '../../pipes/field-style.pipe';
|
||||
import { FormFieldTypes } from '../widgets/core/form-field-types';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { ReactiveFormWidget } from '../widgets/reactive-widget.interface';
|
||||
|
||||
declare const adf: any;
|
||||
|
||||
/**
|
||||
* Component is a wrapper for widget components.
|
||||
* It is responsible for instantiating the correct widget component
|
||||
* based on the field type.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'adf-form-field',
|
||||
templateUrl: './form-field.component.html',
|
||||
@@ -98,7 +104,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
if (FormFieldTypes.isReactiveType(instance?.field?.type)) {
|
||||
if (FormFieldTypes.isReactiveWidget(instance)) {
|
||||
this.updateReactiveFormControlOnFormRulesEvent(instance);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +112,7 @@ export class FormFieldComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private updateReactiveFormControlOnFormRulesEvent(instance: any): void {
|
||||
private updateReactiveFormControlOnFormRulesEvent(instance: ReactiveFormWidget): void {
|
||||
instance?.formService.formRulesEvent.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
instance?.updateReactiveFormControl();
|
||||
instance?.field?.form.validateForm(instance?.field);
|
||||
|
@@ -15,6 +15,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MaybeReactiveFormWidget, ReactiveFormWidget } from '../reactive-widget.interface';
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
export class FormFieldTypes {
|
||||
@@ -71,6 +73,10 @@ export class FormFieldTypes {
|
||||
return FormFieldTypes.REACTIVE_TYPES.includes(type);
|
||||
}
|
||||
|
||||
static isReactiveWidget(instance: MaybeReactiveFormWidget): instance is ReactiveFormWidget {
|
||||
return FormFieldTypes.REACTIVE_TYPES.includes(instance?.field?.type);
|
||||
}
|
||||
|
||||
static isConstantValueType(type: string): boolean {
|
||||
return FormFieldTypes.CONSTANT_VALUE_TYPES.includes(type);
|
||||
}
|
||||
|
@@ -16,8 +16,13 @@
|
||||
*/
|
||||
|
||||
import { FormService } from '../../services/form.service';
|
||||
import { FormFieldModel } from './core/form-field.model';
|
||||
import { WidgetComponent } from './widget.component';
|
||||
|
||||
export interface ReactiveFormWidget {
|
||||
updateReactiveFormControl(): void;
|
||||
formService: FormService;
|
||||
field: FormFieldModel;
|
||||
}
|
||||
|
||||
export type MaybeReactiveFormWidget = WidgetComponent | ReactiveFormWidget;
|
||||
|
@@ -12,7 +12,8 @@
|
||||
<div>
|
||||
<mat-form-field class="adf-form-field-input">
|
||||
@if ( (field.name || this.field?.required) && !field.leftLabels) {
|
||||
<mat-label class="adf-label" [attr.for]="field.id">{{ field.name | translate }}</mat-label> }
|
||||
<mat-label class="adf-label" [attr.for]="field.id">{{ field.name | translate }}</mat-label>
|
||||
}
|
||||
<mat-select
|
||||
class="adf-select"
|
||||
[formControl]="dropdownControl"
|
||||
@@ -22,14 +23,27 @@
|
||||
panelClass="adf-select-filter"
|
||||
[multiple]="field.hasMultipleValues"
|
||||
[disabled]="field.readOnly"
|
||||
[required]="field.required "
|
||||
[required]="field.required"
|
||||
>
|
||||
<adf-select-filter-input *ngIf="showInputFilter" (change)="filter$.next($event)" />
|
||||
|
||||
<mat-option *ngFor="let opt of list$ | async" [value]="opt" [id]="opt.id">{{opt.name}}</mat-option>
|
||||
<mat-option id="readonlyOption" *ngIf="isReadOnlyType" [value]="field.value">{{field.value}}</mat-option>
|
||||
@for(opt of (list$ | async); track opt.id) {
|
||||
<mat-option [value]="opt" [id]="opt.id">
|
||||
{{opt.name}}
|
||||
</mat-option>
|
||||
}
|
||||
|
||||
@if(isReadOnlyType) {
|
||||
<mat-option
|
||||
[id]="'readonlyOption-' + field.id"
|
||||
[value]="field.value"
|
||||
>
|
||||
{{field.value}}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div
|
||||
class="adf-error-messages-container"
|
||||
[ngClass]="!previewState && !field.readOnly ? 'adf-error-messages-container-visible' : 'adf-error-messages-container-hidden'"
|
||||
|
@@ -15,11 +15,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { DEFAULT_OPTION, DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
|
||||
import { FormFieldModel, FormModel, FormService, FormFieldEvent, FormFieldTypes, UnitTestingUtils } from '@alfresco/adf-core';
|
||||
import { DEFAULT_OPTION, DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE, DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
|
||||
import {
|
||||
FormFieldModel,
|
||||
FormModel,
|
||||
FormService,
|
||||
FormFieldEvent,
|
||||
FormFieldTypes,
|
||||
UnitTestingUtils,
|
||||
FormFieldComponent,
|
||||
FormRenderingService
|
||||
} from '@alfresco/adf-core';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
import {
|
||||
fakeOptionList,
|
||||
@@ -294,11 +303,12 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not display required error when selecting a valid option for a required dropdown', async () => {
|
||||
it('should not display required error when selecting a valid option for a required dropdown', fakeAsync(async () => {
|
||||
widget.field.required = true;
|
||||
widget.field.options = [{ id: 'empty', name: 'Choose empty' }, ...fakeOptionList];
|
||||
|
||||
widget.ngOnInit();
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.open();
|
||||
|
||||
@@ -307,7 +317,7 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
|
||||
const requiredErrorElement = fixture.debugElement.query(By.css('.adf-dropdown-required-message .adf-error-text'));
|
||||
expect(requiredErrorElement).toBeFalsy();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not have a value when switching from an available option to the None option', async () => {
|
||||
widget.field.options = [{ id: 'empty', name: 'This is a mock none option' }, ...fakeOptionList];
|
||||
@@ -985,48 +995,64 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
expect(widget.field.options.length).toEqual(0);
|
||||
};
|
||||
|
||||
it('should set dropdownControl value without emitting events if the mapping is a string', () => {
|
||||
it('should set dropdownControl value without emitting events if the mapping is a string', fakeAsync(() => {
|
||||
widget.field = {
|
||||
value: 'testValue',
|
||||
options: [],
|
||||
isVisible: true
|
||||
isVisible: true,
|
||||
markAsValid: () => {}
|
||||
} as any; // Mock field
|
||||
|
||||
fixture.detectChanges();
|
||||
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
|
||||
|
||||
widget['setFormControlValue']();
|
||||
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
|
||||
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValue', name: '' }, { emitEvent: false });
|
||||
expect(widget.dropdownControl.value).toEqual({ id: 'testValue', name: '' });
|
||||
});
|
||||
}));
|
||||
|
||||
it('should set dropdownControl value when form field value gets changed', () => {
|
||||
it('should set dropdownControl value when form field value gets changed', fakeAsync(() => {
|
||||
widget.field = {
|
||||
value: { id: 'Id_1', name: 'Label 1' },
|
||||
options: [],
|
||||
isVisible: true,
|
||||
markAsValid: () => {}
|
||||
} as FormFieldModel;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
|
||||
|
||||
widget.updateReactiveFormControl();
|
||||
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
|
||||
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'Id_1', name: 'Label 1' }, { emitEvent: false });
|
||||
expect(widget.dropdownControl.value).toEqual({ id: 'Id_1', name: 'Label 1' });
|
||||
});
|
||||
}));
|
||||
|
||||
it('should set dropdownControl value without emitting events if is an object', () => {
|
||||
it('should set dropdownControl value without emitting events if is an object', fakeAsync(() => {
|
||||
widget.field = {
|
||||
value: { id: 'testValueObj', name: 'testValueObjName' },
|
||||
options: [],
|
||||
isVisible: true
|
||||
} as any; // Mock field
|
||||
isVisible: true,
|
||||
markAsValid: () => {}
|
||||
} as FormFieldModel;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
spyOn(widget.dropdownControl, 'setValue').and.callThrough();
|
||||
|
||||
widget['setFormControlValue']();
|
||||
|
||||
tick(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE);
|
||||
|
||||
expect(widget.dropdownControl.setValue).toHaveBeenCalledWith({ id: 'testValueObj', name: 'testValueObjName' }, { emitEvent: false });
|
||||
expect(widget.dropdownControl.value).toEqual({ id: 'testValueObj', name: 'testValueObjName' });
|
||||
});
|
||||
}));
|
||||
|
||||
it('should display options persisted from process variable', async () => {
|
||||
widget.field = getVariableDropdownWidget(
|
||||
@@ -1191,3 +1217,68 @@ describe('DropdownCloudWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropdownCloudWidgetComponent instantiated by FormFieldComponent wrapper', () => {
|
||||
let formFieldFixture: ComponentFixture<FormFieldComponent>;
|
||||
let formFieldComponent: FormFieldComponent;
|
||||
let loader: HarnessLoader;
|
||||
let formRenderingService: FormRenderingService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [FormFieldComponent]
|
||||
});
|
||||
|
||||
formFieldFixture = TestBed.createComponent(FormFieldComponent);
|
||||
formFieldComponent = formFieldFixture.componentInstance;
|
||||
|
||||
loader = TestbedHarnessEnvironment.loader(formFieldFixture);
|
||||
|
||||
formRenderingService = TestBed.inject(FormRenderingService);
|
||||
formRenderingService.register({
|
||||
[FormFieldTypes.DROPDOWN]: () => DropdownCloudWidgetComponent
|
||||
});
|
||||
});
|
||||
|
||||
/* Checking if events emitted in FormFieldComponent are NOT triggering unnecessary calls to setValue in DropdownCloudWidgetComponent
|
||||
This may result in setting wrong value in component
|
||||
e.g. FormFieldComponent.updateReactiveFormControlOnFormRulesEvent
|
||||
*/
|
||||
it('should set dropdown controller value only once', async () => {
|
||||
formFieldComponent.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', readOnly: false, id: 'form-id' }), {
|
||||
id: 'multiselect-id',
|
||||
name: 'multiselect',
|
||||
type: 'dropdown',
|
||||
selectionType: 'multiple',
|
||||
options: [
|
||||
{ id: 'option1', name: 'option1' },
|
||||
{ id: 'option2', name: 'option2' },
|
||||
{ id: 'other', name: 'other' }
|
||||
]
|
||||
});
|
||||
|
||||
const dropdown = await loader.getHarness(MatSelectHarness.with({ selector: '.adf-select' }));
|
||||
await dropdown.open();
|
||||
|
||||
const dropdownCloudWidgetInstanceComponent = formFieldFixture.debugElement.query(
|
||||
By.directive(DropdownCloudWidgetComponent)
|
||||
).componentInstance;
|
||||
|
||||
const setValueSpy = spyOn(dropdownCloudWidgetInstanceComponent.dropdownControl, 'setValue').and.callThrough();
|
||||
|
||||
dropdownCloudWidgetInstanceComponent.event(new Event('focusin'));
|
||||
|
||||
// Not using dropdown.clickOptions from harness since it need ot be awaited
|
||||
// I want to simulate other events at the same time
|
||||
const option1 = formFieldFixture.debugElement.query(By.css('[ng-reflect-id="option1"]'));
|
||||
option1.triggerEventHandler('click');
|
||||
dropdownCloudWidgetInstanceComponent.event(new Event('focusout'));
|
||||
|
||||
await dropdown.close();
|
||||
|
||||
const selectedOption = await dropdown.getValueText();
|
||||
|
||||
expect(selectedOption).toEqual('option1');
|
||||
expect(setValueSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@@ -35,8 +35,8 @@ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { debounceTime, filter, map } from 'rxjs/operators';
|
||||
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -48,6 +48,7 @@ export const DEFAULT_OPTION = {
|
||||
name: 'Choose one...'
|
||||
};
|
||||
export const HIDE_FILTER_LIMIT = 5;
|
||||
export const DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE = 100;
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
@@ -91,6 +92,8 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
private readonly defaultVariableOptionLabel = 'name';
|
||||
private readonly defaultVariableOptionPath = 'data';
|
||||
|
||||
private debounceSetValue = new Subject<void>();
|
||||
|
||||
get showRequiredMessage(): boolean {
|
||||
return this.dropdownControl.touched && this.dropdownControl.errors?.required && !this.isRestApiFailed && !this.variableOptionsFailed;
|
||||
}
|
||||
@@ -128,6 +131,26 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
/*
|
||||
We can have a lot of 'control.setValue' caused by form rules events
|
||||
e.g. every time if we focusin/focusout etc. we are calling a setValue.
|
||||
*/
|
||||
this.debounceSetValue.pipe(debounceTime(DROPDOWN_CLOUD_WIDGET_SET_VALUE_DEBOUNCE), takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
let value: Array<FormFieldOption> | FormFieldOption | null | undefined;
|
||||
|
||||
if (Array.isArray(this.field.value)) {
|
||||
value = this.field?.value;
|
||||
} else if (this.field?.value && typeof this.field?.value === 'object') {
|
||||
value = { id: this.field?.value.id, name: this.field?.value.name };
|
||||
} else if (this.field.value === null) {
|
||||
value = this.field.value;
|
||||
} else {
|
||||
value = { id: this.field?.value, name: '' };
|
||||
}
|
||||
|
||||
this.dropdownControl.setValue(value, { emitEvent: false });
|
||||
});
|
||||
|
||||
this.setupDropdown();
|
||||
|
||||
this.formService.onFormVariableChanged.subscribe(({ field }) => {
|
||||
@@ -138,9 +161,8 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
|
||||
updateReactiveFormControl(): void {
|
||||
if (!this.field.hasMultipleValues) {
|
||||
this.setFormControlValue();
|
||||
}
|
||||
this.setFormControlValue();
|
||||
|
||||
this.updateFormControlState();
|
||||
this.handleErrors();
|
||||
}
|
||||
@@ -198,15 +220,7 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
||||
}
|
||||
|
||||
private setFormControlValue(): void {
|
||||
if (Array.isArray(this.field.value)) {
|
||||
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
|
||||
} else if (this.field?.value && typeof this.field?.value === 'object') {
|
||||
this.dropdownControl.setValue({ id: this.field?.value.id, name: this.field?.value.name }, { emitEvent: false });
|
||||
} else if (this.field.value === null) {
|
||||
this.dropdownControl.setValue(this.field?.value, { emitEvent: false });
|
||||
} else {
|
||||
this.dropdownControl.setValue({ id: this.field?.value, name: '' }, { emitEvent: false });
|
||||
}
|
||||
this.debounceSetValue.next();
|
||||
}
|
||||
|
||||
private updateFormControlState(): void {
|
||||
|
Reference in New Issue
Block a user