paginates transactions

This commit is contained in:
jessop 2019-09-22 22:47:07 -04:00 committed by Sean Yesmunt
parent 534d84ef88
commit 5078bd6699
8 changed files with 217 additions and 140 deletions

View file

@ -17,6 +17,7 @@ type Props = {
defaultPath?: string, defaultPath?: string,
filters: Array<string>, filters: Array<string>,
onFileCreated?: string => void, onFileCreated?: string => void,
disabled: boolean,
}; };
class FileExporter extends React.PureComponent<Props> { class FileExporter extends React.PureComponent<Props> {
@ -78,9 +79,15 @@ class FileExporter extends React.PureComponent<Props> {
} }
render() { render() {
const { label } = this.props; const { label, disabled } = this.props;
return ( return (
<Button button="primary" icon={ICONS.DOWNLOAD} label={label || __('Export')} onClick={this.handleButtonClick} /> <Button
button="primary"
disabled={disabled}
icon={ICONS.DOWNLOAD}
label={label || __('Export')}
onClick={this.handleButtonClick}
/>
); );
} }
} }

View file

@ -8,6 +8,7 @@ import {
doSetTransactionListFilter, doSetTransactionListFilter,
selectIsFetchingTransactions, selectIsFetchingTransactions,
} from 'lbry-redux'; } from 'lbry-redux';
import { withRouter } from 'react-router';
import TransactionList from './view'; import TransactionList from './view';
const select = state => ({ const select = state => ({
@ -23,7 +24,9 @@ const perform = dispatch => ({
setTransactionFilter: filterSetting => dispatch(doSetTransactionListFilter(filterSetting)), setTransactionFilter: filterSetting => dispatch(doSetTransactionListFilter(filterSetting)),
}); });
export default connect( export default withRouter(
select, connect(
perform select,
)(TransactionList); perform
)(TransactionList)
);

View file

@ -1,156 +1,110 @@
// @flow // @flow
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import * as MODALS from 'constants/modal_types'; import React from 'react';
import * as React from 'react';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import FileExporter from 'component/common/file-exporter'; import FileExporter from 'component/common/file-exporter';
import { TRANSACTIONS } from 'lbry-redux'; import { TRANSACTIONS } from 'lbry-redux';
import TransactionListItem from './internal/transaction-list-item'; import TransactionListTable from 'component/transactionListTable';
import RefreshTransactionButton from 'component/transactionRefreshButton'; import RefreshTransactionButton from 'component/transactionRefreshButton';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import Paginate from 'component/common/paginate';
type Props = { type Props = {
emptyMessage: ?string, emptyMessage: ?string,
filterSetting: string, filterSetting: string,
loading: boolean, loading: boolean,
mySupports: {},
myClaims: any, myClaims: any,
openModal: (id: string, { nout: number, txid: string }) => void,
rewards: {},
setTransactionFilter: string => void, setTransactionFilter: string => void,
slim?: boolean, slim?: boolean,
title: string, title: string,
transactions: Array<Transaction>, transactions: Array<Transaction>,
transactionCount?: number,
history: { replace: string => void },
}; };
class TransactionList extends React.PureComponent<Props> { function TransactionList(props: Props) {
constructor(props: Props) { const { emptyMessage, slim, filterSetting, title, transactions, loading, history, transactionCount } = props;
super(props); const PAGE_SIZE = 20;
// Flow offers little support for Object.values() typing.
// https://github.com/facebook/flow/issues/2221
// $FlowFixMe
const transactionTypes: Array<string> = Object.values(TRANSACTIONS);
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this); function capitalize(string: string) {
(this: any).filterTransaction = this.filterTransaction.bind(this);
(this: any).revokeClaim = this.revokeClaim.bind(this);
(this: any).isRevokeable = this.isRevokeable.bind(this);
}
capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1); return string.charAt(0).toUpperCase() + string.slice(1);
} }
handleFilterChanged(event: SyntheticInputEvent<*>) { function handleFilterChanged(event: SyntheticInputEvent<*>) {
this.props.setTransactionFilter(event.target.value); props.setTransactionFilter(event.target.value);
history.replace(`#/$/transactions`); //
} }
filterTransaction(transaction: Transaction) { return (
return this.props.filterSetting === TRANSACTIONS.ALL || this.props.filterSetting === transaction.type; <React.Fragment>
} <header className="table__header">
<h2 className="card__title--between">
isRevokeable(txid: string, nout: number) { <span>
const outpoint = `${txid}:${nout}`; {title}
const { mySupports, myClaims } = this.props; {loading && <Spinner type="small" />}
return !!mySupports[outpoint] || myClaims.has(outpoint); </span>
} <div className="card__actions--inline">
{slim && (
revokeClaim(txid: string, nout: number) { <Button button="link" className="button--alt" navigate="/$/transactions" label={__('Full History')} />
this.props.openModal(MODALS.CONFIRM_CLAIM_REVOKE, { txid, nout }); )}
} <RefreshTransactionButton />
</div>
render() { </h2>
const { emptyMessage, rewards, transactions, slim, filterSetting, title, loading } = this.props; </header>
// The shorter "recent transactions" list shouldn't be filtered {!slim && (
const transactionList = slim ? transactions : transactions.filter(this.filterTransaction);
// Flow offers little support for Object.values() typing.
// https://github.com/facebook/flow/issues/2221
// $FlowFixMe
const transactionTypes: Array<string> = Object.values(TRANSACTIONS);
return (
<React.Fragment>
<header className="table__header"> <header className="table__header">
<h2 className="card__title--between"> <div className="card__actions--between">
<span> <FileExporter
{title} data={transactions}
{loading && <Spinner type="small" />} label={__('Export')}
</span> title={__('Export Transactions')}
<div className="card__actions--inline"> filters={['nout']}
{slim && ( defaultPath={__('lbry-transactions-history')}
<Button button="link" className="button--alt" navigate="/$/transactions" label={__('Full History')} /> disabled={!transactions.length}
)} />
<RefreshTransactionButton /> <FormField
</div> type="select"
</h2> name="file-sort"
value={filterSetting || TRANSACTIONS.ALL}
onChange={handleFilterChanged}
label={__('Show')}
postfix={
<Button
button="link"
icon={icons.HELP}
href="https://lbry.com/faq/transaction-types"
title={__('Help')}
/>
}
>
{transactionTypes.map(tt => (
<option key={tt} value={tt}>
{__(`${capitalize(tt)}`)}
</option>
))}
</FormField>
</div>
</header> </header>
{!slim && !!transactions.length && ( )}
<header className="table__header">
<div className="card__actions--between">
<FileExporter
data={transactionList}
label={__('Export')}
title={__('Export Transactions')}
filters={['nout']}
defaultPath={__('lbry-transactions-history')}
/>
<FormField {!loading && !transactions.length && (
type="select" <h2 className="main--empty empty">{emptyMessage || __('No transactions.')}</h2>
name="file-sort" )}
value={filterSetting || TRANSACTIONS.ALL}
onChange={this.handleFilterChanged}
label={__('Show')}
postfix={
<Button
button="link"
icon={icons.HELP}
href="https://lbry.com/faq/transaction-types"
title={__('Help')}
/>
}
>
{transactionTypes.map(tt => (
<option key={tt} value={tt}>
{__(`${this.capitalize(tt)}`)}
</option>
))}
</FormField>
</div>
</header>
)}
{!loading && !transactionList.length && ( {!!transactions && !!transactions.length && <TransactionListTable transactionList={transactions} />}
<h2 className="main--empty empty">{emptyMessage || __('No transactions.')}</h2> {!slim && !!transactionCount && (
)} <Paginate
onPageChange={page => history.replace(`#/$/transactions?page=${Number(page)}`)}
{!!transactionList.length && ( totalPages={Math.ceil(transactionCount / PAGE_SIZE)}
<React.Fragment> />
<table className="table table--transactions"> )}
<thead> </React.Fragment>
<tr> );
<th>{__('Amount')}</th>
<th>{__('Type')} </th>
<th>{__('Details')} </th>
<th>{__('Transaction')}</th>
<th>{__('Date')}</th>
</tr>
</thead>
<tbody>
{transactionList.map(t => (
<TransactionListItem
key={`${t.txid}:${t.nout}`}
transaction={t}
reward={rewards && rewards[t.txid]}
isRevokeable={this.isRevokeable(t.txid, t.nout)}
revokeClaim={this.revokeClaim}
/>
))}
</tbody>
</table>
</React.Fragment>
)}
</React.Fragment>
);
}
} }
export default TransactionList; export default TransactionList;

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import { selectClaimedRewardsByTransactionId } from 'lbryinc';
import { doOpenModal } from 'redux/actions/app';
import {
selectAllMyClaimsByOutpoint,
selectSupportsByOutpoint,
selectTransactionListFilter,
selectIsFetchingTransactions,
} from 'lbry-redux';
import TransactionListTable from './view';
const select = state => ({
rewards: selectClaimedRewardsByTransactionId(state),
mySupports: selectSupportsByOutpoint(state),
myClaims: selectAllMyClaimsByOutpoint(state),
filterSetting: selectTransactionListFilter(state),
loading: selectIsFetchingTransactions(state),
});
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
});
export default connect(
select,
perform
)(TransactionListTable);

