UNPKG

@plone/volto

Version:
560 lines (530 loc) 17.2 kB
/** * Edit container. * @module components/manage/Edit/Edit */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Helmet from '@plone/volto/helpers/Helmet/Helmet'; import { extractInvariantErrors } from '@plone/volto/helpers/FormValidation/FormValidation'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { asyncConnect } from '@plone/volto/helpers/AsyncConnect'; import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils'; import { defineMessages, injectIntl } from 'react-intl'; import { Button, Grid, Menu } from 'semantic-ui-react'; import { createPortal } from 'react-dom'; import qs from 'query-string'; import find from 'lodash/find'; import { toast } from 'react-toastify'; import Forbidden from '@plone/volto/components/theme/Forbidden/Forbidden'; import Icon from '@plone/volto/components/theme/Icon/Icon'; import Sidebar from '@plone/volto/components/manage/Sidebar/Sidebar'; import Toast from '@plone/volto/components/manage/Toast/Toast'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized'; import CompareLanguages from '@plone/volto/components/manage/Multilingual/CompareLanguages'; import TranslationObject from '@plone/volto/components/manage/Multilingual/TranslationObject'; import { Form } from '@plone/volto/components/manage/Form'; import { updateContent, getContent, lockContent, unlockContent, } from '@plone/volto/actions/content/content'; import { getSchema } from '@plone/volto/actions/schema/schema'; import { listActions } from '@plone/volto/actions/actions/actions'; import { setFormData } from '@plone/volto/actions/form/form'; import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers/Url/Url'; import { hasBlocksData } from '@plone/volto/helpers/Blocks/Blocks'; import { preloadLazyLibs } from '@plone/volto/helpers/Loadable'; import { tryParseJSON } from '@plone/volto/helpers/FormValidation/FormValidation'; import saveSVG from '@plone/volto/icons/save.svg'; import clearSVG from '@plone/volto/icons/clear.svg'; import config from '@plone/volto/registry'; const messages = defineMessages({ edit: { id: 'Edit {title}', defaultMessage: 'Edit {title}', }, save: { id: 'Save', defaultMessage: 'Save', }, cancel: { id: 'Cancel', defaultMessage: 'Cancel', }, error: { id: 'Error', defaultMessage: 'Error', }, someErrors: { id: 'There are some errors.', defaultMessage: 'There are some errors.', }, }); /** * Edit class. * @class Edit * @extends Component */ class Edit extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { updateContent: PropTypes.func.isRequired, getContent: PropTypes.func.isRequired, getSchema: PropTypes.func.isRequired, lockContent: PropTypes.func.isRequired, unlockContent: PropTypes.func.isRequired, updateRequest: PropTypes.shape({ loading: PropTypes.bool, loaded: PropTypes.bool, }).isRequired, schemaRequest: PropTypes.shape({ loading: PropTypes.bool, loaded: PropTypes.bool, }).isRequired, getRequest: PropTypes.shape({ loading: PropTypes.bool, loaded: PropTypes.bool, }).isRequired, pathname: PropTypes.string.isRequired, returnUrl: PropTypes.string, content: PropTypes.shape({ '@type': PropTypes.string, }), schema: PropTypes.objectOf(PropTypes.any), objectActions: PropTypes.array, newId: PropTypes.string, }; /** * Default properties * @property {Object} defaultProps Default properties. * @static */ static defaultProps = { schema: null, content: null, returnUrl: null, }; /** * Constructor * @method constructor * @param {Object} props Component properties * @constructs EditComponent */ constructor(props) { super(props); this.state = { visual: true, isClient: false, error: null, formSelected: 'editForm', newId: null, }; this.onCancel = this.onCancel.bind(this); this.onSubmit = this.onSubmit.bind(this); } /** * Component did mount * @method componentDidMount * @returns {undefined} */ componentDidMount() { if (this.props.getRequest.loaded && this.props.content?.['@type']) { this.props.getSchema( this.props.content['@type'], getBaseUrl(this.props.pathname), ); } this.setState({ isClient: true, comparingLanguage: null, }); } /** * Component will receive props * @method componentWillReceiveProps * @param {Object} nextProps Next properties * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.getRequest.loading && nextProps.getRequest.loaded) { if (nextProps.content['@type']) { this.props.getSchema( nextProps.content['@type'], getBaseUrl(this.props.pathname), ); } } if (this.props.schemaRequest.loading && nextProps.schemaRequest.loaded) { if (!hasBlocksData(nextProps.schema.properties)) { this.setState({ visual: false, }); } } // Hack for make the Plone site editable by Volto Editor without checkings if (this.props?.content?.['@type'] === 'Plone Site') { this.setState({ visual: true, }); } if (this.props.updateRequest.loading && nextProps.updateRequest.loaded) { this.props.setFormData({}); this.props.history.push( this.props.returnUrl || getBaseUrl(this.props.pathname), ); } if (this.props.updateRequest.loading && nextProps.updateRequest.error) { const message = nextProps.updateRequest.error?.response?.body?.error?.message || nextProps.updateRequest.error?.response?.body?.message || nextProps.updateRequest.error?.response?.text || ''; const error = new DOMParser().parseFromString(message, 'text/html')?.all[0] ?.textContent || message; const errorsList = tryParseJSON(error); let erroMessage; if (Array.isArray(errorsList)) { const invariantErrors = extractInvariantErrors(errorsList); if (invariantErrors.length > 0) { // Plone invariant validation message. erroMessage = invariantErrors.join(' - '); } else { // Error in specific field. erroMessage = this.props.intl.formatMessage(messages.someErrors); } } else { erroMessage = error; } this.setState({ error: error }); toast.error( <Toast error title={this.props.intl.formatMessage(messages.error)} content={erroMessage} />, ); } if ( nextProps.compare_to && ((this.state.compareTo && nextProps.compare_to['@id'] !== this.state.compareTo['@id']) || !this.state.compareTo) ) { this.setState({ compareTo: nextProps.compare_to }); } } /** * Component will unmount * @method componentWillUnmount * @returns {undefined} */ componentWillUnmount() { if (this.props.content?.lock?.locked) { const baseUrl = getBaseUrl(this.props.pathname); const { newId } = this.state; // Unlock the page, taking a possible id change into account this.props.unlockContent( newId ? baseUrl.replace(/\/[^/]*$/, '/' + newId) : baseUrl, ); } } /** * Submit handler * @method onSubmit * @param {object} data Form data. * @returns {undefined} */ onSubmit(data) { const lock_token = this.props.content?.lock?.token; const headers = lock_token ? { 'Lock-Token': lock_token } : {}; // if the id has changed, remember it for unlock control if ('id' in data) { this.setState({ newId: data.id }); } this.props.updateContent(getBaseUrl(this.props.pathname), data, headers); } /** * Cancel handler * @method onCancel * @returns {undefined} */ onCancel() { this.props.setFormData({}); this.props.history.push( this.props.returnUrl || getBaseUrl(this.props.pathname), ); } setComparingLanguage(lang, content_id) { this.setState({ comparingLanguage: lang }); this.props.getContent( flattenToAppURL(content_id), null, 'compare_to', null, ); } form = React.createRef(); toolbarRef = React.createRef; /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const editPermission = find(this.props.objectActions, { id: 'edit' }); const pageEdit = ( <Form isEditForm ref={this.form} navRoot={this.props.content?.['@components']?.navroot?.navroot || {}} schema={this.props.schema} type={this.props.content?.['@type']} formData={this.props.content} requestError={this.state.error} onSubmit={this.onSubmit} hideActions pathname={this.props.pathname} visual={this.state.visual} title={ this.props?.schema?.title ? this.props.intl.formatMessage(messages.edit, { title: this.props.schema.title, }) : null } loading={this.props.updateRequest.loading} isFormSelected={this.state.formSelected === 'editForm'} onSelectForm={() => { this.setState({ formSelected: 'editForm' }); }} global // Properties to pass to the BlocksForm to match the View ones history={this.props.history} location={this.props.location} token={this.props.token} /> ); return ( <div id="page-edit"> {this.props.objectActions?.length > 0 && ( <> {editPermission && ( <> <Helmet title={ this.props?.content?.title ? this.props.intl.formatMessage(messages.edit, { title: this.props?.content?.title, }) : this.props?.schema?.title ? this.props.intl.formatMessage(messages.edit, { title: this.props.schema.title, }) : null } > {this.props.content?.language && ( <html lang={this.props.content.language.token} /> )} </Helmet> {this.state.comparingLanguage && this.state.compareTo ? ( <Grid celled="internally" stackable columns={2} id="page-compare-translation" > <Grid.Column className="source-object"> <TranslationObject translationObject={this.state.compareTo} schema={this.props.schema} pathname={this.props.pathname} visual={this.state.visual} isFormSelected={ this.state.formSelected === 'translationObjectForm' } onSelectForm={() => { this.setState({ formSelected: 'translationObjectForm', }); }} /> </Grid.Column> <Grid.Column> <div className="new-translation"> <Menu pointing secondary attached tabular> <Menu.Item name={this.props.content.language?.token.toUpperCase()} active={true} > {this.props.content.language?.token.toUpperCase()} </Menu.Item> </Menu> {pageEdit} </div> </Grid.Column> </Grid> ) : ( pageEdit )} </> )} {editPermission && this.state.visual && this.state.isClient && createPortal(<Sidebar />, document.getElementById('sidebar'))} </> )} {!editPermission && ( <> {this.props.token ? ( <Forbidden pathname={this.props.pathname} staticContext={this.props.staticContext} /> ) : ( <Unauthorized pathname={this.props.pathname} staticContext={this.props.staticContext} /> )} </> )} {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.updateRequest.loading} loading={this.props.updateRequest.loading} > <Icon name={saveSVG} className="circled" size="30px" title={this.props.intl.formatMessage(messages.save)} /> </Button> <Button type="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> {config.settings.isMultilingual && ( <CompareLanguages content={this.props.content} visual={this.state.visual} setComparingLanguage={(lang, id) => { this.setComparingLanguage(lang, id); }} comparingLanguage={this.state.comparingLanguage} pathname={this.props.location.pathname} toolbarRef={this.toolbarRef} /> )} </> } />, document.getElementById('toolbar'), )} </div> ); } } export const __test__ = compose( injectIntl, connect( (state, props) => ({ objectActions: state.actions.actions.object, token: state.userSession.token, content: state.content.data, compare_to: state.content.subrequests?.compare_to?.data, schema: state.schema.schema, getRequest: state.content.get, schemaRequest: state.schema, updateRequest: state.content.update, createRequest: state.content.create, pathname: props.location.pathname, returnUrl: qs.parse(props.location.search).return_url, }), { updateContent, getContent, getSchema, lockContent, unlockContent, }, ), )(Edit); export default compose( injectIntl, asyncConnect([ { key: 'actions', promise: async ({ location, store: { dispatch } }) => { // Do not trigger the actions action if the expander is present if (!hasApiExpander('actions', getBaseUrl(location.pathname))) { return await dispatch(listActions(getBaseUrl(location.pathname))); } }, }, { key: 'content', promise: async ({ location, store: { dispatch } }) => { const content = await dispatch( getContent(getBaseUrl(location.pathname)), ); if (content?.lock !== undefined) { await dispatch(lockContent(getBaseUrl(location.pathname))); } return content; }, }, ]), connect( (state, props) => ({ objectActions: state.actions.actions.object, token: state.userSession.token, content: state.content.data, compare_to: state.content.subrequests?.compare_to?.data, schema: state.schema.schema, getRequest: state.content.get, schemaRequest: state.schema, updateRequest: state.content.update, pathname: props.location.pathname, returnUrl: qs.parse(props.location.search).return_url, }), { updateContent, getContent, getSchema, lockContent, unlockContent, setFormData, }, ), preloadLazyLibs('cms'), )(Edit);