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,