diff --git a/package.json b/package.json index 31f62ba01..3296c29e9 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..221343275 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.onSetModeToLarge && + + + } + {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..e2c882220 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,29 @@ 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]) + .filter(item => typeof item === 'string'); // CSV importer can leave undefineds + 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 +189,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 +227,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..36ec7392b --- /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, .txt'); // 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 };