Re-add ability to export transactions (#5899)

* FileExporter: add 'fetch' hook + Web support

* Re-add ability to export transactions

Closes 4793: Export Wallet History For Taxation Purposes

* Move file-creation to the background.

Don't let the file-creation process block the GUI.

Requires lbry-redux update.

* Bump redux | doFetchTransactions: bump pageSize to 999999; remove doFetchSupport

* bump redux

Co-authored-by: Thomas Zarebczan <thomas.zarebczan@gmail.com>
This commit is contained in:
infinite-persistence 2021-04-24 00:10:37 +08:00 committed by GitHub
parent b5cc0bb42d
commit b0193202d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 139 additions and 89 deletions

View file

@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Japanese, Afrikaans, Filipino, Thai and Vietnamese language support ([#5684](https://github.com/lbryio/lbry-desktop/issues/5684))
- Highlight comments made by content owner _community pr!_ ([#5744](https://github.com/lbryio/lbry-desktop/pull/5744))
- Ability to report infringing content directly from the application ([#5808](https://github.com/lbryio/lbry-desktop/pull/5808))
- Re-added ability to export wallet transactions ([#5899](https://github.com/lbryio/lbry-desktop/pull/5899))
### Changed

View file

@ -142,7 +142,7 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#3ca0c8d20466a695acfd7a9c20f8f580afa02206",
"lbry-redux": "lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c",
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",

View file

@ -1859,6 +1859,11 @@
"Learn more and sign petition": "Learn more and sign petition",
"Publishing...": "Publishing...",
"Collection": "Collection",
"Fetch transaction data for export": "Fetch transaction data for export",
"Fetching data": "Fetching data",
"Download fetched file": "Download fetched file",
"No data to export": "No data to export",
"Failed to process fetched data.": "Failed to process fetched data.",
"More from %claim_name%": "More from %claim_name%",
"Upload that unlabeled video you found behind the TV in 1991": "Upload that unlabeled video you found behind the TV in 1991",
"Select Replay": "Select Replay",

View file

@ -3,92 +3,86 @@ import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import parseData from 'util/parse-data';
import { remote } from 'electron';
import path from 'path';
// @if TARGET='app'
import fs from 'fs';
// @endif
import Spinner from 'component/spinner';
type Props = {
data: Array<any>,
title: string,
data: any,
label: string,
defaultPath?: string,
filters: Array<string>,
onFileCreated?: string => void,
disabled: boolean,
tooltip?: string,
defaultFileName?: string,
filters?: Array<string>,
onFetch?: () => void,
progressMsg?: string,
disabled?: boolean,
};
class FileExporter extends React.PureComponent<Props> {
static defaultProps = {
filters: [],
};
constructor() {
super();
(this: any).handleButtonClick = this.handleButtonClick.bind(this);
(this: any).handleDownload = this.handleDownload.bind(this);
}
handleFileCreation(filename: string, data: any) {
const { onFileCreated } = this.props;
// @if TARGET='app'
fs.writeFile(filename, data, err => {
if (err) throw err;
// Do something after creation
handleDownload() {
const { data, defaultFileName } = this.props;
if (onFileCreated) {
onFileCreated(filename);
}
});
// @endif
}
handleButtonClick() {
const { title, data, defaultPath, filters } = this.props;
const options = {
title,
defaultPath,
filters: [
{
name: 'CSV',
extensions: ['csv'],
},
{
name: 'JSON',
extensions: ['json'],
},
],
};
remote.dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
// User hit cancel so do nothing:
if (!filename) return;
// Get extension and remove initial dot
// @if TARGET='app'
const format = path.extname(filename).replace(/\./g, '');
// @endif
// Parse data to string with the chosen format
const parsed = parseData(data, format, filters);
// Write file
if (parsed) {
this.handleFileCreation(filename, parsed);
}
});
const element = document.createElement('a');
const file = new Blob([data], { type: 'text/plain' });
element.href = URL.createObjectURL(file);
element.download = defaultFileName || 'file.txt';
// $FlowFixMe
document.body.appendChild(element);
element.click();
// $FlowFixMe
document.body.removeChild(element);
}
render() {
const { label, disabled } = this.props;
return (
<Button
button="primary"
disabled={disabled}
icon={ICONS.DOWNLOAD}
label={label || __('Export')}
onClick={this.handleButtonClick}
/>
);
const { data, label, tooltip, disabled, onFetch, progressMsg } = this.props;
if (onFetch) {
return (
<>
{!progressMsg && (
<div className="button-group">
<Button
button="alt"
disabled={disabled}
icon={ICONS.FETCH}
label={label}
aria-label={tooltip}
onClick={() => onFetch()}
/>
{data && (
<Button
button="alt"
disabled={disabled}
icon={ICONS.DOWNLOAD}
aria-label={__('Download fetched file')}
onClick={this.handleDownload}
/>
)}
</div>
)}
{progressMsg && (
<>
{__(progressMsg)}
<Spinner type="small" />
</>
)}
</>
);
} else {
return (
<Button
button="primary"
disabled={disabled}
icon={ICONS.DOWNLOAD}
label={label || __('Export')}
aria-label={tooltip}
onClick={this.handleDownload}
/>
);
}
}
}

View file

@ -183,6 +183,13 @@ export const icons = {
/>
</g>
),
[ICONS.FETCH]: buildIcon(
<g fill="none" fillRule="evenodd" strokeLinecap="round">
<polyline points="8 17 12 21 16 17" />
<line x1="12" y1="12" x2="12" y2="21" />
<path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29" />
</g>
),
[ICONS.SUBSCRIBE]: buildIcon(
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
),

View file

@ -2,28 +2,34 @@ import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import {
selectIsFetchingTxos,
selectIsFetchingTransactions,
selectFetchingTxosError,
selectTransactionsFile,
selectTxoPage,
selectTxoPageNumber,
selectTxoItemCount,
doFetchTxoPage,
doFetchTransactions,
doUpdateTxoPageParams,
} from 'lbry-redux';
import { withRouter } from 'react-router';
import TxoList from './view';
const select = state => ({
const select = (state) => ({
txoFetchError: selectFetchingTxosError(state),
txoPage: selectTxoPage(state),
txoPageNumber: selectTxoPageNumber(state),
txoItemCount: selectTxoItemCount(state),
loading: selectIsFetchingTxos(state),
isFetchingTransactions: selectIsFetchingTransactions(state),
transactionsFile: selectTransactionsFile(state),
});
const perform = dispatch => ({
const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
fetchTxoPage: () => dispatch(doFetchTxoPage()),
updateTxoPageParams: params => dispatch(doUpdateTxoPageParams(params)),
fetchTransactions: () => dispatch(doFetchTransactions()),
updateTxoPageParams: (params) => dispatch(doUpdateTxoPageParams(params)),
});
export default withRouter(connect(select, perform)(TxoList));

View file

@ -11,15 +11,20 @@ import Card from 'component/common/card';
import { toCapitalCase } from 'util/string';
import classnames from 'classnames';
import HelpLink from 'component/common/help-link';
import FileExporter from 'component/common/file-exporter';
type Props = {
search: string,
history: { action: string, push: string => void, replace: string => void },
history: { action: string, push: (string) => void, replace: (string) => void },
txoPage: Array<Transaction>,
txoPageNumber: string,
txoItemCount: number,
fetchTxoPage: () => void,
updateTxoPageParams: any => void,
fetchTransactions: () => void,
isFetchingTransactions: boolean,
transactionsFile: string,
updateTxoPageParams: (any) => void,
toast: (string, boolean) => void,
};
type Delta = {
@ -28,7 +33,17 @@ type Delta = {
};
function TxoList(props: Props) {
const { search, txoPage, txoItemCount, fetchTxoPage, updateTxoPageParams, history } = props;
const {
search,
txoPage,
txoItemCount,
fetchTxoPage,
fetchTransactions,
updateTxoPageParams,
history,
isFetchingTransactions,
transactionsFile,
} = props;
const urlParams = new URLSearchParams(search);
const page = urlParams.get(TXO.PAGE) || String(1);
@ -176,6 +191,19 @@ function TxoList(props: Props) {
title={<div className="table__header-text">{__(`Transactions`)}</div>}
titleActions={
<div className="card__actions--inline">
{!isFetchingTransactions && transactionsFile === null && (
<label>{<span className="error__text">{__('Failed to process fetched data.')}</span>}</label>
)}
<div className="txo__export">
<FileExporter
data={transactionsFile}
label={__('Export')}
tooltip={__('Fetch transaction data for export')}
defaultFileName={'transactions-history.csv'}
onFetch={() => fetchTransactions()}
progressMsg={isFetchingTransactions ? __('Fetching data') : ''}
/>
</div>
<Button button="alt" icon={ICONS.REFRESH} label={__('Refresh')} onClick={() => fetchTxoPage()} />
</div>
}
@ -195,9 +223,9 @@ function TxoList(props: Props) {
</>
}
value={type || 'all'}
onChange={e => handleChange({ dkey: TXO.TYPE, value: e.target.value })}
onChange={(e) => handleChange({ dkey: TXO.TYPE, value: e.target.value })}
>
{Object.values(TXO.DROPDOWN_TYPES).map(v => {
{Object.values(TXO.DROPDOWN_TYPES).map((v) => {
const stringV = String(v);
return (
<option key={stringV} value={stringV}>
@ -214,9 +242,9 @@ function TxoList(props: Props) {
name="subtype"
label={__('Payment Type')}
value={subtype || 'all'}
onChange={e => handleChange({ dkey: TXO.SUB_TYPE, value: e.target.value })}
onChange={(e) => handleChange({ dkey: TXO.SUB_TYPE, value: e.target.value })}
>
{Object.values(TXO.DROPDOWN_SUBTYPES).map(v => {
{Object.values(TXO.DROPDOWN_SUBTYPES).map((v) => {
const stringV = String(v);
return (
<option key={stringV} value={stringV}>
@ -234,7 +262,7 @@ function TxoList(props: Props) {
<div className={'txo__radios'}>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'active' })}
onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'active' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === TXO.ACTIVE,
})}
@ -242,7 +270,7 @@ function TxoList(props: Props) {
/>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'spent' })}
onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'spent' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === 'spent',
})}
@ -250,7 +278,7 @@ function TxoList(props: Props) {
/>
<Button
button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'all' })}
onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'all' })}
className={classnames(`button-toggle`, {
'button-toggle--active': active === 'all',
})}

View file

@ -11,6 +11,7 @@ export const ARROW_LEFT = 'ChevronLeft';
export const ARROW_RIGHT = 'ChevronRight';
export const DOWNLOAD = 'Download';
export const PUBLISH = 'UploadCloud';
export const FETCH = 'Fetch';
export const REMOVE = 'X';
export const ADD = 'Plus';
export const SUBTRACT = 'Subtract';

View file

@ -2,3 +2,11 @@
display: flex;
flex-direction: row;
}
.txo__export {
display: none;
@media (min-width: $breakpoint-small) {
display: block;
}
}

View file

@ -6951,9 +6951,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#3ca0c8d20466a695acfd7a9c20f8f580afa02206:
lbry-redux@lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/3ca0c8d20466a695acfd7a9c20f8f580afa02206"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/eb37009a987410a60e9f2ba79708049c9904687c"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"