AAE-50868 Fix some visibility conditions scenarios with form variables (#12206)

* AAE-50868 Fix some visibility conditions senarios with form variables

* form 0 check updates

* copilot review suggestions
This commit is contained in:
Darren Thornton
2026-09-02 11:34:41 -05:00
committed by GitHub
parent b773e2b49d
commit 6bb7ec3b7d
8 changed files with 370 additions and 22 deletions
@@ -20,4 +20,5 @@ export interface FormVariableModel {
name: string;
type: string;
value?: any;
runtimeSet?: boolean;
}
@@ -600,6 +600,33 @@ describe('FormModel', () => {
const missing = form.getProcessVariableValue('missing');
expect(missing).toBeUndefined();
});
it('should return zero process variable value instead of the form default', () => {
const formWithZero = new FormModel({
variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 99 }],
processVariables: [{ name: 'variables.amount', value: 0, type: 'integer' }]
});
expect(formWithZero.getProcessVariableValue('amount')).toBe(0);
});
it('should return false process variable value instead of the form default', () => {
const formWithFalse = new FormModel({
variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }],
processVariables: [{ name: 'variables.flag', value: false, type: 'boolean' }]
});
expect(formWithFalse.getProcessVariableValue('flag')).toBe(false);
});
it('should return empty string process variable value instead of the form default', () => {
const formWithEmpty = new FormModel({
variables: [{ id: 'text-var', name: 'text', type: 'string', value: 'default' }],
processVariables: [{ name: 'variables.text', value: '', type: 'string' }]
});
expect(formWithEmpty.getProcessVariableValue('text')).toBe('');
});
});
describe('add values not present', () => {
@@ -307,22 +307,17 @@ export class FormModel implements ProcessFormModel {
* @returns process variable value
*/
getProcessVariableValue(name: string): any {
let value;
if (this.processVariables?.length) {
const names = [`variables.${name}`, name];
const processVariable = this.processVariables.find((entry) => names.includes(entry.name));
if (processVariable) {
value = this.parseValue(processVariable.type, processVariable.value);
return this.parseValue(processVariable.type, processVariable.value);
}
}
if (!value) {
value = this.getDefaultFormVariableValue(name);
}
return value;
return this.getDefaultFormVariableValue(name);
}
protected parseValue(type: string, value: any): any {
@@ -535,9 +530,20 @@ export class FormModel implements ProcessFormModel {
const variable = this.getFormVariable(variableId);
if (variable) {
variable.value = value;
variable.runtimeSet = true;
}
}
/**
* Checks whether a form variable has been given a value at runtime, for example by a form rule.
*
* @param identifier The `name` or `id` value
* @returns `true` when the value was set at runtime rather than coming from the form definition
*/
isVariableSetAtRuntime(identifier: string): boolean {
return !!this.getFormVariable(identifier)?.runtimeSet;
}
private loadInjectedFieldValidators(injectedFieldValidators: FormFieldValidator[]): void {
this.fieldValidators = injectedFieldValidators ? [...FORM_FIELD_VALIDATORS, ...injectedFieldValidators] : [...FORM_FIELD_VALIDATORS];
}
@@ -18,5 +18,5 @@
export class TaskProcessVariableModel {
id?: string;
type?: string;
value: string;
value: any;
}
@@ -1002,4 +1002,169 @@ describe('WidgetVisibilityService', () => {
expect(textField.isVisible).toBe(true);
});
});
describe('Visibility calculation from form variables', () => {
const hiddenWhileRequestor = new WidgetVisibilityModel({
leftType: 'variable',
leftValue: 'person_type',
operator: '!=',
rightType: 'value',
rightValue: 'Requestor',
nextConditionOperator: '',
nextCondition: null
});
const requestorProcessVariables = [{ id: 'variables.person_type', value: 'Requestor' }];
let formJson: any;
let form: FormModel;
beforeEach(() => {
formJson = {
id: 'person-type-form',
variables: [{ id: 'person-type-var', name: 'person_type', value: null }],
processVariables: [{ name: 'variables.person_type', value: 'Requestor' }]
};
form = new FormModel(formJson);
service.cleanProcessVariable();
});
it('should resolve a variable from the form when the cached process variables no longer hold it', () => {
service.refreshVisibility(form, requestorProcessVariables);
service.refreshVisibility(new FormModel({ id: 'another-form' }), [{ id: 'variables.other', value: 'other' }]);
expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(false);
});
it('should resolve a variable from the form when the refreshed data omits it', () => {
service.refreshVisibility(form, requestorProcessVariables);
service.refreshVisibility(form, [{ id: 'processOutput', value: 'result' }]);
expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(false);
});
it('should prefer a process variable over the value defined in the form definition', () => {
const formWithDefault = new FormModel({
id: 'person-type-form-with-default',
variables: [{ id: 'person-type-var', name: 'person_type', value: 'Approver' }],
processVariables: [{ name: 'variables.person_type', value: 'Requestor' }]
});
expect(service.getVariableValue(formWithDefault, 'person_type', requestorProcessVariables)).toBe('Requestor');
});
it('should prefer a variable changed at runtime over a process variable of the same name', () => {
service.refreshVisibility(form, requestorProcessVariables);
form.changeVariableValue('person-type-var', 'Approver');
service.refreshVisibility(form, requestorProcessVariables);
expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(true);
});
it('should prefer a variable cleared at runtime over a process variable of the same name', () => {
service.refreshVisibility(form, requestorProcessVariables);
form.changeVariableValue('person-type-var', '');
service.refreshVisibility(form, requestorProcessVariables);
expect(service.evaluateVisibility(form, hiddenWhileRequestor)).toBe(true);
});
it('should keep a variable changed at runtime when the form is rebuilt from the same definition', () => {
service.refreshVisibility(form, requestorProcessVariables);
form.changeVariableValue('person-type-var', 'Approver');
const rebuiltForm = new FormModel(formJson);
service.refreshVisibility(rebuiltForm, [{ id: 'processOutput', value: 'result' }]);
expect(service.evaluateVisibility(rebuiltForm, hiddenWhileRequestor)).toBe(true);
});
it('should not convert the type of a variable changed at runtime', () => {
const numericForm = new FormModel({ id: 'numeric-form', variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 0 }] });
const booleanForm = new FormModel({ id: 'boolean-form', variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }] });
service.refreshVisibility(numericForm);
service.refreshVisibility(booleanForm);
numericForm.changeVariableValue('amount-var', 5);
booleanForm.changeVariableValue('flag-var', false);
service.refreshVisibility(numericForm);
service.refreshVisibility(booleanForm);
expect(service.getVariableValue(numericForm, 'amount')).toBe(5);
expect(service.getVariableValue(booleanForm, 'flag', [])).toBe(false);
});
it('should evaluate a numeric variable changed at runtime without string comparison', () => {
const numericForm = new FormModel({
id: 'numeric-visibility-form',
variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 0 }]
});
const amountOverTen = new WidgetVisibilityModel({
leftType: 'variable',
leftValue: 'amount',
operator: '>',
rightType: 'value',
rightValue: '10',
nextConditionOperator: '',
nextCondition: null
});
service.refreshVisibility(numericForm);
numericForm.changeVariableValue('amount-var', 5);
service.refreshVisibility(numericForm);
expect(service.evaluateVisibility(numericForm, amountOverTen)).toBe(false);
});
it('should resolve a zero process variable from the form when the refreshed data omits it', () => {
const zeroForm = new FormModel({
id: 'zero-form',
variables: [{ id: 'amount-var', name: 'amount', type: 'integer', value: 99 }],
processVariables: [{ name: 'variables.amount', value: 0, type: 'integer' }]
});
const amountIsZero = new WidgetVisibilityModel({
leftType: 'variable',
leftValue: 'amount',
operator: '==',
rightType: 'value',
rightValue: '0',
nextConditionOperator: '',
nextCondition: null
});
service.refreshVisibility(zeroForm, [{ id: 'variables.amount', value: 0 }]);
service.refreshVisibility(zeroForm, [{ id: 'processOutput', value: 'result' }]);
expect(service.getVariableValue(zeroForm, 'amount', [])).toBe(0);
expect(service.evaluateVisibility(zeroForm, amountIsZero)).toBe(true);
});
it('should resolve a false process variable from the form when the refreshed data omits it', () => {
const falseForm = new FormModel({
id: 'false-form',
variables: [{ id: 'flag-var', name: 'flag', type: 'boolean', value: true }],
processVariables: [{ name: 'variables.flag', value: false, type: 'boolean' }]
});
const flagIsFalse = new WidgetVisibilityModel({
leftType: 'variable',
leftValue: 'flag',
operator: '==',
rightType: 'value',
rightValue: 'false',
nextConditionOperator: '',
nextCondition: null
});
service.refreshVisibility(falseForm, [{ id: 'variables.flag', value: false }]);
service.refreshVisibility(falseForm, [{ id: 'processOutput', value: 'result' }]);
expect(service.getVariableValue(falseForm, 'flag', [])).toBe(false);
expect(service.evaluateVisibility(falseForm, flagIsFalse)).toBe(true);
});
});
});
@@ -29,7 +29,7 @@ import { FormService } from './form.service';
export class WidgetVisibilityService {
private readonly formService = inject(FormService);
private processVarList: TaskProcessVariableModel[];
private processVarList: TaskProcessVariableModel[] = [];
private form: FormModel;
public refreshVisibility(form: FormModel, processVarList?: TaskProcessVariableModel[]) {
@@ -110,7 +110,7 @@ export class WidgetVisibilityService {
}
}
getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
getLeftValue(form: FormModel, visibilityObj: WidgetVisibilityModel): any {
let leftValue = '';
if (visibilityObj.leftType === WidgetTypeEnum.variable) {
leftValue = this.getVariableValue(form, visibilityObj.leftValue, this.processVarList);
@@ -124,7 +124,7 @@ export class WidgetVisibilityService {
return leftValue;
}
getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): string {
getRightValue(form: FormModel, visibilityObj: WidgetVisibilityModel): any {
let valueFound = '';
if (visibilityObj.rightType === WidgetTypeEnum.variable) {
@@ -270,20 +270,21 @@ export class WidgetVisibilityService {
return field.id && fieldToFind ? field.id.toUpperCase() === fieldToFind.toUpperCase() : false;
}
public getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[]): string {
const processVariableValue = this.getProcessVariableValue(name, processVarList);
const variableDefaultValue = form.getDefaultFormVariableValue(name);
public getVariableValue(form: FormModel, name: string, processVarList: TaskProcessVariableModel[] = []): any {
if (form.isVariableSetAtRuntime(name)) {
return form.getDefaultFormVariableValue(name);
}
return processVariableValue === undefined ? variableDefaultValue : processVariableValue;
const processVariableValue = this.getProcessVariableValue(name, processVarList);
return processVariableValue === undefined ? form.getProcessVariableValue(name) : processVariableValue;
}
private getProcessVariableValue(name: string, processVarList: TaskProcessVariableModel[]): string {
if (processVarList) {
const processVariable = processVarList.find((variable) => variable.id === name || variable.id === `variables.${name}`);
private getProcessVariableValue(name: string, processVarList: TaskProcessVariableModel[]): any {
const processVariable = processVarList.find((variable) => variable.id === name || variable.id === `variables.${name}`);
if (processVariable) {
return processVariable.value;
}
if (processVariable) {
return processVariable.value;
}
return undefined;
}