UNPKG

@plone/volto

Version:
522 lines (493 loc) 15.3 kB
/** * Content Type component. * @module components/manage/Controlpanels/ContentTypeLayout */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Link } from 'react-router-dom'; import { getParentUrl } from '@plone/volto/helpers/Url/Url'; import { hasBlocksData, getBlocksFieldname, getBlocksLayoutFieldname, } from '@plone/volto/helpers/Blocks/Blocks'; import { createPortal } from 'react-dom'; import { Button, Segment } from 'semantic-ui-react'; import { toast } from 'react-toastify'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import nth from 'lodash/nth'; import join from 'lodash/join'; import Error from '@plone/volto/components/theme/Error/Error'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import Sidebar from '@plone/volto/components/manage/Sidebar/Sidebar'; import Toast from '@plone/volto/components/manage/Toast/Toast'; import { Form } from '@plone/volto/components/manage/Form'; import { getSchema, updateSchema } from '@plone/volto/actions/schema/schema'; import { getControlpanel, updateControlpanel, } from '@plone/volto/actions/controlpanels/controlpanels'; import saveSVG from '@plone/volto/icons/save.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import backSVG from '@plone/volto/icons/back.svg'; const messages = defineMessages({ changesSaved: { id: 'Changes saved.', defaultMessage: 'Changes saved.', }, back: { id: 'Back', defaultMessage: 'Back', }, save: { id: 'Save', defaultMessage: 'Save', }, cancel: { id: 'Cancel', defaultMessage: 'Cancel', }, info: { id: 'Info', defaultMessage: 'Info', }, enable: { id: 'Enable editable Blocks', defaultMessage: 'Enable editable Blocks', }, }); /** * ContentTypeLayout class. * @class ContentTypeLayout * @extends Component */ class ContentTypeLayout extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { updateControlpanel: PropTypes.func.isRequired, getControlpanel: PropTypes.func.isRequired, getSchema: PropTypes.func.isRequired, updateSchema: PropTypes.func.isRequired, id: PropTypes.string.isRequired, parent: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired, schemaRequest: PropTypes.objectOf(PropTypes.any).isRequired, cpanelRequest: PropTypes.objectOf(PropTypes.any).isRequired, schema: PropTypes.objectOf(PropTypes.any), controlpanel: PropTypes.shape({ '@id': PropTypes.string, data: PropTypes.object, schema: PropTypes.object, title: PropTypes.string, }), }; /** * Default properties. * @property {Object} defaultProps Default properties. * @static */ static defaultProps = { schema: {}, controlpanel: null, }; /** * Constructor * @method constructor * @param {Object} props Component properties * @constructs ContentTypeLayout */ constructor(props) { super(props); this.state = { visual: false, content: null, readOnlyBehavior: null, error: null, isClient: false, }; this.form = React.createRef(); } /** * Component did mount * @method componentDidMount * @returns {undefined} */ componentDidMount() { this.props.getControlpanel(join([this.props.parent, this.props.id], '/')); this.props.getSchema(this.props.id); this.setState({ isClient: true }); } /** * Component will receive props * @method componentWillReceiveProps * @param {Object} nextProps Next properties * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { // Control Panel GET if ( this.props.cpanelRequest.get.loading && nextProps.cpanelRequest.get.error ) { this.setState({ error: nextProps.cpanelRequest.get.error, }); } // Schema GET if (this.props.schemaRequest.loading && nextProps.schemaRequest.loaded) { const properties = nextProps.schema?.properties || {}; const content = {}; for (const key in properties) { const value = properties[key].default; if (value) { content[key] = value; } } if (hasBlocksData(properties)) { this.setState({ visual: true, }); const blocksFieldName = getBlocksFieldname(properties); const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); content[blocksFieldName] = properties[blocksFieldName]?.default || {}; content[blocksLayoutFieldname] = properties[blocksLayoutFieldname] ?.default || { items: [] }; const blocksBehavior = properties[blocksFieldName]?.behavior || ''; this.setState({ readOnlyBehavior: !blocksBehavior.includes('generated') ? blocksBehavior : '', }); } else { this.setState({ visual: false, readOnlyBehavior: '', }); } this.setState({ content: content, }); } // Schema updated if ( this.props.schemaRequest.update.loading && nextProps.schemaRequest.update.loaded ) { this.props.getSchema(this.props.id); toast.info( <Toast info title={this.props.intl.formatMessage(messages.info)} content={this.props.intl.formatMessage(messages.changesSaved)} />, ); } // Blocks behavior disabled if ( this.props.cpanelRequest.update.loading && nextProps.cpanelRequest.update.loaded ) { this.onEnableBlocks(); } } /** * Submit handler * @method onSubmit * @param {object} data Form data. * @returns {undefined} */ onSubmit = (data) => { const schema = { properties: {} }; Object.keys(data) .filter((k) => data[k]) .forEach((k) => (schema.properties[k] = { default: data[k] })); this.props.updateSchema(this.props.id, schema); }; /** * Cancel handler * @method onCancel * @returns {undefined} */ onCancel = () => { const url = getParentUrl(this.props.pathname); this.props.history.push(getParentUrl(url)); }; /** * Enable blocks handler * @method onEnableBlocks * @returns {undefined} */ onEnableBlocks = () => { const { properties = {} } = this.props.schema; const blocksFieldName = getBlocksFieldname(properties); const blocksLayoutFieldname = getBlocksLayoutFieldname(properties); const schema = { fieldsets: [ { id: 'layout', title: 'Layout', fields: ['blocks', 'blocks_layout'], }, ], properties: { blocks: { title: 'Blocks', type: 'dict', widget: 'json', factory: 'JSONField', default: properties[blocksFieldName]?.default || {}, }, blocks_layout: { title: 'Blocks Layout', type: 'dict', widget: 'json', factory: 'JSONField', default: properties[blocksLayoutFieldname]?.default || { items: [] }, }, }, }; this.props.updateSchema(this.props.id, schema); }; /** * Disable Blocks behavior handler * @method onDisableBlocksBehavior * @returns {undefined} */ onDisableBlocksBehavior = () => { this.props.updateControlpanel(this.props.controlpanel['@id'], { [this.state.readOnlyBehavior]: false, 'volto.blocks.editable.layout': true, }); }; /** * Enable Blocks behavior handler * @method onEnableBlocksBehavior * @returns {undefined} */ onEnableBlocksBehavior = () => { this.props.updateControlpanel(this.props.controlpanel['@id'], { 'volto.blocks.editable.layout': true, }); }; /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { // Error if (this.state.error) { return <Error error={this.state.error} />; } if (!this.state.visual) { // Still loading if (!this.state.content) { return <div />; } // Blocks are not enabled return ( <> <Segment placeholder id="page-controlpanel-layout" className="ui container center aligned" > <div> <FormattedMessage id="Can not edit Layout for <strong>{type}</strong> content-type as it doesn't have support for <strong>Volto Blocks</strong> enabled" defaultMessage="Can not edit Layout for <strong>{type}</strong> content-type as it doesn't have support for <strong>Volto Blocks</strong> enabled" values={{ strong: (...chunks) => <strong>{chunks}</strong>, type: this.props?.controlpanel?.title || this.props.id, }} /> </div> <div className="ui divider"></div> <Button primary onClick={this.onEnableBlocksBehavior} content={this.props.intl.formatMessage(messages.enable)} /> </Segment> {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={ <> <Link className="item" to="#" onClick={() => this.onCancel()} > <Icon name={backSVG} size="30px" className="contents circled" title={this.props.intl.formatMessage(messages.back)} /> </Link> </> } />, document.getElementById('toolbar'), )} </> ); } if (this.state.readOnlyBehavior) { return ( <> <Segment placeholder id="page-controlpanel-layout" className="ui container center aligned" > <div> <FormattedMessage id="Can not edit Layout for <strong>{type}</strong> content-type as the <strong>Blocks behavior</strong> is enabled and <strong>read-only</strong>" defaultMessage="Can not edit Layout for <strong>{type}</strong> content-type as the <strong>Blocks behavior</strong> is enabled and <strong>read-only</strong>" values={{ strong: (...chunks) => <strong>{chunks}</strong>, type: this.props?.controlpanel?.title || this.props.id, }} /> </div> <div className="ui divider"></div> <Button primary onClick={this.onDisableBlocksBehavior} content={this.props.intl.formatMessage(messages.enable)} /> </Segment> {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={ <> <Link className="item" to="#" onClick={() => this.onCancel()} > <Icon name={backSVG} size="30px" className="contents circled" title={this.props.intl.formatMessage(messages.back)} /> </Link> </> } />, document.getElementById('toolbar'), )} </> ); } // Render layout editor const blocksFieldName = getBlocksFieldname( this.props.schema?.properties || {}, ); const blocksLayoutFieldname = getBlocksLayoutFieldname( this.props.schema?.properties || {}, ); return ( <div id="page-controlpanel-layout"> <Form isAdminForm ref={this.form} schema={{ fieldsets: [ { id: 'layout', title: 'Layout', fields: [blocksFieldName, blocksLayoutFieldname], }, ], properties: { ...this.props.schema.properties[blocksFieldName], ...this.props.schema.properties[blocksLayoutFieldname], }, required: [], }} formData={this.state.content} onSubmit={this.onSubmit} onCancel={this.onCancel} pathname={this.props.pathname} visual={this.state.visual} hideActions /> {this.state.isClient && createPortal( <Sidebar settingsTab={true} documentTab={false} />, document.getElementById('sidebar'), )} {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={ <> <Button id="toolbar-save" className="save" aria-label={this.props.intl.formatMessage(messages.save)} onClick={() => this.form.current.onSubmit()} disabled={this.props.schemaRequest.update.loading} loading={this.props.schemaRequest.update.loading} > <Icon name={saveSVG} className="circled" size="30px" title={this.props.intl.formatMessage(messages.save)} /> </Button> <Button className="cancel" aria-label={this.props.intl.formatMessage(messages.cancel)} onClick={() => this.onCancel()} > <Icon name={clearSVG} className="circled" size="30px" title={this.props.intl.formatMessage(messages.cancel)} /> </Button> </> } />, document.getElementById('toolbar'), )} </div> ); } } export default compose( injectIntl, connect( (state, props) => ({ schema: state.schema.schema, schemaRequest: state.schema, cpanelRequest: state.controlpanels, controlpanel: state.controlpanels.controlpanel, pathname: props.location.pathname, id: nth(props.location.pathname.split('/'), -2), parent: nth(props.location.pathname.split('/'), -3), }), { getSchema, updateSchema, getControlpanel, updateControlpanel }, ), )(ContentTypeLayout);