diff --git a/src/components/Field.jsx b/src/components/Field.jsx
index 8e52cc9..d2c69e5 100644
--- a/src/components/Field.jsx
+++ b/src/components/Field.jsx
@@ -26,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' && (
@@ -136,7 +165,7 @@ const Field = ({
{...(isInvalid() ? { className: 'is-invalid' } : {})}
/>
)}
- {field_type === 'checkbox' && (
+ {(field_type === 'yes_no' || field_type === 'checkbox') && (
{
+export const FromSchemaExtender = ({ intl }) => {
return {
fields: ['use_as_reply_to', 'use_as_bcc'],
properties: {
diff --git a/src/components/FieldTypeSchemaExtenders/HiddenSchemaExtender.js b/src/components/FieldTypeSchemaExtenders/HiddenSchemaExtender.js
index 48e89c6..714096f 100644
--- a/src/components/FieldTypeSchemaExtenders/HiddenSchemaExtender.js
+++ b/src/components/FieldTypeSchemaExtenders/HiddenSchemaExtender.js
@@ -6,7 +6,7 @@ const messages = defineMessages({
},
});
-export const HiddenSchemaExtender = (intl) => {
+export const HiddenSchemaExtender = ({ intl }) => {
return {
fields: ['value'],
properties: {
diff --git a/src/components/FieldTypeSchemaExtenders/SelectionSchemaExtender.js b/src/components/FieldTypeSchemaExtenders/SelectionSchemaExtender.js
index 98488aa..567f131 100644
--- a/src/components/FieldTypeSchemaExtenders/SelectionSchemaExtender.js
+++ b/src/components/FieldTypeSchemaExtenders/SelectionSchemaExtender.js
@@ -6,7 +6,7 @@ const messages = defineMessages({
},
});
-export const SelectionSchemaExtender = (intl) => {
+export const SelectionSchemaExtender = ({ intl }) => {
return {
fields: ['input_values'],
properties: {
diff --git a/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js b/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js
new file mode 100644
index 0000000..ad74f55
--- /dev/null
+++ b/src/components/FieldTypeSchemaExtenders/YesNoSchemaExtender.js
@@ -0,0 +1,64 @@
+import { defineMessages } from 'react-intl';
+const messages = defineMessages({
+ field_widget: {
+ id: 'form_field_widget',
+ defaultMessage: 'Widget',
+ },
+ display_values_title: {
+ id: 'form_field_display_values_title',
+ defaultMessage: 'Display values as',
+ },
+ display_values_description: {
+ id: 'form_field_display_values_description',
+ defaultMessage:
+ 'Change how values appear in forms and emails. Data stores and sent, such as CSV exports and XML attachments, will remain unchanged.',
+ },
+});
+
+function InternalValueSchema() {
+ return {
+ title: 'Test',
+ fieldsets: [
+ {
+ id: 'default',
+ title: 'Default',
+ fields: ['yes', 'no'],
+ },
+ ],
+ properties: {
+ yes: {
+ title: 'True',
+ placeholder: 'Yes',
+ },
+ no: {
+ title: 'False',
+ placeholder: 'No',
+ },
+ },
+ };
+}
+
+export const YesNoSchemaExtender = ({ intl, formData }) => {
+ return {
+ fields: ['widget', 'display_values'],
+ properties: {
+ widget: {
+ title: intl.formatMessage(messages.field_widget),
+ type: 'string',
+ choices: [
+ ['checkbox', 'Checkbox'],
+ ['single_choice', 'Radio'],
+ ],
+ default: 'checkbox',
+ },
+ display_values: {
+ title: 'Display values as',
+ description: '',
+ widget: 'object',
+ schema: InternalValueSchema(),
+ collapsible: true,
+ },
+ },
+ required: ['widget'],
+ };
+};
diff --git a/src/components/FieldTypeSchemaExtenders/index.js b/src/components/FieldTypeSchemaExtenders/index.js
index 81e0302..a8874e6 100644
--- a/src/components/FieldTypeSchemaExtenders/index.js
+++ b/src/components/FieldTypeSchemaExtenders/index.js
@@ -1,3 +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 f493057..0a82644 100644
--- a/src/components/View.jsx
+++ b/src/components/View.jsx
@@ -98,9 +98,18 @@ const View = ({ data, id, path }) => {
const [formErrors, setFormErrors] = useState([]);
const submitResults = useSelector((state) => state.submitForm);
const captchaToken = useRef();
+ const formid = `form-${id}`;
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(() => {
@@ -164,7 +173,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: field.default_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
+ };
+ return { ...returnValue, [fieldName]: dataToAdd };
+ },
+ {},
+ );
data.subblocks.forEach((subblock) => {
let name = getFieldName(subblock.label, subblock.id);
if (formattedFormData[name]?.value) {
@@ -172,20 +196,11 @@ const View = ({ data, id, path }) => {
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];
}
-
- if (isDate) {
- formattedFormData[name].value = formatDate({
- date: formattedFormData[name].value,
- format: 'DD-MM-YYYY',
- locale: intl.locale,
- });
- }
}
});
dispatch(
@@ -201,6 +216,10 @@ const View = ({ data, id, path }) => {
);
setFormState({ type: FORM_STATES.loading });
} else {
+ const errorBox = document.getElementById(`${formid}-errors`);
+ if (errorBox) {
+ errorBox.scrollIntoView({ behavior: 'smooth' });
+ }
setFormState({ type: FORM_STATES.error });
}
})
@@ -225,8 +244,6 @@ const View = ({ data, id, path }) => {
onChangeFormData,
});
- const formid = `form-${id}`;
-
useEffect(() => {
if (submitResults?.loaded) {
setFormState({
diff --git a/src/fieldSchema.js b/src/fieldSchema.js
index 83925fb..930520b 100644
--- a/src/fieldSchema.js
+++ b/src/fieldSchema.js
@@ -1,6 +1,5 @@
import config from '@plone/volto/registry';
-import { defineMessages } from 'react-intl';
-import { useIntl } from 'react-intl';
+import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
field_label: {
@@ -15,6 +14,10 @@ const messages = defineMessages({
id: 'form_field_required',
defaultMessage: 'Required',
},
+ field_default: {
+ id: 'form_field_default',
+ defaultMessage: 'Default value',
+ },
field_type: {
id: 'form_field_type',
defaultMessage: 'Field type',
@@ -39,9 +42,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',
@@ -67,8 +70,41 @@ const messages = defineMessages({
id: 'form_field_type_hidden',
defaultMessage: 'Hidden',
},
+ field_show_when_when: {
+ id: 'form_field_show_when',
+ defaultMessage: 'Show 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',
+ },
});
+const choiceTypes = ['select', 'single_choice', 'multiple_choice'];
+
+// TODO: Anyway to inrospect this?
+const fieldTypeDefaultValueTypeMapping = {
+ yes_no: 'boolean',
+ multiple_choice: 'array',
+ date: 'date',
+};
+
export default (props) => {
var intl = useIntl();
const baseFieldTypeChoices = [
@@ -80,7 +116,7 @@ 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)],
@@ -99,8 +135,16 @@ export default (props) => {
var schemaExtender =
config.blocks.blocksConfig.form.fieldTypeSchemaExtenders[props?.field_type];
const schemaExtenderValues = schemaExtender
- ? schemaExtender(intl)
+ ? schemaExtender({ intl, ...props })
: { properties: [], fields: [], required: [] };
+
+ const show_when_when_field =
+ props.show_when_when && props.show_when_when
+ ? props.formData?.subblocks?.find(
+ (field) => field.field_id === props.show_when_when,
+ )
+ : undefined;
+
return {
title: props?.label || '',
fieldsets: [
@@ -113,6 +157,14 @@ export default (props) => {
'field_type',
...schemaExtenderValues.fields,
'required',
+ 'default_value',
+ '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']
+ : []),
],
},
],
@@ -141,12 +193,101 @@ export default (props) => {
type: 'boolean',
default: false,
},
+ default_value: {
+ title: intl.formatMessage(messages.field_default),
+ type: fieldTypeDefaultValueTypeMapping[props?.field_type]
+ ? fieldTypeDefaultValueTypeMapping[props?.field_type]
+ : 'string',
+ ...(props?.field_type === 'yes_no' && {
+ choices: [
+ [true, 'Yes'],
+ [false, 'No'],
+ ],
+ noValueOption: false,
+ }),
+ ...(['select', 'single_choice', 'multiple_choice'].includes(
+ props?.field_type,
+ ) && {
+ choices: props?.formData?.subblocks
+ .filter((block) => block.field_id === props.field_id)?.[0]
+ ?.input_values?.map((input_value) => {
+ return [input_value, input_value];
+ }),
+ noValueOption: 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.reduce((choices, subblock, index) => {
+ const currentFieldIndex = props.formData.subblocks.findIndex(
+ (field) => field.field_id === props.field_id,
+ );
+ if (index > currentFieldIndex) {
+ if (props.show_when_when === subblock.field_id) {
+ choices.push([subblock.field_id, subblock.label]);
+ }
+ return choices;
+ }
+ if (subblock.field_id === props.field_id) {
+ return choices;
+ }
+ choices.push([subblock.field_id, subblock.label]);
+ return choices;
+ }, [])
+ : []),
+ ],
+ 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,
+ required: true,
+ },
+ show_when_to: {
+ title: intl.formatMessage(messages.field_show_when_to),
+ type: 'array',
+ required: true,
+ creatable: true,
+ noValueOption: false,
+ ...(show_when_when_field &&
+ choiceTypes.includes(show_when_when_field.field_type) && {
+ choices: show_when_when_field.input_values,
+ }),
+ ...(show_when_when_field &&
+ show_when_when_field.field_type === 'yes_no' && {
+ choices: [
+ [true, 'Yes'],
+ [false, 'No'],
+ ],
+ }),
+ },
...schemaExtenderValues.properties,
},
required: [
'label',
'field_type',
'input_values',
+ ...(props.show_when_when && props.show_when_when !== 'always'
+ ? ['show_when_is', 'show_when_to']
+ : []),
...schemaExtenderValues.required,
],
};
diff --git a/src/formSchema.js b/src/formSchema.js
index b464503..ab842a1 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,73 @@ 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', 'email_format'],
+ });
+ }
+
+ if (formData?.send || formData?.store) {
+ fieldsets.push({
+ id: 'storedDataIds',
+ title: intl.formatMessage(messages.storedDataIds),
+ fields: formData?.subblocks?.map((subblock) => subblock.field_id),
+ });
+ }
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),
@@ -103,9 +150,68 @@ export default () => {
title: intl.formatMessage(messages.store),
},
send: {
- type: 'boolean',
title: intl.formatMessage(messages.send),
- description: intl.formatMessage(messages.attachmentSendEmail),
+ 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 e7f614c..68a0400 100644
--- a/src/index.js
+++ b/src/index.js
@@ -19,6 +19,7 @@ import {
SelectionSchemaExtender,
FromSchemaExtender,
HiddenSchemaExtender,
+ YesNoSchemaExtender,
} from './components/FieldTypeSchemaExtenders';
export {
submitForm,
@@ -45,6 +46,7 @@ const applyConfig = (config) => {
multiple_choice: SelectionSchemaExtender,
from: FromSchemaExtender,
hidden: HiddenSchemaExtender,
+ yes_no: YesNoSchemaExtender,
},
attachment_fields: ['attachment'],
restricted: false,