Compare commits
	
		
			2 Commits
		
	
	
		
			AUT-1371
			...
			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