Skip to content

Commit

Permalink
Merge pull request #1043 from lbryio/export-csv
Browse files Browse the repository at this point in the history
Feature: export  wallet transactions
  • Loading branch information
liamcardenas committed Mar 2, 2018
2 parents 7490390 + df1f67c commit c6daff5
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 66 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,7 +9,9 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
* Save app state when closing to tray ([#968](https://github.com/lbryio/lbry-app/issues/968))
* Added ability to export wallet transactions to JSON and CSV format ([#976](https://github.com/lbryio/lbry-app/pull/976))
* Add Rewards FAQ to LBRY app ([#1041](https://github.com/lbryio/lbry-app/pull/1041))
*

### Changed
*
Expand Down
22 changes: 11 additions & 11 deletions src/renderer/analytics.js
Expand Up @@ -5,38 +5,38 @@ mixpanel.init('691723e855cabb9d27a7a79002216967');

type Analytics = {
track: (string, ?Object) => void,
setUser: (Object) => void,
toggle: (boolean, ?boolean) => void
}
setUser: Object => void,
toggle: (boolean, ?boolean) => void,
};

let analyticsEnabled: boolean = false;

const analytics: Analytics = {
track: (name: string, payload: ?Object): void => {
if(analyticsEnabled) {
if(payload) {
if (analyticsEnabled) {
if (payload) {
mixpanel.track(name, payload);
} else {
mixpanel.track(name);
}
}
},
setUser: (user: Object): void => {
if(user.id) {
if (user.id) {
mixpanel.identify(user.id);
}
if(user.primary_email) {
if (user.primary_email) {
mixpanel.people.set({
"$email": user.primary_email
$email: user.primary_email,
});
}
},
toggle: (enabled: boolean, logDisabled: ?boolean): void => {
if(!enabled && logDisabled) {
if (!enabled && logDisabled) {
mixpanel.track('DISABLED');
}
analyticsEnabled = enabled;
}
}
},
};

export default analytics;
67 changes: 67 additions & 0 deletions src/renderer/component/file-exporter.js
@@ -0,0 +1,67 @@
import fs from 'fs';
import path from 'path';
import React from 'react';
import PropTypes from 'prop-types';
import Link from 'component/link';
import parseData from 'util/parseData';
import * as icons from 'constants/icons';
const { remote } = require('electron');

class FileExporter extends React.PureComponent {
static propTypes = {
data: PropTypes.array,
title: PropTypes.string,
label: PropTypes.string,
defaultPath: PropTypes.string,
onFileCreated: PropTypes.func,
};

constructor(props) {
super(props);
}

handleFileCreation(filename, data) {
const { onFileCreated } = this.props;
fs.writeFile(filename, data, err => {
if (err) throw err;
// Do something after creation
onFileCreated && onFileCreated(filename);
});
}

handleButtonClick() {
const { title, defaultPath, data } = this.props;

const options = {
title,
defaultPath,
filters: [{ name: 'JSON', extensions: ['json'] }, { name: 'CSV', extensions: ['csv'] }],
};

remote.dialog.showSaveDialog(options, filename => {
// User hit cancel so do nothing:
if (!filename) return;
// Get extension and remove initial dot
const format = path.extname(filename).replace(/\./g, '');
// Parse data to string with the chosen format
const parsed = parseData(data, format);
// Write file
parsed && this.handleFileCreation(filename, parsed);
});
}

render() {
const { title, label } = this.props;
return (
<Link
button="primary"
icon={icons.DOWNLOAD}
title={title || __('Export')}
label={label || __('Export')}
onClick={() => this.handleButtonClick()}
/>
);
}
}

export default FileExporter;
8 changes: 8 additions & 0 deletions src/renderer/component/transactionList/view.jsx
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import TransactionListItem from './internal/TransactionListItem';
import FormField from 'component/formField';
import Link from 'component/link';
import FileExporter from 'component/file-exporter.js';
import * as icons from 'constants/icons';
import * as modals from 'constants/modal_types';

Expand Down Expand Up @@ -43,6 +44,13 @@ class TransactionList extends React.PureComponent {

return (
<div>
{Boolean(transactionList.length) && (
<FileExporter
data={transactionList}
title={__('Export Transactions')}
label={__('Export')}
/>
)}
{(transactionList.length || this.state.filter) && (
<span className="sort-section">
{__('Filter')}{' '}
Expand Down
1 change: 1 addition & 0 deletions src/renderer/constants/icons.js
Expand Up @@ -3,3 +3,4 @@ export const LOCAL = 'folder';
export const FILE = 'file';
export const HISTORY = 'history';
export const HELP_CIRCLE = 'question-circle';
export const DOWNLOAD = 'download';
2 changes: 1 addition & 1 deletion src/renderer/constants/modal_types.js
Expand Up @@ -2,7 +2,7 @@ export const CONFIRM_FILE_REMOVE = 'confirmFileRemove';
export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon';
export const FILE_TIMEOUT = 'file_timeout';
export const DOWNLOADING = 'downloading';
export const AUTO_UPDATE_DOWNLOADED = "auto_update_downloaded";
export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded';
export const AUTO_UPDATE_CONFIRM = 'auto_update_confirm';
export const ERROR = 'error';
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
Expand Down
25 changes: 15 additions & 10 deletions src/renderer/index.js
Expand Up @@ -8,7 +8,12 @@ import lbry from 'lbry';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doShowSnackBar, doAutoUpdate } from 'redux/actions/app';
import {
doConditionalAuthNavigate,
doDaemonReady,
doShowSnackBar,
doAutoUpdate,
} from 'redux/actions/app';
import { doUpdateIsNightAsync } from 'redux/actions/settings';
import { doNavigate } from 'redux/actions/navigation';
import { doDownloadLanguages } from 'redux/actions/settings';
Expand All @@ -20,7 +25,7 @@ import analytics from './analytics';

const { autoUpdater } = remote.require('electron-updater');

autoUpdater.logger = remote.require("electron-log");
autoUpdater.logger = remote.require('electron-log');

window.addEventListener('contextmenu', event => {
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
Expand Down Expand Up @@ -97,19 +102,19 @@ document.addEventListener('click', event => {
});

const init = () => {
autoUpdater.on("update-downloaded", () => {
autoUpdater.on('update-downloaded', () => {
app.store.dispatch(doAutoUpdate());
});

if (["win32", "darwin"].includes(process.platform)) {
autoUpdater.on("update-available", () => {
console.log("Update available");
if (['win32', 'darwin'].includes(process.platform)) {
autoUpdater.on('update-available', () => {
console.log('Update available');
});
autoUpdater.on("update-not-available", () => {
console.log("Update not available");
autoUpdater.on('update-not-available', () => {
console.log('Update not available');
});
autoUpdater.on("update-downloaded", () => {
console.log("Update downloaded");
autoUpdater.on('update-downloaded', () => {
console.log('Update downloaded');
app.store.dispatch(doAutoUpdate());
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/modal/modalAutoUpdateConfirm/index.js
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
import ModalAutoUpdateConfirm from "./view";
import React from 'react';
import { connect } from 'react-redux';
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
import ModalAutoUpdateConfirm from './view';

const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
Expand Down
26 changes: 11 additions & 15 deletions src/renderer/modal/modalAutoUpdateConfirm/view.jsx
@@ -1,9 +1,9 @@
import React from "react";
import { Modal } from "modal/modal";
import { Line } from "rc-progress";
import Link from "component/link/index";
import React from 'react';
import { Modal } from 'modal/modal';
import { Line } from 'rc-progress';
import Link from 'component/link/index';

const { ipcRenderer } = require("electron");
const { ipcRenderer } = require('electron');

class ModalAutoUpdateConfirm extends React.PureComponent {
render() {
Expand All @@ -13,24 +13,20 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Update Downloaded")}
confirmButtonLabel={__("Upgrade")}
abortButtonLabel={__("Not now")}
contentLabel={__('Update Downloaded')}
confirmButtonLabel={__('Upgrade')}
abortButtonLabel={__('Not now')}
onConfirmed={() => {
ipcRenderer.send("autoUpdateAccepted");
ipcRenderer.send('autoUpdateAccepted');
}}
onAborted={() => {
declineAutoUpdate();
closeModal();
}}
>
<section>
<h3 className="text-center">{__("LBRY Update Ready")}</h3>
<p>
{__(
'Your LBRY update is ready. Restart LBRY now to use it!'
)}
</p>
<h3 className="text-center">{__('LBRY Update Ready')}</h3>
<p>{__('Your LBRY update is ready. Restart LBRY now to use it!')}</p>
<p className="meta text-center">
{__('Want to know what has changed?')} See the{' '}
<Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />.
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/modal/modalAutoUpdateDownloaded/index.js
@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
import ModalAutoUpdateDownloaded from "./view";
import React from 'react';
import { connect } from 'react-redux';
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
import ModalAutoUpdateDownloaded from './view';

const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
Expand Down
22 changes: 11 additions & 11 deletions src/renderer/modal/modalAutoUpdateDownloaded/view.jsx
@@ -1,9 +1,9 @@
import React from "react";
import { Modal } from "modal/modal";
import { Line } from "rc-progress";
import Link from "component/link/index";
import React from 'react';
import { Modal } from 'modal/modal';
import { Line } from 'rc-progress';
import Link from 'component/link/index';

const { ipcRenderer } = require("electron");
const { ipcRenderer } = require('electron');

class ModalAutoUpdateDownloaded extends React.PureComponent {
render() {
Expand All @@ -13,20 +13,20 @@ class ModalAutoUpdateDownloaded extends React.PureComponent {
<Modal
isOpen={true}
type="confirm"
contentLabel={__("Update Downloaded")}
confirmButtonLabel={__("Use it Now")}
abortButtonLabel={__("Upgrade on Close")}
contentLabel={__('Update Downloaded')}
confirmButtonLabel={__('Use it Now')}
abortButtonLabel={__('Upgrade on Close')}
onConfirmed={() => {
ipcRenderer.send("autoUpdateAccepted");
ipcRenderer.send('autoUpdateAccepted');
}}
onAborted={() => {
declineAutoUpdate();
ipcRenderer.send("autoUpdateDeclined");
ipcRenderer.send('autoUpdateDeclined');
closeModal();
}}
>
<section>
<h3 className="text-center">{__("LBRY Leveled Up")}</h3>
<h3 className="text-center">{__('LBRY Leveled Up')}</h3>
<p>
{__(
'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.'
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/page/settings/view.jsx
Expand Up @@ -334,7 +334,7 @@ class SettingsPage extends React.PureComponent {
<FormRow
type="checkbox"
disabled={theme === 'dark'}
onChange={(e) => this.onAutomaticDarkModeChange(e.target.checked)}
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
checked={automaticDarkModeEnabled}
label={__('Automatic dark mode (9pm to 8am)')}
/>
Expand Down
12 changes: 7 additions & 5 deletions src/renderer/redux/actions/app.js
Expand Up @@ -10,7 +10,7 @@ import { doAuthNavigate } from 'redux/actions/navigation';
import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthenticate } from 'redux/actions/user';
import { doBalanceSubscribe } from 'redux/actions/wallet';
import { doPause } from "redux/actions/media";
import { doPause } from 'redux/actions/media';

import {
selectCurrentModal,
Expand Down Expand Up @@ -84,7 +84,8 @@ export function doDownloadUpgradeRequested() {

const autoUpdateDeclined = selectAutoUpdateDeclined(state);

if (['win32', 'darwin'].includes(process.platform)) { // electron-updater behavior
if (['win32', 'darwin'].includes(process.platform)) {
// electron-updater behavior
if (autoUpdateDeclined) {
// The user declined an update before, so show the "confirm" dialog
dispatch({
Expand All @@ -99,7 +100,8 @@ export function doDownloadUpgradeRequested() {
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
});
}
} else { // Old behavior for Linux
} else {
// Old behavior for Linux
dispatch(doDownloadUpgrade());
}
};
Expand Down Expand Up @@ -164,7 +166,7 @@ export function doAutoUpdateDeclined() {
dispatch({
type: ACTIONS.AUTO_UPDATE_DECLINED,
});
}
};
}

export function doCancelUpgrade() {
Expand Down Expand Up @@ -197,7 +199,7 @@ export function doCheckUpgradeAvailable() {
type: ACTIONS.CHECK_UPGRADE_START,
});

if (["win32", "darwin"].includes(process.platform)) {
if (['win32', 'darwin'].includes(process.platform)) {
// On Windows and Mac, updates happen silently through
// electron-updater.
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/redux/actions/settings.js
Expand Up @@ -74,7 +74,7 @@ export function doUpdateIsNight() {
const startNightMoment = moment('21:00', 'HH:mm');
const endNightMoment = moment('8:00', 'HH:mm');
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
})()
})(),
},
};
}
Expand Down

0 comments on commit c6daff5

Please sign in to comment.