View file

@ -0,0 +1,65 @@
// @flow
import * as MODALS from 'constants/modal_types';
import React from 'react';
import TransactionListItem from './internal/transaction-list-item';
type Props = {
emptyMessage: ?string,
loading: boolean,
mySupports: {},
myClaims: any,
openModal: (id: string, { nout: number, txid: string }) => void,
rewards: {},
transactionList: Array<any>,
};
// class TransactionList extends React.PureComponent<Props> {
function TransactionListTable(props: Props) {
const { emptyMessage, rewards, loading, transactionList } = props;
function isRevokeable(txid: string, nout: number) {
const outpoint = `${txid}:${nout}`;
const { mySupports, myClaims } = props;
return !!mySupports[outpoint] || myClaims.has(outpoint);
}
function revokeClaim(txid: string, nout: number) {
props.openModal(MODALS.CONFIRM_CLAIM_REVOKE, { txid, nout });
}
return (
<React.Fragment>
{!loading && !transactionList.length && (
<h2 className="main--empty empty">{emptyMessage || __('No transactions.')}</h2>
)}
{!!transactionList.length && (
<React.Fragment>
<table className="table table--transactions">
<thead>
<tr>
<th>{__('Amount')}</th>
<th>{__('Type')} </th>
<th>{__('Details')} </th>
<th>{__('Transaction')}</th>
<th>{__('Date')}</th>
</tr>
</thead>
<tbody>
{transactionList.map(t => (
<TransactionListItem
key={`${t.txid}:${t.nout}`}
transaction={t}
reward={rewards && rewards[t.txid]}
isRevokeable={isRevokeable(t.txid, t.nout)}
revokeClaim={revokeClaim}
/>
))}
</tbody>
</table>
</React.Fragment>
)}
</React.Fragment>
);
}
export default TransactionListTable;

