Compare commits
2 Commits
v0.14.0
...
AUT-157-AU
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8f3ecb6d4d | ||
![]() |
47caa5aa37 |
@@ -68,7 +68,10 @@ function AccountDropdownMenu(props) {
|
||||
AccountDropdownMenu.propTypes = {
|
||||
open: PropTypes.bool.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,
|
||||
};
|
||||
|
||||
|
@@ -21,7 +21,9 @@ const CustomOptions = (props) => {
|
||||
label,
|
||||
initialTabIndex,
|
||||
} = props;
|
||||
|
||||
const [activeTabIndex, setActiveTabIndex] = React.useState(undefined);
|
||||
|
||||
React.useEffect(
|
||||
function applyInitialActiveTabIndex() {
|
||||
setActiveTabIndex((currentActiveTabIndex) => {
|
||||
@@ -33,6 +35,7 @@ const CustomOptions = (props) => {
|
||||
},
|
||||
[initialTabIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popper
|
||||
open={open}
|
||||
@@ -76,7 +79,10 @@ const CustomOptions = (props) => {
|
||||
|
||||
CustomOptions.propTypes = {
|
||||
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(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@@ -11,9 +11,6 @@ import IconButton from '@mui/material/IconButton';
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import * as yup from 'yup';
|
||||
|
||||
import { EditorContext } from 'contexts/Editor';
|
||||
import { StepExecutionsProvider } from 'contexts/StepExecutions';
|
||||
import TestSubstep from 'components/TestSubstep';
|
||||
@@ -33,77 +30,18 @@ import {
|
||||
Header,
|
||||
Wrapper,
|
||||
} from './style';
|
||||
import isEmpty from 'helpers/isEmpty';
|
||||
import { StepPropType } from 'propTypes/propTypes';
|
||||
import useTriggers from 'hooks/useTriggers';
|
||||
import useActions from 'hooks/useActions';
|
||||
import useTriggerSubsteps from 'hooks/useTriggerSubsteps';
|
||||
import useActionSubsteps from 'hooks/useActionSubsteps';
|
||||
import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions';
|
||||
import { validationSchemaResolver } from './validation';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const validIcon = <CheckCircleIcon color="success" />;
|
||||
const errorIcon = <ErrorIcon color="error" />;
|
||||
|
||||
function generateValidationSchema(substeps) {
|
||||
const fieldValidations = substeps?.reduce(
|
||||
(allValidations, { arguments: args }) => {
|
||||
if (!args || !Array.isArray(args)) return allValidations;
|
||||
const substepArgumentValidations = {};
|
||||
for (const arg of args) {
|
||||
const { key, required } = arg;
|
||||
// base validation for the field if not exists
|
||||
if (!substepArgumentValidations[key]) {
|
||||
substepArgumentValidations[key] = yup.mixed();
|
||||
}
|
||||
if (
|
||||
typeof substepArgumentValidations[key] === 'object' &&
|
||||
(arg.type === 'string' || arg.type === 'dropdown')
|
||||
) {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
substepArgumentValidations[key] = substepArgumentValidations[key]
|
||||
.required(`${key} is required.`)
|
||||
.test(
|
||||
'empty-check',
|
||||
`${key} must be not empty`,
|
||||
(value) => !isEmpty(value),
|
||||
);
|
||||
}
|
||||
// if the field depends on another field, add the dependsOn required validation
|
||||
if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) {
|
||||
for (const dependsOnKey of arg.dependsOn) {
|
||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||
substepArgumentValidations[key] = substepArgumentValidations[
|
||||
key
|
||||
].when(`${dependsOnKey.replace('parameters.', '')}`, {
|
||||
is: (value) => Boolean(value) === false,
|
||||
then: (schema) =>
|
||||
schema
|
||||
.notOneOf([''], missingDependencyValueMessage)
|
||||
.required(missingDependencyValueMessage),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...allValidations,
|
||||
...substepArgumentValidations,
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const validationSchema = yup.object({
|
||||
parameters: yup.object(fieldValidations),
|
||||
});
|
||||
|
||||
return yupResolver(validationSchema);
|
||||
}
|
||||
|
||||
function FlowStep(props) {
|
||||
const { collapsed, onChange, onContinue, flowId } = props;
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
@@ -114,6 +52,10 @@ function FlowStep(props) {
|
||||
const isAction = step.type === 'action';
|
||||
const formatMessage = useFormatMessage();
|
||||
const [currentSubstep, setCurrentSubstep] = React.useState(0);
|
||||
const [formResolverContext, setFormResolverContext] = React.useState({
|
||||
substeps: [],
|
||||
additionalFields: {},
|
||||
});
|
||||
const useAppsOptions = {};
|
||||
|
||||
if (isTrigger) {
|
||||
@@ -168,6 +110,12 @@ function FlowStep(props) {
|
||||
? triggerSubstepsData
|
||||
: actionSubstepsData || [];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isEqual(substeps, formResolverContext.substeps)) {
|
||||
setFormResolverContext({ substeps, additionalFields: {} });
|
||||
}
|
||||
}, [substeps]);
|
||||
|
||||
const handleChange = React.useCallback(({ step }) => {
|
||||
onChange(step);
|
||||
}, []);
|
||||
@@ -180,11 +128,6 @@ function FlowStep(props) {
|
||||
handleChange({ step: val });
|
||||
};
|
||||
|
||||
const stepValidationSchema = React.useMemo(
|
||||
() => generateValidationSchema(substeps),
|
||||
[substeps],
|
||||
);
|
||||
|
||||
if (!apps?.data) {
|
||||
return (
|
||||
<CircularProgress
|
||||
@@ -213,6 +156,15 @@ function FlowStep(props) {
|
||||
value !== substepIndex ? substepIndex : null,
|
||||
);
|
||||
|
||||
const addAdditionalFieldsValidation = (additionalFields) => {
|
||||
if (additionalFields) {
|
||||
setFormResolverContext((prev) => ({
|
||||
...prev,
|
||||
additionalFields: { ...prev.additionalFields, ...additionalFields },
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const validationStatusIcon =
|
||||
step.status === 'completed' ? validIcon : errorIcon;
|
||||
|
||||
@@ -266,7 +218,8 @@ function FlowStep(props) {
|
||||
<Form
|
||||
defaultValues={step}
|
||||
onSubmit={handleSubmit}
|
||||
resolver={stepValidationSchema}
|
||||
resolver={validationSchemaResolver}
|
||||
context={formResolverContext}
|
||||
>
|
||||
<ChooseAppAndEventSubstep
|
||||
expanded={currentSubstep === 0}
|
||||
@@ -330,6 +283,9 @@ function FlowStep(props) {
|
||||
onSubmit={expandNextStep}
|
||||
onChange={handleChange}
|
||||
step={step}
|
||||
addAdditionalFieldsValidation={
|
||||
addAdditionalFieldsValidation
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
120
packages/web/src/components/FlowStep/validation.js
Normal file
120
packages/web/src/components/FlowStep/validation.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as yup from 'yup';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import isEmpty from 'helpers/isEmpty';
|
||||
|
||||
function addRequiredValidation({ required, schema, key }) {
|
||||
// if the field is required, add the required validation
|
||||
if (required) {
|
||||
return schema
|
||||
.required(`${key} is required.`)
|
||||
.test(
|
||||
'empty-check',
|
||||
`${key} must be not empty`,
|
||||
(value) => !isEmpty(value),
|
||||
);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
function addDependsOnValidation({ schema, dependsOn, key, args }) {
|
||||
// if the field depends on another field, add the dependsOn required validation
|
||||
if (Array.isArray(dependsOn) && dependsOn.length > 0) {
|
||||
for (const dependsOnKey of dependsOn) {
|
||||
const dependsOnKeyShort = dependsOnKey.replace('parameters.', '');
|
||||
const dependsOnField = args.find(({ key }) => key === dependsOnKeyShort);
|
||||
|
||||
if (dependsOnField?.required) {
|
||||
const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`;
|
||||
|
||||
// TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported.
|
||||
// So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed.
|
||||
return schema.when(dependsOnKeyShort, {
|
||||
is: (dependsOnValue) => Boolean(dependsOnValue) === false,
|
||||
then: (schema) =>
|
||||
schema
|
||||
.notOneOf([''], missingDependencyValueMessage)
|
||||
.required(missingDependencyValueMessage),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
export function validationSchemaResolver(data, context, options) {
|
||||
const { substeps = [], additionalFields = {} } = context;
|
||||
|
||||
const fieldValidations = [
|
||||
...substeps,
|
||||
{
|
||||
arguments: Object.values(additionalFields)
|
||||
.filter((field) => !!field)
|
||||
.flat(),
|
||||
},
|
||||
].reduce((allValidations, { arguments: args }) => {
|
||||
if (!args || !Array.isArray(args)) return allValidations;
|
||||
|
||||
const substepArgumentValidations = {};
|
||||
|
||||
for (const arg of args) {
|
||||
const { key, required } = arg;
|
||||
|
||||
// base validation for the field if not exists
|
||||
if (!substepArgumentValidations[key]) {
|
||||
substepArgumentValidations[key] = yup.mixed();
|
||||
}
|
||||
|
||||
if (arg.type === 'dynamic') {
|
||||
const fieldsSchema = {};
|
||||
|
||||
for (const field of arg.fields) {
|
||||
fieldsSchema[field.key] = yup.mixed();
|
||||
|
||||
fieldsSchema[field.key] = addRequiredValidation({
|
||||
required: field.required,
|
||||
schema: fieldsSchema[field.key],
|
||||
key: field.key,
|
||||
});
|
||||
|
||||
fieldsSchema[field.key] = addDependsOnValidation({
|
||||
schema: fieldsSchema[field.key],
|
||||
dependsOn: field.dependsOn,
|
||||
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({
|
||||
schema: substepArgumentValidations[key],
|
||||
dependsOn: arg.dependsOn,
|
||||
key,
|
||||
args,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...allValidations,
|
||||
...substepArgumentValidations,
|
||||
};
|
||||
}, {});
|
||||
|
||||
const validationSchema = yup.object({
|
||||
parameters: yup.object(fieldValidations),
|
||||
});
|
||||
|
||||
return yupResolver(validationSchema)(data, context, options);
|
||||
}
|
@@ -43,7 +43,10 @@ function FlowStepContextMenu(props) {
|
||||
FlowStepContextMenu.propTypes = {
|
||||
stepId: PropTypes.string.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,
|
||||
};
|
||||
|
||||
|
@@ -19,7 +19,9 @@ function FlowSubstep(props) {
|
||||
onCollapse,
|
||||
onSubmit,
|
||||
step,
|
||||
addAdditionalFieldsValidation,
|
||||
} = props;
|
||||
|
||||
const { name, arguments: args } = substep;
|
||||
const editorContext = React.useContext(EditorContext);
|
||||
const formContext = useFormContext();
|
||||
@@ -54,6 +56,7 @@ function FlowSubstep(props) {
|
||||
stepId={step.id}
|
||||
disabled={editorContext.readOnly}
|
||||
showOptionValue={true}
|
||||
addAdditionalFieldsValidation={addAdditionalFieldsValidation}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
const noop = () => null;
|
||||
|
||||
export default function Form(props) {
|
||||
const {
|
||||
children,
|
||||
@@ -9,24 +11,31 @@ export default function Form(props) {
|
||||
resolver,
|
||||
render,
|
||||
mode = 'all',
|
||||
context,
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const methods = useForm({
|
||||
defaultValues,
|
||||
reValidateMode: 'onBlur',
|
||||
resolver,
|
||||
mode,
|
||||
context,
|
||||
});
|
||||
|
||||
const form = useWatch({ control: methods.control });
|
||||
|
||||
/**
|
||||
* For fields having `dependsOn` fields, we need to re-validate the form.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
methods.trigger();
|
||||
}, [methods.trigger, form]);
|
||||
|
||||
React.useEffect(() => {
|
||||
methods.reset(defaultValues);
|
||||
}, [defaultValues]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)} {...formProps}>
|
||||
|
@@ -23,7 +23,9 @@ export default function InputCreator(props) {
|
||||
disabled,
|
||||
showOptionValue,
|
||||
shouldUnregister,
|
||||
addAdditionalFieldsValidation,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
key: name,
|
||||
label,
|
||||
@@ -33,6 +35,7 @@ export default function InputCreator(props) {
|
||||
description,
|
||||
type,
|
||||
} = schema;
|
||||
|
||||
const { data, loading } = useDynamicData(stepId, schema);
|
||||
const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } =
|
||||
useDynamicFields(stepId, schema);
|
||||
@@ -40,6 +43,10 @@ export default function InputCreator(props) {
|
||||
|
||||
const computedName = namePrefix ? `${namePrefix}.${name}` : name;
|
||||
|
||||
React.useEffect(() => {
|
||||
addAdditionalFieldsValidation?.({ [name]: additionalFields });
|
||||
}, [additionalFields]);
|
||||
|
||||
if (type === 'dynamic') {
|
||||
return (
|
||||
<DynamicField
|
||||
|
@@ -7,6 +7,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import api from 'helpers/api';
|
||||
|
||||
const variableRegExp = /({.*?})/;
|
||||
|
||||
// TODO: extract this function to a separate file
|
||||
function computeArguments(args, getValues) {
|
||||
const initialValue = {};
|
||||
|
Reference in New Issue
Block a user