[AAE-15082][AAE-15081] Resolve the options coming from a json variabl… (#8673)

* [AAE-15082][AAE-15081] Resolve the options coming from a json variable for a dropdown

* split method to smaller parts and remove duplications in units

* fix unit tests

* get variables from API call

* [AAE-15082] Add handle form variable

* replace variableId by variableName

* improve code
This commit is contained in:
Tomasz Gnyp
2023-06-29 17:21:14 +02:00
committed by GitHub
parent dabe4ca279
commit e3ea23da37
7 changed files with 379 additions and 20 deletions

View File

@@ -37,5 +37,7 @@
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"></error-widget>
<error-widget class="adf-dropdown-failed-message" *ngIf="isRestApiFailed"
required="{{ 'FORM.FIELD.REST_API_FAILED' | translate: { hostname: restApiHostName } }}"></error-widget>
<error-widget class="adf-dropdown-failed-message" *ngIf="variableOptionsFailed"
required="{{ 'FORM.FIELD.VARIABLE_DROPDOWN_OPTIONS_FAILED' | translate }}"></error-widget>
</div>
</div>

View File

@@ -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<DropdownCloudWidgetComponent>;
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');
});
});
});

View File

@@ -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<FormFieldOption[]>;
filter$ = new BehaviorSubject<string>('');
private readonly defaultVariableOptionId = 'id';
private readonly defaultVariableOptionLabel = 'name';
private readonly defaultVariableOptionPath = 'data';
protected onDestroy$ = new Subject<boolean>();
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 {

View File

@@ -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' })
];