UNPKG

@plone/volto

Version:
371 lines (355 loc) 12.4 kB
/** * History component. * @module components/manage/History/History */ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Helmet from '@plone/volto/helpers/Helmet/Helmet'; import { Link } from 'react-router-dom'; import { connect } from 'react-redux'; import { compose } from 'redux'; import { Container as SemanticContainer, Dropdown, Icon, Segment, Table, } from 'semantic-ui-react'; import concat from 'lodash/concat'; import map from 'lodash/map'; import reverse from 'lodash/reverse'; import find from 'lodash/find'; import { createPortal } from 'react-dom'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { asyncConnect } from '@plone/volto/helpers/AsyncConnect'; import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate'; import IconNext from '@plone/volto/components/theme/Icon/Icon'; import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar'; import Forbidden from '@plone/volto/components/theme/Forbidden/Forbidden'; import Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized'; import { getHistory, revertHistory, } from '@plone/volto/actions/history/history'; import { listActions } from '@plone/volto/actions/actions/actions'; import { getBaseUrl } from '@plone/volto/helpers/Url/Url'; import config from '@plone/volto/registry'; import backSVG from '@plone/volto/icons/back.svg'; const messages = defineMessages({ back: { id: 'Back', defaultMessage: 'Back', }, history: { id: 'History', defaultMessage: 'History', }, }); /** * History class. * @class History * @extends Component */ class History extends Component { /** * Property types. * @property {Object} propTypes Property types. * @static */ static propTypes = { getHistory: PropTypes.func.isRequired, revertHistory: PropTypes.func.isRequired, revertRequest: PropTypes.shape({ loaded: PropTypes.bool, loading: PropTypes.bool, }).isRequired, pathname: PropTypes.string.isRequired, entries: PropTypes.arrayOf( PropTypes.shape({ transition_title: PropTypes.string, type: PropTypes.string, action: PropTypes.string, state_title: PropTypes.string, time: PropTypes.string, comments: PropTypes.string, actor: PropTypes.shape({ fullname: PropTypes.string }), }), ).isRequired, title: PropTypes.string.isRequired, }; /** * Constructor * @method constructor * @param {Object} props Component properties * @constructs Workflow */ constructor(props) { super(props); this.onRevert = this.onRevert.bind(this); this.state = { isClient: false }; } /** * Component did mount * @method componentDidMount * @returns {undefined} */ componentDidMount() { this.props.getHistory(getBaseUrl(this.props.pathname)); this.setState({ isClient: true }); } /** * Component will receive props * @method componentWillReceiveProps * @param {Object} nextProps Next properties * @returns {undefined} */ UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.revertRequest.loading && nextProps.revertRequest.loaded) { this.props.getHistory(getBaseUrl(this.props.pathname)); } } /** * On revert * @method onRevert * @param {object} event Event object * @param {number} value Value * @returns {undefined} */ onRevert(event, { value }) { this.props.revertHistory(getBaseUrl(this.props.pathname), value); } processHistoryEntries = () => { // Getting the history entries from the props // No clue why the reverse(concat()) is necessary const entries = reverse(concat(this.props.entries)); let title = entries.length > 0 ? entries[0].state_title : ''; for (let x = 1; x < entries.length; x += 1) { entries[x].prev_state_title = title; title = entries[x].state_title || title; } // We reverse them again reverse(entries); // We identify the latest 'versioning' entry and mark it const current_version = find(entries, (item) => item.type === 'versioning'); if (current_version) { current_version.is_current = true; } return entries; }; /** * Render method. * @method render * @returns {string} Markup for the component. */ render() { const historyAction = find(this.props.objectActions, { id: 'history', }); const entries = this.processHistoryEntries(); const Container = config.getComponent({ name: 'Container' }).component || SemanticContainer; return !historyAction ? ( <> {this.props.token ? ( <Forbidden pathname={this.props.pathname} staticContext={this.props.staticContext} /> ) : ( <Unauthorized pathname={this.props.pathname} staticContext={this.props.staticContext} /> )} </> ) : ( <Container id="page-history"> <Helmet title={this.props.intl.formatMessage(messages.history)} /> <Segment.Group raised> <Segment className="primary"> <FormattedMessage id="History of {title}" defaultMessage="History of {title}" values={{ title: <q>{this.props.title}</q>, }} /> </Segment> <Segment secondary> <FormattedMessage id="You can view the history of your item below." defaultMessage="You can view the history of your item below." /> </Segment> <Table selectable compact singleLine attached> <Table.Header> <Table.Row> <Table.HeaderCell width={1}> <FormattedMessage id="History Version Number" defaultMessage="#" /> </Table.HeaderCell> <Table.HeaderCell width={4}> <FormattedMessage id="What" defaultMessage="What" /> </Table.HeaderCell> <Table.HeaderCell width={4}> <FormattedMessage id="Who" defaultMessage="Who" /> </Table.HeaderCell> <Table.HeaderCell width={4}> <FormattedMessage id="When" defaultMessage="When" /> </Table.HeaderCell> <Table.HeaderCell width={4}> <FormattedMessage id="Change Note" defaultMessage="Change Note" /> </Table.HeaderCell> <Table.HeaderCell /> </Table.Row> </Table.Header> <Table.Body> {map(entries, (entry) => ( <Table.Row key={entry.time}> <Table.Cell> {('version' in entry && entry.version > 0 && ( <Link className="item" to={`${getBaseUrl(this.props.pathname)}/diff?one=${ entry.version - 1 }&two=${entry.version}`} > {entry.version} </Link> )) || <span>{entry.version}</span>} </Table.Cell> <Table.Cell> {('version' in entry && entry.version > 0 && ( <Link className="item" to={`${getBaseUrl(this.props.pathname)}/diff?one=${ entry.version - 1 }&two=${entry.version}`} > {entry.transition_title} </Link> )) || ( <span> {entry.transition_title} {entry.type === 'workflow' && ` (${ entry.action ? `${entry.prev_state_title} → ` : '' }${entry.state_title})`} </span> )} </Table.Cell> <Table.Cell>{entry.actor.fullname}</Table.Cell> <Table.Cell> <FormattedDate date={entry.time} /> </Table.Cell> <Table.Cell>{entry.comments}</Table.Cell> <Table.Cell> {entry.type === 'versioning' && ( <Dropdown icon="ellipsis horizontal"> <Dropdown.Menu className="left"> {'version' in entry && entry.version > 0 && ( <Link className="item" to={`${getBaseUrl( this.props.pathname, )}/diff?one=${entry.version - 1}&two=${ entry.version }`} > <Icon name="copy" />{' '} <FormattedMessage id="View changes" defaultMessage="View changes" /> </Link> )} {'version' in entry && ( <Link className="item" to={`${getBaseUrl(this.props.pathname)}?version=${ entry.version }`} > <Icon name="eye" />{' '} <FormattedMessage id="View this revision" defaultMessage="View this revision" /> </Link> )} {'version' in entry && entry.may_revert && !entry.is_current && ( <Dropdown.Item value={entry.version} onClick={this.onRevert} > <Icon name="undo" />{' '} <FormattedMessage id="Revert to this revision" defaultMessage="Revert to this revision" /> </Dropdown.Item> )} </Dropdown.Menu> </Dropdown> )} </Table.Cell> </Table.Row> ))} </Table.Body> </Table> </Segment.Group> {this.state.isClient && createPortal( <Toolbar pathname={this.props.pathname} hideDefaultViewButtons inner={ <Link to={`${getBaseUrl(this.props.pathname)}`} className="item" > <IconNext name={backSVG} className="contents circled" size="30px" title={this.props.intl.formatMessage(messages.back)} /> </Link> } />, document.getElementById('toolbar'), )} </Container> ); } } export default compose( injectIntl, asyncConnect([ { key: 'actions', // Dispatch async/await to make the operation synchronous, otherwise it returns // before the promise is resolved promise: async ({ location, store: { dispatch } }) => await dispatch(listActions(getBaseUrl(location.pathname))), }, ]), connect( (state, props) => ({ objectActions: state.actions.actions.object, token: state.userSession.token, entries: state.history.entries, pathname: props.location.pathname, title: state.content.data?.title, revertRequest: state.history.revert, }), { getHistory, revertHistory }, ), )(History);