Skip to content

Commit

Permalink
Merge pull request #188 from formio/fio-9308-2
Browse files Browse the repository at this point in the history
FIO-9308: Fixed the paths with nested forms by ensuring we are always…
  • Loading branch information
brendanbond authored Nov 5, 2024
2 parents 2278307 + 2870d6d commit 310d86d
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 22 deletions.
148 changes: 147 additions & 1 deletion src/process/__tests__/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
skipValidForLogicallyHiddenComp,
skipValidWithHiddenParentComp,
} from './fixtures';
import { get } from 'lodash';

/*
describe('Process Tests', () => {
Expand Down Expand Up @@ -975,6 +976,148 @@ describe('Process Tests', function () {
assert.equal(context.scope.errors.length, 0);
});

it('Should allow data from a Conditionally shown nested form when another nested form is conditionally not shown.', async function () {
const form = {
components: [
{
label: 'Radio',
values: [
{
label: 'Show A',
value: 'a',
shortcut: '',
},
{
label: 'Show B',
value: 'b',
shortcut: '',
},
],
key: 'radio',
type: 'radio',
input: true,
},
{
label: 'Form',
conditional: {
show: true,
conjunction: 'all',
conditions: [
{
component: 'radio',
operator: 'isEqual',
value: 'a',
},
],
},
type: 'form',
key: 'form',
input: true,
components: [
{
label: 'Form',
key: 'form',
type: 'form',
input: true,
components: [
{
label: 'Text Field',
validate: {
required: true,
},
key: 'textField',
type: 'textfield',
input: true,
},
{
label: 'Text Field',
key: 'textField1',
type: 'textfield',
input: true,
},
],
},
],
},
{
label: 'Form',
key: 'form1',
conditional: {
show: true,
conjunction: 'all',
conditions: [
{
component: 'radio',
operator: 'isEqual',
value: 'b',
},
],
},
type: 'form',
input: true,
components: [
{
label: 'Form',
key: 'form',
type: 'form',
input: true,
components: [
{
label: 'Text Field',
validate: {
required: true,
},
key: 'textField',
type: 'textfield',
input: true,
},
{
label: 'Text Field',
key: 'textField1',
type: 'textfield',
input: true,
},
],
},
],
},
],
};
const submission = {
data: {
radio: 'b',
form1: {
data: {
form: {
data: {
textField: 'one 1',
textField1: 'two 2',
},
},
},
},
},
};
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);
assert.equal(get(context.submission.data, 'form1.data.form.data.textField'), 'one 1');
assert.equal(get(context.submission.data, 'form1.data.form.data.textField1'), 'two 2');
});

it('should remove submission data not in a nested form definition', async function () {
const form = {
_id: {},
Expand Down Expand Up @@ -4398,7 +4541,10 @@ describe('Process Tests', function () {
processSync(context);
assert.deepEqual(context.data, data);
context.scope.conditionals.forEach((cond: any) => {
assert.equal(cond.conditionallyHidden, cond.path === 'postalCode');
assert.equal(
cond.conditionallyHidden,
cond.path === 'pmta.data.contacts.data.applicantOrganization.data.address.data.postalCode',
);
});
});

Expand Down
10 changes: 6 additions & 4 deletions src/process/clearHidden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ProcessorFnSync,
ConditionsScope,
} from 'types';
import { getComponentAbsolutePath } from 'utils/formUtil';

type ClearHiddenScope = ProcessorScope & {
clearHidden: {
Expand All @@ -17,7 +18,8 @@ type ClearHiddenScope = ProcessorScope & {
* This processor function checks components for the `hidden` property and unsets corresponding data
*/
export const clearHiddenProcess: ProcessorFnSync<ClearHiddenScope> = (context) => {
const { component, data, path, value, scope } = context;
const { component, data, value, scope, path } = context;
const absolutePath = getComponentAbsolutePath(component) || path;

// No need to unset the value if it's undefined
if (value === undefined) {
Expand All @@ -30,7 +32,7 @@ export const clearHiddenProcess: ProcessorFnSync<ClearHiddenScope> = (context) =

// Check if there's a conditional set for the component and if it's marked as conditionally hidden
const isConditionallyHidden = (scope as ConditionsScope).conditionals?.find((cond) => {
return path === cond.path && cond.conditionallyHidden;
return absolutePath === cond.path && cond.conditionallyHidden;
});

const shouldClearValueWhenHidden =
Expand All @@ -40,8 +42,8 @@ export const clearHiddenProcess: ProcessorFnSync<ClearHiddenScope> = (context) =
shouldClearValueWhenHidden &&
(isConditionallyHidden || component.hidden || component.ephemeralState?.conditionallyHidden)
) {
unset(data, path);
scope.clearHidden[path] = true;
unset(data, absolutePath);
scope.clearHidden[absolutePath] = true;
}
};

