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)) - 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)) - 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)) - 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 ### Changed

View file

@ -142,7 +142,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "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", "lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1859,6 +1859,11 @@
"Learn more and sign petition": "Learn more and sign petition", "Learn more and sign petition": "Learn more and sign petition",
"Publishing...": "Publishing...", "Publishing...": "Publishing...",
"Collection": "Collection", "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%", "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", "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", "Select Replay": "Select Replay",

View file

@ -3,93 +3,87 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import parseData from 'util/parse-data'; import Spinner from 'component/spinner';
import { remote } from 'electron';
import path from 'path';
// @if TARGET='app'
import fs from 'fs';
// @endif
type Props = { type Props = {
data: Array<any>, data: any,
title: string,
label: string, label: string,
defaultPath?: string, tooltip?: string,
filters: Array<string>, defaultFileName?: string,
onFileCreated?: string => void, filters?: Array<string>,
disabled: boolean, onFetch?: () => void,
progressMsg?: string,
disabled?: boolean,
}; };
class FileExporter extends React.PureComponent<Props> { class FileExporter extends React.PureComponent<Props> {
static defaultProps = {
filters: [],
};
constructor() { constructor() {
super(); super();
(this: any).handleButtonClick = this.handleButtonClick.bind(this); (this: any).handleDownload = this.handleDownload.bind(this);
} }
handleFileCreation(filename: string, data: any) { handleDownload() {
const { onFileCreated } = this.props; const { data, defaultFileName } = this.props;
// @if TARGET='app'
fs.writeFile(filename, data, err => {
if (err) throw err;
// Do something after creation
if (onFileCreated) { const element = document.createElement('a');
onFileCreated(filename); const file = new Blob([data], { type: 'text/plain' });
} element.href = URL.createObjectURL(file);
}); element.download = defaultFileName || 'file.txt';
// @endif // $FlowFixMe
} document.body.appendChild(element);
element.click();
handleButtonClick() { // $FlowFixMe
const { title, data, defaultPath, filters } = this.props; document.body.removeChild(element);
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);
}
});
} }
render() { render() {
const { label, disabled } = this.props; 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 ( return (
<Button <Button
button="primary" button="primary"
disabled={disabled} disabled={disabled}
icon={ICONS.DOWNLOAD} icon={ICONS.DOWNLOAD}
label={label || __('Export')} label={label || __('Export')}
onClick={this.handleButtonClick} aria-label={tooltip}
onClick={this.handleDownload}
/> />
); );
} }
}
} }
export default FileExporter; export default FileExporter;

View file

