diff --git a/packages/web/src/components/AccountDropdownMenu/index.jsx b/packages/web/src/components/AccountDropdownMenu/index.jsx index e4889c89..4c69a8be 100644 --- a/packages/web/src/components/AccountDropdownMenu/index.jsx +++ b/packages/web/src/components/AccountDropdownMenu/index.jsx @@ -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, }; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx index bf3c3748..9aa3b741 100644 --- a/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx +++ b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx @@ -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 ( { 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, diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx index 0dee4bbe..451d67e5 100644 --- a/packages/web/src/components/FlowStep/index.jsx +++ b/packages/web/src/components/FlowStep/index.jsx @@ -36,7 +36,8 @@ import useActions from 'hooks/useActions'; import useTriggerSubsteps from 'hooks/useTriggerSubsteps'; import useActionSubsteps from 'hooks/useActionSubsteps'; import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions'; -import { generateValidationSchema } from './validation'; +import { validationSchemaResolver } from './validation'; +import { isEqual } from 'lodash'; const validIcon = ; const errorIcon = ; @@ -51,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) { @@ -105,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); }, []); @@ -117,11 +128,6 @@ function FlowStep(props) { handleChange({ step: val }); }; - const stepValidationSchema = React.useMemo( - () => generateValidationSchema(substeps), - [substeps], - ); - if (!apps?.data) { return ( { + if (additionalFields) { + setFormResolverContext((prev) => ({ + ...prev, + additionalFields: { ...prev.additionalFields, ...additionalFields }, + })); + } + }; + const validationStatusIcon = step.status === 'completed' ? validIcon : errorIcon; @@ -203,7 +218,8 @@ function FlowStep(props) {
)} diff --git a/packages/web/src/components/FlowStep/validation.js b/packages/web/src/components/FlowStep/validation.js index 134a4f62..4d1589e7 100644 --- a/packages/web/src/components/FlowStep/validation.js +++ b/packages/web/src/components/FlowStep/validation.js @@ -41,74 +41,80 @@ function addDependsOnValidation({ schema, dependsOn, key, args }) { return schema; } -export function generateValidationSchema(substeps) { - const fieldValidations = substeps?.reduce( - (allValidations, { arguments: args }) => { - if (!args || !Array.isArray(args)) return allValidations; +export function validationSchemaResolver(data, context, options) { + const { substeps = [], additionalFields = {} } = context; - 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 { key, required } = arg; + const substepArgumentValidations = {}; - // base validation for the field if not exists - if (!substepArgumentValidations[key]) { - substepArgumentValidations[key] = yup.mixed(); - } + for (const arg of args) { + const { key, required } = arg; - if (arg.type === 'dynamic') { - const fieldsSchema = {}; + // base validation for the field if not exists + if (!substepArgumentValidations[key]) { + substepArgumentValidations[key] = yup.mixed(); + } - for (const field of arg.fields) { - fieldsSchema[field.key] = yup.mixed(); + if (arg.type === 'dynamic') { + const fieldsSchema = {}; - fieldsSchema[field.key] = addRequiredValidation({ - required: field.required, - schema: fieldsSchema[field.key], - key: field.key, - }); + for (const field of arg.fields) { + fieldsSchema[field.key] = yup.mixed(); - 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, + fieldsSchema[field.key] = addRequiredValidation({ + required: field.required, + schema: fieldsSchema[field.key], + key: field.key, }); - substepArgumentValidations[key] = addDependsOnValidation({ - schema: substepArgumentValidations[key], - dependsOn: arg.dependsOn, - key, + fieldsSchema[field.key] = addDependsOnValidation({ + schema: fieldsSchema[field.key], + dependsOn: field.dependsOn, + key: field.key, args, }); } - } - return { - ...allValidations, - ...substepArgumentValidations, - }; - }, - {}, - ); + 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); + return yupResolver(validationSchema)(data, context, options); } diff --git a/packages/web/src/components/FlowStepContextMenu/index.jsx b/packages/web/src/components/FlowStepContextMenu/index.jsx index 9fedb469..2c538c7d 100644 --- a/packages/web/src/components/FlowStepContextMenu/index.jsx +++ b/packages/web/src/components/FlowStepContextMenu/index.jsx @@ -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, }; diff --git a/packages/web/src/components/FlowSubstep/index.jsx b/packages/web/src/components/FlowSubstep/index.jsx index 55bb5cd6..46352ae0 100644 --- a/packages/web/src/components/FlowSubstep/index.jsx +++ b/packages/web/src/components/FlowSubstep/index.jsx @@ -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} /> ))} diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx index 061e10d8..73752cb0 100644 --- a/packages/web/src/components/Form/index.jsx +++ b/packages/web/src/components/Form/index.jsx @@ -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 ( diff --git a/packages/web/src/components/InputCreator/index.jsx b/packages/web/src/components/InputCreator/index.jsx index 613244cf..a2acab8e 100644 --- a/packages/web/src/components/InputCreator/index.jsx +++ b/packages/web/src/components/InputCreator/index.jsx @@ -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 (