Merge pull request #1043 from lbryio/export-csv
Feature: export wallet transactions
This commit is contained in:
commit
c6daff53c5
17 changed files with 191 additions and 66 deletions
|
@ -9,7 +9,9 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
* Save app state when closing to tray ([#968](https://github.com/lbryio/lbry-app/issues/968))
|
* 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))
|
* Add Rewards FAQ to LBRY app ([#1041](https://github.com/lbryio/lbry-app/pull/1041))
|
||||||
|
*
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
*
|
*
|
||||||
|
|
|
@ -5,16 +5,16 @@ mixpanel.init('691723e855cabb9d27a7a79002216967');
|
||||||
|
|
||||||
type Analytics = {
|
type Analytics = {
|
||||||
track: (string, ?Object) => void,
|
track: (string, ?Object) => void,
|
||||||
setUser: (Object) => void,
|
setUser: Object => void,
|
||||||
toggle: (boolean, ?boolean) => void
|
toggle: (boolean, ?boolean) => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
let analyticsEnabled: boolean = false;
|
let analyticsEnabled: boolean = false;
|
||||||
|
|
||||||
const analytics: Analytics = {
|
const analytics: Analytics = {
|
||||||
track: (name: string, payload: ?Object): void => {
|
track: (name: string, payload: ?Object): void => {
|
||||||
if(analyticsEnabled) {
|
if (analyticsEnabled) {
|
||||||
if(payload) {
|
if (payload) {
|
||||||
mixpanel.track(name, payload);
|
mixpanel.track(name, payload);
|
||||||
} else {
|
} else {
|
||||||
mixpanel.track(name);
|
mixpanel.track(name);
|
||||||
|
@ -22,21 +22,21 @@ const analytics: Analytics = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setUser: (user: Object): void => {
|
setUser: (user: Object): void => {
|
||||||
if(user.id) {
|
if (user.id) {
|
||||||
mixpanel.identify(user.id);
|
mixpanel.identify(user.id);
|
||||||
}
|
}
|
||||||
if(user.primary_email) {
|
if (user.primary_email) {
|
||||||
mixpanel.people.set({
|
mixpanel.people.set({
|
||||||
"$email": user.primary_email
|
$email: user.primary_email,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggle: (enabled: boolean, logDisabled: ?boolean): void => {
|
toggle: (enabled: boolean, logDisabled: ?boolean): void => {
|
||||||
if(!enabled && logDisabled) {
|
if (!enabled && logDisabled) {
|
||||||
mixpanel.track('DISABLED');
|
mixpanel.track('DISABLED');
|
||||||
}
|
}
|
||||||
analyticsEnabled = enabled;
|
analyticsEnabled = enabled;
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default analytics;
|
export default analytics;
|
||||||
|
|
67
src/renderer/component/file-exporter.js
Normal file
67
src/renderer/component/file-exporter.js
Normal file
|
@ -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;
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import TransactionListItem from './internal/TransactionListItem';
|
import TransactionListItem from './internal/TransactionListItem';
|
||||||
import FormField from 'component/formField';
|
import FormField from 'component/formField';
|
||||||
import Link from 'component/link';
|
import Link from 'component/link';
|
||||||
|
import FileExporter from 'component/file-exporter.js';
|
||||||
import * as icons from 'constants/icons';
|
import * as icons from 'constants/icons';
|
||||||
import * as modals from 'constants/modal_types';
|
import * as modals from 'constants/modal_types';
|
||||||
|
|
||||||
|
@ -43,6 +44,13 @@ class TransactionList extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{Boolean(transactionList.length) && (
|
||||||
|
<FileExporter
|
||||||
|
data={transactionList}
|
||||||
|
title={__('Export Transactions')}
|
||||||
|
label={__('Export')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(transactionList.length || this.state.filter) && (
|
{(transactionList.length || this.state.filter) && (
|
||||||
<span className="sort-section">
|
<span className="sort-section">
|
||||||
{__('Filter')}{' '}
|
{__('Filter')}{' '}
|
||||||
|
|
|
@ -3,3 +3,4 @@ export const LOCAL = 'folder';
|
||||||
export const FILE = 'file';
|
export const FILE = 'file';
|
||||||
export const HISTORY = 'history';
|
export const HISTORY = 'history';
|
||||||
export const HELP_CIRCLE = 'question-circle';
|
export const HELP_CIRCLE = 'question-circle';
|
||||||
|
export const DOWNLOAD = 'download';
|
||||||
|
|
|
@ -2,7 +2,7 @@ export const CONFIRM_FILE_REMOVE = 'confirmFileRemove';
|
||||||
export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon';
|
export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon';
|
||||||
export const FILE_TIMEOUT = 'file_timeout';
|
export const FILE_TIMEOUT = 'file_timeout';
|
||||||
export const DOWNLOADING = 'downloading';
|
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 AUTO_UPDATE_CONFIRM = 'auto_update_confirm';
|
||||||
export const ERROR = 'error';
|
export const ERROR = 'error';
|
||||||
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
|
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
|
||||||
|
|
|
@ -8,7 +8,12 @@ import lbry from 'lbry';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
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 { doUpdateIsNightAsync } from 'redux/actions/settings';
|
||||||
import { doNavigate } from 'redux/actions/navigation';
|
import { doNavigate } from 'redux/actions/navigation';
|
||||||
import { doDownloadLanguages } from 'redux/actions/settings';
|
import { doDownloadLanguages } from 'redux/actions/settings';
|
||||||
|
@ -20,7 +25,7 @@ import analytics from './analytics';
|
||||||
|
|
||||||
const { autoUpdater } = remote.require('electron-updater');
|
const { autoUpdater } = remote.require('electron-updater');
|
||||||
|
|
||||||
autoUpdater.logger = remote.require("electron-log");
|
autoUpdater.logger = remote.require('electron-log');
|
||||||
|
|
||||||
window.addEventListener('contextmenu', event => {
|
window.addEventListener('contextmenu', event => {
|
||||||
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
|
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
|
||||||
|
@ -97,19 +102,19 @@ document.addEventListener('click', event => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
autoUpdater.on("update-downloaded", () => {
|
autoUpdater.on('update-downloaded', () => {
|
||||||
app.store.dispatch(doAutoUpdate());
|
app.store.dispatch(doAutoUpdate());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (["win32", "darwin"].includes(process.platform)) {
|
if (['win32', 'darwin'].includes(process.platform)) {
|
||||||
autoUpdater.on("update-available", () => {
|
autoUpdater.on('update-available', () => {
|
||||||
console.log("Update available");
|
console.log('Update available');
|
||||||
});
|
});
|
||||||
autoUpdater.on("update-not-available", () => {
|
autoUpdater.on('update-not-available', () => {
|
||||||
console.log("Update not available");
|
console.log('Update not available');
|
||||||
});
|
});
|
||||||
autoUpdater.on("update-downloaded", () => {
|
autoUpdater.on('update-downloaded', () => {
|
||||||
console.log("Update downloaded");
|
console.log('Update downloaded');
|
||||||
app.store.dispatch(doAutoUpdate());
|
app.store.dispatch(doAutoUpdate());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
|
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
|
||||||
import ModalAutoUpdateConfirm from "./view";
|
import ModalAutoUpdateConfirm from './view';
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
closeModal: () => dispatch(doCloseModal()),
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Modal } from "modal/modal";
|
import { Modal } from 'modal/modal';
|
||||||
import { Line } from "rc-progress";
|
import { Line } from 'rc-progress';
|
||||||
import Link from "component/link/index";
|
import Link from 'component/link/index';
|
||||||
|
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
class ModalAutoUpdateConfirm extends React.PureComponent {
|
class ModalAutoUpdateConfirm extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
@ -13,11 +13,11 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
type="confirm"
|
type="confirm"
|
||||||
contentLabel={__("Update Downloaded")}
|
contentLabel={__('Update Downloaded')}
|
||||||
confirmButtonLabel={__("Upgrade")}
|
confirmButtonLabel={__('Upgrade')}
|
||||||
abortButtonLabel={__("Not now")}
|
abortButtonLabel={__('Not now')}
|
||||||
onConfirmed={() => {
|
onConfirmed={() => {
|
||||||
ipcRenderer.send("autoUpdateAccepted");
|
ipcRenderer.send('autoUpdateAccepted');
|
||||||
}}
|
}}
|
||||||
onAborted={() => {
|
onAborted={() => {
|
||||||
declineAutoUpdate();
|
declineAutoUpdate();
|
||||||
|
@ -25,12 +25,8 @@ class ModalAutoUpdateConfirm extends React.PureComponent {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-center">{__("LBRY Update Ready")}</h3>
|
<h3 className="text-center">{__('LBRY Update Ready')}</h3>
|
||||||
<p>
|
<p>{__('Your LBRY update is ready. Restart LBRY now to use it!')}</p>
|
||||||
{__(
|
|
||||||
'Your LBRY update is ready. Restart LBRY now to use it!'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="meta text-center">
|
<p className="meta text-center">
|
||||||
{__('Want to know what has changed?')} See the{' '}
|
{__('Want to know what has changed?')} See the{' '}
|
||||||
<Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />.
|
<Link label={__('release notes')} href="https://github.com/lbryio/lbry-app/releases" />.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { connect } from "react-redux";
|
import { connect } from 'react-redux';
|
||||||
import { doCloseModal, doAutoUpdateDeclined } from "redux/actions/app";
|
import { doCloseModal, doAutoUpdateDeclined } from 'redux/actions/app';
|
||||||
import ModalAutoUpdateDownloaded from "./view";
|
import ModalAutoUpdateDownloaded from './view';
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
closeModal: () => dispatch(doCloseModal()),
|
closeModal: () => dispatch(doCloseModal()),
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { Modal } from "modal/modal";
|
import { Modal } from 'modal/modal';
|
||||||
import { Line } from "rc-progress";
|
import { Line } from 'rc-progress';
|
||||||
import Link from "component/link/index";
|
import Link from 'component/link/index';
|
||||||
|
|
||||||
const { ipcRenderer } = require("electron");
|
const { ipcRenderer } = require('electron');
|
||||||
|
|
||||||
class ModalAutoUpdateDownloaded extends React.PureComponent {
|
class ModalAutoUpdateDownloaded extends React.PureComponent {
|
||||||
render() {
|
render() {
|
||||||
|
@ -13,20 +13,20 @@ class ModalAutoUpdateDownloaded extends React.PureComponent {
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
type="confirm"
|
type="confirm"
|
||||||
contentLabel={__("Update Downloaded")}
|
contentLabel={__('Update Downloaded')}
|
||||||
confirmButtonLabel={__("Use it Now")}
|
confirmButtonLabel={__('Use it Now')}
|
||||||
abortButtonLabel={__("Upgrade on Close")}
|
abortButtonLabel={__('Upgrade on Close')}
|
||||||
onConfirmed={() => {
|
onConfirmed={() => {
|
||||||
ipcRenderer.send("autoUpdateAccepted");
|
ipcRenderer.send('autoUpdateAccepted');
|
||||||
}}
|
}}
|
||||||
onAborted={() => {
|
onAborted={() => {
|
||||||
declineAutoUpdate();
|
declineAutoUpdate();
|
||||||
ipcRenderer.send("autoUpdateDeclined");
|
ipcRenderer.send('autoUpdateDeclined');
|
||||||
closeModal();
|
closeModal();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<section>
|
<section>
|
||||||
<h3 className="text-center">{__("LBRY Leveled Up")}</h3>
|
<h3 className="text-center">{__('LBRY Leveled Up')}</h3>
|
||||||
<p>
|
<p>
|
||||||
{__(
|
{__(
|
||||||
'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.'
|
'A new version of LBRY has been released, downloaded, and is ready for you to use pending a restart.'
|
||||||
|
|
|
@ -334,7 +334,7 @@ class SettingsPage extends React.PureComponent {
|
||||||
<FormRow
|
<FormRow
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
disabled={theme === 'dark'}
|
disabled={theme === 'dark'}
|
||||||
onChange={(e) => this.onAutomaticDarkModeChange(e.target.checked)}
|
onChange={e => this.onAutomaticDarkModeChange(e.target.checked)}
|
||||||
checked={automaticDarkModeEnabled}
|
checked={automaticDarkModeEnabled}
|
||||||
label={__('Automatic dark mode (9pm to 8am)')}
|
label={__('Automatic dark mode (9pm to 8am)')}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { doAuthNavigate } from 'redux/actions/navigation';
|
||||||
import { doFetchDaemonSettings } from 'redux/actions/settings';
|
import { doFetchDaemonSettings } from 'redux/actions/settings';
|
||||||
import { doAuthenticate } from 'redux/actions/user';
|
import { doAuthenticate } from 'redux/actions/user';
|
||||||
import { doBalanceSubscribe } from 'redux/actions/wallet';
|
import { doBalanceSubscribe } from 'redux/actions/wallet';
|
||||||
import { doPause } from "redux/actions/media";
|
import { doPause } from 'redux/actions/media';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectCurrentModal,
|
selectCurrentModal,
|
||||||
|
@ -84,7 +84,8 @@ export function doDownloadUpgradeRequested() {
|
||||||
|
|
||||||
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
||||||
|
|
||||||
if (['win32', 'darwin'].includes(process.platform)) { // electron-updater behavior
|
if (['win32', 'darwin'].includes(process.platform)) {
|
||||||
|
// electron-updater behavior
|
||||||
if (autoUpdateDeclined) {
|
if (autoUpdateDeclined) {
|
||||||
// The user declined an update before, so show the "confirm" dialog
|
// The user declined an update before, so show the "confirm" dialog
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -99,7 +100,8 @@ export function doDownloadUpgradeRequested() {
|
||||||
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
|
data: { modal: MODALS.AUTO_UPDATE_DOWNLOADED },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else { // Old behavior for Linux
|
} else {
|
||||||
|
// Old behavior for Linux
|
||||||
dispatch(doDownloadUpgrade());
|
dispatch(doDownloadUpgrade());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -164,7 +166,7 @@ export function doAutoUpdateDeclined() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.AUTO_UPDATE_DECLINED,
|
type: ACTIONS.AUTO_UPDATE_DECLINED,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCancelUpgrade() {
|
export function doCancelUpgrade() {
|
||||||
|
@ -197,7 +199,7 @@ export function doCheckUpgradeAvailable() {
|
||||||
type: ACTIONS.CHECK_UPGRADE_START,
|
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
|
// On Windows and Mac, updates happen silently through
|
||||||
// electron-updater.
|
// electron-updater.
|
||||||
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
|
||||||
|
|
|
@ -74,7 +74,7 @@ export function doUpdateIsNight() {
|
||||||
const startNightMoment = moment('21:00', 'HH:mm');
|
const startNightMoment = moment('21:00', 'HH:mm');
|
||||||
const endNightMoment = moment('8:00', 'HH:mm');
|
const endNightMoment = moment('8:00', 'HH:mm');
|
||||||
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
|
return !(momentNow.isAfter(endNightMoment) && momentNow.isBefore(startNightMoment));
|
||||||
})()
|
})(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ reducers[ACTIONS.AUTO_UPDATE_DECLINED] = state => {
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
autoUpdateDeclined: true,
|
autoUpdateDeclined: true,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) =>
|
reducers[ACTIONS.UPGRADE_DOWNLOAD_COMPLETED] = (state, action) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
|
|
|
@ -56,9 +56,15 @@ export const selectUpgradeDownloadPath = createSelector(selectState, state => st
|
||||||
|
|
||||||
export const selectUpgradeDownloadItem = createSelector(selectState, state => state.downloadItem);
|
export const selectUpgradeDownloadItem = createSelector(selectState, state => state.downloadItem);
|
||||||
|
|
||||||
export const selectAutoUpdateDownloaded = createSelector(selectState, state => state.autoUpdateDownloaded);
|
export const selectAutoUpdateDownloaded = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.autoUpdateDownloaded
|
||||||
|
);
|
||||||
|
|
||||||
export const selectAutoUpdateDeclined = createSelector(selectState, state => state.autoUpdateDeclined);
|
export const selectAutoUpdateDeclined = createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state.autoUpdateDeclined
|
||||||
|
);
|
||||||
|
|
||||||
export const selectModalsAllowed = createSelector(selectState, state => state.modalsAllowed);
|
export const selectModalsAllowed = createSelector(selectState, state => state.modalsAllowed);
|
||||||
|
|
||||||
|
|
38
src/renderer/util/parseData.js
Normal file
38
src/renderer/util/parseData.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Beautify JSON
|
||||||
|
const parseJson = data => JSON.stringify(data, null, '\t');
|
||||||
|
|
||||||
|
// No need for an external module:
|
||||||
|
// https://gist.github.com/btzr-io/55c3450ea3d709fc57540e762899fb85
|
||||||
|
const parseCsv = data => {
|
||||||
|
// Get items for header
|
||||||
|
const getHeaders = temp =>
|
||||||
|
Object.entries(temp)
|
||||||
|
.map(([key]) => key)
|
||||||
|
.join(',');
|
||||||
|
// Get rows content
|
||||||
|
const getData = list =>
|
||||||
|
list
|
||||||
|
.map(item => {
|
||||||
|
const row = Object.entries(item)
|
||||||
|
.map(([key, value]) => value)
|
||||||
|
.join(',');
|
||||||
|
return row;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
// Return CSV string
|
||||||
|
return `${getHeaders(data[0])} \n ${getData(data)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseData = (data, format) => {
|
||||||
|
// Check for validation
|
||||||
|
const valid = data && data[0] && format;
|
||||||
|
// Pick a format
|
||||||
|
const formats = {
|
||||||
|
csv: list => parseCsv(list),
|
||||||
|
json: list => parseJson(list),
|
||||||
|
};
|
||||||
|
// Return parsed data: JSON || CSV
|
||||||
|
return valid && formats[format] ? formats[format](data) : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseData;
|
Loading…
Reference in a new issue