From b9997be4ab7a1dfbbf7e993de61f4c390ac82ae5 Mon Sep 17 00:00:00 2001 From: Dharan <14145706+dhrn@users.noreply.github.com> Date: Wed, 27 Oct 2021 13:45:28 +0530 Subject: [PATCH] [AAE-5971] [Form] Support for multi select drop down (#7321) * [AAE-5971] [ADF][Form-cloud] Support for multi select drop down * * fix drop down validation * * minor changes * * fix tests * * minor changes * * fix comments * * fix e2e --- .../process/custom-process-filters.e2e.ts | 9 +++ .../widgets/core/form-field-validator.spec.ts | 19 +++++++ .../widgets/core/form-field-validator.ts | 4 ++ .../widgets/core/form-field.model.spec.ts | 18 +++++- .../widgets/core/form-field.model.ts | 33 +++++++++-- .../components/widgets/core/form.model.ts | 10 +++- .../widget-visibility.service.spec.ts | 26 +++++++-- .../services/widget-visibility.service.ts | 16 +++++- .../dropdown/dropdown-cloud.widget.html | 3 +- .../dropdown/dropdown-cloud.widget.spec.ts | 56 +++++++++++++++++++ .../widgets/dropdown/dropdown-cloud.widget.ts | 26 ++++++++- 11 files changed, 201 insertions(+), 19 deletions(-) diff --git a/e2e/process-services/process/custom-process-filters.e2e.ts b/e2e/process-services/process/custom-process-filters.e2e.ts index 51f48a2b9c..f1f3cdf537 100644 --- a/e2e/process-services/process/custom-process-filters.e2e.ts +++ b/e2e/process-services/process/custom-process-filters.e2e.ts @@ -110,6 +110,15 @@ describe('New Process Filters', () => { }); it('[C260474] Should be able to edit a filter on APS and check it on ADF', async () => { + customProcessFilter = await userFiltersApi.createUserProcessInstanceFilter({ + 'appId': null, + 'name': processFilter.new_icon, + 'icon': 'glyphicon-cloud', + 'filter': { 'sort': 'created-desc', 'name': '', 'state': 'running' } + }); + + filterId = customProcessFilter.id; + await userFiltersApi.updateUserProcessInstanceFilter(filterId, { 'appId': null, 'name': processFilter.edited, diff --git a/lib/core/form/components/widgets/core/form-field-validator.spec.ts b/lib/core/form/components/widgets/core/form-field-validator.spec.ts index 1cd8387dbf..6077d1ff35 100644 --- a/lib/core/form/components/widgets/core/form-field-validator.spec.ts +++ b/lib/core/form/components/widgets/core/form-field-validator.spec.ts @@ -71,6 +71,9 @@ describe('FormFieldValidator', () => { const field = new FormFieldModel(new FormModel(), { type: FormFieldTypes.DROPDOWN, value: '', + options: [ + {id: 'empty', name: 'Choose option...'} + ], hasEmptyValue: true, required: true }); @@ -82,6 +85,22 @@ describe('FormFieldValidator', () => { expect(validator.validate(field)).toBeTruthy(); }); + it('should fail for dropdown with zero selection', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + value: [], + hasEmptyValue: true, + required: true, + selectionType: 'multiple' + }); + + field.emptyOption = { id: 'empty' }; + expect(validator.validate(field)).toBeFalsy(); + + field.value = []; + expect(validator.validate(field)).toBe(false); + }); + it('should fail for radio buttons', () => { const field = new FormFieldModel(new FormModel(), { type: FormFieldTypes.RADIO_BUTTONS, diff --git a/lib/core/form/components/widgets/core/form-field-validator.ts b/lib/core/form/components/widgets/core/form-field-validator.ts index 558dc4f020..5f67126b5c 100644 --- a/lib/core/form/components/widgets/core/form-field-validator.ts +++ b/lib/core/form/components/widgets/core/form-field-validator.ts @@ -59,6 +59,10 @@ export class RequiredFieldValidator implements FormFieldValidator { if (this.isSupported(field) && field.isVisible) { if (field.type === FormFieldTypes.DROPDOWN) { + if (field.hasMultipleValues) { + return !!field.value.length; + } + if (field.hasEmptyValue && field.emptyOption) { if (field.value === field.emptyOption.id) { return false; diff --git a/lib/core/form/components/widgets/core/form-field.model.spec.ts b/lib/core/form/components/widgets/core/form-field.model.spec.ts index 3ad1392b4e..3a3c9da89c 100644 --- a/lib/core/form/components/widgets/core/form-field.model.spec.ts +++ b/lib/core/form/components/widgets/core/form-field.model.spec.ts @@ -460,13 +460,29 @@ describe('FormFieldModel', () => { const field = new FormFieldModel(new FormModel(), { type: FormFieldTypes.DROPDOWN, options: [ - {id: 'fake-option-1', name: 'fake label 1'}, + {id: 'empty', name: 'Choose option...'}, {id: 'fake-option-2', name: 'fake label 2'}, {id: 'fake-option-3', name: 'fake label 3'} ], value: 'fake-option-2' }); expect(field.getOptionName()).toBe('fake label 2'); + expect(field.hasEmptyValue).toBe(true); + }); + + it('should parse dropdown with multiple options', () => { + const field = new FormFieldModel(new FormModel(), { + type: FormFieldTypes.DROPDOWN, + options: [ + {id: 'fake-option-1', name: 'fake label 1'}, + {id: 'fake-option-2', name: 'fake label 2'}, + {id: 'fake-option-3', name: 'fake label 3'} + ], + value: [], + selectionType: 'multiple' + }); + expect(field.hasMultipleValues).toBe(true); + expect(field.hasEmptyValue).toBe(false); }); it('should parse and resolve radio button value', () => { diff --git a/lib/core/form/components/widgets/core/form-field.model.ts b/lib/core/form/components/widgets/core/form-field.model.ts index 9a9f09fcb5..f15c6113fe 100644 --- a/lib/core/form/components/widgets/core/form-field.model.ts +++ b/lib/core/form/components/widgets/core/form-field.model.ts @@ -71,6 +71,7 @@ export class FormFieldModel extends FormWidgetModel { enableFractions: boolean = false; currency: string = null; dateDisplayFormat: string = this.defaultDateFormat; + selectionType: 'single' | 'multiple' = null; // container model members numberOfColumns: number = 1; @@ -115,6 +116,10 @@ export class FormFieldModel extends FormWidgetModel { return this._isValid; } + get hasMultipleValues() { + return this.selectionType === 'multiple'; + } + markAsInvalid() { this._isValid = false; } @@ -172,6 +177,7 @@ export class FormFieldModel extends FormWidgetModel { this._value = this.parseValue(json); this.validationSummary = new ErrorMessageModel(); this.tooltip = json.tooltip; + this.selectionType = json.selectionType; if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') { this.placeholder = json.placeholder; @@ -206,8 +212,13 @@ export class FormFieldModel extends FormWidgetModel { } } - if (this.hasEmptyValue && this.options && this.options.length > 0) { - this.emptyOption = this.options[0]; + const emptyOption = Array.isArray(this.options) ? this.options.find(({ id }) => id === 'empty') : undefined; + if (this.hasEmptyValue === undefined) { + this.hasEmptyValue = json?.hasEmptyValue ?? !!emptyOption; + } + + if (this.options && this.options.length > 0 && this.hasEmptyValue) { + this.emptyOption = emptyOption; } this.updateForm(); @@ -291,6 +302,10 @@ export class FormFieldModel extends FormWidgetModel { } } } + + if (this.hasMultipleValues) { + value = Array.isArray(json.value) ? json.value : []; + } } /* @@ -344,9 +359,17 @@ export class FormFieldModel extends FormWidgetModel { This is needed due to Activiti reading dropdown values as string but saving back as object: { id: , name: } */ - if (this.value === 'empty' || this.value === '') { - this.form.values[this.id] = {}; - } else { + if (Array.isArray(this.value)) { + this.form.values[this.id] = this.value; + break; + } + + if (typeof this.value === 'string') { + if (this.value === 'empty' || this.value === '') { + this.form.values[this.id] = {}; + break; + } + const entry: FormFieldOption[] = this.options.filter((opt) => opt.id === this.value); if (entry.length > 0) { this.form.values[this.id] = entry[0]; diff --git a/lib/core/form/components/widgets/core/form.model.ts b/lib/core/form/components/widgets/core/form.model.ts index bc0b45ca32..41afa6ad7e 100644 --- a/lib/core/form/components/widgets/core/form.model.ts +++ b/lib/core/form/components/widgets/core/form.model.ts @@ -385,7 +385,7 @@ export class FormModel { addValuesNotPresent(valuesToSetIfNotPresent: FormValues) { this.getFormFields().forEach(field => { - if (valuesToSetIfNotPresent[field.id] && (!this.values[field.id] || this.isEmptyDropdownOption(field.id))) { + if (valuesToSetIfNotPresent[field.id] && (!this.values[field.id] || this.isValidDropDown(field.id))) { this.values[field.id] = valuesToSetIfNotPresent[field.id]; field.json.value = this.values[field.id]; field.value = field.parseValue(field.json); @@ -393,8 +393,12 @@ export class FormModel { }); } - private isEmptyDropdownOption(key: string): boolean { - if (this.getFieldById(key) && (this.getFieldById(key).type === FormFieldTypes.DROPDOWN)) { + private isValidDropDown(key: string): boolean { + const field = this.getFieldById(key); + if (field.type === FormFieldTypes.DROPDOWN) { + if (field.hasMultipleValues) { + return Array.isArray(this.values[key]); + } return typeof this.values[key] === 'string' ? this.values[key] === 'empty' : Object.keys(this.values[key]).length === 0; } return false; diff --git a/lib/core/form/services/widget-visibility.service.spec.ts b/lib/core/form/services/widget-visibility.service.spec.ts index d969522c75..36b41cee19 100644 --- a/lib/core/form/services/widget-visibility.service.spec.ts +++ b/lib/core/form/services/widget-visibility.service.spec.ts @@ -35,7 +35,7 @@ import { tabInvalidFormVisibility, fakeFormChainedVisibilityJson, fakeFormCheckBoxVisibilityJson -} from 'core/mock/form/widget-visibility.service.mock'; +} from '../../mock/form/widget-visibility.service.mock'; import { CoreTestingModule } from '../../testing/core.testing.module'; import { TranslateModule } from '@ngx-translate/core'; @@ -63,10 +63,6 @@ describe('WidgetVisibilityService', () => { jasmine.Ajax.uninstall(); }); - describe('should be able to evaluate logic operations', () => { - - }); - describe('should be able to evaluate next condition operations', () => { it('using == and return true', () => { @@ -143,6 +139,26 @@ describe('WidgetVisibilityService', () => { booleanResult = service.evaluateCondition(null, null, undefined); expect(booleanResult).toBeUndefined(); }); + + it('should return true when element contains', () => { + booleanResult = service.evaluateCondition(['one', 'two'], ['one'], 'contains'); + expect(booleanResult).toBe(true); + }); + + it('should return false when element not contains', () => { + booleanResult = service.evaluateCondition(['two'], ['one'], 'contains'); + expect(booleanResult).toBe(false); + }); + + it('should return true when element not contains', () => { + booleanResult = service.evaluateCondition(['two'], ['one'], '!contains'); + expect(booleanResult).toBe(true); + }); + + it('should return false when element contains', () => { + booleanResult = service.evaluateCondition(['one', 'two'], ['one'], '!contains'); + expect(booleanResult).toBe(false); + }); }); describe('should retrieve the process variables', () => { diff --git a/lib/core/form/services/widget-visibility.service.ts b/lib/core/form/services/widget-visibility.service.ts index 33a5f02742..4b74590d3a 100644 --- a/lib/core/form/services/widget-visibility.service.ts +++ b/lib/core/form/services/widget-visibility.service.ts @@ -178,10 +178,16 @@ export class WidgetVisibilityService { if (fieldId && fieldId.indexOf('_LABEL') > 0) { labelFilterByName = fieldId.substring(0, fieldId.length - 6); if (valueList[labelFilterByName]) { - valueFound = valueList[labelFilterByName].name; + if (Array.isArray(valueList[labelFilterByName])) { + valueFound = valueList[labelFilterByName].map(({name}) => name); + } else { + valueFound = valueList[labelFilterByName].name; + } } } else if (valueList[fieldId] && valueList[fieldId].id) { valueFound = valueList[fieldId].id; + } else if (valueList[fieldId] && Array.isArray(valueList[fieldId])) { + valueFound = valueList[fieldId].map(({id}) => id); } else { valueFound = valueList[fieldId]; } @@ -315,12 +321,20 @@ export class WidgetVisibilityService { return leftValue ? leftValue === '' : true; case '!empty': return leftValue ? leftValue !== '' : false; + case 'contains': + return this.contains(leftValue, rightValue); + case '!contains': + return !this.contains(leftValue, rightValue); default: this.logService.error(`Invalid operator: ${operator}`); return undefined; } } + private contains(leftValue: any, rightValue: any) { + return Array.isArray(leftValue) && Array.isArray(rightValue) && rightValue.every((element) => leftValue.includes(element)); + } + cleanProcessVariable() { this.processVarList = []; } diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html index 9026333726..ec010e469b 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.html @@ -10,7 +10,8 @@ (ngModelChange)="onFieldChanged(field)" [matTooltip]="field.tooltip" matTooltipPosition="above" - matTooltipShowDelay="1000"> + matTooltipShowDelay="1000" + [multiple]="field.hasMultipleValues"> {{opt.name}} diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts index 8e9da4d213..dc9ec45330 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.spec.ts @@ -380,4 +380,60 @@ describe('DropdownCloudWidgetComponent', () => { }); }); }); + + describe('multiple selection', () => { + + it('should show preselected option', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), { + id: 'dropdown-id', + name: 'date-name', + type: 'dropdown-cloud', + readOnly: 'false', + options: fakeOptionList, + selectionType: 'multiple', + value: [ + { id: 'opt_1', name: 'option_1' }, + { id: 'opt_2', name: 'option_2' } + ] + }); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const selectedPlaceHolder = fixture.debugElement.query(By.css('.mat-select-value-text span')); + expect(selectedPlaceHolder.nativeElement.getInnerHTML()).toEqual('option_1, option_2'); + + openSelect('#dropdown-id'); + await fixture.whenStable(); + fixture.detectChanges(); + + const options = fixture.debugElement.queryAll(By.css('.mat-selected span')); + expect(Array.from(options).map(({ nativeElement }) => nativeElement.getInnerHTML().trim())) + .toEqual(['option_1', 'option_2']); + }); + + it('should support multiple options', async () => { + widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id' }), { + id: 'dropdown-id', + name: 'date-name', + type: 'dropdown-cloud', + readOnly: 'false', + selectionType: 'multiple', + options: fakeOptionList + }); + fixture.detectChanges(); + openSelect('#dropdown-id'); + await fixture.whenStable(); + fixture.detectChanges(); + + const optionOne = fixture.debugElement.query(By.css('[id="opt_1"]')); + const optionTwo = fixture.debugElement.query(By.css('[id="opt_2"]')); + optionOne.triggerEventHandler('click', null); + optionTwo.triggerEventHandler('click', null); + expect(widget.field.value).toEqual([ + { id: 'opt_1', name: 'option_1' }, + { id: 'opt_2', name: 'option_2' } + ]); + }); + }); }); diff --git a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts index 03ad25610d..2db44c04f2 100644 --- a/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts +++ b/lib/process-services-cloud/src/lib/form/components/widgets/dropdown/dropdown-cloud.widget.ts @@ -80,11 +80,31 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI }); } - compareDropdownValues(opt1: string, opt2: FormFieldOption | string): boolean { - return opt1 && opt2 && typeof opt2 !== 'string' ? (opt1 === opt2.id || opt1 === opt2.name) : opt1 === opt2; + compareDropdownValues(opt1: FormFieldOption | string, opt2: FormFieldOption | string): boolean { + if (!opt1 || !opt2) { + return false; + } + + if (typeof opt1 === 'string' && typeof opt2 === 'object') { + return opt1 === opt2.id || opt1 === opt2.name; + } + + if (typeof opt1 === 'object' && typeof opt2 === 'string') { + return opt1.id === opt2 || opt1.name === opt2; + } + + if (typeof opt1 === 'object' && typeof opt2 === 'object') { + return opt1.id === opt2.id && opt1.name === opt2.name; + } + + return opt1 === opt2; } - getOptionValue(option: FormFieldOption, fieldValue: string): string { + getOptionValue(option: FormFieldOption, fieldValue: string): string | FormFieldOption { + if (this.field.hasMultipleValues) { + return option; + } + let optionValue: string = ''; if (option.id === 'empty' || option.name !== fieldValue) { optionValue = option.id;