blob: 4ae29e066f5e182706cc4e8e8605334e167b4d53 [file] [log] [blame]
import app from "../../../app";
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import PropTypes from 'prop-types';
import React from "react";
import ReactDOM from "react-dom";
import RevActions from "./rev-browser.actions";
import RevStores from "./rev-browser.stores";
import ReactComponents from "../../components/react-components";
import { ButtonGroup, Button, Modal } from "react-bootstrap";
import ReactSelect from "react-select";
import jdp from "jsondiffpatch";
import jdpformatters from "jsondiffpatch/src/formatters/html";
import ace from "brace";
import "jsondiffpatch/public/formatters-styles/html.css";
const storageKeyDeleteConflictsModal = 'deleteConflictsHideModal';
const store = RevStores.revBrowserStore;
const ConfirmButton = ReactComponents.ConfirmButton;
require('brace/ext/static_highlight');
const highlight = ace.acequire('ace/ext/static_highlight');
require('brace/mode/json');
const JavaScriptMode = ace.acequire('ace/mode/json').Mode;
require('brace/theme/idle_fingers');
const theme = ace.acequire('ace/theme/idle_fingers');
class DiffyController extends React.Component {
constructor (props) {
super(props);
this.state = this.getStoreState();
}
getStoreState () {
return {
tree: store.getRevTree(),
ours: store.getOurs(),
theirs: store.getTheirs(),
conflictingRevs: store.getConflictingRevs(),
dropdownData: store.getDropdownData(),
isDiffViewEnabled: store.getIsDiffViewEnabled(),
databaseName: store.getDatabaseName()
};
}
componentDidMount () {
store.on('change', this.onChange, this);
}
componentWillUnmount () {
store.off('change', this.onChange);
}
onChange () {
this.setState(this.getStoreState());
}
toggleDiffView (enableDiff) {
RevActions.toggleDiffView(enableDiff);
}
render () {
const {tree, ours, theirs, conflictingRevs, isDiffViewEnabled} = this.state;
if (!tree) {
return null;
}
// no conflicts happened for this doc
if (!theirs || !conflictingRevs.length) {
return <div style={{textAlign: 'center', color: '#fff'}}><h2>No conflicts</h2></div>;
}
return (
<div className="revision-wrapper scrollable">
<RevisionBrowserControls {...this.state} />
<div className="revision-view-controls">
<ButtonGroup className="two-sides-toggle-button">
<Button
style={{width: '120px'}}
className={isDiffViewEnabled ? 'active' : ''}
onClick={this.toggleDiffView.bind(this, true)}
>
<i className="icon-columns" /> Diff
</Button>
<Button
style={{width: '120px'}}
className={isDiffViewEnabled ? '' : 'active'}
onClick={this.toggleDiffView.bind(this, false)}
>
<i className="icon-file-text" /> Document
</Button>
</ButtonGroup>
</div>
{isDiffViewEnabled ?
<RevisionDiffArea ours={ours} theirs={theirs} /> :
<SplitScreenArea ours={ours} theirs={theirs} /> }
</div>
);
}
}
class SplitScreenArea extends React.Component {
constructor (props) {
super(props);
}
componentDidUpdate () {
this.hightlightAfterRender();
}
componentDidMount () {
this.hightlightAfterRender();
}
hightlightAfterRender () {
const format = (input) => { return JSON.stringify(input, null, ' '); };
const jsmode = new JavaScriptMode();
const left = this.revLeftOurs;
const right = this.revRightTheirs;
const leftRes = highlight.render(format(this.props.ours), jsmode, theme, 0, true);
left.innerHTML = leftRes.html;
const rightRes = highlight.render(format(this.props.theirs), jsmode, theme, 0, true);
right.innerHTML = rightRes.html;
}
render () {
const {ours, theirs} = this.props;
if (!ours || !theirs) {
return <div></div>;
}
return (
<div className="revision-split-area">
<div data-id="ours" style={{width: '50%'}}>
<div style={{marginBottom: '20px'}}>{ours._rev} (Server-Selected Rev)</div>
<pre ref={node => this.revLeftOurs = node}></pre>
</div>
<div data-id="theirs" style={{width: '50%'}}>
<div style={{marginBottom: '20px'}}>{theirs._rev}</div>
<pre ref={node => this.revRightTheirs = node}></pre>
</div>
</div>
);
}
}
const RevisionDiffArea = ({ours, theirs}) => {
if (!ours || !theirs) {
return <div></div>;
}
const delta = jdp.diff(ours, theirs);
const html = jdpformatters.format(delta, ours);
return (
<div className="revision-diff-area">
<div
style={{marginTop: '30px'}}
dangerouslySetInnerHTML={{__html: html}}
></div>
</div>
);
};
RevisionDiffArea.propTypes = {
ours: PropTypes.object,
theirs: PropTypes.object,
currentRev: PropTypes.string
};
const ConflictingRevisionsDropDown = ({options, selected, onRevisionClick, onBackwardClick, onForwardClick}) => {
return (
<div className="conflicting-revs-dropdown">
<BackForwardControls backward onClick={onBackwardClick} />
<div style={{width: '345px', margin: '0 5px'}}>
<ReactSelect
name="form-field-name"
value={selected}
options={options}
clearable={false}
onChange={onRevisionClick} />
</div>
<BackForwardControls forward onClick={onForwardClick} />
</div>
);
};
ConflictingRevisionsDropDown.propTypes = {
options: PropTypes.array.isRequired,
selected: PropTypes.string.isRequired,
onRevisionClick: PropTypes.func.isRequired,
onBackwardClick: PropTypes.func.isRequired,
onForwardClick: PropTypes.func.isRequired,
};
class RevisionBrowserControls extends React.Component {
constructor (props) {
super(props);
this.state = {showModal: false};
}
onRevisionClick (revTheirs) {
RevActions.chooseLeaves(this.props.ours, revTheirs.value);
}
onForwardClick () {
const conflictingRevs = this.props.conflictingRevs;
const index = conflictingRevs.indexOf(this.props.theirs._rev);
const next = conflictingRevs[index + 1];
if (!next) {
return;
}
RevActions.chooseLeaves(this.props.ours, next);
}
onBackwardClick () {
const conflictingRevs = this.props.conflictingRevs;
const index = conflictingRevs.indexOf(this.props.theirs._rev);
const next = conflictingRevs[index - 1];
if (!next) {
return;
}
RevActions.chooseLeaves(this.props.ours, next);
}
selectAsWinner (docToWin, doNotShowModalAgain) {
if (doNotShowModalAgain) {
app.utils.localStorageSet(storageKeyDeleteConflictsModal, true);
}
RevActions.selectRevAsWinner(this.props.databaseName, docToWin._id, this.props.tree.paths, docToWin._rev);
}
onSelectAsWinnerClick (docToWin) {
if (app.utils.localStorageGet(storageKeyDeleteConflictsModal) !== true) {
RevActions.showConfirmModal(true, docToWin);
return;
}
this.selectAsWinner(docToWin);
}
render () {
const {tree} = this.props;
const cellStyle = {paddingRight: '30px'};
return (
<div className="revision-browser-controls">
<ConfirmModal onConfirm={this.selectAsWinner.bind(this)} />
<table style={{margin: '10px 60px', width: '100%'}}>
<tbody>
<tr style={{height: '60px'}}>
<td style={cellStyle}>Server-Selected Rev: </td>
<td style={cellStyle}>
<div style={{lineHeight: '36px', height: '36px', width: '337px', color: '#000', backgroundColor: '#ffbbbb'}}>
<b style={{paddingLeft: '10px'}}>{tree.winner}</b>
</div>
</td>
<td>
<ConfirmButton
onClick={this.onSelectAsWinnerClick.bind(this, this.props.ours)}
style={{marginRight: '10px', width: '220px'}}
text="Delete Other Conflicts"
buttonType="btn-secondary"
customIcon="icon-trophy" />
</td>
</tr>
<tr style={{height: '60px'}}>
<td style={cellStyle}>Conflicting Revisions: </td>
<td style={cellStyle}>
<ConflictingRevisionsDropDown
onRevisionClick={this.onRevisionClick.bind(this)}
onForwardClick={this.onForwardClick.bind(this)}
onBackwardClick={this.onBackwardClick.bind(this)}
options={this.props.dropdownData}
selected={this.props.theirs._rev} />
</td>
<td>
<ConfirmButton
data-id="button-select-theirs"
onClick={this.onSelectAsWinnerClick.bind(this, this.props.theirs)}
style={{marginRight: '10px', width: '220px'}}
text="Select as Winner"
buttonType="btn-secondary"
customIcon="icon-trophy" />
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
RevisionBrowserControls.propTypes = {
tree: PropTypes.object.isRequired,
ours: PropTypes.object.isRequired,
conflictingRevs: PropTypes.array.isRequired,
};
class ConfirmModal extends React.Component {
constructor (props) {
super(props);
this.state = this.getStoreState();
}
getStoreState () {
return {
show: store.getShowConfirmModal(),
docToWin: store.getDocToWin(),
checked: false
};
}
componentDidMount () {
store.on('change', this.onChange, this);
}
componentWillUnmount () {
store.off('change', this.onChange);
}
onChange () {
this.setState(this.getStoreState());
}
close () {
RevActions.showConfirmModal(false, null);
}
onDeleteConflicts () {
const hideModal = this.state.checked;
this.props.onConfirm(this.state.docToWin, hideModal);
}
render () {
return (
<Modal dialogClassName="delete-conflicts-modal" show={this.state.show} onHide={this.close}>
<Modal.Header closeButton={false}>
<Modal.Title>Solve Conflicts</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>
<i className="icon-warning-sign"></i> Do you want to delete all conflicting revisions for this document?
</p>
</Modal.Body>
<Modal.Footer>
<div style={{float: 'left', marginTop: '10px'}}>
<label>
<input
style={{margin: '0 5px 3px 0'}}
onChange={() => { this.setState({checked: !this.state.checked }); }}
type="checkbox" />
Do not show this warning message again
</label>
</div>
<a
style={{marginRight: '10px', cursor: 'pointer'}}
onClick={this.close}
data-bypass="true"
>
Cancel
</a>
<ConfirmButton
onClick={this.onDeleteConflicts.bind(this)}
text="Delete Revisions"
buttonType="btn-danger" />
</Modal.Footer>
</Modal>
);
}
}
ConfirmModal.propTypes = {
onConfirm: PropTypes.func.isRequired,
};
const BackForwardControls = ({onClick, forward}) => {
const icon = forward ? 'fonticon-right-open' : 'fonticon-left-open';
const style = {height: '20px', width: '11px', marginTop: '7px'};
return <div style={style} className={icon} onClick={onClick}></div>;
};
BackForwardControls.propTypes = {
onClick: PropTypes.func.isRequired,
};
export default {
DiffyController: DiffyController
};