diff --git a/src/components/Field.jsx b/src/components/Field.jsx index a5a022b..d2c69e5 100644 --- a/src/components/Field.jsx +++ b/src/components/Field.jsx @@ -12,6 +12,7 @@ import { TextareaWidget, CheckboxListWidget, RadioWidget, + HiddenWidget, } from 'volto-form-block/components/Widget'; import config from '@plone/volto/registry'; @@ -25,32 +26,61 @@ const messages = defineMessages({ }, }); +const widgetMapping = { + single_choice: RadioWidget, + checkbox: CheckboxWidget, +}; + /** * Field class. * @class View * @extends Component */ -const Field = ({ - label, - description, - name, - field_type, - required, - input_values, - value, - onChange, - isOnEdit, - valid, - disabled = false, - formHasErrors = false, - id, -}) => { +const Field = (props) => { + const { + label, + description, + name, + field_type, + required, + input_values, + value, + onChange, + isOnEdit, + valid, + disabled = false, + formHasErrors = false, + id, + widget, + } = props; const intl = useIntl(); const isInvalid = () => { return !isOnEdit && !valid; }; + if (widget) { + const Widget = widgetMapping[widget]; + const valueList = + field_type === 'yes_no' + ? [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ] + : [...(input_values?.map((v) => ({ value: v, label: v })) ?? [])]; + + return ( + + ); + } + return (
{field_type === 'text' && ( @@ -135,7 +165,7 @@ const Field = ({ {...(isInvalid() ? { className: 'is-invalid' } : {})} /> )} - {field_type === 'checkbox' && ( + {(field_type === 'yes_no' || field_type === 'checkbox') && ( )} + {field_type === 'hidden' && ( + + )} {field_type === 'static_text' && (isOnEdit ? ( { + return { + fields: ['value'], + properties: { + value: { + title: intl.formatMessage(messages.field_input_value), + type: 'text', + }, + }, + required: ['value'], + }; +}; diff --git a/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js b/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js new file mode 100644 index 0000000..99b5062 --- /dev/null +++ b/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js @@ -0,0 +1,25 @@ +import { defineMessages } from 'react-intl'; +const messages = defineMessages({ + field_widget: { + id: 'form_field_widget', + defaultMessage: 'Widget', + }, +}); + +export const YesNoSchemaExtender = (intl) => { + return { + fields: ['widget'], + properties: { + widget: { + title: intl.formatMessage(messages.field_widget), + type: 'array', + choices: [ + ['checkbox', 'Checkbox'], + ['single_choice', 'Radio'], + ], + default: 'checkbox', + }, + }, + required: ['widget'], + }; +}; diff --git a/src/components/FieldTypeSchemaExtenders/index.js b/src/components/FieldTypeSchemaExtenders/index.js index fb9659a..a8874e6 100644 --- a/src/components/FieldTypeSchemaExtenders/index.js +++ b/src/components/FieldTypeSchemaExtenders/index.js @@ -1,2 +1,4 @@ export { SelectionSchemaExtender } from './SelectionSchemaExtender'; export { FromSchemaExtender } from './FromSchemaExtender'; +export { HiddenSchemaExtender } from './HiddenSchemaExtender'; +export { YesNoSchemaExtender } from './YesNoSchemaExtender'; diff --git a/src/components/FormView.jsx b/src/components/FormView.jsx index db65cea..b44569f 100644 --- a/src/components/FormView.jsx +++ b/src/components/FormView.jsx @@ -10,6 +10,7 @@ import { } from 'semantic-ui-react'; import { getFieldName } from 'volto-form-block/components/utils'; import Field from 'volto-form-block/components/Field'; +import { showWhenValidator } from 'volto-form-block/helpers/show_when'; import config from '@plone/volto/registry'; /* Style */ @@ -134,6 +135,30 @@ const FormView = ({ }), ); + const value = + subblock.field_type === 'static_text' + ? subblock.value + : formData[name]?.value; + const { show_when, target_value } = subblock; + + const shouldShowValidator = showWhenValidator[show_when]; + const shouldShowTargetValue = + formData[subblock.target_field]?.value; + + // Only checking for false here to preserve backwards compatibility with blocks that haven't been updated and so have a value of 'undefined' or 'null' + const shouldShow = shouldShowValidator + ? shouldShowValidator({ + value: shouldShowTargetValue, + target_value: target_value, + }) !== false + : true; + + const shouldHide = __CLIENT__ && !shouldShow; + + if (shouldHide) { + return

Empty

; + } + return ( @@ -148,11 +173,7 @@ const FormView = ({ fields_to_send_with_value, ) } - value={ - subblock.field_type === 'static_text' - ? subblock.value - : formData[name]?.value - } + value={value} valid={isValidField(name)} formHasErrors={formErrors?.length > 0} /> diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 7edf0b2..e2696cd 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -191,7 +191,7 @@ const Sidebar = ({ { var update_values = {}; diff --git a/src/components/View.jsx b/src/components/View.jsx index 393fcc9..52b3759 100644 --- a/src/components/View.jsx +++ b/src/components/View.jsx @@ -48,12 +48,32 @@ const formStateReducer = (state, action) => { } }; -const getInitialData = (data) => ({ - ...data.reduce( - (acc, field) => ({ ...acc, [getFieldName(field.label, field.id)]: field }), - {}, - ), -}); +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 @@ -62,18 +82,17 @@ const getInitialData = (data) => ({ const View = ({ data, id, path }) => { const intl = useIntl(); const dispatch = useDispatch(); - const { static_fields = [] } = data; const [formData, setFormData] = useReducer((state, action) => { if (action.reset) { - return getInitialData(static_fields); + return getInitialData(data); } return { ...state, [action.field]: action.value, }; - }, getInitialData(static_fields)); + }, getInitialData(data)); const [formState, setFormState] = useReducer(formStateReducer, initialState); const [formErrors, setFormErrors] = useState([]); @@ -81,7 +100,15 @@ const View = ({ data, id, path }) => { const captchaToken = useRef(); const onChangeFormData = (field_id, field, value, extras) => { - setFormData({ field, value: { field_id, value, ...extras } }); + setFormData({ + field, + value: { + field_id, + value, + ...(data[field_id] && { custom_field_id: data[field_id] }), // Conditionally add the key. Nicer to work with than having a key with a null value + ...extras, + }, + }); }; useEffect(() => { @@ -145,7 +172,22 @@ const View = ({ data, id, path }) => { captcha.value = formData[data.captcha_props.id]?.value ?? ''; } - let formattedFormData = { ...formData }; + let formattedFormData = data.subblocks.reduce( + (returnValue, field) => { + if (field.field_type === 'static_text') { + return returnValue; + } + const fieldName = getFieldName(field.label, field.id); + const dataToAdd = formData[fieldName] ?? { + field_id: field.id, + label: field.label, + value: null, + ...(data[field.id] && { custom_field_id: data[field.id] }), // Conditionally add the key. Nicer to work with than having a key with a null value + }; + return { ...returnValue, [fieldName]: dataToAdd }; + }, + {}, + ); data.subblocks.forEach((subblock) => { let name = getFieldName(subblock.label, subblock.id); if (formattedFormData[name]?.value) { diff --git a/src/components/Widget/HiddenWidget.jsx b/src/components/Widget/HiddenWidget.jsx new file mode 100644 index 0000000..b314854 --- /dev/null +++ b/src/components/Widget/HiddenWidget.jsx @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; + +/** + * Displays an ``. + */ +export const HiddenWidget = ({ id, title, value, isOnEdit }) => { + const inputId = `field-${id}`; + return ( + <> + {isOnEdit ? : null} + + + ); +}; + +HiddenWidget.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string, + description: PropTypes.string, + required: PropTypes.bool, + error: PropTypes.arrayOf(PropTypes.string), + value: PropTypes.string, + focus: PropTypes.bool, + onChange: PropTypes.func, + onBlur: PropTypes.func, + onClick: PropTypes.func, + onEdit: PropTypes.func, + onDelete: PropTypes.func, + minLength: PropTypes.number, + maxLength: PropTypes.number, + wrapped: PropTypes.bool, + placeholder: PropTypes.string, +}; diff --git a/src/components/Widget/index.js b/src/components/Widget/index.js index d26f3ca..1b2cb56 100644 --- a/src/components/Widget/index.js +++ b/src/components/Widget/index.js @@ -6,6 +6,7 @@ export { default as EmailWidget } from 'volto-form-block/components/Widget/Email export { default as FileWidget } from 'volto-form-block/components/Widget/FileWidget'; export { default as GoogleReCaptchaWidget } from 'volto-form-block/components/Widget/GoogleReCaptchaWidget'; export { default as HCaptchaWidget } from 'volto-form-block/components/Widget/HCaptchaWidget'; +export { HiddenWidget } from 'volto-form-block/components/Widget/HiddenWidget'; export { default as HoneypotCaptchaWidget } from 'volto-form-block/components/Widget/HoneypotCaptchaWidget'; export { default as NoRobotsCaptchaWidget } from 'volto-form-block/components/Widget/NoRobotsCaptchaWidget'; export { default as RadioWidget } from 'volto-form-block/components/Widget/RadioWidget'; diff --git a/src/fieldSchema.js b/src/fieldSchema.js index 95916b5..2be9a5d 100644 --- a/src/fieldSchema.js +++ b/src/fieldSchema.js @@ -39,9 +39,9 @@ const messages = defineMessages({ id: 'form_field_type_multiple_choice', defaultMessage: 'Multiple choice', }, - field_type_checkbox: { - id: 'form_field_type_checkbox', - defaultMessage: 'Checkbox', + field_type_yes_no: { + id: 'field_type_yes_no', + defaultMessage: 'Yes/ No', }, field_type_date: { id: 'form_field_type_date', @@ -63,6 +63,34 @@ const messages = defineMessages({ id: 'form_field_type_static_text', defaultMessage: 'Static text', }, + field_type_hidden: { + id: 'form_field_type_hidden', + defaultMessage: 'Hidden', + }, + field_show_when_when: { + id: 'form_field_show_when', + defaultMessage: 'When', + }, + field_show_when_is: { + id: 'form_field_show_is', + defaultMessage: 'Is', + }, + field_show_when_to: { + id: 'form_field_show_to', + defaultMessage: 'To', + }, + field_show_when_option_always: { + id: 'form_field_show_when_option_', + defaultMessage: 'Always', + }, + field_show_when_option_value_is: { + id: 'form_field_show_when_option_value_is', + defaultMessage: 'equal', + }, + field_show_when_option_value_is_not: { + id: 'form_field_show_when_option_value_is_not', + defaultMessage: 'not equal', + }, }); export default (props) => { @@ -76,11 +104,12 @@ export default (props) => { 'multiple_choice', intl.formatMessage(messages.field_type_multiple_choice), ], - ['checkbox', intl.formatMessage(messages.field_type_checkbox)], + ['yes_no', intl.formatMessage(messages.field_type_yes_no)], ['date', intl.formatMessage(messages.field_type_date)], ['attachment', intl.formatMessage(messages.field_type_attachment)], ['from', intl.formatMessage(messages.field_type_from)], ['static_text', intl.formatMessage(messages.field_type_static_text)], + ['hidden', intl.formatMessage(messages.field_type_hidden)], ]; var attachmentDescription = props?.field_type === 'attachment' @@ -96,6 +125,7 @@ export default (props) => { const schemaExtenderValues = schemaExtender ? schemaExtender(intl) : { properties: [], fields: [], required: [] }; + return { title: props?.label || '', fieldsets: [ @@ -108,6 +138,13 @@ export default (props) => { 'field_type', ...schemaExtenderValues.fields, 'required', + 'show_when_when', + ...(props.show_when_when && props.show_when_when !== 'always' + ? ['show_when_is'] + : []), + ...(props.show_when_when && props.show_when_when !== 'always' + ? ['show_when_to'] + : []), ], }, ], @@ -122,7 +159,7 @@ export default (props) => { }, field_type: { title: intl.formatMessage(messages.field_type), - type: 'array', + type: 'string', choices: [ ...baseFieldTypeChoices, ...(config.blocks.blocksConfig.form.additionalFields?.map( @@ -136,6 +173,43 @@ export default (props) => { type: 'boolean', default: false, }, + show_when_when: { + title: intl.formatMessage(messages.field_show_when_when), + type: 'string', + choices: [ + [ + 'always', + intl.formatMessage(messages.field_show_when_option_always), + ], + ...(props?.formData?.subblocks + ? props.formData.subblocks.map((subblock) => { + // Using getFieldName as it is what is used for the formData later. Saves + // performing `getFieldName` for every block every render. + return [subblock.field_id, subblock.label]; + }) + : []), + ], + default: 'always', + }, + show_when_is: { + title: intl.formatMessage(messages.field_show_when_is), + type: 'string', + choices: [ + [ + 'value_is', + intl.formatMessage(messages.field_show_when_option_value_is), + ], + [ + 'value_is_not', + intl.formatMessage(messages.field_show_when_option_value_is_not), + ], + ], + noValueOption: false, + }, + show_when_to: { + title: intl.formatMessage(messages.field_show_when_to), + type: 'string', + }, ...schemaExtenderValues.properties, }, required: [ diff --git a/src/formSchema.js b/src/formSchema.js index e351aad..fa708cd 100644 --- a/src/formSchema.js +++ b/src/formSchema.js @@ -1,5 +1,4 @@ -import { defineMessages } from 'react-intl'; -import { useIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; const messages = defineMessages({ form: { @@ -34,7 +33,14 @@ const messages = defineMessages({ id: 'captcha', defaultMessage: 'Captcha provider', }, - + headers: { + id: 'Headers', + defaultMessage: 'Headers', + }, + headersDescription: { + id: 'Headers Description', + defaultMessage: "These headers aren't included in the sent email by default. Use this dropdown to include them in the sent email", + }, store: { id: 'form_save_persistent_data', defaultMessage: 'Store compiled data', @@ -45,32 +51,82 @@ const messages = defineMessages({ }, send: { id: 'form_send_email', - defaultMessage: 'Send email to recipient', + defaultMessage: 'Send email to', + }, + attachXml: { + id: 'form_attach_xml', + defaultMessage: 'Attach XML to email', + }, + storedDataIds: { + id: 'form_stored_data_ids', + defaultMessage: 'Data ID mapping', + }, + email_format: { + id: 'form_email_format', + defaultMessage: 'Email format', }, }); -export default () => { +export default (formData) => { var intl = useIntl(); + const emailFields = + formData?.subblocks?.reduce((acc, field) => { + return ['from', 'email'].includes(field.field_type) + ? [...acc, [field.id, field.label]] + : acc; + }, []) ?? []; + + const fieldsets = [ + { + id: 'default', + title: 'Default', + fields: [ + 'title', + 'description', + 'default_to', + 'default_from', + 'default_subject', + 'submit_label', + 'captcha', + 'store', + 'send', + ...(formData?.send && + Array.isArray(formData.send) && + formData.send.includes('acknowledgement') + ? ['acknowledgementFields', 'acknowledgementMessage'] + : []), + ], + }, + ]; + + if (formData?.send) { + fieldsets.push({ + id: 'sendingOptions', + title: 'Sending options', + fields: ['attachXml', 'httpHeaders'], + }); + } + + if (formData?.send || formData?.store) { + fieldsets.push({ + id: 'storedDataIds', + title: intl.formatMessage(messages.storedDataIds), + fields: formData?.subblocks?.map((subblock) => subblock.field_id), + }); + } + + + if (formData?.send) { + fieldsets.push({ + id: 'sendingOptions', + title: 'Sending options', + fields: ['email_format'], + }); + } return { title: intl.formatMessage(messages.form), - fieldsets: [ - { - id: 'default', - title: 'Default', - fields: [ - 'title', - 'description', - 'default_to', - 'default_from', - 'default_subject', - 'submit_label', - 'captcha', - 'store', - 'send', - ], - }, - ], + fieldsets: fieldsets, properties: { title: { title: intl.formatMessage(messages.title), @@ -104,8 +160,68 @@ export default () => { description: intl.formatMessage(messages.attachmentSendEmail), }, send: { - type: 'boolean', title: intl.formatMessage(messages.send), + isMulti: 'true', + default: 'recipient', + choices: [ + ['recipient', 'Recipient'], + ['acknowledgement', 'Acknowledgement'], + ], + }, + acknowledgementMessage: { + // TODO: i18n + title: 'Acknowledgement message', + widget: 'richtext', + }, + acknowledgementFields: { + // TODO: i18n + title: 'Acknowledgement field', + decription: + 'Select which fields will contain an email address to send an acknowledgement to.', + isMulti: false, + noValueOption: false, + choices: formData?.subblocks ? emailFields : [], + ...(emailFields.length === 1 && { default: emailFields[0][0] }), + }, + attachXml: { + type: 'boolean', + title: intl.formatMessage(messages.attachXml), + }, + // Add properties for each of the fields for use in the data mapping + ...(formData?.subblocks + ? Object.assign( + {}, + ...formData?.subblocks?.map((subblock) => { + return { [subblock.field_id]: { title: subblock.label } }; + }), + ) + : {}), + httpHeaders: { + type: 'boolean', + title: intl.formatMessage(messages.headers), + description: intl.formatMessage(messages.headersDescription), + type: 'string', + factory: 'Choice', + default: '', + isMulti: true, + noValueOption: false, + choices: [ + ['HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED_FOR'], + ['HTTP_X_FORWARDED_PORT','HTTP_X_FORWARDED_PORT'], + ['REMOTE_ADDR','REMOTE_ADDR'], + ['PATH_INFO','PATH_INFO'], + ['HTTP_USER_AGENT','HTTP_USER_AGENT'], + ['HTTP_REFERER','HTTP_REFERER'], + ], + }, + email_format: { + title: intl.formatMessage(messages.email_format), + type: 'string', + choices: [ + ['list', 'List'], + ['table', 'Table'], + ], + noValueOption: false, }, }, required: ['default_to', 'default_from', 'default_subject'], diff --git a/src/helpers/show_when.js b/src/helpers/show_when.js new file mode 100644 index 0000000..1e6ad93 --- /dev/null +++ b/src/helpers/show_when.js @@ -0,0 +1,10 @@ +const always = () => true; +const value_is = ({ value, target_value }) => value === target_value; +const value_is_not = ({ value, target_value }) => value !== target_value; + +export const showWhenValidator = { + '': always, + always: always, + value_is: value_is, + value_is_not: value_is_not, +}; diff --git a/src/index.js b/src/index.js index 676ed26..06ee126 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,8 @@ import FieldSchema from 'volto-form-block/fieldSchema'; import { SelectionSchemaExtender, FromSchemaExtender, + HiddenSchemaExtender, + YesNoSchemaExtender } from './components/FieldTypeSchemaExtenders'; export { submitForm, @@ -43,6 +45,8 @@ const applyConfig = (config) => { single_choice: SelectionSchemaExtender, multiple_choice: SelectionSchemaExtender, from: FromSchemaExtender, + hidden: HiddenSchemaExtender, + yes_no: YesNoSchemaExtender, }, restricted: false, mostUsed: true,