@plone/volto
Version:
Volto
560 lines (530 loc) • 17.2 kB
JSX
/**
* 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);