blob: 3c5d1c961a6931fb91d7d4bc222655a8b304d31d [file] [log] [blame]
import FauxtonAPI from "../../../core/api";
// 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 Actions from "./actions";
import Stores from "./stores";
import Components from "../../fauxton/components";
import ReactComponents from "../../components/react-components";
import {TransitionMotion, spring, presets} from 'react-motion';
import "../../../../assets/js/plugins/prettify";
import uuid from 'uuid';
const store = Stores.changesStore;
const BadgeList = ReactComponents.BadgeList;
const {Copy} = ReactComponents;
class ChangesController extends React.Component {
constructor (props) {
super(props);
this.state = this.getStoreState();
}
getStoreState () {
return {
changes: store.getChanges(),
loaded: store.isLoaded(),
databaseName: store.getDatabaseName(),
isShowingSubset: store.isShowingSubset()
};
}
onChange () {
this.setState(this.getStoreState());
}
componentDidMount () {
store.on('change', this.onChange, this);
}
componentWillUnmount () {
store.off('change', this.onChange);
}
showingSubsetMsg () {
const { isShowingSubset, changes } = this.state;
let msg = '';
if (isShowingSubset) {
let numChanges = changes.length;
msg = <p className="changes-result-limit">Limiting results to latest <b>{numChanges}</b> changes.</p>;
}
return msg;
}
getRows () {
const { changes, loaded, databaseName } = this.state;
if (!changes.length && loaded) {
return (
<p className="no-doc-changes">
There are no document changes to display.
</p>
);
}
return changes.map((change, i) => {
return <ChangeRow change={change} key={i} databaseName={databaseName} />;
});
}
render () {
return (
<div>
<div className="js-changes-view">
{this.showingSubsetMsg()}
{this.getRows()}
</div>
</div>
);
}
}
class ChangesTabContent extends React.Component {
constructor (props) {
super(props);
this.state = this.getStoreState();
}
getStoreState () {
return {
filters: store.getFilters()
};
}
onChange () {
this.setState(this.getStoreState());
}
componentDidMount () {
store.on('change', this.onChange, this);
}
componentWillUnmount () {
store.off('change', this.onChange);
}
addFilter (newFilter) {
if (_.isEmpty(newFilter)) {
return;
}
Actions.addFilter(newFilter);
}
hasFilter (filter) {
return store.hasFilter(filter);
}
render () {
return (
<div className="changes-header">
<AddFilterForm filter={(label) => Actions.removeFilter(label)} addFilter={this.addFilter}
hasFilter={this.hasFilter} />
<BadgeList elements={this.state.filters} removeBadge={(label) => Actions.removeFilter(label)} />
</div>
);
}
}
class AddFilterForm extends React.Component {
constructor (props) {
super(props);
this.state = {
filter: '',
error: false
};
this.submitForm = this.submitForm.bind(this);
}
submitForm (e) {
e.preventDefault();
e.stopPropagation();
if (this.props.hasFilter(this.state.filter)) {
this.setState({ error: true });
// Yuck. This removes the class after the effect has completed so it can occur again. The
// other option is to use jQuery to add the flash. This seemed slightly less crumby
let component = this;
setTimeout(function () {
component.setState({ error: false });
}, 1000);
} else {
this.props.addFilter(this.state.filter);
this.setState({ filter: '', error: false });
}
}
componentDidMount () {
this.focusFilterField();
}
componentDidUpdate () {
this.focusFilterField();
}
focusFilterField () {
this.addItem.focus();
}
inputClassNames () {
let className = 'js-changes-filter-field';
if (this.state.error) {
className += ' errorHighlight';
}
return className;
}
render () {
return (
<form className="form-inline js-filter-form" onSubmit={this.submitForm}>
<fieldset>
<i className="fonticon-filter" />
<input
type="text"
ref={node => this.addItem = node}
className={this.inputClassNames()}
placeholder="Sequence or ID"
onChange={(e) => this.setState({ filter: e.target.value })}
value={this.state.filter} />
<button type="submit" className="btn btn-secondary">Filter</button>
<div className="help-block"></div>
</fieldset>
</form>
);
}
}
AddFilterForm.propTypes = {
addFilter: PropTypes.func.isRequired,
hasFilter: PropTypes.func.isRequired,
tooltips: PropTypes.string
};
AddFilterForm.defaultProps = {
tooltip: ''
};
class ChangeRow extends React.Component {
constructor (props) {
super(props);
this.state = {
codeVisible: false
};
}
toggleJSON (e) {
e.preventDefault();
this.setState({ codeVisible: !this.state.codeVisible });
}
getChangesCode () {
return (this.state.codeVisible) ? <Components.CodeFormat key="changesCodeSection" code={this.getChangeCode()} /> : null;
}
getChangeCode () {
return {
changes: this.props.change.changes,
doc: this.props.change.doc
};
}
showCopiedMessage (target) {
let msg = 'The document ID has been copied to your clipboard.';
if (target === 'seq') {
msg = 'The document seq number has been copied to your clipboard.';
}
FauxtonAPI.addNotification({
msg: msg,
type: 'info',
clear: true
});
}
render () {
const { codeVisible } = this.state;
const { change, databaseName } = this.props;
const wrapperClass = 'change-wrapper' + (change.isNew ? ' new-change-row' : '');
return (
<div className={wrapperClass}>
<div className="change-box" data-id={change.id}>
<div className="row-fluid">
<div className="span2">seq</div>
<div className="span8 change-sequence">{change.seq}</div>
<div className="span2 text-right">
<Copy
uniqueKey={uuid.v4()}
text={change.seq.toString()}
onClipboardClick={() => this.showCopiedMessage('seq')} />
</div>
</div>
<div className="row-fluid">
<div className="span2">id</div>
<div className="span8">
<ChangeID id={change.id} deleted={change.deleted} databaseName={databaseName} />
</div>
<div className="span2 text-right">
<Copy
uniqueKey={uuid.v4()}
text={change.id}
onClipboardClick={() => this.showCopiedMessage('id')} />
</div>
</div>
<div className="row-fluid">
<div className="span2">deleted</div>
<div className="span10">{change.deleted ? 'True' : 'False'}</div>
</div>
<div className="row-fluid">
<div className="span2">changes</div>
<div className="span10">
<button type="button" className='btn btn-small btn-secondary' onClick={this.toggleJSON.bind(this)}>
{codeVisible ? 'Close JSON' : 'View JSON'}
</button>
</div>
</div>
<ChangesCodeTransition
codeVisible={this.state.codeVisible}
code={this.getChangeCode()}
/>
</div>
</div>
);
}
}
ChangeRow.propTypes = {
change: PropTypes.object,
databaseName: PropTypes.string.isRequired
};
export class ChangesCodeTransition extends React.Component {
willEnter () {
return {
opacity: spring(1, presets.gentle),
height: spring(160, presets.gentle)
};
}
willLeave () {
return {
opacity: spring(0, presets.gentle),
height: spring(0, presets.gentle)
};
}
getStyles (prevStyle) {
if (!prevStyle && this.props.codeVisible) {
return [{
key: '1',
style: this.willEnter()
}];
}
if (!prevStyle && !this.props.codeVisible) {
return [{
key: '1',
style: this.willLeave()
}];
}
return prevStyle.map(item => {
return {
key: '1',
style: item.style
};
});
}
getChildren (items) {
const code = items.map(({style}) => {
if (this.props.codeVisible === false && style.opacity === 0) {
return null;
}
return (
<div key='1' style={{opacity: style.opacity, height: style.height + 'px'}}>
<Components.CodeFormat
code={this.props.code}
/>
</div>
);
});
return (
<span>
{code}
</span>
);
}
render () {
return (
<TransitionMotion
styles={this.getStyles()}
willLeave={this.willLeave}
willEnter={this.willEnter}
>
{this.getChildren.bind(this)}
</TransitionMotion>
);
}
}
class ChangeID extends React.Component {
render () {
const { deleted, id, databaseName } = this.props;
if (deleted) {
return (
<span className="js-doc-id">{id}</span>
);
}
const link = '#' + FauxtonAPI.urls('document', 'app', databaseName, id);
return (
<a href={link} className="js-doc-link">{id}</a>
);
}
}
export default {
ChangesController,
ChangesTabContent,
ChangeRow,
ChangeID
};