@plone/volto
Version:
Volto
384 lines (368 loc) • 11.6 kB
JSX
/**
* Diff component.
* @module components/manage/Diff/Diff
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Helmet from '@plone/volto/helpers/Helmet/Helmet';
import { connect } from 'react-redux';
import { compose } from 'redux';
import filter from 'lodash/filter';
import isEqual from 'lodash/isEqual';
import map from 'lodash/map';
import { Container, Button, Dropdown, Grid, Table } from 'semantic-ui-react';
import { Link, withRouter } from 'react-router-dom';
import { createPortal } from 'react-dom';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import qs from 'query-string';
import { getDiff } from '@plone/volto/actions/diff/diff';
import { getSchema } from '@plone/volto/actions/schema/schema';
import { getHistory } from '@plone/volto/actions/history/history';
import { getBaseUrl } from '@plone/volto/helpers/Url/Url';
import {
getBlocksFieldname,
getBlocksLayoutFieldname,
hasBlocksData,
} from '@plone/volto/helpers/Blocks/Blocks';
import FormattedDate from '@plone/volto/components/theme/FormattedDate/FormattedDate';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import Toolbar from '@plone/volto/components/manage/Toolbar/Toolbar';
import Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized';
import DiffField from '@plone/volto/components/manage/Diff/DiffField';
import backSVG from '@plone/volto/icons/back.svg';
const messages = defineMessages({
diff: {
id: 'Diff',
defaultMessage: 'Diff',
},
back: {
id: 'Back',
defaultMessage: 'Back',
},
split: {
id: 'Split',
defaultMessage: 'Split',
},
unified: {
id: 'Unified',
defaultMessage: 'Unified',
},
});
/**
* Diff class.
* @class Diff
* @extends Component
*/
class Diff extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
getDiff: PropTypes.func.isRequired,
getSchema: PropTypes.func.isRequired,
getHistory: PropTypes.func.isRequired,
schema: PropTypes.objectOf(PropTypes.any),
error: PropTypes.objectOf(PropTypes.any),
pathname: PropTypes.string.isRequired,
one: PropTypes.string.isRequired,
two: PropTypes.string.isRequired,
view: PropTypes.string.isRequired,
data: PropTypes.arrayOf(
PropTypes.shape({
'@id': PropTypes.string,
}),
).isRequired,
historyEntries: PropTypes.arrayOf(
PropTypes.shape({
version: PropTypes.number,
time: PropTypes.string,
actor: PropTypes.shape({ fullname: PropTypes.string }),
}),
).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
/**
* Default properties
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
schema: null,
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
* @constructs DiffComponent
*/
constructor(props) {
super(props);
this.onChangeOne = this.onChangeOne.bind(this);
this.onChangeTwo = this.onChangeTwo.bind(this);
this.onSelectView = this.onSelectView.bind(this);
this.state = { isClient: false };
}
/**
* Component did mount
* @method componentDidMount
* @returns {undefined}
*/
componentDidMount() {
this.props.getSchema(this.props.type);
this.props.getHistory(getBaseUrl(this.props.pathname));
this.props.getDiff(
getBaseUrl(this.props.pathname),
this.props.one,
this.props.two,
);
this.setState({ isClient: true });
}
/**
* Component will receive props
* @method componentWillReceiveProps
* @param {Object} nextProps Next properties
* @returns {undefined}
*/
UNSAFE_componentWillReceiveProps(nextProps) {
if (
this.props.pathname !== nextProps.pathname ||
this.props.one !== nextProps.one ||
this.props.two !== nextProps.two
) {
this.props.getDiff(
getBaseUrl(nextProps.pathname),
nextProps.one,
nextProps.two,
);
}
}
/**
* On select view handler
* @method onSelectView
* @param {object} event Event object
* @param {string} value Value
* @returns {undefined}
*/
onSelectView(event, { value }) {
this.props.history.push(
`${this.props.pathname}?one=${this.props.one}&two=${this.props.two}&view=${value}`,
);
}
/**
* On change one handler
* @method onChangeOne
* @param {object} event Event object
* @param {string} value Value
* @returns {undefined}
*/
onChangeOne(event, { value }) {
this.props.history.push(
`${this.props.pathname}?one=${value}&two=${this.props.two}&view=${this.props.view}`,
);
}
/**
* On change two handler
* @method onChangeTwo
* @param {object} event Event object
* @param {string} value Value
* @returns {undefined}
*/
onChangeTwo(event, { value }) {
this.props.history.push(
`${this.props.pathname}?one=${this.props.one}&two=${value}&view=${this.props.view}`,
);
}
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const versions = map(
filter(this.props.historyEntries, (entry) => 'version' in entry),
(entry, index) => ({
text: (
<>
{index === 0 ? 'Current' : entry.version} (
<FormattedDate date={entry.time} long className="text" />,
{entry.actor.fullname})
</>
),
value: `${entry.version}`,
key: `${entry.version}`,
}),
);
return this.props.error?.status === 401 ? (
<Unauthorized />
) : (
<Container id="page-diff">
<Helmet title={this.props.intl.formatMessage(messages.diff)} />
<h1>
<FormattedMessage
id="Difference between revision {one} and {two} of {title}"
defaultMessage="Difference between revision {one} and {two} of {title}"
values={{
one: this.props.one,
two: this.props.two,
title: this.props.title,
}}
/>
</h1>
<Grid>
<Grid.Column width={9}>
<p className="description">
<FormattedMessage
id="You can view the difference of the revisions below."
defaultMessage="You can view the difference of the revisions below."
/>
</p>
</Grid.Column>
<Grid.Column width={3} textAlign="right">
<Button.Group>
{map(
[
{
id: 'split',
label: this.props.intl.formatMessage(messages.split),
},
{
id: 'unified',
label: this.props.intl.formatMessage(messages.unified),
},
],
(view) => (
<Button
type="button"
key={view.id}
value={view.id}
active={this.props.view === view.id}
onClick={this.onSelectView}
>
{view.label}
</Button>
),
)}
</Button.Group>
</Grid.Column>
</Grid>
{this.props.historyEntries.length > 0 && (
<Table basic="very">
<Table.Header>
<Table.Row>
<Table.HeaderCell width={6}>
<FormattedMessage id="Base" defaultMessage="Base" />
<Dropdown
onChange={this.onChangeOne}
value={this.props.one}
selection
fluid
options={versions}
/>
</Table.HeaderCell>
<Table.HeaderCell width={6}>
<FormattedMessage id="Compare" defaultMessage="Compare" />
<Dropdown
onChange={this.onChangeTwo}
value={this.props.two}
selection
fluid
options={versions}
/>
</Table.HeaderCell>
</Table.Row>
</Table.Header>
</Table>
)}
{this.props.schema &&
this.props.data.length > 0 &&
map(this.props.schema.fieldsets, (fieldset) =>
map(
fieldset.fields,
(field) =>
!isEqual(
this.props.data[0][field],
this.props.data[1][field],
) &&
field !== getBlocksFieldname(this.props.data[0]) &&
field !== getBlocksLayoutFieldname(this.props.data[0]) && (
<DiffField
key={field}
one={this.props.data[0][field]}
two={this.props.data[1][field]}
schema={this.props.schema.properties[field]}
view={this.props.view}
/>
),
),
)}
{this.props.schema &&
this.props.data.length > 0 &&
hasBlocksData(this.props.data[0]) &&
(!isEqual(
this.props.data[0][getBlocksFieldname(this.props.data[0])],
this.props.data[1][getBlocksFieldname(this.props.data[1])],
) ||
!isEqual(
this.props.data[0][getBlocksLayoutFieldname(this.props.data[0])],
this.props.data[1][getBlocksLayoutFieldname(this.props.data[1])],
)) && (
<DiffField
one={this.props.data[0][getBlocksFieldname(this.props.data[0])]}
two={this.props.data[1][getBlocksFieldname(this.props.data[1])]}
contentOne={this.props.data[0]}
contentTwo={this.props.data[1]}
schema={
this.props.schema.properties[
getBlocksFieldname(this.props.data[0])
]
}
view={this.props.view}
/>
)}
{this.state.isClient &&
createPortal(
<Toolbar
pathname={this.props.pathname}
hideDefaultViewButtons
inner={
<Link
to={`${getBaseUrl(this.props.pathname)}/historyview`}
className="item"
>
<Icon
name={backSVG}
className="contents circled"
size="30px"
title={this.props.intl.formatMessage(messages.back)}
/>
</Link>
}
/>,
document.getElementById('toolbar'),
)}
</Container>
);
}
}
export default compose(
withRouter,
injectIntl,
connect(
(state, props) => ({
data: state.diff.data,
historyEntries: state.history.entries,
schema: state.schema.schema,
error: state.diff.error,
pathname: props.location.pathname,
one: qs.parse(props.location.search).one,
two: qs.parse(props.location.search).two,
view: qs.parse(props.location.search).view || 'split',
title: state.content.data?.title,
type: state.content.data?.['@type'],
}),
{ getDiff, getSchema, getHistory },
),
)(Diff);