@ -183,6 +183,13 @@ export const icons = {
/> />
</g> </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( [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" /> <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 { doOpenModal } from 'redux/actions/app';
import { import {
selectIsFetchingTxos, selectIsFetchingTxos,
selectIsFetchingTransactions,
selectFetchingTxosError, selectFetchingTxosError,
selectTransactionsFile,
selectTxoPage, selectTxoPage,
selectTxoPageNumber, selectTxoPageNumber,
selectTxoItemCount, selectTxoItemCount,
doFetchTxoPage, doFetchTxoPage,
doFetchTransactions,
doUpdateTxoPageParams, doUpdateTxoPageParams,
} from 'lbry-redux'; } from 'lbry-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import TxoList from './view'; import TxoList from './view';
const select = state => ({ const select = (state) => ({
txoFetchError: selectFetchingTxosError(state), txoFetchError: selectFetchingTxosError(state),
txoPage: selectTxoPage(state), txoPage: selectTxoPage(state),
txoPageNumber: selectTxoPageNumber(state), txoPageNumber: selectTxoPageNumber(state),
txoItemCount: selectTxoItemCount(state), txoItemCount: selectTxoItemCount(state),
loading: selectIsFetchingTxos(state), loading: selectIsFetchingTxos(state),
isFetchingTransactions: selectIsFetchingTransactions(state),
transactionsFile: selectTransactionsFile(state),
}); });
const perform = dispatch => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
fetchTxoPage: () => dispatch(doFetchTxoPage()), fetchTxoPage: () => dispatch(doFetchTxoPage()),
updateTxoPageParams: params => dispatch(doUpdateTxoPageParams(params)), fetchTransactions: () => dispatch(doFetchTransactions()),
updateTxoPageParams: (params) => dispatch(doUpdateTxoPageParams(params)),
}); });
export default withRouter(connect(select, perform)(TxoList)); 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 { toCapitalCase } from 'util/string';
import classnames from 'classnames'; import classnames from 'classnames';
import HelpLink from 'component/common/help-link'; import HelpLink from 'component/common/help-link';
import FileExporter from 'component/common/file-exporter';
type Props = { type Props = {
search: string, search: string,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: (string) => void, replace: (string) => void },
txoPage: Array<Transaction>, txoPage: Array<Transaction>,
txoPageNumber: string, txoPageNumber: string,
txoItemCount: number, txoItemCount: number,
fetchTxoPage: () => void, fetchTxoPage: () => void,
updateTxoPageParams: any => void, fetchTransactions: () => void,
isFetchingTransactions: boolean,
transactionsFile: string,
updateTxoPageParams: (any) => void,
toast: (string, boolean) => void,
}; };
type Delta = { type Delta = {
@ -28,7 +33,17 @@ type Delta = {
}; };
function TxoList(props: Props) { 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 urlParams = new URLSearchParams(search);
const page = urlParams.get(TXO.PAGE) || String(1); const page = urlParams.get(TXO.PAGE) || String(1);
@ -176,6 +191,19 @@ function TxoList(props: Props) {
title={<div className="table__header-text">{__(`Transactions`)}</div>} title={<div className="table__header-text">{__(`Transactions`)}</div>}
titleActions={ titleActions={
<div className="card__actions--inline"> <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()} /> <Button button="alt" icon={ICONS.REFRESH} label={__('Refresh')} onClick={() => fetchTxoPage()} />
</div> </div>
} }
@ -195,9 +223,9 @@ function TxoList(props: Props) {
</> </>
} }
value={type || 'all'} 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); const stringV = String(v);
return ( return (
<option key={stringV} value={stringV}> <option key={stringV} value={stringV}>
@ -214,9 +242,9 @@ function TxoList(props: Props) {
name="subtype" name="subtype"
label={__('Payment Type')} label={__('Payment Type')}
value={subtype || 'all'} 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); const stringV = String(v);
return ( return (
<option key={stringV} value={stringV}> <option key={stringV} value={stringV}>
@ -234,7 +262,7 @@ function TxoList(props: Props) {
<div className={'txo__radios'}> <div className={'txo__radios'}>
<Button <Button
button="alt" button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'active' })} onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'active' })}
className={classnames(`button-toggle`, { className={classnames(`button-toggle`, {
'button-toggle--active': active === TXO.ACTIVE, 'button-toggle--active': active === TXO.ACTIVE,
})} })}
@ -242,7 +270,7 @@ function TxoList(props: Props) {
/> />
<Button <Button
button="alt" button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'spent' })} onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'spent' })}
className={classnames(`button-toggle`, { className={classnames(`button-toggle`, {
'button-toggle--active': active === 'spent', 'button-toggle--active': active === 'spent',
})} })}
@ -250,7 +278,7 @@ function TxoList(props: Props) {
/> />
<Button <Button
button="alt" button="alt"
onClick={e => handleChange({ dkey: TXO.ACTIVE, value: 'all' })} onClick={(e) => handleChange({ dkey: TXO.ACTIVE, value: 'all' })}
className={classnames(`button-toggle`, { className={classnames(`button-toggle`, {
'button-toggle--active': active === 'all', 'button-toggle--active': active === 'all',
})} })}

View file

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

View file

@ -2,3 +2,11 @@
display: flex; display: flex;
flex-direction: row; 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" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#3ca0c8d20466a695acfd7a9c20f8f580afa02206: lbry-redux@lbryio/lbry-redux#eb37009a987410a60e9f2ba79708049c9904687c:
version "0.0.1" 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: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"