UNPKG

@plone/volto

Version:
361 lines (343 loc) 9.73 kB
/** * Modal form component. * @module components/manage/Form/ModalForm */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import isEqual from 'lodash/isEqual'; import keys from 'lodash/keys'; import map from 'lodash/map'; import { Button, Form as UiForm, Header, Menu, Message, Modal, Dimmer, Loader, } from 'semantic-ui-react'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import FormValidation from '@plone/volto/helpers/FormValidation/FormValidation'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import { Field } from '@plone/volto/components/manage/Form'; import aheadSVG from '@plone/volto/icons/ahead.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; const messages = defineMessages({ required: { id: 'Required input is missing.', defaultMessage: 'Required input is missing.', }, minLength: { id: 'Minimum length is {len}.', defaultMessage: 'Minimum length is {len}.', }, uniqueItems: { id: 'Items must be unique.', defaultMessage: 'Items must be unique.', }, save: { id: 'Save', defaultMessage: 'Save', }, cancel: { id: 'Cancel', defaultMessage: 'Cancel', }, }); /** * Modal form container class. * @class ModalForm * @extends Component */ class ModalForm extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { schema: PropTypes.shape({ fieldsets: PropTypes.arrayOf( PropTypes.shape({ fields: PropTypes.arrayOf(PropTypes.string), id: PropTypes.string, title: PropTypes.string, }), ), properties: PropTypes.objectOf(PropTypes.any), required: PropTypes.arrayOf(PropTypes.string), }).isRequired, title: PropTypes.string.isRequired, description: PropTypes.objectOf(PropTypes.any), formData: PropTypes.objectOf(PropTypes.any), submitError: PropTypes.string, onSubmit: PropTypes.func.isRequired, onCancel: PropTypes.func, onChangeFormData: PropTypes.func, open: PropTypes.bool, submitLabel: PropTypes.string, loading: PropTypes.bool, loadingMessage: PropTypes.string, className: PropTypes.string, }; /** * Default properties. * @property {Object} defaultProps Default properties. * @static */ static defaultProps = { submitLabel: null, onCancel: null, formData: {}, open: true, loading: null, loadingMessage: null, submitError: null, className: null, dimmer: null, }; /** * Constructor * @method constructor * @param {Object} props Component properties * @constructs ModalForm */ constructor(props) { super(props); this.state = { currentTab: 0, errors: {}, isFormPristine: true, formData: props.formData, }; this.selectTab = this.selectTab.bind(this); this.onChangeField = this.onChangeField.bind(this); this.onBlurField = this.onBlurField.bind(this); this.onClickInput = this.onClickInput.bind(this); this.onSubmit = this.onSubmit.bind(this); } /** * Change field handler * @method onChangeField * @param {string} id Id of the field * @param {*} value Value of the field * @returns {undefined} */ onChangeField(id, value) { this.setState({ formData: { ...this.state.formData, [id]: value, }, }); } /** * If user clicks on input, the form will be not considered pristine * this will avoid onBlur effects without interaction with the form * @param {Object} e event */ onClickInput(e) { this.setState({ isFormPristine: false }); } /** * Validate fields on blur * @method onBlurField * @param {string} id Id of the field * @param {*} value Value of the field * @returns {undefined} */ onBlurField(id, value) { if (!this.state.isFormPristine) { const errors = FormValidation.validateFieldsPerFieldset({ schema: this.props.schema, formData: this.state.formData, formatMessage: this.props.intl.formatMessage, touchedField: { [id]: value }, }); this.setState({ errors, }); } } /** * Submit handler * @method onSubmit * @param {Object} event Event object. * @returns {undefined} */ onSubmit(event) { event.preventDefault(); const errors = FormValidation.validateFieldsPerFieldset({ schema: this.props.schema, formData: this.state.formData, formatMessage: this.props.intl.formatMessage, }); if (keys(errors).length > 0) { this.setState({ errors, }); } else { let setFormDataCallback = (formData) => { this.setState({ formData: formData, errors: {} }); }; this.props.onSubmit(this.state.formData, setFormDataCallback); } } /** * Select tab handler * @method selectTab * @param {Object} event Event object. * @param {number} index Selected tab index. * @returns {undefined} */ selectTab(event, { index }) { this.setState({ currentTab: index, }); } /** * Component did update lifecycle handler * @param {Object} prevProps * @param {Object} prevState */ async componentDidUpdate(prevProps, prevState) { if (this.props.onChangeFormData) { if (!isEqual(prevState?.formData, this.state.formData)) { this.props.onChangeFormData(this.state.formData); } } if (!isEqual(prevProps.formData, this.props.formData)) { let newFormData = {}; map(keys(this.props.formData), (field) => { if (!isEqual(prevProps.formData[field], this.props.formData[field])) { newFormData[field] = this.props.formData[field]; } }); this.setState({ formData: { ...this.state.formData, ...newFormData, }, }); } } /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const { schema, onCancel, description } = this.props; const currentFieldset = schema.fieldsets[this.state.currentTab]; const fields = currentFieldset ? map(currentFieldset.fields, (field) => ({ ...schema.properties[field], id: field, value: this.state.formData[field], required: schema.required.indexOf(field) !== -1, onChange: this.onChangeField, onBlur: this.onBlurField, onClick: this.onClickInput, })) : []; const state_errors = keys(this.state.errors).length > 0; return ( <Modal dimmer={this.props.dimmer} open={this.props.open} className={this.props.className} > <Header>{this.props.title}</Header> <Dimmer active={this.props.loading}> <Loader> {this.props.loadingMessage || ( <FormattedMessage id="Loading" defaultMessage="Loading." /> )} </Loader> </Dimmer> <Modal.Content scrolling> <UiForm method="post" onSubmit={this.onSubmit} error={state_errors || Boolean(this.props.submitError)} > {description} <Message error> {state_errors ? ( <FormattedMessage id="There were some errors." defaultMessage="There were some errors." /> ) : ( '' )} <div>{this.props.submitError}</div> </Message> {schema.fieldsets?.length > 1 && ( <Menu tabular stackable> {map(schema.fieldsets, (item, index) => ( <Menu.Item name={item.id} index={index} key={item.id} active={this.state.currentTab === index} onClick={this.selectTab} > {item.title} </Menu.Item> ))} </Menu> )} {fields.map((field) => ( <Field {...field} key={field.id} onBlur={this.onBlurField} onClick={this.onClickInput} error={this.state.errors[field.id]} /> ))} </UiForm> </Modal.Content> <Modal.Actions> <Button basic circular primary floated="right" aria-label={ this.props.submitLabel ? this.props.submitLabel : this.props.intl.formatMessage(messages.save) } title={ this.props.submitLabel ? this.props.submitLabel : this.props.intl.formatMessage(messages.save) } onClick={this.onSubmit} loading={this.props.loading} > <Icon name={aheadSVG} className="contents circled" size="30px" /> </Button> {onCancel && ( <Button type="button" basic circular secondary aria-label={this.props.intl.formatMessage(messages.cancel)} title={this.props.intl.formatMessage(messages.cancel)} floated="right" onClick={onCancel} > <Icon name={clearSVG} className="circled" size="30px" /> </Button> )} </Modal.Actions> </Modal> ); } } export default injectIntl(ModalForm);