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))
|
||||
- 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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -3,93 +3,87 @@ 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;
|
||||
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')}
|
||||
onClick={this.handleButtonClick}
|
||||
aria-label={tooltip}
|
||||
onClick={this.handleDownload}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileExporter;
|
||||
|
|
|
@ -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" />
|
||||
),
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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',
|
||||
})}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -2,3 +2,11 @@
|
|||
display: flex;
|
||||
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"
|
||||
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"
|
||||
|
|
Loading…
Reference in a new issue