Merge pull request #4146 from paulkaplan/list-import
Implement list import and export from files
This commit is contained in:
6 files changed
+119
-24
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
@@ -19,6 +19,7 @@ const setVariableValue = (vm, targetId, variableId, value) => {
|
||||
};
|
||||
|
||||
export {
|
||||
getVariable,
|
||||
getVariableValue,
|
||||
setVariableValue
|
||||
};
|
||||
Reference in new issue
Block a user