From aa253d7de0e3750775425103155d38d56aba17bf Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 19 Dec 2018 10:54:37 -0500 Subject: [PATCH 1/4] Implement list import and export from files --- package.json | 1 + src/components/monitor/monitor.jsx | 59 ++++++++++++++++++++---------- src/containers/monitor.jsx | 45 +++++++++++++++++++++-- src/lib/download-text.js | 13 +++++++ src/lib/import-csv.js | 23 ++++++++++++ src/lib/variable-utils.js | 1 + 6 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 src/lib/download-text.js create mode 100644 src/lib/import-csv.js diff --git a/package.json b/package.json index c13702732..b2c04e37f 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "lodash.throttle": "4.0.1", "minilog": "3.1.0", "mkdirp": "^0.5.1", + "papaparse": "4.6.2", "postcss-import": "^12.0.0", "postcss-loader": "^3.0.0", "postcss-simple-vars": "^5.0.1", diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index 7e6156ff0..be2dabe91 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -54,35 +54,52 @@ const MonitorComponent = props => ( })} - {props.mode === 'list' ? null : ReactDOM.createPortal(( + {ReactDOM.createPortal(( // Use a portal to render the context menu outside the flow to avoid // positioning conflicts between the monitors `transform: scale` and // the context menus `position: fixed`. For more details, see // http://meyerweb.com/eric/thoughts/2011/09/12/un-fixing-fixed-elements-with-css-transforms/ - - - - - - - {props.onSetModeToSlider ? ( + {props.onSetModeToDefault && + + + } + {props.onSetModeToDefault && + + + } + {props.onSetModeToSlider && - - ) : null} + } + {props.onImport && + + + } + {props.onExport && + + + } ), document.body)} @@ -100,9 +117,11 @@ MonitorComponent.propTypes = { label: PropTypes.string.isRequired, mode: PropTypes.oneOf(monitorModes), onDragEnd: PropTypes.func.isRequired, + onExport: PropTypes.func, + onImport: PropTypes.func, onNextMode: PropTypes.func.isRequired, - onSetModeToDefault: PropTypes.func.isRequired, - onSetModeToLarge: PropTypes.func.isRequired, + onSetModeToDefault: PropTypes.func, + onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func }; diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx index 0a02e8268..804ae4858 100644 --- a/src/containers/monitor.jsx +++ b/src/containers/monitor.jsx @@ -1,10 +1,14 @@ import bindAll from 'lodash.bindall'; import React from 'react'; import PropTypes from 'prop-types'; +import {injectIntl, intlShape, defineMessages} from 'react-intl'; import monitorAdapter from '../lib/monitor-adapter.js'; import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx'; import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout'; +import {getVariable, setVariableValue} from '../lib/variable-utils'; +import importCSV from '../lib/import-csv'; +import downloadText from '../lib/download-text'; import {connect} from 'react-redux'; import {Map} from 'immutable'; @@ -21,6 +25,14 @@ const availableModes = opcode => ( }) ); +const messages = defineMessages({ + columnPrompt: { + defaultMessage: 'Which column should be used (1-{numberOfColumns})?', + description: 'Prompt for which column should be used', + id: 'gui.monitors.importListColumnPrompt' + } +}); + class Monitor extends React.Component { constructor (props) { super(props); @@ -30,6 +42,8 @@ class Monitor extends React.Component { 'handleSetModeToDefault', 'handleSetModeToLarge', 'handleSetModeToSlider', + 'handleImport', + 'handleExport', 'setElement' ]); } @@ -124,9 +138,28 @@ class Monitor extends React.Component { setElement (monitorElt) { this.element = monitorElt; } + handleImport () { + importCSV().then(rows => { + const numberOfColumns = rows[0].length; + let columnNumber = 1; + if (numberOfColumns > 1) { + const msg = this.props.intl.formatMessage(messages.columnPrompt, {numberOfColumns}); + columnNumber = parseInt(prompt(msg), 10); // eslint-disable-line no-alert + } + const newListValue = rows.map(row => row[columnNumber - 1]); + const {vm, targetId, id: variableId} = this.props; + setVariableValue(vm, targetId, variableId, newListValue); + }); + } + handleExport () { + const {vm, targetId, id: variableId} = this.props; + const variable = getVariable(vm, targetId, variableId); + downloadText(`${variable.name}.txt`, variable.value.join('\r\n')); + } render () { const monitorProps = monitorAdapter(this.props); const showSliderOption = availableModes(this.props.opcode).indexOf('slider') !== -1; + const isList = this.props.mode === 'list'; return ( ); @@ -153,6 +188,7 @@ Monitor.propTypes = { draggable: PropTypes.bool, height: PropTypes.number, id: PropTypes.string.isRequired, + intl: intlShape, max: PropTypes.number, min: PropTypes.number, mode: PropTypes.oneOf(['default', 'slider', 'large', 'list']), @@ -190,7 +226,8 @@ const mapDispatchToProps = dispatch => ({ resizeMonitorRect: (id, newWidth, newHeight) => dispatch(resizeMonitorRect(id, newWidth, newHeight)), removeMonitorRect: id => dispatch(removeMonitorRect(id)) }); -export default connect( + +export default injectIntl(connect( mapStateToProps, mapDispatchToProps -)(Monitor); +)(Monitor)); diff --git a/src/lib/download-text.js b/src/lib/download-text.js new file mode 100644 index 000000000..2c0bc91a5 --- /dev/null +++ b/src/lib/download-text.js @@ -0,0 +1,13 @@ +export default (filename, text) => { + const pom = document.createElement('a'); + pom.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`); + pom.setAttribute('download', filename); + + if (document.createEvent) { + const event = document.createEvent('MouseEvents'); + event.initEvent('click', true, true); + pom.dispatchEvent(event); + } else { + pom.click(); + } +}; diff --git a/src/lib/import-csv.js b/src/lib/import-csv.js new file mode 100644 index 000000000..88c3c4490 --- /dev/null +++ b/src/lib/import-csv.js @@ -0,0 +1,23 @@ +import Papa from 'papaparse'; + +export default () => new Promise((resolve, reject) => { + const fileInput = document.createElement('input'); + fileInput.setAttribute('type', 'file'); + fileInput.setAttribute('accept', '.csv, .tsv'); // parser auto-detects delimiter + fileInput.onchange = e => { + const file = e.target.files[0]; + Papa.parse(file, { + header: false, + complete: results => { + document.body.removeChild(fileInput); + resolve(results.data); + }, + error: err => { + document.body.removeChild(fileInput); + reject(err); + } + }); + }; + document.body.appendChild(fileInput); + fileInput.click(); +}); diff --git a/src/lib/variable-utils.js b/src/lib/variable-utils.js index 12bcf4704..10df7df25 100644 --- a/src/lib/variable-utils.js +++ b/src/lib/variable-utils.js @@ -19,6 +19,7 @@ const setVariableValue = (vm, targetId, variableId, value) => { }; export { + getVariable, getVariableValue, setVariableValue }; From 5ed340c7893d9a185b79e8b546bdcb17456ed16c Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Wed, 19 Dec 2018 13:51:52 -0500 Subject: [PATCH 2/4] Fix typo --- src/components/monitor/monitor.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/monitor/monitor.jsx b/src/components/monitor/monitor.jsx index be2dabe91..221343275 100644 --- a/src/components/monitor/monitor.jsx +++ b/src/components/monitor/monitor.jsx @@ -68,7 +68,7 @@ const MonitorComponent = props => ( id="gui.monitor.contextMenu.default" /> } - {props.onSetModeToDefault && + {props.onSetModeToLarge && Date: Thu, 20 Dec 2018 08:43:07 -0500 Subject: [PATCH 3/4] Add txt import so file can be imported/exported from symmetrically --- src/lib/import-csv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/import-csv.js b/src/lib/import-csv.js index 88c3c4490..36ec7392b 100644 --- a/src/lib/import-csv.js +++ b/src/lib/import-csv.js @@ -3,7 +3,7 @@ import Papa from 'papaparse'; export default () => new Promise((resolve, reject) => { const fileInput = document.createElement('input'); fileInput.setAttribute('type', 'file'); - fileInput.setAttribute('accept', '.csv, .tsv'); // parser auto-detects delimiter + fileInput.setAttribute('accept', '.csv, .tsv, .txt'); // parser auto-detects delimiter fileInput.onchange = e => { const file = e.target.files[0]; Papa.parse(file, { From 80df9d6f1316ae8fa340cdf205e399b746e3b1ef Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 20 Dec 2018 09:23:33 -0500 Subject: [PATCH 4/4] Fix issue with undefineds in the CSV --- src/containers/monitor.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/monitor.jsx b/src/containers/monitor.jsx index 804ae4858..e2c882220 100644 --- a/src/containers/monitor.jsx +++ b/src/containers/monitor.jsx @@ -146,7 +146,8 @@ class Monitor extends React.Component { const msg = this.props.intl.formatMessage(messages.columnPrompt, {numberOfColumns}); columnNumber = parseInt(prompt(msg), 10); // eslint-disable-line no-alert } - const newListValue = rows.map(row => row[columnNumber - 1]); + const newListValue = rows.map(row => row[columnNumber - 1]) + .filter(item => typeof item === 'string'); // CSV importer can leave undefineds const {vm, targetId, id: variableId} = this.props; setVariableValue(vm, targetId, variableId, newListValue); });