Merge pull request #4146 from paulkaplan/list-import

Implement list import and export from files
This commit is contained in:
Paul Kaplan authored and GitHub committed 2018-12-20 13:35:54 -05:00
commit 57e841b19b
6 files changed
+119 -24

No files matched your search

+1
View File
@@ -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",
+39 -20
View File
@@ -54,35 +54,52 @@ const MonitorComponent = props => (
})}
</Box>
</Draggable>
{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/
<ContextMenu id={`monitor-${props.label}`}>
<MenuItem onClick={props.onSetModeToDefault}>
<FormattedMessage
defaultMessage="normal readout"
description="Menu item to switch to the default monitor"
id="gui.monitor.contextMenu.default"
/>
</MenuItem>
<MenuItem onClick={props.onSetModeToLarge}>
<FormattedMessage
defaultMessage="large readout"
description="Menu item to switch to the large monitor"
id="gui.monitor.contextMenu.large"
/>
</MenuItem>
{props.onSetModeToSlider ? (
{props.onSetModeToDefault &&
<MenuItem onClick={props.onSetModeToDefault}>
<FormattedMessage
defaultMessage="normal readout"
description="Menu item to switch to the default monitor"
id="gui.monitor.contextMenu.default"
/>
</MenuItem>}
{props.onSetModeToLarge &&
<MenuItem onClick={props.onSetModeToLarge}>
<FormattedMessage
defaultMessage="large readout"
description="Menu item to switch to the large monitor"
id="gui.monitor.contextMenu.large"
/>
</MenuItem>}
{props.onSetModeToSlider &&
<MenuItem onClick={props.onSetModeToSlider}>
<FormattedMessage
defaultMessage="slider"
description="Menu item to switch to the slider monitor"
id="gui.monitor.contextMenu.slider"
/>
</MenuItem>
) : null}
</MenuItem>}
{props.onImport &&
<MenuItem onClick={props.onImport}>
<FormattedMessage
defaultMessage="import"
description="Menu item to import into list monitors"
id="gui.monitor.contextMenu.import"
/>
</MenuItem>}
{props.onExport &&
<MenuItem onClick={props.onExport}>
<FormattedMessage
defaultMessage="export"
description="Menu item to export from list monitors"
id="gui.monitor.contextMenu.export"
/>
</MenuItem>}
</ContextMenu>
), document.body)}
</ContextMenuTrigger>
@@ -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
};
+42 -4
View File
@@ -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 (
<MonitorComponent
componentRef={this.setElement}
@@ -139,9 +173,11 @@ class Monitor extends React.Component {
targetId={this.props.targetId}
width={this.props.width}
onDragEnd={this.handleDragEnd}
onExport={isList ? this.handleExport : null}
onImport={isList ? this.handleImport : null}
onNextMode={this.handleNextMode}
onSetModeToDefault={this.handleSetModeToDefault}
onSetModeToLarge={this.handleSetModeToLarge}
onSetModeToDefault={isList ? null : this.handleSetModeToDefault}
onSetModeToLarge={isList ? null : this.handleSetModeToLarge}
onSetModeToSlider={showSliderOption ? this.handleSetModeToSlider : null}
/>
);
@@ -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));
+13
View File
@@ -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();
}
};
+23
View File
@@ -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();
});
+1
View File
@@ -19,6 +19,7 @@ const setVariableValue = (vm, targetId, variableId, value) => {
};
export {
getVariable,
getVariableValue,
setVariableValue
};