View file

@ -1,17 +1,33 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doFetchTransactions, selectTransactionItems, doFetchClaimListMine } from 'lbry-redux'; import {
doFetchTransactions,
doFetchClaimListMine,
makeSelectFilteredTransactionsForPage,
selectFilteredTransactionCount,
} from 'lbry-redux';
import { withRouter } from 'react-router';
import TransactionHistoryPage from './view'; import TransactionHistoryPage from './view';
const select = state => ({ const select = (state, props) => {
transactions: selectTransactionItems(state), const { search } = props.location;
}); const urlParams = new URLSearchParams(search);
const page = Number(urlParams.get('page')) || 1;
return {
page,
filteredTransactionPage: makeSelectFilteredTransactionsForPage(page)(state),
filteredTransactionsCount: selectFilteredTransactionCount(state),
};
};
const perform = dispatch => ({ const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()), fetchTransactions: () => dispatch(doFetchTransactions()),
fetchMyClaims: () => dispatch(doFetchClaimListMine()), fetchMyClaims: () => dispatch(doFetchClaimListMine()),
}); });
export default connect( export default withRouter(
select, connect(
perform select,
)(TransactionHistoryPage); perform
)(TransactionHistoryPage)
);

View file

@ -9,7 +9,8 @@ type Props = {
fetchMyClaims: () => void, fetchMyClaims: () => void,
fetchTransactions: () => void, fetchTransactions: () => void,
fetchingTransactions: boolean, fetchingTransactions: boolean,
transactions: Array<{}>, filteredTransactionPage: Array<{}>,
filteredTransactionsCount: number,
}; };
class TransactionHistoryPage extends React.PureComponent<Props> { class TransactionHistoryPage extends React.PureComponent<Props> {
@ -21,7 +22,7 @@ class TransactionHistoryPage extends React.PureComponent<Props> {
} }
render() { render() {
const { transactions } = this.props; const { filteredTransactionPage, filteredTransactionsCount } = this.props;
return ( return (
<Page> <Page>
@ -31,7 +32,11 @@ class TransactionHistoryPage extends React.PureComponent<Props> {
'card--disabled': IS_WEB, 'card--disabled': IS_WEB,
})} })}
> >
<TransactionList transactions={transactions} title={__('Transaction History')} /> <TransactionList
transactions={filteredTransactionPage}
transactionCount={filteredTransactionsCount}
title={__('Transaction History')}
/>
</section> </section>
</Page> </Page>
); );