@plone/volto
Version:
Volto
290 lines (271 loc) • 8.35 kB
JSX
/**
* View container.
* @module components/theme/View/View
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { Redirect } from 'react-router-dom';
import { createPortal } from 'react-dom';
import { injectIntl } from 'react-intl';
import qs from 'query-string';
import ContentMetadataTags from '@plone/volto/components/theme/ContentMetadataTags/ContentMetadataTags';
import Comments from '@plone/volto/components/theme/Comments/Comments';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import { listActions } from '@plone/volto/actions/actions/actions';
import { getContent } from '@plone/volto/actions/content/content';
import BodyClass from '@plone/volto/helpers/BodyClass/BodyClass';
import { getBaseUrl, flattenToAppURL } from '@plone/volto/helpers/Url/Url';
import { getLayoutFieldname } from '@plone/volto/helpers/Content/Content';
import { hasApiExpander } from '@plone/volto/helpers/Utils/Utils';
import { AlternateHrefLangs } from '@plone/volto/components/theme/AlternateHrefLangs/AlternateHrefLangs';
import config from '@plone/volto/registry';
import SlotRenderer from '../SlotRenderer/SlotRenderer';
/**
* View container class.
* @class View
* @extends Component
*/
class View extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
actions: PropTypes.shape({
object: PropTypes.arrayOf(PropTypes.object),
object_buttons: PropTypes.arrayOf(PropTypes.object),
user: PropTypes.arrayOf(PropTypes.object),
}),
listActions: PropTypes.func.isRequired,
/**
* Action to get the content
*/
getContent: PropTypes.func.isRequired,
/**
* Pathname of the object
*/
pathname: PropTypes.string.isRequired,
location: PropTypes.shape({
search: PropTypes.string,
pathname: PropTypes.string,
}).isRequired,
/**
* Version id of the object
*/
versionId: PropTypes.string,
/**
* Content of the object
*/
content: PropTypes.shape({
/**
* Layout of the object
*/
layout: PropTypes.string,
/**
* Allow discussion of the object
*/
allow_discussion: PropTypes.bool,
/**
* Title of the object
*/
title: PropTypes.string,
/**
* Description of the object
*/
description: PropTypes.string,
/**
* Type of the object
*/
'@type': PropTypes.string,
/**
* Subjects of the object
*/
subjects: PropTypes.arrayOf(PropTypes.string),
is_folderish: PropTypes.bool,
}),
error: PropTypes.shape({
/**
* Error type
*/
status: PropTypes.number,
}),
};
/**
* Default properties.
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
actions: null,
content: null,
versionId: null,
error: null,
};
state = {
hasObjectButtons: null,
isClient: false,
};
componentDidMount() {
// Do not trigger the actions action if the expander is present
if (!hasApiExpander('actions', getBaseUrl(this.props.pathname))) {
this.props.listActions(getBaseUrl(this.props.pathname));
}
this.props.getContent(
getBaseUrl(this.props.pathname),
this.props.versionId,
);
this.setState({ isClient: true });
}
/**
* Component will receive props
* @method componentWillReceiveProps
* @param {Object} nextProps Next properties
* @returns {undefined}
*/
UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.pathname !== this.props.pathname) {
// Do not trigger the actions action if the expander is present
if (!hasApiExpander('actions', getBaseUrl(nextProps.pathname))) {
this.props.listActions(getBaseUrl(nextProps.pathname));
}
this.props.getContent(
getBaseUrl(nextProps.pathname),
this.props.versionId,
);
}
if (nextProps.actions.object_buttons) {
const objectButtons = nextProps.actions.object_buttons;
this.setState({
hasObjectButtons: !!objectButtons.length,
});
}
}
/**
* Default fallback view
* @method getViewDefault
* @returns {string} Markup for component.
*/
getViewDefault = () => config.views.defaultView;
/**
* Get view by content type
* @method getViewByType
* @returns {string} Markup for component.
*/
getViewByType = () =>
config.views.contentTypesViews[this.props.content['@type']] || null;
/**
* Get view by content layout property
* @method getViewByLayout
* @returns {string} Markup for component.
*/
getViewByLayout = () =>
config.views.layoutViews[
this.props.content[getLayoutFieldname(this.props.content)]
] || null;
/**
* Cleans the component displayName (specially for connected components)
* which have the Connect(componentDisplayName)
* @method cleanViewName
* @param {string} dirtyDisplayName The displayName
* @returns {string} Clean displayName (no Connect(...)).
*/
cleanViewName = (dirtyDisplayName) =>
dirtyDisplayName
.replace('Connect(', '')
.replace('injectIntl(', '')
.replace(')', '')
.replace('connect(', '')
.toLowerCase();
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const { views } = config;
if ([301, 302].includes(this.props.error?.code)) {
const redirect = flattenToAppURL(this.props.error.url)
.split('?')[0]
.replace('/++api++', '');
return <Redirect to={`${redirect}${this.props.location.search}`} />;
} else if (this.props.error && !this.props.connectionRefused) {
let FoundView;
if (this.props.error.status === undefined) {
// For some reason, while development and if CORS is in place and the
// requested resource is 404, it returns undefined as status, then the
// next statement will fail
FoundView = views.errorViews.corsError;
} else {
FoundView = views.errorViews[this.props.error.status.toString()];
}
if (!FoundView) {
FoundView = views.errorViews['404']; // default to 404
}
return (
<div id="view">
<FoundView {...this.props} />
</div>
);
}
if (!this.props.content) {
return <span />;
}
const RenderedView =
this.getViewByLayout() || this.getViewByType() || this.getViewDefault();
return (
<div id="view" tabIndex="-1">
<ContentMetadataTags content={this.props.content} />
<AlternateHrefLangs content={this.props.content} />
{/* Body class if displayName in component is set */}
<BodyClass
className={
RenderedView.displayName
? `view-${this.cleanViewName(RenderedView.displayName)}`
: null
}
/>
<SlotRenderer name="aboveContent" content={this.props.content} />
<RenderedView
key={this.props.content['@id']}
content={this.props.content}
location={this.props.location}
token={this.props.token}
history={this.props.history}
/>
<SlotRenderer name="belowContent" content={this.props.content} />
{this.props.content.allow_discussion && (
<Comments pathname={this.props.pathname} />
)}
{this.state.isClient &&
createPortal(
<Toolbar pathname={this.props.pathname} inner={<span />} />,
document.getElementById('toolbar'),
)}
</div>
);
}
}
export default compose(
injectIntl,
connect(
(state, props) => ({
actions: state.actions.actions,
token: state.userSession.token,
content: state.content.data,
error: state.content.get.error,
apiError: state.apierror.error,
connectionRefused: state.apierror.connectionRefused,
pathname: props.location.pathname,
versionId:
qs.parse(props.location.search) &&
qs.parse(props.location.search).version,
}),
{
listActions,
getContent,
},
),
)(View);