forked from italia/design-comuni-plone-theme
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: backport collective/volto-form-block#122
- Loading branch information
Showing
1 changed file
with
371 additions
and
0 deletions.
There are no files selected for viewing
371 changes: 371 additions & 0 deletions
371
src/customizations/volto-form-block/components/View.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,371 @@ | ||
/* | ||
* backport https://github.com/collective/volto-form-block/pull/122 | ||
*/ | ||
|
||
import React, { useState, useEffect, useReducer, useRef } from 'react'; | ||
import { useSelector, useDispatch } from 'react-redux'; | ||
import PropTypes from 'prop-types'; | ||
import { useIntl, defineMessages } from 'react-intl'; | ||
import { submitForm, resetOTP } from 'volto-form-block/actions'; | ||
import { getFieldName } from 'volto-form-block/components/utils'; | ||
import FormView from 'volto-form-block/components/FormView'; | ||
// import { formatDate } from '@plone/volto/helpers/Utils/Date'; | ||
import config from '@plone/volto/registry'; | ||
import { Captcha } from 'volto-form-block/components/Widget'; | ||
import { isValidEmail } from 'volto-form-block/helpers/validators'; | ||
import ValidateConfigForm from 'volto-form-block/components/ValidateConfigForm'; | ||
import { OTP_FIELDNAME_EXTENDER } from 'volto-form-block/components/Widget'; | ||
|
||
const messages = defineMessages({ | ||
formSubmitted: { | ||
id: 'formSubmitted', | ||
defaultMessage: ' ', | ||
}, | ||
defaultInvalidFieldMessage: { | ||
id: 'formblock_defaultInvalidFieldMessage', | ||
defaultMessage: 'Invalid field value', | ||
}, | ||
requiredFieldMessage: { | ||
id: 'formblock_requiredFieldMessage', | ||
defaultMessage: 'Fill-in this field', | ||
}, | ||
invalidEmailMessage: { | ||
id: 'formblock_invalidEmailMessage', | ||
defaultMessage: 'The email is incorrect', | ||
}, | ||
insertOtp: { | ||
id: 'formblock_insertOtp_error', | ||
defaultMessage: 'Please, insert the OTP code recived via email.', | ||
}, | ||
}); | ||
|
||
const initialState = { | ||
loading: false, | ||
error: null, | ||
result: null, | ||
}; | ||
|
||
const FORM_STATES = { | ||
normal: 'normal', | ||
loading: 'loading', | ||
error: 'error', | ||
success: 'success', | ||
}; | ||
|
||
const formStateReducer = (state, action) => { | ||
switch (action.type) { | ||
case FORM_STATES.normal: | ||
return initialState; | ||
|
||
case FORM_STATES.loading: | ||
return { loading: true, error: null, result: null }; | ||
|
||
case FORM_STATES.error: | ||
return { loading: false, error: action.error, result: null }; | ||
|
||
case FORM_STATES.success: | ||
return { loading: false, error: null, result: action.result }; | ||
|
||
default: | ||
return initialState; | ||
} | ||
}; | ||
|
||
const getInitialData = (data) => { | ||
const { static_fields = [], subblocks = [] } = data; | ||
|
||
return { | ||
...subblocks.reduce( | ||
(acc, field) => | ||
field.field_type === 'hidden' | ||
? { | ||
...acc, | ||
[getFieldName(field.label, field.id)]: { | ||
...field, | ||
...(data[field.id] && { custom_field_id: data[field.id] }), | ||
}, | ||
} | ||
: acc, | ||
{}, | ||
), | ||
...static_fields.reduce( | ||
(acc, field) => ({ | ||
...acc, | ||
[getFieldName(field.label, field.id)]: field, | ||
}), | ||
{}, | ||
), | ||
}; | ||
}; | ||
|
||
/** | ||
* Form view | ||
* @class View | ||
*/ | ||
const View = ({ data, id, path }) => { | ||
const intl = useIntl(); | ||
const dispatch = useDispatch(); | ||
|
||
const [formData, setFormData] = useReducer((state, action) => { | ||
if (action.reset) { | ||
if (data.email_otp_verification) { | ||
dispatch(resetOTP(id)); | ||
} | ||
return getInitialData(data); | ||
} | ||
|
||
return { | ||
...state, | ||
[action.field]: action.value, | ||
}; | ||
}, getInitialData(data)); | ||
|
||
const [formState, setFormState] = useReducer(formStateReducer, initialState); | ||
const [formErrors, setFormErrors] = useState([]); | ||
|
||
const submitResults = useSelector( | ||
(state) => state.submitForm?.subrequests?.[id], | ||
); | ||
const captchaToken = useRef(); | ||
|
||
const onChangeFormData = (field_id, field, value, extras) => { | ||
setFormData({ field, value: { field_id, value, ...extras } }); | ||
}; | ||
|
||
useEffect(() => { | ||
if (formErrors.length > 0) { | ||
isValidForm(); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [formData]); | ||
|
||
const isValidForm = () => { | ||
const v = []; | ||
data.subblocks.forEach((subblock, index) => { | ||
const name = getFieldName(subblock.label, subblock.id); | ||
const fieldType = subblock.field_type; | ||
const isBCC = subblock.use_as_bcc; | ||
const additionalField = | ||
config.blocks.blocksConfig.form.additionalFields?.filter( | ||
(f) => f.id === fieldType && f.isValid !== undefined, | ||
)?.[0] ?? null; | ||
|
||
if ( | ||
subblock.required && | ||
additionalField && | ||
!additionalField?.isValid(formData, name) | ||
) { | ||
const validation = additionalField?.isValid(formData, name); | ||
if (typeof validation === 'boolean') { | ||
//for retro-compatibility with previous formErrors structure | ||
v.push({ | ||
field: name, | ||
message: intl.formatMessage(messages.defaultInvalidFieldMessage), | ||
}); | ||
} else if (typeof validation === 'object') { | ||
v.push(validation); | ||
} | ||
} else if ( | ||
subblock.required && | ||
fieldType === 'checkbox' && | ||
!formData[name]?.value | ||
) { | ||
v.push({ | ||
field: name, | ||
message: intl.formatMessage(messages.requiredFieldMessage), | ||
}); | ||
} else if ( | ||
subblock.required && | ||
(!formData[name] || | ||
formData[name]?.value?.length === 0 || | ||
JSON.stringify(formData[name]?.value ?? {}) === '{}') | ||
) { | ||
v.push({ | ||
field: name, | ||
message: intl.formatMessage(messages.requiredFieldMessage), | ||
}); | ||
} else if ( | ||
(fieldType === 'from' || fieldType === 'email') && | ||
formData[name]?.value | ||
) { | ||
if (!isValidEmail(formData[name].value)) { | ||
v.push({ | ||
field: name, | ||
message: intl.formatMessage(messages.invalidEmailMessage), | ||
}); | ||
} else if ( | ||
data.email_otp_verification && | ||
isBCC && | ||
!formData[name].otp | ||
) { | ||
v.push({ | ||
field: name + OTP_FIELDNAME_EXTENDER, | ||
message: intl.formatMessage(messages.insertOtp), | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
if (data.captcha && !captchaToken.current) { | ||
v.push({ | ||
field: 'captcha', | ||
message: intl.formatMessage(messages.requiredFieldMessage), | ||
}); | ||
} | ||
|
||
setFormErrors(v); | ||
return v.length === 0; | ||
}; | ||
|
||
const submit = (e) => { | ||
e.preventDefault(); | ||
captcha | ||
.verify() | ||
.then(() => { | ||
if (isValidForm()) { | ||
let attachments = {}; | ||
let captcha = { | ||
provider: data.captcha, | ||
token: captchaToken.current, | ||
}; | ||
if (data.captcha === 'honeypot') { | ||
captcha.value = formData[data.captcha_props.id]?.value ?? ''; | ||
} | ||
|
||
let formattedFormData = { ...formData }; | ||
data.subblocks.forEach((subblock) => { | ||
let name = getFieldName(subblock.label, subblock.id); | ||
if (formattedFormData[name]?.value) { | ||
formattedFormData[name].field_id = subblock.field_id; | ||
const isAttachment = | ||
config.blocks.blocksConfig.form.attachment_fields.includes( | ||
subblock.field_type, | ||
); | ||
// const isDate = subblock.field_type === 'date'; | ||
|
||
if (isAttachment) { | ||
attachments[name] = formattedFormData[name].value; | ||
delete formattedFormData[name]; | ||
} | ||
|
||
// XXX: date should be sent as ISO format, not DD-MM-YYYY ! | ||
// if (isDate) { | ||
// formattedFormData[name].value = formatDate({ | ||
// date: formattedFormData[name].value, | ||
// format: 'DD-MM-YYYY', | ||
// locale: intl.locale, | ||
// }); | ||
// } | ||
} | ||
}); | ||
dispatch( | ||
submitForm( | ||
path, | ||
id, | ||
Object.keys(formattedFormData).map((name) => ({ | ||
...formattedFormData[name], | ||
})), | ||
attachments, | ||
captcha, | ||
), | ||
); | ||
setFormState({ type: FORM_STATES.loading }); | ||
} else { | ||
setFormState({ type: FORM_STATES.error }); | ||
} | ||
}) | ||
.catch(() => { | ||
setFormState({ type: FORM_STATES.error }); | ||
}); | ||
}; | ||
|
||
const resetFormState = () => { | ||
setFormData({ reset: true }); | ||
setFormState({ type: FORM_STATES.normal }); | ||
}; | ||
|
||
const resetFormOnError = () => { | ||
setFormState({ type: FORM_STATES.normal }); | ||
}; | ||
|
||
const getErrorMessage = (field) => { | ||
const e = formErrors?.filter((e) => e.field === field); | ||
return e.length > 0 ? e[0].message : null; | ||
}; | ||
|
||
const captcha = new Captcha({ | ||
captchaToken, | ||
captcha: data.captcha, | ||
captcha_props: data.captcha_props, | ||
errorMessage: getErrorMessage('captcha'), | ||
onChangeFormData, | ||
}); | ||
|
||
const formid = `form-${id}`; | ||
|
||
useEffect(() => { | ||
if (submitResults?.loaded) { | ||
setFormState({ | ||
type: FORM_STATES.success, | ||
result: { | ||
message: intl.formatMessage(messages.formSubmitted), | ||
...submitResults.result, | ||
}, | ||
}); | ||
captcha.reset(); | ||
const formItem = document.getElementById(formid); | ||
if (formItem !== null) { | ||
const formItemPosition = formItem.getBoundingClientRect(); | ||
formItemPosition !== null && | ||
window.scrollTo({ | ||
top: formItemPosition.x, | ||
left: formItemPosition.y, | ||
behavior: 'smooth', | ||
}); | ||
} | ||
} else if (submitResults?.error) { | ||
let errorDescription = `${ | ||
JSON.parse(submitResults.error.response?.text ?? '{}')?.message | ||
}`; | ||
|
||
setFormState({ type: FORM_STATES.error, error: errorDescription }); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [submitResults]); | ||
|
||
useEffect(() => { | ||
resetFormState(); | ||
}, []); | ||
|
||
return ( | ||
<ValidateConfigForm data={data}> | ||
<FormView | ||
id={formid} | ||
formState={formState} | ||
formErrors={formErrors} | ||
formData={formData} | ||
captcha={captcha} | ||
onChangeFormData={onChangeFormData} | ||
data={data} | ||
onSubmit={submit} | ||
resetFormState={resetFormState} | ||
resetFormOnError={resetFormOnError} | ||
getErrorMessage={getErrorMessage} | ||
path={path} | ||
block_id={id} | ||
/> | ||
</ValidateConfigForm> | ||
); | ||
}; | ||
|
||
/** | ||
* Property types. | ||
* @property {Object} propTypes Property types. | ||
* @static | ||
*/ | ||
View.propTypes = { | ||
data: PropTypes.objectOf(PropTypes.any).isRequired, | ||
}; | ||
|
||
export default View; | ||
|