Expand Down
8 changes: 5 additions & 3 deletions src/process/conditions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isSimpleConditional,
isJSONConditional,
} from 'utils/conditions';
import { getComponentAbsolutePath } from 'utils/formUtil';

const hasCustomConditions = (context: ConditionsContext): boolean => {
const { component } = context;
Expand Down Expand Up @@ -83,17 +84,18 @@ export const isConditionallyHidden = (context: ConditionsContext): boolean => {
export type ConditionallyHidden = (context: ConditionsContext) => boolean;

export const conditionalProcess = (context: ConditionsContext, isHidden: ConditionallyHidden) => {
const { scope, path } = context;
const { scope, path, component } = context;
const absolutePath = getComponentAbsolutePath(component) || path;
if (!hasConditions(context)) {
return;
}

if (!scope.conditionals) {
scope.conditionals = [];
}
let conditionalComp = scope.conditionals.find((cond) => cond.path === path);
let conditionalComp = scope.conditionals.find((cond) => cond.path === absolutePath);
if (!conditionalComp) {
conditionalComp = { path, conditionallyHidden: false };
conditionalComp = { path: absolutePath, conditionallyHidden: false };
scope.conditionals.push(conditionalComp);
}

Expand Down
4 changes: 2 additions & 2 deletions src/process/filter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { Utils } from 'utils';
import { get, isObject } from 'lodash';
import { getComponentAbsolutePath } from 'utils/formUtil';
export const filterProcessSync: ProcessorFnSync<FilterScope> = (context: FilterContext) => {
const { scope, component } = context;
const { scope, component, path } = context;
const { value } = context;
const absolutePath = getComponentAbsolutePath(component);
const absolutePath = getComponentAbsolutePath(component) || path;
if (!scope.filter) scope.filter = {};
if (value !== undefined) {
const modelType = Utils.getModelType(component);
Expand Down
4 changes: 3 additions & 1 deletion src/process/hideChildren.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import {
ProcessorFn,
} from 'types';
import { registerEphemeralState } from 'utils';
import { getComponentAbsolutePath } from 'utils/formUtil';

/**
* This processor function checks components for the `hidden` property and, if children are present, sets them to hidden as well.
*/
export const hideChildrenProcessor: ProcessorFnSync<ConditionsScope> = (context) => {
const { component, path, parent, scope } = context;
const absolutePath = getComponentAbsolutePath(component) || path;
// Check if there's a conditional set for the component and if it's marked as conditionally hidden
const isConditionallyHidden = scope.conditionals?.find((cond) => {
return path === cond.path && cond.conditionallyHidden;
return absolutePath === cond.path && cond.conditionallyHidden;
});

if (!scope.conditionals) {
Expand Down
9 changes: 5 additions & 4 deletions src/process/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { evaluationRules, rules, serverRules } from './rules';
import find from 'lodash/find';
import get from 'lodash/get';
import pick from 'lodash/pick';
import { getComponentAbsolutePath, getComponentPath } from 'utils/formUtil';
import { getComponentAbsolutePath } from 'utils/formUtil';
import { getErrorMessage } from 'utils/error';
import { FieldError } from 'error';
import {
Expand Down Expand Up @@ -107,11 +107,12 @@ export const _shouldSkipValidation = (
isConditionallyHidden: ConditionallyHidden,
) => {
const { component, scope, path } = context;
const absolutePath = getComponentAbsolutePath(component) || path;

if (
(scope as ConditionsScope)?.conditionals &&
(find((scope as ConditionsScope).conditionals, {
path: getComponentPath(component, path),
path: absolutePath,
conditionallyHidden: true,
}) ||
component.ephemeralState?.conditionallyHidden === true)
Expand Down Expand Up @@ -169,8 +170,8 @@ export function shouldValidateServer(context: ValidationContext): boolean {
}

function handleError(error: FieldError | null, context: ValidationContext) {
const { scope, component } = context;
const absolutePath = getComponentAbsolutePath(component);
const { scope, component, path } = context;
const absolutePath = getComponentAbsolutePath(component) || path;
if (error) {
const cleanedError = cleanupValidationError(error);
cleanedError.context.path = absolutePath;
Expand Down
14 changes: 10 additions & 4 deletions src/utils/conditions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ConditionsContext, JSONConditional, LegacyConditional, SimpleConditional } from 'types';
import { EvaluatorFn, evaluate, JSONLogicEvaluator } from 'modules/jsonlogic';
import { flattenComponents, getComponent, getComponentActualValue } from './formUtil';
import {
flattenComponents,
getComponent,
getComponentAbsolutePath,
getComponentActualValue,
} from './formUtil';
import { has, isObject, map, every, some, find, filter, isBoolean, split } from 'lodash';
import ConditionOperators from './operators';

Expand All @@ -17,10 +22,11 @@ export const isSimpleConditional = (conditional: any): conditional is SimpleCond
};

export function conditionallyHidden(context: ConditionsContext) {
const { scope, path } = context;
if (scope.conditionals && path) {
const { scope, path, component } = context;
const absolutePath = getComponentAbsolutePath(component) || path;
if (scope.conditionals && absolutePath) {
const hidden = find(scope.conditionals, (conditional) => {
return conditional.path === path;
return conditional.path === absolutePath;
});
return hidden?.conditionallyHidden;
}
Expand Down
8 changes: 5 additions & 3 deletions src/utils/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { get, set, clone, isEqual, assign } from 'lodash';
import { evaluate, interpolate } from 'modules/jsonlogic';
import { registerEphemeralState } from './utils';
import { getComponentAbsolutePath } from './formUtil';

export const hasLogic = (context: LogicContext): boolean => {
const { component } = context;
Expand Down Expand Up @@ -69,6 +70,7 @@ export function setActionBooleanProperty(
action: LogicActionPropertyBoolean,
): boolean {
const { component, scope, path } = context;
const absolutePath = getComponentAbsolutePath(component) || path;
const property = action.property.value;
const currentValue = get(component, property, false).toString();
const newValue = action.state.toString();
Expand All @@ -77,19 +79,19 @@ export function setActionBooleanProperty(

// If this is "logic" forcing a component to set hidden property, then we will set the "conditionallyHidden"
// flag which will trigger the clearOnHide functionality.
if (property === 'hidden' && path) {
if (property === 'hidden' && absolutePath) {
if (!(scope as ConditionsScope).conditionals) {
(scope as ConditionsScope).conditionals = [];
}
const conditionallyHidden = (scope as ConditionsScope).conditionals?.find((cond: any) => {
return cond.path === path;
return cond.path === absolutePath;
});
if (conditionallyHidden) {
conditionallyHidden.conditionallyHidden = !!component.hidden;
registerEphemeralState(component, 'conditionallyHidden', !!component.hidden);
} else {
(scope as ConditionsScope).conditionals?.push({
path,
path: absolutePath,
conditionallyHidden: !!component.hidden,
});
}
Expand Down

0 comments on commit 310d86d

Please sign in to comment.