feat: include dynamic fields in the form validation
This commit is contained in:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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>
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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}>
|
||||||
|
@@ -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
|
||||||
|
@@ -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 = {};
|
||||||
|
Reference in New Issue
Block a user