UNPKG

@plone/volto

Version:
297 lines (278 loc) 8.57 kB
/** * 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"> <BodyClass className={ FoundView.displayName ? `view-${this.cleanViewName(FoundView.displayName)}` : null } /> <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={flattenToAppURL(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);