feat: include dynamic fields in the form validation

This commit is contained in:
kasia.oczkowska
2024-03-15 09:33:41 +00:00
parent 47caa5aa37
commit 8f3ecb6d4d
9 changed files with 119 additions and 62 deletions

View File

@@ -68,7 +68,10 @@ function AccountDropdownMenu(props) {
AccountDropdownMenu.propTypes = { AccountDropdownMenu.propTypes = {
open: PropTypes.bool.isRequired, open: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
}; };

View File

@@ -21,7 +21,9 @@ const CustomOptions = (props) => {
label, label,
initialTabIndex, initialTabIndex,
} = props; } = props;
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined); const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
React.useEffect( React.useEffect(
function applyInitialActiveTabIndex() { function applyInitialActiveTabIndex() {
setActiveTabIndex((currentActiveTabIndex) => { setActiveTabIndex((currentActiveTabIndex) => {
@@ -33,6 +35,7 @@ const CustomOptions = (props) => {
}, },
[initialTabIndex], [initialTabIndex],
); );
return ( return (
<Popper <Popper
open={open} open={open}
@@ -76,7 +79,10 @@ const CustomOptions = (props) => {
CustomOptions.propTypes = { CustomOptions.propTypes = {
open: PropTypes.bool.isRequired, open: PropTypes.bool.isRequired,
anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]),
data: PropTypes.arrayOf( data: PropTypes.arrayOf(
PropTypes.shape({ PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,

View File

@@ -36,7 +36,8 @@ import useActions from 'hooks/useActions';
import useTriggerSubsteps from 'hooks/useTriggerSubsteps'; import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
import useActionSubsteps from 'hooks/useActionSubsteps'; import useActionSubsteps from 'hooks/useActionSubsteps';
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions'; import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
import { generateValidationSchema } from './validation'; import { validationSchemaResolver } from './validation';
import { isEqual } from 'lodash';
const validIcon = <CheckCircleIcon color="success" />; const validIcon = <CheckCircleIcon color="success" />;
const errorIcon = <ErrorIcon color="error" />; const errorIcon = <ErrorIcon color="error" />;
@@ -51,6 +52,10 @@ function FlowStep(props) {
const isAction = step.type === 'action'; const isAction = step.type === 'action';
const formatMessage = useFormatMessage(); const formatMessage = useFormatMessage();
const [currentSubstep, setCurrentSubstep] = React.useState(0); const [currentSubstep, setCurrentSubstep] = React.useState(0);
const [formResolverContext, setFormResolverContext] = React.useState({
substeps: [],
additionalFields: {},
});
const useAppsOptions = {}; const useAppsOptions = {};
if (isTrigger) { if (isTrigger) {
@@ -105,6 +110,12 @@ function FlowStep(props) {
? triggerSubstepsData ? triggerSubstepsData
: actionSubstepsData || []; : actionSubstepsData || [];
React.useEffect(() => {
if (!isEqual(substeps, formResolverContext.substeps)) {
setFormResolverContext({ substeps, additionalFields: {} });
}
}, [substeps]);
const handleChange = React.useCallback(({ step }) => { const handleChange = React.useCallback(({ step }) => {
onChange(step); onChange(step);
}, []); }, []);
@@ -117,11 +128,6 @@ function FlowStep(props) {
handleChange({ step: val }); handleChange({ step: val });
}; };
const stepValidationSchema = React.useMemo(
() => generateValidationSchema(substeps),
[substeps],
);
if (!apps?.data) { if (!apps?.data) {
return ( return (
<CircularProgress <CircularProgress
@@ -150,6 +156,15 @@ function FlowStep(props) {
value !== substepIndex ? substepIndex : null, value !== substepIndex ? substepIndex : null,
); );
const addAdditionalFieldsValidation = (additionalFields) => {
if (additionalFields) {
setFormResolverContext((prev) => ({
...prev,
additionalFields: { ...prev.additionalFields, ...additionalFields },
}));
}
};
const validationStatusIcon = const validationStatusIcon =
step.status === 'completed' ? validIcon : errorIcon; step.status === 'completed' ? validIcon : errorIcon;
@@ -203,7 +218,8 @@ function FlowStep(props) {
<Form <Form
defaultValues={step} defaultValues={step}
onSubmit={handleSubmit} onSubmit={handleSubmit}
resolver={stepValidationSchema} resolver={validationSchemaResolver}
context={formResolverContext}
> >
<ChooseAppAndEventSubstep <ChooseAppAndEventSubstep
expanded={currentSubstep === 0} expanded={currentSubstep === 0}
@@ -267,6 +283,9 @@ function FlowStep(props) {
onSubmit={expandNextStep} onSubmit={expandNextStep}
onChange={handleChange} onChange={handleChange}
step={step} step={step}
addAdditionalFieldsValidation={
addAdditionalFieldsValidation
}
/> />
)} )}
</React.Fragment> </React.Fragment>

View File

@@ -41,74 +41,80 @@ function addDependsOnValidation({ schema, dependsOn, key, args }) {
return schema; return schema;
} }
export function generateValidationSchema(substeps) { export function validationSchemaResolver(data, context, options) {
const fieldValidations = substeps?.reduce( const { substeps = [], additionalFields = {} } = context;
(allValidations, { arguments: args }) => {
if (!args || !Array.isArray(args)) return allValidations;
const substepArgumentValidations = {}; const fieldValidations = [
...substeps,
{
arguments: Object.values(additionalFields)
.filter((field) => !!field)
.flat(),
},
].reduce((allValidations, { arguments: args }) => {
if (!args || !Array.isArray(args)) return allValidations;
for (const arg of args) { const substepArgumentValidations = {};
const { key, required } = arg;
// base validation for the field if not exists for (const arg of args) {
if (!substepArgumentValidations[key]) { const { key, required } = arg;
substepArgumentValidations[key] = yup.mixed();
}
if (arg.type === 'dynamic') { // base validation for the field if not exists
const fieldsSchema = {}; if (!substepArgumentValidations[key]) {
substepArgumentValidations[key] = yup.mixed();
}
for (const field of arg.fields) { if (arg.type === 'dynamic') {
fieldsSchema[field.key] = yup.mixed(); const fieldsSchema = {};
fieldsSchema[field.key] = addRequiredValidation({ for (const field of arg.fields) {
required: field.required, fieldsSchema[field.key] = yup.mixed();
schema: fieldsSchema[field.key],
key: field.key,
});
fieldsSchema[field.key] = addDependsOnValidation({ fieldsSchema[field.key] = addRequiredValidation({
schema: fieldsSchema[field.key], required: field.required,
dependsOn: field.dependsOn, schema: fieldsSchema[field.key],
key: field.key, key: field.key,
args,
});
}
substepArgumentValidations[key] = yup
.array()
.of(yup.object(fieldsSchema));
} else if (
typeof substepArgumentValidations[key] === 'object' &&
(arg.type === 'string' || arg.type === 'dropdown')
) {
substepArgumentValidations[key] = addRequiredValidation({
required,
schema: substepArgumentValidations[key],
key,
}); });
substepArgumentValidations[key] = addDependsOnValidation({ fieldsSchema[field.key] = addDependsOnValidation({
schema: substepArgumentValidations[key], schema: fieldsSchema[field.key],
dependsOn: arg.dependsOn, dependsOn: field.dependsOn,
key, key: field.key,
args, args,
}); });
} }
}
return { substepArgumentValidations[key] = yup
...allValidations, .array()
...substepArgumentValidations, .of(yup.object(fieldsSchema));
}; } else if (
}, typeof substepArgumentValidations[key] === 'object' &&
{}, (arg.type === 'string' || arg.type === 'dropdown')
); ) {
substepArgumentValidations[key] = addRequiredValidation({
required,
schema: substepArgumentValidations[key],
key,
});
substepArgumentValidations[key] = addDependsOnValidation({
schema: substepArgumentValidations[key],
dependsOn: arg.dependsOn,
key,
args,
});
}
}
return {
...allValidations,
...substepArgumentValidations,
};
}, {});
const validationSchema = yup.object({ const validationSchema = yup.object({
parameters: yup.object(fieldValidations), parameters: yup.object(fieldValidations),
}); });
return yupResolver(validationSchema); return yupResolver(validationSchema)(data, context, options);
} }

View File

@@ -43,7 +43,10 @@ function FlowStepContextMenu(props) {
FlowStepContextMenu.propTypes = { FlowStepContextMenu.propTypes = {
stepId: PropTypes.string.isRequired, stepId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
anchorEl: PropTypes.element.isRequired, anchorEl: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
deletable: PropTypes.bool.isRequired, deletable: PropTypes.bool.isRequired,
}; };

View File

@@ -19,7 +19,9 @@ function FlowSubstep(props) {
onCollapse, onCollapse,
onSubmit, onSubmit,
step, step,
addAdditionalFieldsValidation,
} = props; } = props;
const { name, arguments: args } = substep; const { name, arguments: args } = substep;
const editorContext = React.useContext(EditorContext); const editorContext = React.useContext(EditorContext);
const formContext = useFormContext(); const formContext = useFormContext();
@@ -54,6 +56,7 @@ function FlowSubstep(props) {
stepId={step.id} stepId={step.id}
disabled={editorContext.readOnly} disabled={editorContext.readOnly}
showOptionValue={true} showOptionValue={true}
addAdditionalFieldsValidation={addAdditionalFieldsValidation}
/> />
))} ))}
</Stack> </Stack>

View File

@@ -1,6 +1,8 @@
import * as React from 'react'; import * as React from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form'; import { FormProvider, useForm, useWatch } from 'react-hook-form';
const noop = () => null; const noop = () => null;
export default function Form(props) { export default function Form(props) {
const { const {
children, children,
@@ -9,24 +11,31 @@ export default function Form(props) {
resolver, resolver,
render, render,
mode = 'all', mode = 'all',
context,
...formProps ...formProps
} = props; } = props;
const methods = useForm({ const methods = useForm({
defaultValues, defaultValues,
reValidateMode: 'onBlur', reValidateMode: 'onBlur',
resolver, resolver,
mode, mode,
context,
}); });
const form = useWatch({ control: methods.control }); const form = useWatch({ control: methods.control });
/** /**
* For fields having `dependsOn` fields, we need to re-validate the form. * For fields having `dependsOn` fields, we need to re-validate the form.
*/ */
React.useEffect(() => { React.useEffect(() => {
methods.trigger(); methods.trigger();
}, [methods.trigger, form]); }, [methods.trigger, form]);
React.useEffect(() => { React.useEffect(() => {
methods.reset(defaultValues); methods.reset(defaultValues);
}, [defaultValues]); }, [defaultValues]);
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}> <form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>

View File

@@ -23,7 +23,9 @@ export default function InputCreator(props) {
disabled, disabled,
showOptionValue, showOptionValue,
shouldUnregister, shouldUnregister,
addAdditionalFieldsValidation,
} = props; } = props;
const { const {
key: name, key: name,
label, label,
@@ -33,6 +35,7 @@ export default function InputCreator(props) {
description, description,
type, type,
} = schema; } = schema;
const { data, loading } = useDynamicData(stepId, schema); const { data, loading } = useDynamicData(stepId, schema);
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } = const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
useDynamicFields(stepId, schema); useDynamicFields(stepId, schema);
@@ -40,6 +43,10 @@ export default function InputCreator(props) {
const computedName = namePrefix ? `${namePrefix}.${name}` : name; const computedName = namePrefix ? `${namePrefix}.${name}` : name;
React.useEffect(() => {
addAdditionalFieldsValidation?.({ [name]: additionalFields });
}, [additionalFields]);
if (type === 'dynamic') { if (type === 'dynamic') {
return ( return (
<DynamicField <DynamicField

View File

@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query';
import api from 'helpers/api'; import api from 'helpers/api';
const variableRegExp = /({.*?})/; const variableRegExp = /({.*?})/;
// TODO: extract this function to a separate file // TODO: extract this function to a separate file
function computeArguments(args, getValues) { function computeArguments(args, getValues) {
const initialValue = {}; const initialValue = {};