Skip to content

Commit

Permalink
Merge pull request #79 from tferreira/scroll
Browse files Browse the repository at this point in the history
Infinite scrolling on transactions list
  • Loading branch information
tferreira authored Jan 6, 2018
2 parents f9b2339 + 1a0a6e6 commit a93422b
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 65 deletions.
23 changes: 22 additions & 1 deletion application/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,29 @@ def delete_account():
def get_transactions():
incoming = request.args
account_id = incoming["account_id"]
limit = incoming["limit"]
transactionsList = []
transactionsObjects = Transaction.get_transactions(account_id)
transactionsObjects = Transaction.get_transactions(account_id, limit)
for transaction in transactionsObjects:
transactionsList.append({
'transaction_id': transaction.transaction_id,
'account_id': transaction.account_id,
'label': transaction.label,
'amount': str(transaction.amount), # Decimal is not JSON serializable
'recurring_group_id': transaction.recurring_group_id,
'date': transaction.date.strftime('%Y-%m-%d'),
'tick': transaction.tick,
})
return jsonify(result=transactionsList)


@app.route("/api/transactions/future", methods=["GET"])
@requires_auth
def get_future_transactions():
incoming = request.args
account_id = incoming["account_id"]
transactionsList = []
transactionsObjects = Transaction.get_recurring_until_projected(account_id)
for transaction in transactionsObjects:
transactionsList.append({
'transaction_id': transaction.transaction_id,
Expand Down
17 changes: 15 additions & 2 deletions application/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from index import db, bcrypt
from sqlalchemy import desc, or_
from datetime import datetime


class User(db.Model):
Expand Down Expand Up @@ -79,10 +81,21 @@ def __init__(self, transaction_id, account_id, label, amount, recurring_group_id
self.tick = tick

@staticmethod
def get_transactions(account_id):
def get_transactions(account_id, limit):
return Transaction.query \
.filter(Transaction.account_id == account_id) \
.filter(or_(db.func.date(Transaction.date) <= datetime.now().date(), Transaction.recurring_group_id == None)) \
.order_by(desc(Transaction.date)) \
.limit(limit) \
.all()

@staticmethod
def get_recurring_until_projected(account_id):
return Transaction.query \
.filter((Transaction.account_id == account_id),
(db.func.date(Transaction.date) <= Account.get_projected_date(account_id))) \
(db.func.date(Transaction.date) <= Account.get_projected_date(account_id)),
(db.func.date(Transaction.date) > datetime.now().date()),
(Transaction.recurring_group_id != None)) \
.all()


Expand Down
2 changes: 1 addition & 1 deletion static/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"material-ui": "^0.16.4",
"mocha": "^3.0.2",
"morgan": "^1.6.1",
"node-sass": "^3.4.2",
"node-sass": "^4.7.2",
"postcss-import": "^9.0.0",
"postcss-loader": "^2.0.6",
"prettier": "^1.5.3",
Expand Down
38 changes: 36 additions & 2 deletions static/src/actions/transactions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST,
RECEIVE_FUTURE_TRANSACTIONS_DATA,
FETCH_TRANSACTIONS_DATA_REQUEST,
RECEIVE_TRANSACTIONS_DATA,
ADD_TRANSACTION,
Expand All @@ -8,6 +10,7 @@ import {
import { parseJSON } from '../utils/misc'
import {
data_about_transactions,
data_about_future_transactions,
create_transaction,
edit_transaction,
delete_transaction,
Expand All @@ -31,6 +34,21 @@ export function fetchTransactionsDataRequest() {
}
}

export function receiveFutureTransactionsData(data) {
return {
type: RECEIVE_FUTURE_TRANSACTIONS_DATA,
payload: {
data
}
}
}

export function fetchFutureTransactionsDataRequest() {
return {
type: FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST
}
}

export function addTransactionToStore(data) {
return {
type: ADD_TRANSACTION,
Expand Down Expand Up @@ -58,10 +76,26 @@ export function deleteTransactionFromStore(transaction_id) {
}
}

export function fetchTransactionsData(token, account_id) {
export function fetchFutureTransactionsData(token, account_id) {
return dispatch => {
dispatch(fetchFutureTransactionsDataRequest())
data_about_future_transactions(token, account_id)
.then(parseJSON)
.then(response => {
dispatch(receiveFutureTransactionsData(response.result))
})
.catch(error => {
if (error.status === 401) {
dispatch(logoutAndRedirect(error))
}
})
}
}

export function fetchTransactionsData(token, account_id, limit) {
return dispatch => {
dispatch(fetchTransactionsDataRequest())
data_about_transactions(token, account_id)
data_about_transactions(token, account_id, limit)
.then(parseJSON)
.then(response => {
dispatch(receiveTransactionsData(response.result))
Expand Down
138 changes: 90 additions & 48 deletions static/src/components/TransactionsList/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import 'babel-polyfill'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
Expand Down Expand Up @@ -40,20 +41,26 @@ function mapDispatchToProps(dispatch) {

@connect(mapStateToProps, mapDispatchToProps)
export default class TransactionsList extends React.Component {
state = {
fixedHeader: true,
fixedFooter: true,
stripedRows: true,
showRowHover: false,
selectable: false,
multiSelectable: false,
enableSelectAll: false,
deselectOnClickaway: false,
showCheckboxes: false,
height: '400px',
snackOpen: false,
snackMessage: '',
showRecurring: false
constructor(props) {
super(props)
this.state = {
fixedHeader: true,
fixedFooter: true,
stripedRows: true,
showRowHover: false,
selectable: false,
multiSelectable: false,
enableSelectAll: false,
deselectOnClickaway: false,
showCheckboxes: false,
height: '400px',
snackOpen: false,
snackMessage: '',
showRecurring: false,
recurringLoaded: false,
preventScroll: false
}
this.fireOnScroll = this.fireOnScroll.bind(this)
}

fetchData(account_id = null) {
Expand All @@ -62,7 +69,49 @@ export default class TransactionsList extends React.Component {
}
if (account_id !== null) {
const token = this.props.token
this.props.fetchTransactionsData(token, account_id)
const limit = this.props.data ? this.props.data.length + 50 : 50
this.props.fetchTransactionsData(token, account_id, limit)
}
this.setState({
showRecurring: false
})
}

fetchFutureData(account_id = null) {
if (!this.state.recurringLoaded) {
if (account_id === null) {
account_id = this.props.selectedAccount
}
if (account_id !== null) {
const token = this.props.token
let call = async () =>
await await this.props.fetchFutureTransactionsData(token, account_id)
call().then(() => {
this.setState({
recurringLoaded: true
})
})
}
}
}

fireOnScroll(event) {
const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv)
if (elem.scrollTop == 0 && !this.props.isFetching) {
this.setState({
preventScroll: true
})
this.fetchData()
}
}

toggleRecurring(event, isChecked) {
this.setState({
showRecurring: isChecked,
recurringLoaded: false
})
if (!isChecked) {
this.fetchData()
}
}

Expand All @@ -79,6 +128,11 @@ export default class TransactionsList extends React.Component {
})
}

componentWillUnmount() {
const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv)
elem.removeEventListener('scroll', this.fireOnScroll)
}

componentWillReceiveProps(nextProps) {
if (nextProps.selectedAccount !== this.props.selectedAccount) {
this.fetchData(nextProps.selectedAccount)
Expand All @@ -88,7 +142,17 @@ export default class TransactionsList extends React.Component {
componentDidUpdate(prevProps, prevState) {
// Do not scroll on ticking
if (prevProps.data !== this.props.data) {
this.scrollToBottom()
if (!this.state.preventScroll) {
this.scrollToBottom()
} else {
this.setState({
preventScroll: false
})
}
}
if (this.refs.table) {
const elem = ReactDOM.findDOMNode(this.refs.table.refs.tableDiv)
elem.addEventListener('scroll', this.fireOnScroll)
}
}

Expand Down Expand Up @@ -141,53 +205,29 @@ export default class TransactionsList extends React.Component {
call().then(() => {})
}

toggleRecurring(event, isChecked) {
this.setState({
showRecurring: isChecked
})
}

renderTransactionsList(transactions) {
if (this.state.showRecurring) {
// add recurring transactions
this.fetchFutureData()
}

transactions.sort((a, b) => {
if (new Date(a.date) < new Date(b.date)) return -1
if (new Date(a.date) > new Date(b.date)) return 1
if (a.id < b.id) return -1
if (a.id > b.id) return 1
return 0
})
// get projected balance date for current account
let projectedDate = this.props.accounts.filter(
element => element.id == this.props.selectedAccount
)[0].projected_date

if (this.state.showRecurring) {
// display recurring transactions of the month
transactions = transactions.filter(element => {
return (
element.recurring_group_id === null ||
(element.recurring_group_id !== null &&
new Date(element.date) <= new Date()) ||
(element.recurring_group_id !== null &&
new Date(element.date) <= new Date(projectedDate))
)
})
} else {
// filter all future recurring transactions
transactions = transactions.filter(element => {
return (
element.recurring_group_id === null ||
(element.recurring_group_id !== null &&
new Date(element.date) <= new Date())
)
})
}
const rows = transactions.map((row, index) => {
let credit =
parseFloat(row.amount) < 0 ? '' : Number(row.amount).toFixed(2)
let debit =
parseFloat(row.amount) < 0 ? Number(row.amount).toFixed(2) : ''
let isFutureAndRecurring =
new Date(row.date) > new Date() && row.recurring_group_id !== null
new Date(row.date) > new Date() &&
row.recurring_group_id !== null &&
row.recurring_group_id !== undefined
return (
<TableRow
key={row.transaction_id}
Expand Down Expand Up @@ -296,6 +336,7 @@ export default class TransactionsList extends React.Component {
<TableRowColumn colSpan="1">
<Toggle
label="Display recurring"
toggled={this.state.showRecurring}
onToggle={this.toggleRecurring.bind(this)}
/>
</TableRowColumn>
Expand All @@ -316,6 +357,7 @@ export default class TransactionsList extends React.Component {

TransactionsList.propTypes = {
fetchTransactionsData: React.PropTypes.func,
fetchFutureTransactionsData: React.PropTypes.func,
loaded: React.PropTypes.bool,
data: React.PropTypes.any,
token: React.PropTypes.string,
Expand Down
4 changes: 4 additions & 0 deletions static/src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const EDIT_TRANSACTION = 'EDIT_TRANSACTION'
export const DELETE_TRANSACTION = 'DELETE_TRANSACTION'
export const FETCH_TRANSACTIONS_DATA_REQUEST = 'FETCH_TRANSACTIONS_DATA_REQUEST'
export const RECEIVE_TRANSACTIONS_DATA = 'RECEIVE_TRANSACTIONS_DATA'
export const FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST =
'FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST'
export const RECEIVE_FUTURE_TRANSACTIONS_DATA =
'RECEIVE_FUTURE_TRANSACTIONS_DATA'

export const FETCH_BALANCES_DATA_REQUEST = 'FETCH_BALANCES_DATA_REQUEST'
export const RECEIVE_BALANCES_DATA = 'RECEIVE_BALANCES_DATA'
Expand Down
16 changes: 15 additions & 1 deletion static/src/reducers/transactions.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
RECEIVE_FUTURE_TRANSACTIONS_DATA,
FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST,
RECEIVE_TRANSACTIONS_DATA,
FETCH_TRANSACTIONS_DATA_REQUEST,
ADD_TRANSACTION,
Expand All @@ -10,10 +12,22 @@ import { createReducer } from '../utils/misc'
const initialState = {
data: null,
isFetching: false,
loaded: false
loaded: false,
isFetchingFuture: false,
loadedFuture: false
}

export default createReducer(initialState, {
[RECEIVE_FUTURE_TRANSACTIONS_DATA]: (state, payload) =>
Object.assign({}, state, {
data: [...state.data, ...payload.data],
isFetchingFuture: false,
loadedFuture: true
}),
[FETCH_FUTURE_TRANSACTIONS_DATA_REQUEST]: state =>
Object.assign({}, state, {
isFetchingFuture: true
}),
[RECEIVE_TRANSACTIONS_DATA]: (state, payload) =>
Object.assign({}, state, {
data: payload.data,
Expand Down
Loading

0 comments on commit a93422b

Please sign in to comment.