mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2025-07-24 17:32:15 +00:00
[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
This commit is contained in:
@@ -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 () => {
|
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, {
|
await userFiltersApi.updateUserProcessInstanceFilter(filterId, {
|
||||||
'appId': null,
|
'appId': null,
|
||||||
'name': processFilter.edited,
|
'name': processFilter.edited,
|
||||||
|
@@ -71,6 +71,9 @@ describe('FormFieldValidator', () => {
|
|||||||
const field = new FormFieldModel(new FormModel(), {
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
type: FormFieldTypes.DROPDOWN,
|
type: FormFieldTypes.DROPDOWN,
|
||||||
value: '<empty>',
|
value: '<empty>',
|
||||||
|
options: [
|
||||||
|
{id: 'empty', name: 'Choose option...'}
|
||||||
|
],
|
||||||
hasEmptyValue: true,
|
hasEmptyValue: true,
|
||||||
required: true
|
required: true
|
||||||
});
|
});
|
||||||
@@ -82,6 +85,22 @@ describe('FormFieldValidator', () => {
|
|||||||
expect(validator.validate(field)).toBeTruthy();
|
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 = <FormFieldOption> { id: 'empty' };
|
||||||
|
expect(validator.validate(field)).toBeFalsy();
|
||||||
|
|
||||||
|
field.value = [];
|
||||||
|
expect(validator.validate(field)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should fail for radio buttons', () => {
|
it('should fail for radio buttons', () => {
|
||||||
const field = new FormFieldModel(new FormModel(), {
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
type: FormFieldTypes.RADIO_BUTTONS,
|
type: FormFieldTypes.RADIO_BUTTONS,
|
||||||
|
@@ -59,6 +59,10 @@ export class RequiredFieldValidator implements FormFieldValidator {
|
|||||||
if (this.isSupported(field) && field.isVisible) {
|
if (this.isSupported(field) && field.isVisible) {
|
||||||
|
|
||||||
if (field.type === FormFieldTypes.DROPDOWN) {
|
if (field.type === FormFieldTypes.DROPDOWN) {
|
||||||
|
if (field.hasMultipleValues) {
|
||||||
|
return !!field.value.length;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.hasEmptyValue && field.emptyOption) {
|
if (field.hasEmptyValue && field.emptyOption) {
|
||||||
if (field.value === field.emptyOption.id) {
|
if (field.value === field.emptyOption.id) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -460,13 +460,29 @@ describe('FormFieldModel', () => {
|
|||||||
const field = new FormFieldModel(new FormModel(), {
|
const field = new FormFieldModel(new FormModel(), {
|
||||||
type: FormFieldTypes.DROPDOWN,
|
type: FormFieldTypes.DROPDOWN,
|
||||||
options: [
|
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-2', name: 'fake label 2'},
|
||||||
{id: 'fake-option-3', name: 'fake label 3'}
|
{id: 'fake-option-3', name: 'fake label 3'}
|
||||||
],
|
],
|
||||||
value: 'fake-option-2'
|
value: 'fake-option-2'
|
||||||
});
|
});
|
||||||
expect(field.getOptionName()).toBe('fake label 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', () => {
|
it('should parse and resolve radio button value', () => {
|
||||||
|
@@ -71,6 +71,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
enableFractions: boolean = false;
|
enableFractions: boolean = false;
|
||||||
currency: string = null;
|
currency: string = null;
|
||||||
dateDisplayFormat: string = this.defaultDateFormat;
|
dateDisplayFormat: string = this.defaultDateFormat;
|
||||||
|
selectionType: 'single' | 'multiple' = null;
|
||||||
|
|
||||||
// container model members
|
// container model members
|
||||||
numberOfColumns: number = 1;
|
numberOfColumns: number = 1;
|
||||||
@@ -115,6 +116,10 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
return this._isValid;
|
return this._isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasMultipleValues() {
|
||||||
|
return this.selectionType === 'multiple';
|
||||||
|
}
|
||||||
|
|
||||||
markAsInvalid() {
|
markAsInvalid() {
|
||||||
this._isValid = false;
|
this._isValid = false;
|
||||||
}
|
}
|
||||||
@@ -172,6 +177,7 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
this._value = this.parseValue(json);
|
this._value = this.parseValue(json);
|
||||||
this.validationSummary = new ErrorMessageModel();
|
this.validationSummary = new ErrorMessageModel();
|
||||||
this.tooltip = json.tooltip;
|
this.tooltip = json.tooltip;
|
||||||
|
this.selectionType = json.selectionType;
|
||||||
|
|
||||||
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
|
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
|
||||||
this.placeholder = json.placeholder;
|
this.placeholder = json.placeholder;
|
||||||
@@ -206,8 +212,13 @@ export class FormFieldModel extends FormWidgetModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasEmptyValue && this.options && this.options.length > 0) {
|
const emptyOption = Array.isArray(this.options) ? this.options.find(({ id }) => id === 'empty') : undefined;
|
||||||
this.emptyOption = this.options[0];
|
if (this.hasEmptyValue === undefined) {
|
||||||
|
this.hasEmptyValue = json?.hasEmptyValue ?? !!emptyOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options && this.options.length > 0 && this.hasEmptyValue) {
|
||||||
|
this.emptyOption = emptyOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateForm();
|
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
|
This is needed due to Activiti reading dropdown values as string
|
||||||
but saving back as object: { id: <id>, name: <name> }
|
but saving back as object: { id: <id>, name: <name> }
|
||||||
*/
|
*/
|
||||||
if (this.value === 'empty' || this.value === '') {
|
if (Array.isArray(this.value)) {
|
||||||
this.form.values[this.id] = {};
|
this.form.values[this.id] = this.value;
|
||||||
} else {
|
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);
|
const entry: FormFieldOption[] = this.options.filter((opt) => opt.id === this.value);
|
||||||
if (entry.length > 0) {
|
if (entry.length > 0) {
|
||||||
this.form.values[this.id] = entry[0];
|
this.form.values[this.id] = entry[0];
|
||||||
|
@@ -385,7 +385,7 @@ export class FormModel {
|
|||||||
|
|
||||||
addValuesNotPresent(valuesToSetIfNotPresent: FormValues) {
|
addValuesNotPresent(valuesToSetIfNotPresent: FormValues) {
|
||||||
this.getFormFields().forEach(field => {
|
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];
|
this.values[field.id] = valuesToSetIfNotPresent[field.id];
|
||||||
field.json.value = this.values[field.id];
|
field.json.value = this.values[field.id];
|
||||||
field.value = field.parseValue(field.json);
|
field.value = field.parseValue(field.json);
|
||||||
@@ -393,8 +393,12 @@ export class FormModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private isEmptyDropdownOption(key: string): boolean {
|
private isValidDropDown(key: string): boolean {
|
||||||
if (this.getFieldById(key) && (this.getFieldById(key).type === FormFieldTypes.DROPDOWN)) {
|
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 typeof this.values[key] === 'string' ? this.values[key] === 'empty' : Object.keys(this.values[key]).length === 0;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@@ -35,7 +35,7 @@ import {
|
|||||||
tabInvalidFormVisibility,
|
tabInvalidFormVisibility,
|
||||||
fakeFormChainedVisibilityJson,
|
fakeFormChainedVisibilityJson,
|
||||||
fakeFormCheckBoxVisibilityJson
|
fakeFormCheckBoxVisibilityJson
|
||||||
} from 'core/mock/form/widget-visibility.service.mock';
|
} from '../../mock/form/widget-visibility.service.mock';
|
||||||
import { CoreTestingModule } from '../../testing/core.testing.module';
|
import { CoreTestingModule } from '../../testing/core.testing.module';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -63,10 +63,6 @@ describe('WidgetVisibilityService', () => {
|
|||||||
jasmine.Ajax.uninstall();
|
jasmine.Ajax.uninstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should be able to evaluate logic operations', () => {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should be able to evaluate next condition operations', () => {
|
describe('should be able to evaluate next condition operations', () => {
|
||||||
|
|
||||||
it('using == and return true', () => {
|
it('using == and return true', () => {
|
||||||
@@ -143,6 +139,26 @@ describe('WidgetVisibilityService', () => {
|
|||||||
booleanResult = service.evaluateCondition(null, null, undefined);
|
booleanResult = service.evaluateCondition(null, null, undefined);
|
||||||
expect(booleanResult).toBeUndefined();
|
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', () => {
|
describe('should retrieve the process variables', () => {
|
||||||
|
@@ -178,10 +178,16 @@ export class WidgetVisibilityService {
|
|||||||
if (fieldId && fieldId.indexOf('_LABEL') > 0) {
|
if (fieldId && fieldId.indexOf('_LABEL') > 0) {
|
||||||
labelFilterByName = fieldId.substring(0, fieldId.length - 6);
|
labelFilterByName = fieldId.substring(0, fieldId.length - 6);
|
||||||
if (valueList[labelFilterByName]) {
|
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) {
|
} else if (valueList[fieldId] && valueList[fieldId].id) {
|
||||||
valueFound = valueList[fieldId].id;
|
valueFound = valueList[fieldId].id;
|
||||||
|
} else if (valueList[fieldId] && Array.isArray(valueList[fieldId])) {
|
||||||
|
valueFound = valueList[fieldId].map(({id}) => id);
|
||||||
} else {
|
} else {
|
||||||
valueFound = valueList[fieldId];
|
valueFound = valueList[fieldId];
|
||||||
}
|
}
|
||||||
@@ -315,12 +321,20 @@ export class WidgetVisibilityService {
|
|||||||
return leftValue ? leftValue === '' : true;
|
return leftValue ? leftValue === '' : true;
|
||||||
case '!empty':
|
case '!empty':
|
||||||
return leftValue ? leftValue !== '' : false;
|
return leftValue ? leftValue !== '' : false;
|
||||||
|
case 'contains':
|
||||||
|
return this.contains(leftValue, rightValue);
|
||||||
|
case '!contains':
|
||||||
|
return !this.contains(leftValue, rightValue);
|
||||||
default:
|
default:
|
||||||
this.logService.error(`Invalid operator: ${operator}`);
|
this.logService.error(`Invalid operator: ${operator}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private contains(leftValue: any, rightValue: any) {
|
||||||
|
return Array.isArray(leftValue) && Array.isArray(rightValue) && rightValue.every((element) => leftValue.includes(element));
|
||||||
|
}
|
||||||
|
|
||||||
cleanProcessVariable() {
|
cleanProcessVariable() {
|
||||||
this.processVarList = [];
|
this.processVarList = [];
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,8 @@
|
|||||||
(ngModelChange)="onFieldChanged(field)"
|
(ngModelChange)="onFieldChanged(field)"
|
||||||
[matTooltip]="field.tooltip"
|
[matTooltip]="field.tooltip"
|
||||||
matTooltipPosition="above"
|
matTooltipPosition="above"
|
||||||
matTooltipShowDelay="1000">
|
matTooltipShowDelay="1000"
|
||||||
|
[multiple]="field.hasMultipleValues">
|
||||||
<mat-option *ngFor="let opt of field.options"
|
<mat-option *ngFor="let opt of field.options"
|
||||||
[value]="getOptionValue(opt, field.value)"
|
[value]="getOptionValue(opt, field.value)"
|
||||||
[id]="opt.id">{{opt.name}}
|
[id]="opt.id">{{opt.name}}
|
||||||
|
@@ -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' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -80,11 +80,31 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
compareDropdownValues(opt1: string, opt2: FormFieldOption | string): boolean {
|
compareDropdownValues(opt1: FormFieldOption | string, opt2: FormFieldOption | string): boolean {
|
||||||
return opt1 && opt2 && typeof opt2 !== 'string' ? (opt1 === opt2.id || opt1 === opt2.name) : opt1 === opt2;
|
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 = '';
|
let optionValue: string = '';
|
||||||
if (option.id === 'empty' || option.name !== fieldValue) {
|
if (option.id === 'empty' || option.name !== fieldValue) {
|
||||||
optionValue = option.id;
|
optionValue = option.id;
|
||||||
|
Reference in New Issue
Block a user