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:
parent
b5cc0bb42d
commit
b0193202d1
10 changed files with 139 additions and 89 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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" />
|
||||||
),
|
),
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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',
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue