Commit 194b9eb0 authored by Benjamin Bouvier's avatar Benjamin Bouvier
Browse files

Fixes #457: Enhance performance of the operations list;

parent cac87af2
......@@ -35,7 +35,7 @@
# Forbid certain propTypes
'react/forbid-prop-types':
- 2
- {'forbid': ['any', 'object', 'array']}
- {'forbid': ['any']}
# Enforce boolean attributes notation in JSX
'react/jsx-boolean-value':
- 2
......
import React from 'react';
import { connect } from 'react-redux';
import { actions, get } from '../../store';
import { assertHas } from '../../helpers';
import ButtonSelect from '../ui/button-select';
export default connect(state => {
return {
categories: get.categories(state),
export default props => {
assertHas(props, 'operation');
assertHas(props, 'categories');
assertHas(props, 'getCategoryTitle');
assertHas(props, 'onSelectId');
getCategoryTitle(id) {
return get.categoryById(state, id).title;
}
};
}, (dispatch, props) => {
let ret = {};
// Only define setOperationCategory if none was provided.
if (!props.onSelectId) {
ret.onSelectId = function(id) {
actions.setOperationCategory(dispatch, props.operation, id);
};
}
return ret;
})(props => {
let getThisCategoryId = () => props.operation.categoryId;
return (
<ButtonSelect
key={ `category-select-operation${props.operation.id}` }
......@@ -36,4 +22,4 @@ export default connect(state => {
onSelectId={ props.onSelectId }
/>
);
});
};
import React from 'react';
import { assertHas, translate as $t } from '../../helpers';
import { translate as $t } from '../../helpers';
import { DetailedViewLabel } from './label';
import DeleteOperation from './delete-operation';
import OperationTypeSelect from './operation-type-select';
import CategorySelect from './category-select';
export function computeAttachmentLink(op) {
let file = op.binary.fileName || 'file';
return `operations/${op.id}/${file}`;
}
export default class OperationDetails extends React.Component {
constructor(props) {
assertHas(props, 'onToggleDetails');
assertHas(props, 'operation');
assertHas(props, 'rowClassName');
assertHas(props, 'formatCurrency');
super(props);
}
render() {
let op = this.props.operation;
......@@ -70,11 +59,11 @@ export default class OperationDetails extends React.Component {
</li>
<li className="form-inline">
{ $t('client.operations.type') }
<OperationTypeSelect operation={ op } />
{ this.props.typeSelect }
</li>
<li className="form-inline">
{ $t('client.operations.category') }
<CategorySelect operation={ op } />
{ this.props.categorySelect }
</li>
{ maybeAttachment }
<li>
......@@ -90,3 +79,23 @@ export default class OperationDetails extends React.Component {
);
}
}
OperationDetails.propTypes = {
// The Operation itself.
operation: React.PropTypes.object.isRequired,
// Function describing what happens when we untoggle details.
onToggleDetails: React.PropTypes.func.isRequired,
// Function called to format amounts.
formatCurrency: React.PropTypes.func.isRequired,
// CSS class name for the current row.
rowClassName: React.PropTypes.string.isRequired,
// Type select for the current operation.
typeSelect: React.PropTypes.object.isRequired,
// Category select for the current operation.
categorySelect: React.PropTypes.object.isRequired,
};
......@@ -15,7 +15,12 @@ import SyncButton from './sync-button';
import InfiniteList from '../ui/infinite-list';
// Infinite list properties.
const OPERATION_BALLAST = 5;
const OPERATION_BALLAST = 10;
// Keep in sync with style.css.
function computeOperationHeight() {
return window.innerWidth < 768 ? 41 : 54;
}
// Filter functions used in amount wells.
function noFilter() {
......@@ -45,31 +50,46 @@ class OperationsComponent extends React.Component {
this.computeHeightAbove = this.computeHeightAbove.bind(this);
this.getOperationHeight = this.getOperationHeight.bind(this);
this.getNumItems = this.getNumItems.bind(this);
this.handleWindowResize = this.handleWindowResize.bind(this);
this.operationHeight = computeOperationHeight();
}
// Implementation of infinite list.
renderItems(low, high) {
let formatCurrency = this.props.account.formatCurrency;
return this.props.filteredOperations
.slice(low, high)
.map(o =>
<OperationItem key={ o.id }
operation={ o }
formatCurrency={ formatCurrency }
/>);
.slice(low, high)
.map(o =>
<OperationItem key={ o.id }
operation={ o }
formatCurrency={ this.props.account.formatCurrency }
categories={ this.props.categories }
getCategoryTitle={ this.props.getCategoryTitle }
/>);
}
computeHeightAbove() {
componentDidMount() {
// Called after first render => safe to use findDOMNode.
this.handleWindowResize();
}
handleWindowResize() {
let wellH = ReactDOM.findDOMNode(this.refs.wells).scrollHeight;
let searchH = ReactDOM.findDOMNode(this.refs.search).scrollHeight;
let panelH = ReactDOM.findDOMNode(this.refs.panelHeading).scrollHeight;
let theadH = ReactDOM.findDOMNode(this.refs.thead).scrollHeight;
return wellH + searchH + panelH + theadH;
this.heightAbove = wellH + searchH + panelH + theadH;
this.operationHeight = computeOperationHeight();
}
computeHeightAbove() {
return this.heightAbove;
}
getOperationHeight() {
// Keep in sync with style.css.
return window.innerWidth < 768 ? 41 : 54;
return this.operationHeight;
}
getNumItems() {
......@@ -189,6 +209,7 @@ class OperationsComponent extends React.Component {
getItemHeight={ this.getOperationHeight }
getHeightAbove={ this.computeHeightAbove }
renderItems={ this.renderItems }
onResizeUser={ this.handleWindowResize }
/>
</table>
</div>
......@@ -274,11 +295,17 @@ const Export = connect(state => {
let hasSearchFields = get.hasSearchFields(state);
let operations = selectOperations(state);
let filteredOperations = selectFilteredOperations(state);
let categories = get.categories(state);
let getCategoryTitle = categoryId => get.categoryById(state, categoryId).title;
return {
account,
operations,
filteredOperations,
hasSearchFields
hasSearchFields,
categories,
getCategoryTitle
};
})(OperationsComponent);
......
import React from 'react';
import { connect } from 'react-redux';
import { translate as $t, assertHas } from '../../helpers';
import { actions } from '../../store';
import { translate as $t } from '../../helpers';
import { default as OperationDetails, computeAttachmentLink } from './details';
import { OperationListViewLabel } from './label';
import OperationTypeSelect from './operation-type-select';
import OperationTypeSelect from './type-select';
import CategorySelect from './category-select';
export default class Operation extends React.Component {
class Operation extends React.Component {
constructor(props) {
assertHas(props, 'operation');
assertHas(props, 'formatCurrency');
super(props);
this.state = {
showDetails: false
......@@ -21,8 +22,8 @@ export default class Operation extends React.Component {
}
handleToggleDetails(e) {
this.setState({ showDetails: !this.state.showDetails });
e.preventDefault();
this.setState({ showDetails: !this.state.showDetails });
}
render() {
......@@ -30,13 +31,31 @@ export default class Operation extends React.Component {
let rowClassName = op.amount > 0 ? 'success' : '';
let typeSelect = (
<OperationTypeSelect
operation={ op }
onSelectId={ this.props.handleSelectType }
/>
);
let categorySelect = (
<CategorySelect
operation={ op }
onSelectId={ this.props.handleSelectCategory }
categories={ this.props.categories }
getCategoryTitle={ this.props.getCategoryTitle }
/>
);
if (this.state.showDetails) {
return (
<OperationDetails
onToggleDetails ={ this.handleToggleDetails }
onToggleDetails={ this.handleToggleDetails }
operation={ op }
rowClassName={ rowClassName }
formatCurrency= { this.props.formatCurrency }
formatCurrency={ this.props.formatCurrency }
typeSelect={ typeSelect }
categorySelect={ categorySelect }
/>
);
}
......@@ -62,9 +81,8 @@ export default class Operation extends React.Component {
);
}
let maybeLink;
if (link) {
maybeLink = (
link = (
<label htmlFor={ op.id } className="input-group-addon box-transparent">
{ link }
</label>
......@@ -78,16 +96,50 @@ export default class Operation extends React.Component {
<i className="fa fa-plus-square"></i>
</a>
</td>
<td>{ op.date.toLocaleDateString() }</td>
<td>
{ op.date.toLocaleDateString() }
</td>
<td className="hidden-xs">
<OperationTypeSelect operation={ op } />
{ typeSelect }
</td>
<td>
<OperationListViewLabel
operation={ op }
link={ link }
/>
</td>
<td className="text-right">
{ this.props.formatCurrency(op.amount) }
</td>
<td><OperationListViewLabel operation={ op } link={ maybeLink } /></td>
<td className="text-right">{ this.props.formatCurrency(op.amount) }</td>
<td className="hidden-xs">
<CategorySelect operation={ op } />
{ categorySelect }
</td>
</tr>
);
}
}
Operation.propTypes = {
// The operation this item is representing.
operation: React.PropTypes.object.isRequired,
// A method to compute the currency.
formatCurrency: React.PropTypes.func.isRequired,
// An array of categories.
categories: React.PropTypes.array.isRequired,
// A function mapping category id => title.
getCategoryTitle: React.PropTypes.func.isRequired
};
export default connect(null, (dispatch, props) => {
return {
handleSelectType: type => {
actions.setOperationType(dispatch, props.operation, type);
},
handleSelectCategory: category => {
actions.setOperationCategory(dispatch, props.operation, category);
}
};
})(Operation);
import React from 'react';
import { connect } from 'react-redux';
import { assertHas, assert, translate as $t } from '../../helpers';
import { assert, translate as $t } from '../../helpers';
import { actions } from '../../store';
// If the length of the short label (of an operation) is smaller than this
......@@ -12,11 +12,12 @@ const SMALL_TITLE_THRESHOLD = 4;
class LabelComponent extends React.Component {
constructor(props) {
assertHas(props, 'operation');
super(props);
this.state = {
editMode: false
};
this.handleClickEditMode = this.handleClickEditMode.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
......@@ -64,6 +65,8 @@ class LabelComponent extends React.Component {
}
}
// Returns the customLabel if there's one, or the label (or even the raw
// label is the label is too short).
defaultValue() {
let op = this.props.operation;
......@@ -95,7 +98,7 @@ class LabelComponent extends React.Component {
className="form-control text-left btn-transparent hidden-xs"
id={ this.props.operation.id }
onClick={ this.handleClickEditMode }>
{ this.buttonLabel() }
{ this.buttonLabel() }
</button>
</div>
......@@ -128,11 +131,6 @@ function mapDispatch(component) {
}
class DetailedViewLabel_ extends LabelComponent {
constructor(props) {
assertHas(props, 'operation');
super(props);
}
buttonLabel() {
let customLabel = this.props.operation.customLabel;
if (customLabel === null || customLabel.trim().length === 0) {
......@@ -146,15 +144,13 @@ class DetailedViewLabel_ extends LabelComponent {
}
}
DetailedViewLabel_.propTypes = {
operation: React.PropTypes.object.isRequired
};
export const DetailedViewLabel = mapDispatch(DetailedViewLabel_);
class OperationListViewLabel_ extends LabelComponent {
constructor(props) {
assertHas(props, 'operation');
assertHas(props, 'link');
super(props);
}
buttonLabel() {
return (
<div className="label-button text-uppercase">
......@@ -176,4 +172,9 @@ class OperationListViewLabel_ extends LabelComponent {
}
}
OperationListViewLabel_.propTypes = {
operation: React.PropTypes.object.isRequired,
link: React.PropTypes.object
};
export const OperationListViewLabel = mapDispatch(OperationListViewLabel_);
import React from 'react';
import { connect } from 'react-redux';
import { actions } from '../../store';
import { translate as $t } from '../../helpers';
import { assertHas, translate as $t } from '../../helpers';
import ButtonSelect from '../ui/button-select';
import OperationTypes from '../../../shared/operation-types.json';
export default connect(null, (dispatch, props) => {
let ret = {};
for (let type of OperationTypes) {
type.id = type.name;
}
// Only define handleSelectId if none was provided.
if (!props.onSelectId) {
ret.onSelectId = function(type) {
actions.setOperationType(dispatch, props.operation, type);
};
}
export default props => {
assertHas(props, 'operation');
assertHas(props, 'onSelectId');
return ret;
})(props => {
let getThisType = () => props.operation.type;
let idToLabel = type => $t(`client.${type}`);
let opTypes = [];
for (let type of OperationTypes) {
type.id = type.name;
opTypes.push(type);
}
return (
<ButtonSelect
key={ `operation-type-select-operation-${props.operation.id}` }
optionsArray={ opTypes }
optionsArray={ OperationTypes }
selectedId={ getThisType }
idToLabel={ idToLabel }
onSelectId={ props.onSelectId }
/>
);
});
};
import React from 'react';
import { connect } from 'react-redux';
import { actions } from '../../../store';
import { assertHas,
translate as $t,
NONE_CATEGORY_ID,
UNKNOWN_OPERATION_TYPE } from '../../../helpers';
import { actions, get } from '../../../store';
import {
assertHas,
translate as $t,
NONE_CATEGORY_ID,
UNKNOWN_OPERATION_TYPE
} from '../../../helpers';
import CategorySelect from '../../operations/category-select';
import OperationTypeSelect from '../../operations/operation-type-select';
import OperationTypeSelect from '../../operations/type-select';
import Modal from '../../ui/modal';
import ValidableInputText from '../../ui/checked-text';
......@@ -129,6 +132,8 @@ class AddOperationModal extends React.Component {
<CategorySelect
operation={ this.state }
onSelectId={ this.handleOnSelectCategory }
categories={ this.props.categories }
getCategoryTitle={ this.props.getCategoryTitle }
/>
</div>
</form>
......@@ -161,7 +166,12 @@ class AddOperationModal extends React.Component {
}
}
let Export = connect(null, dispatch => {
let Export = connect(state => {
return {
categories: get.categories(state),
getCategoryTitle: categoryId => get.categoryById(state, categoryId).title
};
}, dispatch => {
return {
createOperation(operation) {
actions.createOperation(dispatch, operation);
......
......@@ -34,11 +34,13 @@ export default class InfiniteList extends React.Component {
handleResize(e) {
e.preventDefault();
this.props.onResizeUser();
this.handleScroll();
}
handleScroll(e) {
e.preventDefault();
if (e)
e.preventDefault();
let heightAbove = this.props.getHeightAbove();
......@@ -89,5 +91,8 @@ InfiniteList.propTypes = {
// Function to be called for rendering all the items, with the signature:
// (firstItemShown: Number, lastItemShown: Number) -> [React elements]
renderItems: React.PropTypes.func.isRequired
renderItems: React.PropTypes.func.isRequired,
// Function called on each window resize.
onResizeUser: React.PropTypes.func
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment