Skip to content

Commit

Permalink
Merge pull request #163 from formio/FIO-9160-fixed-condtions-for-sele…
Browse files Browse the repository at this point in the history
…ctboxes

FIO-9160: added support of different condition formats for selectboxes
  • Loading branch information
brendanbond authored Oct 9, 2024
2 parents d3e8fbc + b64dfbb commit 60bf96d
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 8 deletions.
153 changes: 150 additions & 3 deletions src/process/__tests__/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import assert from 'node:assert'
import type { ContainerComponent, ProcessContext, ProcessorScope, ValidationScope } from 'types';
import { getComponent } from 'utils/formUtil';
import { process, processSync, ProcessTargets } from '../index';
import { fastCloneDeep } from 'utils';
import { addressComponentWithOtherCondComponents, addressComponentWithOtherCondComponents2, clearOnHideWithCustomCondition, clearOnHideWithHiddenParent, forDataGridRequired, skipValidForConditionallyHiddenComp, skipValidForLogicallyHiddenComp, skipValidWithHiddenParentComp } from './fixtures'

/*
describe('Process Tests', () => {
it('Should perform the processes using the processReduced method.', async () => {
Expand Down Expand Up @@ -3509,7 +3511,153 @@ describe('Process Tests', () => {
assert.equal(context.scope.errors.length, 0);
});

it('Should not return error for the form with conditionals based on the Day component', () => {
it('Should not unset values for conditionally visible fields with different formats of condtion based on selectboxes value', async () => {
const form = {
_id: '66ffa92ac25689df8702f283',
title: 'cond NEW',
name: 'condnew',
path: 'condnew',
type: 'form',
display: 'form',
owner: '637b2e6b48c1227e60b1f910',
components: [
{
label: 'Container',
tableView: false,
validateWhenHidden: false,
key: 'container',
type: 'container',
input: true,
components: [
{
label: 'Select Boxes',
optionsLabelPosition: 'right',
tableView: false,
values: [
{
label: 'a',
value: 'a',
shortcut: '',
},
{
label: 'b',
value: 'b',
shortcut: '',
},
{
label: 'c',
value: 'c',
shortcut: '',
},
],
validateWhenHidden: false,
key: 'selectBoxes',
type: 'selectboxes',
input: true,
inputType: 'checkbox',
},
],
},
{
label: 'Text Field',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textField',
conditional: {
show: true,
conjunction: 'all',
conditions: [
{
component: 'container.selectBoxes',
operator: 'isEqual',
value: 'a',
},
],
},
type: 'textfield',
input: true,
},
{
label: 'Text Field new wrong format',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textFieldNewWrong',
conditional: {
show: true,
conjunction: 'all',
conditions: [
{
component: 'container.selectBoxes.a',
operator: 'isEqual',
value: 'true',
},
],
},
type: 'textfield',
input: true,
},
{
label: 'Text Field Old Format',
applyMaskOn: 'change',
tableView: true,
validateWhenHidden: false,
key: 'textFieldOldFormat',
conditional: {
show: true,
eq: 'true',
when: 'container.selectBoxes.a',
},
type: 'textfield',
input: true,
},
{
label: 'Submit',
showValidations: false,
tableView: false,
key: 'submit',
type: 'button',
input: true,
saveOnEnter: false,
},
]
};
const data= {
textField: 'correct condition',
textFieldNewWrong: 'new condition with wrong format',
textFieldOldFormat: 'legacy condtion',
container: { selectBoxes: { a: true, b: false, c: false } },
submit: true,
}

const submission = {
data: fastCloneDeep(data),
_id: '66f68c17481ea2ffbf5bb310',
state: 'submitted',
};

const errors: any = [];
const context = {
form,
submission,
data: submission.data,
components: form.components,
processors: ProcessTargets.submission,
scope: { errors },
config: {
server: true,
},
};
processSync(context);
submission.data = context.data;
context.processors = ProcessTargets.evaluator;
processSync(context);
(context.scope as any).conditionals.forEach((v: any) => assert.equal(v.conditionallyHidden, false))
assert.deepEqual(context.data, data);
});

it('Should not return error for the form with conditionals based on the Day component', () => {
const form = {
_id: '66ffe59b598a729e707869bf',
title: '9143 condition day',
Expand Down Expand Up @@ -3609,8 +3757,7 @@ describe('Process Tests', () => {
expect(context.scope.conditionals).to.have.length(1);
expect(context.scope.conditionals?.[0].conditionallyHidden).to.be.false;
assert.equal(context.scope.errors.length, 0);
})

});

describe('Required component validation in nested form in DataGrid/EditGrid', () => {
const nestedForm = {
Expand Down
51 changes: 46 additions & 5 deletions src/utils/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConditionsContext, JSONConditional, LegacyConditional, SimpleConditional } from "types";
import { EvaluatorFn, evaluate, JSONLogicEvaluator } from 'modules/jsonlogic';
import { getComponent, getComponentActualValue } from "./formUtil";
import { has, isObject, map, every, some, find, filter } from 'lodash';
import { flattenComponents, getComponent, getComponentActualValue } from "./formUtil";
import { has, isObject, map, every, some, find, filter, isBoolean, split } from 'lodash';
import ConditionOperators from './operators';

export const isJSONConditional = (conditional: any): conditional is JSONConditional => {
Expand Down Expand Up @@ -88,14 +88,30 @@ export function checkJsonConditional(conditional: JSONConditional, context: Cond
return JSONLogicEvaluator.evaluate(conditional.json, evalContextValue);
}

/**
* Checks if condition can potentially have a value path instead of component path.
* @param condition
* @returns {boolean}
*/
function isConditionPotentiallyBasedOnValuePath(condition: any = {}) {
let comparedValue;
try {
comparedValue = JSON.parse(condition.value);
}
catch(e) {
comparedValue = condition.value;
}
return isBoolean(comparedValue) && (condition.component || '').split('.').length > 1 && condition.operator === 'isEqual';
}

