diff --git a/src/client/components/forms/Form/fieldReducer.js b/src/client/components/forms/Form/fieldReducer.js index 847ebaf8ea..c75fac7ccb 100644 --- a/src/client/components/forms/Form/fieldReducer.js +++ b/src/client/components/forms/Form/fieldReducer.js @@ -150,6 +150,7 @@ function fieldReducer(state, action) { ignoreWhileFlattening: action.ignoreWhileFlattening, initialValue: action.initialValue, stringify: action.stringify, + validate: action.validate, }; return { diff --git a/src/client/components/forms/Form/index.js b/src/client/components/forms/Form/index.js index 1b2fb13ba7..7032f59e4c 100644 --- a/src/client/components/forms/Form/index.js +++ b/src/client/components/forms/Form/index.js @@ -14,6 +14,7 @@ import initContextState from './initContextState'; import reduceFieldsToValues from './reduceFieldsToValues'; import getSiblingDataFunc from './getSiblingData'; import getDataByPathFunc from './getDataByPath'; +import wait from '../../../../utilities/wait'; import { SubmittedContext, ProcessingContext, ModifiedContext, FormContext, FieldContext } from './context'; @@ -35,6 +36,7 @@ const Form = (props) => { disableSuccessStatus, initialState, disableScrollOnSuccess, + waitForAutocomplete, } = props; const history = useHistory(); @@ -53,21 +55,50 @@ const Form = (props) => { const [fields, dispatchFields] = useReducer(fieldReducer, {}); contextRef.current.fields = fields; - const submit = useCallback((e) => { + const validateForm = useCallback(async () => { + const validatedFieldState = {}; + let isValid = true; + + const validationPromises = Object.entries(contextRef.current.fields).map(async ([path, field]) => { + const validatedField = { ...field }; + + validatedField.valid = typeof field.validate === 'function' ? await field.validate(field.value) : true; + + if (typeof validatedField.valid === 'string') { + validatedField.errorMessage = validatedField.valid; + validatedField.valid = false; + isValid = false; + } + + validatedFieldState[path] = validatedField; + }); + + await Promise.all(validationPromises); + + dispatchFields({ type: 'REPLACE_STATE', state: validatedFieldState }); + + return isValid; + }, [contextRef]); + + const submit = useCallback(async (e) => { if (disabled) { e.preventDefault(); return false; } e.stopPropagation(); - setSubmitted(true); + e.preventDefault(); - const isValid = contextRef.current.validateForm(); + setProcessing(true); + + if (waitForAutocomplete) await wait(100); + + const isValid = await contextRef.current.validateForm(); + + setSubmitted(true); // If not valid, prevent submission if (!isValid) { - e.preventDefault(); - addStatus({ message: 'Please correct the fields below.', type: 'error', @@ -85,12 +116,9 @@ const Form = (props) => { // If submit handler comes through via props, run that if (onSubmit) { - e.preventDefault(); return onSubmit(fields); } - e.preventDefault(); - if (!disableScrollOnSuccess) { window.scrollTo({ top: 0, @@ -99,103 +127,104 @@ const Form = (props) => { } const formData = contextRef.current.createFormData(); - setProcessing(true); - // Make the API call from the action - return requests[method.toLowerCase()](action, { - body: formData, - }).then((res) => { - setModified(false); + try { + const res = await requests[method.toLowerCase()](action, { + body: formData, + }); + if (typeof handleResponse === 'function') return handleResponse(res); - return res.json().then((json) => { - setProcessing(false); - clearStatus(); + const json = await res.json(); - if (res.status < 400) { - setSubmitted(false); + setProcessing(false); + clearStatus(); - if (typeof onSuccess === 'function') onSuccess(json); + if (res.status < 400) { + setSubmitted(false); - if (redirect) { - const destination = { - pathname: redirect, + if (typeof onSuccess === 'function') onSuccess(json); + + if (redirect) { + const destination = { + pathname: redirect, + }; + + if (json.message && !disableSuccessStatus) { + destination.state = { + status: [ + { + message: json.message, + type: 'success', + }, + ], }; - - if (json.message && !disableSuccessStatus) { - destination.state = { - status: [ - { - message: json.message, - type: 'success', - }, - ], - }; - } - - history.push(destination); - } else if (!disableSuccessStatus) { - replaceStatus([{ - message: json.message, - type: 'success', - disappear: 3000, - }]); - } - } else { - contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form - - if (json.message) { - addStatus({ - message: json.message, - type: 'error', - }); - - return json; } - if (Array.isArray(json.errors)) { - const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]); - - fieldErrors.forEach((err) => { - dispatchFields({ - ...(contextRef.current?.fields?.[err.field] || {}), - valid: false, - errorMessage: err.message, - path: err.field, - }); - }); - - nonFieldErrors.forEach((err) => { - addStatus({ - message: err.message || 'An unknown error occurred.', - type: 'error', - }); - }); - - if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { - addStatus({ - message: 'Please correct the fields below.', - type: 'error', - }); - } - - return json; - } + history.push(destination); + } else if (!disableSuccessStatus) { + replaceStatus([{ + message: json.message, + type: 'success', + disappear: 3000, + }]); + } + } else { + contextRef.current = { ...contextRef.current }; // triggers rerender of all components that subscribe to form + if (json.message) { addStatus({ - message: 'An unknown error occurred.', + message: json.message, type: 'error', }); + + return json; } - return json; - }); - }).catch((err) => { - addStatus({ + if (Array.isArray(json.errors)) { + const [fieldErrors, nonFieldErrors] = json.errors.reduce(([fieldErrs, nonFieldErrs], err) => (err.field && err.message ? [[...fieldErrs, err], nonFieldErrs] : [fieldErrs, [...nonFieldErrs, err]]), [[], []]); + + fieldErrors.forEach((err) => { + dispatchFields({ + ...(contextRef.current?.fields?.[err.field] || {}), + valid: false, + errorMessage: err.message, + path: err.field, + }); + }); + + nonFieldErrors.forEach((err) => { + addStatus({ + message: err.message || 'An unknown error occurred.', + type: 'error', + }); + }); + + if (fieldErrors.length > 0 && nonFieldErrors.length === 0) { + addStatus({ + message: 'Please correct the fields below.', + type: 'error', + }); + } + + return json; + } + + addStatus({ + message: 'An unknown error occurred.', + type: 'error', + }); + } + + return json; + } catch (err) { + setProcessing(false); + + return addStatus({ message: err, type: 'error', }); - }); + } }, [ action, addStatus, @@ -211,6 +240,7 @@ const Form = (props) => { redirect, replaceStatus, disableScrollOnSuccess, + waitForAutocomplete, ]); @@ -224,8 +254,6 @@ const Form = (props) => { const getUnflattenedValues = useCallback(() => reduceFieldsToValues(contextRef.current.fields), [contextRef]); - const validateForm = useCallback(() => !Object.values(contextRef.current.fields).some((field) => field.valid === false), [contextRef]); - const createFormData = useCallback(() => { const data = reduceFieldsToValues(contextRef.current.fields); @@ -306,6 +334,7 @@ Form.defaultProps = { disabled: false, initialState: {}, disableScrollOnSuccess: false, + waitForAutocomplete: false, }; Form.propTypes = { @@ -324,6 +353,7 @@ Form.propTypes = { disabled: PropTypes.bool, initialState: PropTypes.shape({}), disableScrollOnSuccess: PropTypes.bool, + waitForAutocomplete: PropTypes.bool, }; export default Form; diff --git a/src/client/components/forms/useFieldType/index.js b/src/client/components/forms/useFieldType/index.js index 2dbf45d868..2a22aeb52a 100644 --- a/src/client/components/forms/useFieldType/index.js +++ b/src/client/components/forms/useFieldType/index.js @@ -56,6 +56,7 @@ const useFieldType = (options) => { fieldToDispatch.disableFormData = disableFormData; fieldToDispatch.ignoreWhileFlattening = ignoreWhileFlattening; fieldToDispatch.initialValue = initialValue; + fieldToDispatch.validate = validate; dispatchFields(fieldToDispatch); }, [path, dispatchFields, validate, disableFormData, ignoreWhileFlattening, initialValue, stringify]); diff --git a/src/client/components/providers/Authentication.js b/src/client/components/providers/Authentication.js index 6424226cf8..66241ed647 100644 --- a/src/client/components/providers/Authentication.js +++ b/src/client/components/providers/Authentication.js @@ -140,7 +140,7 @@ const AuthenticationProvider = ({ children }) => { if (remainingTime > 0) { forceLogOut = setTimeout(() => { setUser(null); - history.push(`${admin}/logout`); + history.push(`${admin}/logout-inactivity`); closeAllModals(); }, remainingTime * 1000); } diff --git a/src/client/components/views/Login/index.js b/src/client/components/views/Login/index.js index c2313004c9..1d6023fcb0 100644 --- a/src/client/components/views/Login/index.js +++ b/src/client/components/views/Login/index.js @@ -69,6 +69,7 @@ const Login = () => {