diff --git a/lib/core/src/lib/form/components/widgets/core/form-field-variable-options.ts b/lib/core/src/lib/form/components/widgets/core/form-field-variable-options.ts
new file mode 100644
index 0000000000..11f3cd9d72
--- /dev/null
+++ b/lib/core/src/lib/form/components/widgets/core/form-field-variable-options.ts
@@ -0,0 +1,23 @@
+/*!
+ * @license
+ * Copyright © 2005-2023 Hyland Software, Inc. and its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export interface VariableConfig {
+ variableName: string;
+ optionsPath?: string;
+ optionsId?: string;
+ optionsLabel?: string;
+}
diff --git a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts
index 6105b6204b..8488bd2757 100644
--- a/lib/core/src/lib/form/components/widgets/core/form-field.model.ts
+++ b/lib/core/src/lib/form/components/widgets/core/form-field.model.ts
@@ -27,6 +27,7 @@ import { FormWidgetModel } from './form-widget.model';
import { FormFieldRule } from './form-field-rule';
import { ProcessFormModel } from './process-form-model.interface';
import { isNumberValue } from './form-field-utils';
+import { VariableConfig } from './form-field-variable-options';
// Maps to FormFieldRepresentation
export class FormFieldModel extends FormWidgetModel {
@@ -66,7 +67,7 @@ export class FormFieldModel extends FormWidgetModel {
restLabelProperty: string;
hasEmptyValue: boolean;
className: string;
- optionType: 'rest' | 'manual' ;
+ optionType: 'rest' | 'manual' | 'variable';
params: FormFieldMetadata = {};
hyperlinkUrl: string;
displayText: string;
@@ -81,6 +82,7 @@ export class FormFieldModel extends FormWidgetModel {
selectLoggedUser: boolean;
groupsRestriction: string[];
leftLabels: boolean = false;
+ variableConfig: VariableConfig;
// container model members
numberOfColumns: number = 1;
@@ -194,6 +196,7 @@ export class FormFieldModel extends FormWidgetModel {
this.rule = json.rule;
this.selectLoggedUser = json.selectLoggedUser;
this.groupsRestriction = json.groupsRestriction?.groups;
+ this.variableConfig = json.variableConfig;
if (json.placeholder && json.placeholder !== '' && json.placeholder !== 'null') {
this.placeholder = json.placeholder;
diff --git a/lib/core/src/lib/i18n/en.json b/lib/core/src/lib/i18n/en.json
index 9a6a2f690f..0665c6adf8 100644
--- a/lib/core/src/lib/i18n/en.json
+++ b/lib/core/src/lib/i18n/en.json
@@ -46,6 +46,7 @@
"UPLOAD": "UPLOAD",
"REQUIRED": "This is a required field",
"REST_API_FAILED": "The server `{{ hostname }}` is not reachable",
+ "VARIABLE_DROPDOWN_OPTIONS_FAILED": "There was a problem loading dropdown elements. Please contact administrator.",
"FILE_NAME": "File Name",
"NO_FILE_ATTACHED": "No file attached",
"VALIDATOR": {
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 ef2320dc0a..abf377e0c3 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
@@ -37,5 +37,7 @@
required="{{ 'FORM.FIELD.REQUIRED' | translate }}">
+
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 9565709000..b93287497c 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
@@ -19,7 +19,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { of, throwError } from 'rxjs';
import { DropdownCloudWidgetComponent } from './dropdown-cloud.widget';
-import { FormFieldModel, FormModel, FormService, setupTestBed, FormFieldEvent, FormFieldTypes } from '@alfresco/adf-core';
+import {
+ FormFieldModel,
+ FormModel,
+ FormService,
+ setupTestBed,
+ FormFieldEvent,
+ FormFieldTypes,
+ LogService
+} from '@alfresco/adf-core';
import { FormCloudService } from '../../../services/form-cloud.service';
import { ProcessServiceCloudTestingModule } from '../../../../testing/process-service-cloud.testing.module';
import { TranslateModule } from '@ngx-translate/core';
@@ -27,16 +35,22 @@ import {
fakeOptionList,
filterOptionList,
mockConditionalEntries,
+ mockFormVariableWithJson,
+ mockPlayersResponse,
mockRestDropdownOptions,
- mockSecondRestDropdownOptions
+ mockSecondRestDropdownOptions,
+ mockVariablesWithDefaultJson,
+ mockProcessVariablesWithJson
} from '../../../mocks/dropdown.mock';
import { OverlayContainer } from '@angular/cdk/overlay';
+import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
describe('DropdownCloudWidgetComponent', () => {
let formService: FormService;
let widget: DropdownCloudWidgetComponent;
let formCloudService: FormCloudService;
+ let logService: LogService;
let overlayContainer: OverlayContainer;
let fixture: ComponentFixture;
let element: HTMLElement;
@@ -64,6 +78,7 @@ describe('DropdownCloudWidgetComponent', () => {
formService = TestBed.inject(FormService);
formCloudService = TestBed.inject(FormCloudService);
overlayContainer = TestBed.inject(OverlayContainer);
+ logService = TestBed.inject(LogService);
});
afterEach(() => fixture.destroy());
@@ -876,4 +891,136 @@ describe('DropdownCloudWidgetComponent', () => {
});
});
});
+
+ describe('variable options', () => {
+ let logServiceSpy: jasmine.Spy;
+ const errorIcon: string = 'error_outline';
+
+ const getVariableDropdownWidget = (
+ variableName: string,
+ optionsPath: string,
+ optionsId: string,
+ optionsLabel: string,
+ processVariables?: TaskVariableCloud[],
+ variables?: TaskVariableCloud[]
+ ) => new FormFieldModel(
+ new FormModel({ taskId: 'fake-task-id', processVariables, variables }), {
+ id: 'variable-dropdown-id',
+ name: 'variable-options-dropdown',
+ type: 'dropdown',
+ readOnly: 'false',
+ optionType: 'variable',
+ variableConfig: {
+ variableName,
+ optionsPath,
+ optionsId,
+ optionsLabel
+ }
+ });
+
+ const checkDropdownVariableOptionsFailed = () => {
+ const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-dropdown-failed-message'));
+ expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe(errorIcon.concat('FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED'));
+
+ expect(widget.field.options.length).toEqual(0);
+ };
+
+ beforeEach(() => {
+ logServiceSpy = spyOn(logService, 'error');
+ });
+
+ it('should display options persisted from process variable', async () => {
+ widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'playerId', 'playerFullName', mockProcessVariablesWithJson);
+ fixture.detectChanges();
+ await openSelect('variable-dropdown-id');
+
+ const optOne: any = fixture.debugElement.query(By.css('[id="player-1"]'));
+ const optTwo: any = fixture.debugElement.query(By.css('[id="player-2"]'));
+ const optThree: any = fixture.debugElement.query(By.css('[id="player-3"]'));
+
+ expect(widget.field.options.length).toEqual(3);
+ expect(optOne.context.value).toBe('player-1');
+ expect(optOne.context.viewValue).toBe('Lionel Messi');
+ expect(optTwo.context.value).toBe('player-2');
+ expect(optTwo.context.viewValue).toBe('Cristiano Ronaldo');
+ expect(optThree.context.value).toBe('player-3');
+ expect(optThree.context.viewValue).toBe('Robert Lewandowski');
+ });
+
+ it('should display options persisted from form variable if there are NO process variables', async () => {
+ widget.field = getVariableDropdownWidget('json-form-variable', 'countries', 'id', 'name', [], mockFormVariableWithJson);
+ fixture.detectChanges();
+ await openSelect('variable-dropdown-id');
+
+ const optOne: any = fixture.debugElement.query(By.css('[id="PL"]'));
+ const optTwo: any = fixture.debugElement.query(By.css('[id="UK"]'));
+ const optThree: any = fixture.debugElement.query(By.css('[id="GR"]'));
+
+ expect(widget.field.options.length).toEqual(3);
+ expect(optOne.context.value).toBe('PL');
+ expect(optOne.context.viewValue).toBe('Poland');
+ expect(optTwo.context.value).toBe('UK');
+ expect(optTwo.context.viewValue).toBe('United Kingdom');
+ expect(optThree.context.value).toBe('GR');
+ expect(optThree.context.viewValue).toBe('Greece');
+ });
+
+ it('should display default options if config options are NOT provided', async () => {
+ widget.field = getVariableDropdownWidget('variables.json-default-variable', null, null, null, mockVariablesWithDefaultJson);
+ fixture.detectChanges();
+ await openSelect('variable-dropdown-id');
+
+ const optOne: any = fixture.debugElement.query(By.css('[id="default-pet-1"]'));
+ const optTwo: any = fixture.debugElement.query(By.css('[id="default-pet-2"]'));
+ const optThree: any = fixture.debugElement.query(By.css('[id="default-pet-3"]'));
+
+ expect(widget.field.options.length).toEqual(3);
+ expect(optOne.context.value).toBe('default-pet-1');
+ expect(optOne.context.viewValue).toBe('Dog');
+ expect(optTwo.context.value).toBe('default-pet-2');
+ expect(optTwo.context.viewValue).toBe('Cat');
+ expect(optThree.context.value).toBe('default-pet-3');
+ expect(optThree.context.viewValue).toBe('Parrot');
+ });
+
+ it('should return empty array and display error when path is incorrect', () => {
+ widget.field = getVariableDropdownWidget('variables.json-variable', 'response.wrongPath.players', 'playerId', 'playerFullName', mockProcessVariablesWithJson);
+ fixture.detectChanges();
+
+ checkDropdownVariableOptionsFailed();
+ expect(logServiceSpy).toHaveBeenCalledWith(`wrongPath not found in ${JSON.stringify(mockPlayersResponse.response)}`);
+ });
+
+ it('should return empty array and display error when id is incorrect', () => {
+ widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'wrongId', 'playerFullName', mockProcessVariablesWithJson);
+ fixture.detectChanges();
+
+ checkDropdownVariableOptionsFailed();
+ expect(logServiceSpy).toHaveBeenCalledWith(`'id' or 'label' is not properly defined`);
+ });
+
+ it('should return empty array and display error when label is incorrect', () => {
+ widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'playerId', 'wrongFullName', mockProcessVariablesWithJson);
+ fixture.detectChanges();
+
+ checkDropdownVariableOptionsFailed();
+ expect(logServiceSpy).toHaveBeenCalledWith(`'id' or 'label' is not properly defined`);
+ });
+
+ it('should return empty array and display error when variable is NOT found', () => {
+ widget.field = getVariableDropdownWidget('variables.wrong-variable-id', 'response.people.players', 'playerId', 'playerFullName', mockProcessVariablesWithJson);
+ fixture.detectChanges();
+
+ checkDropdownVariableOptionsFailed();
+ expect(logServiceSpy).toHaveBeenCalledWith(`variables.wrong-variable-id not found`);
+ });
+
+ it('should return empty array and display error if there are NO process and form variables', () => {
+ widget.field = getVariableDropdownWidget('variables.json-variable', 'response.people.players', 'playerId', 'playerFullName', [], []);
+ fixture.detectChanges();
+
+ checkDropdownVariableOptionsFailed();
+ expect(logServiceSpy).toHaveBeenCalledWith('variables.json-variable not found');
+ });
+ });
});
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 5f93c5a80d..1ae7673e66 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
@@ -30,6 +30,7 @@ import {
import { FormCloudService } from '../../../services/form-cloud.service';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import { filter, map, takeUntil } from 'rxjs/operators';
+import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
export const DEFAULT_OPTION = {
id: 'empty',
@@ -60,10 +61,15 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
typeId = 'DropdownCloudWidgetComponent';
showInputFilter = false;
isRestApiFailed = false;
+ variableOptionsFailed = false;
restApiHostName: string;
list$: Observable;
filter$ = new BehaviorSubject('');
+ private readonly defaultVariableOptionId = 'id';
+ private readonly defaultVariableOptionLabel = 'name';
+ private readonly defaultVariableOptionPath = 'data';
+
protected onDestroy$ = new Subject();
constructor(public formService: FormService,
@@ -74,25 +80,113 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
ngOnInit() {
- if (this.field.restUrl && !this.isLinkedWidget()) {
- this.persistFieldOptionsFromRestApi();
- }
-
- if (this.isLinkedWidget()) {
- this.loadFieldOptionsForLinkedWidget();
-
- this.formService.formFieldValueChanged
- .pipe(
- filter((event: FormFieldEvent) => this.isFormFieldEventOfTypeDropdown(event) && this.isParentFormFieldEvent(event)),
- takeUntil(this.onDestroy$))
- .subscribe((event: FormFieldEvent) => {
- const valueOfParentWidget = event.field.value;
- this.parentValueChanged(valueOfParentWidget);
- });
- }
+ this.checkFieldOptionsSource();
this.updateOptions();
}
+ private checkFieldOptionsSource(): void {
+ switch (true) {
+ case this.field.restUrl && !this.isLinkedWidget():
+ this.persistFieldOptionsFromRestApi();
+ break;
+
+ case this.isLinkedWidget():
+ this.loadFieldOptionsForLinkedWidget();
+ break;
+
+ case this.isVariableOptionType():
+ this.persistFieldOptionsFromVariable();
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ private persistFieldOptionsFromVariable(): void {
+ const optionsPath = this.field?.variableConfig?.optionsPath ?? this.defaultVariableOptionPath;
+ const variableName = this.field?.variableConfig?.variableName;
+ const processVariables = this.field?.form?.processVariables;
+ const formVariables = this.field?.form?.variables;
+
+ const dropdownOptions = this.getOptionsFromVariable(processVariables, formVariables, variableName);
+
+ if (dropdownOptions) {
+ const formVariableOptions: FormFieldOption[] = this.getOptionsFromPath(dropdownOptions, optionsPath);
+ this.field.options = formVariableOptions;
+ this.resetInvalidValue();
+ this.field.updateForm();
+ } else {
+ this.handleError(`${variableName} not found`);
+ this.resetOptions();
+ this.variableOptionsFailed = true;
+ }
+ }
+
+ private getOptionsFromPath(data: any, path: string): FormFieldOption[] {
+ const optionsId = this.field?.variableConfig?.optionsId ?? this.defaultVariableOptionId;
+ const optionsLabel = this.field?.variableConfig?.optionsLabel ?? this.defaultVariableOptionLabel;
+
+ const properties = path.split('.');
+ const currentProperty = properties.shift();
+
+ if (!data.hasOwnProperty(currentProperty)) {
+ this.handleError(`${currentProperty} not found in ${JSON.stringify(data)}`);
+ this.variableOptionsFailed = true;
+ return [];
+ }
+
+ const nestedData = data[currentProperty];
+
+ if (Array.isArray(nestedData)) {
+ return this.getOptionsFromArray(nestedData, optionsId, optionsLabel);
+ }
+
+ return this.getOptionsFromPath(nestedData, properties.join('.'));
+ }
+
+ private getOptionsFromArray(nestedData: any[], id: string, label: string): FormFieldOption[] {
+ const options = nestedData.map(item => this.createOption(item, id, label));
+ const hasInvalidOption = options.some(option => !option);
+
+ if (hasInvalidOption) {
+ this.variableOptionsFailed = true;
+ return [];
+ }
+
+ this.variableOptionsFailed = false;
+ return options;
+ }
+
+ private createOption(item: any, id: string, label: string): FormFieldOption {
+ const option: FormFieldOption = {
+ id: item[id],
+ name: item[label]
+ };
+
+ if (!option.id || !option.name) {
+ this.handleError(`'id' or 'label' is not properly defined`);
+ return undefined;
+ }
+
+ return option;
+ }
+
+ private getOptionsFromVariable(processVariables: TaskVariableCloud[], formVariables: TaskVariableCloud[], variableName: string): TaskVariableCloud {
+ const processVariableDropdownOptions: TaskVariableCloud = this.getVariableValueByName(processVariables, variableName);
+ const formVariableDropdownOptions: TaskVariableCloud = this.getVariableValueByName(formVariables, variableName);
+
+ return processVariableDropdownOptions ?? formVariableDropdownOptions;
+ }
+
+ private getVariableValueByName(variables: TaskVariableCloud[], variableName: string): any {
+ return variables?.find((variable: TaskVariableCloud) => variable?.name === `variables.${variableName}` || variable?.name === variableName)?.value;
+ }
+
+ private isVariableOptionType(): boolean {
+ return this.field?.optionType === 'variable';
+ }
+
private persistFieldOptionsFromRestApi() {
if (this.isValidRestType()) {
this.resetRestApiErrorMessage();
@@ -125,6 +219,15 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
private loadFieldOptionsForLinkedWidget() {
const parentWidgetValue = this.getParentWidgetValue();
this.parentValueChanged(parentWidgetValue);
+
+ this.formService.formFieldValueChanged
+ .pipe(
+ filter((event: FormFieldEvent) => this.isFormFieldEventOfTypeDropdown(event) && this.isParentFormFieldEvent(event)),
+ takeUntil(this.onDestroy$))
+ .subscribe((event: FormFieldEvent) => {
+ const valueOfParentWidget = event.field.value;
+ this.parentValueChanged(valueOfParentWidget);
+ });
}
private getParentWidgetValue(): string {
@@ -317,7 +420,10 @@ export class DropdownCloudWidgetComponent extends WidgetComponent implements OnI
}
showRequiredMessage(): boolean {
- return (this.isInvalidFieldRequired() || (this.isNoneValueSelected(this.field.value) && this.isRequired())) && this.isTouched() && !this.isRestApiFailed;
+ return (this.isInvalidFieldRequired() || (this.isNoneValueSelected(this.field.value) && this.isRequired())) &&
+ this.isTouched() &&
+ !this.isRestApiFailed &&
+ !this.variableOptionsFailed;
}
getDefaultOption(options: FormFieldOption[]): FormFieldOption {
diff --git a/lib/process-services-cloud/src/lib/form/mocks/dropdown.mock.ts b/lib/process-services-cloud/src/lib/form/mocks/dropdown.mock.ts
index a13488c8a0..20262f4297 100644
--- a/lib/process-services-cloud/src/lib/form/mocks/dropdown.mock.ts
+++ b/lib/process-services-cloud/src/lib/form/mocks/dropdown.mock.ts
@@ -16,6 +16,7 @@
*/
import { FormFieldOption } from '@alfresco/adf-core';
+import { TaskVariableCloud } from '../models/task-variable-cloud.model';
export const mockConditionalEntries = [
{
@@ -103,3 +104,79 @@ export const filterOptionList = [
{ id: 'opt_5', name: 'option_5' },
{ id: 'opt_6', name: 'option_6' }
];
+
+export const mockPlayersResponse = {
+ response: {
+ people: {
+ players:
+ [
+ {
+ playerId: 'player-1',
+ playerFullName: 'Lionel Messi',
+ totalGoals: 999,
+ shirtNumber: 10
+ },
+ {
+ playerId: 'player-2',
+ playerFullName: 'Cristiano Ronaldo',
+ totalGoals: 15,
+ shirtNumber: 7
+ },
+ {
+ playerId: 'player-3',
+ playerFullName: 'Robert Lewandowski',
+ totalGoals: 500,
+ shirtNumber: 9
+ }
+ ]
+ }
+ }
+};
+
+export const mockDefaultResponse = {
+ data:
+ [
+ {
+ id: 'default-pet-1',
+ name: 'Dog'
+ },
+ {
+ id: 'default-pet-2',
+ name: 'Cat'
+ },
+ {
+ id: 'default-pet-3',
+ name: 'Parrot'
+ }
+ ]
+};
+
+export const mockCountriesResponse = {
+ countries: [
+ {
+ id: 'PL',
+ name: 'Poland'
+ },
+ {
+ id: 'UK',
+ name: 'United Kingdom'
+ },
+ {
+ id: 'GR',
+ name: 'Greece'
+ }
+ ]
+};
+
+export const mockFormVariableWithJson = [
+ new TaskVariableCloud({ name: 'json-form-variable', value: mockCountriesResponse, type: 'json', id: 'fake-id-1' })
+];
+
+export const mockProcessVariablesWithJson = [
+ new TaskVariableCloud({ name: 'variables.json-variable', value: mockPlayersResponse, type: 'json', id: 'fake-id-1' }),
+ new TaskVariableCloud({ name: 'variables.different-variable', value: 'fake-value', type: 'json', id: 'fake-id-2' })
+];
+
+export const mockVariablesWithDefaultJson = [
+ new TaskVariableCloud({ name: 'variables.json-default-variable', value: mockDefaultResponse, type: 'json', id: 'fake-id-1' })
+];