/**
* Checks the simple conditionals.
* @param conditional
* @param context
* @returns
*/
export function checkSimpleConditional(conditional: SimpleConditional, context: ConditionsContext): boolean | null {
const { component, data, row, instance, form, components = [] } = context;
const { component, data, row, instance, form } = context;
if (!conditional || !isSimpleConditional(conditional)) {
return null;
}
Expand All @@ -105,13 +121,38 @@ export function checkSimpleConditional(conditional: SimpleConditional, context:
}

const conditionsResult = filter(map(conditions, (cond) => {
const { value: comparedValue, operator, component: conditionComponentPath } = cond;
let { value: comparedValue, operator, component: conditionComponentPath } = cond;
if (!conditionComponentPath) {
// Ignore conditions if there is no component path.
return null;
}
const formComponents = form?.components || [];
let conditionComponent = getComponent(formComponents, conditionComponentPath, true);
// If condition componenet is not found, check if conditionComponentPath is value path.
// Need to handle condtions like:
// {
// "component": "selectBoxes.a",
// "operator": "isEqual",
// "value": "true"
// }
if (!conditionComponent && isConditionPotentiallyBasedOnValuePath(cond) && formComponents.length) {
const flattenedComponents = flattenComponents(formComponents, true);
const pathParts = split(conditionComponentPath, '.');
const valuePathParts = [];

while (!conditionComponent && pathParts.length) {
conditionComponent = flattenedComponents[`${pathParts.join('.')}`];
if (!conditionComponent) {
valuePathParts.unshift(pathParts.pop());
}
}
if (conditionComponent && conditionComponent.type === 'selectboxes' && valuePathParts.length) {
console.warn('Condition based on selectboxes has wrong format. Resave the form in the form builder to fix it.');
conditionComponentPath = pathParts.join('.');
comparedValue = valuePathParts.join('.');
}
}

const conditionComponent = getComponent(form?.components || components, conditionComponentPath, true);
const value = conditionComponent ? getComponentActualValue(conditionComponent, conditionComponentPath, data, row) : null;

const ConditionOperator = ConditionOperators[operator];
Expand Down

0 comments on commit 60bf96d

Please sign in to comment.