Consistent file uploads (#4699)

* FileSelect component

* Use FileSelect component in Actionbar

* Convert CreateAccount/Import to FileSelect
This commit is contained in:
Jaco Greeff 2017-02-28 14:24:12 +01:00 committed by GitHub
parent 88449671a1
commit 190ed76e30
12 changed files with 340 additions and 133 deletions

View File

@ -14,19 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { FloatingActionButton } from 'material-ui';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { FormattedMessage } from 'react-intl';
import { Form, Input } from '~/ui';
import { AttachFileIcon } from '~/ui/Icons';
import { Form, FileSelect, Input } from '~/ui';
import styles from '../createAccount.css';
const STYLE_HIDDEN = { display: 'none' };
@observer
export default class NewImport extends Component {
static propTypes = {
@ -34,7 +29,7 @@ export default class NewImport extends Component {
}
render () {
const { name, nameError, password, passwordHint, walletFile, walletFileError } = this.props.store;
const { name, nameError, password, passwordHint } = this.props.store;
return (
<Form>
@ -93,58 +88,48 @@ export default class NewImport extends Component {
/>
</div>
</div>
<div>
<Input
disabled
error={ walletFileError }
hint={
<FormattedMessage
id='createAccount.newImport.file.hint'
defaultMessage='the wallet file for import'
/>
}
label={
<FormattedMessage
id='createAccount.newImport.file.label'
defaultMessage='wallet file'
/>
}
value={ walletFile }
/>
<div className={ styles.upload }>
<FloatingActionButton
mini
onTouchTap={ this.openFileDialog }
>
<AttachFileIcon />
</FloatingActionButton>
<input
onChange={ this.onFileChange }
ref='fileUpload'
style={ STYLE_HIDDEN }
type='file'
/>
</div>
</div>
{ this.renderFileSelector() }
</Form>
);
}
onFileChange = (event) => {
const { store } = this.props;
renderFileSelector () {
const { walletFile, walletFileError } = this.props.store;
if (event.target.files.length) {
const reader = new FileReader();
reader.onload = (event) => store.setWalletJson(event.target.result);
reader.readAsText(event.target.files[0]);
}
store.setWalletFile(event.target.value);
return walletFile
? (
<Input
disabled
error={ walletFileError }
hint={
<FormattedMessage
id='createAccount.newImport.file.hint'
defaultMessage='the wallet file for import'
/>
}
label={
<FormattedMessage
id='createAccount.newImport.file.label'
defaultMessage='wallet file'
/>
}
value={ walletFile }
/>
)
: (
<FileSelect
className={ styles.fileImport }
error={ walletFileError }
onSelect={ this.onFileSelect }
/>
);
}
openFileDialog = () => {
ReactDOM.findDOMNode(this.refs.fileUpload).click();
onFileSelect = (fileName, fileContent) => {
const { store } = this.props;
store.setWalletFile(fileName);
store.setWalletJson(fileContent);
}
onEditName = (event, name) => {

View File

@ -101,17 +101,6 @@
width: 10% !important;
}
.upload {
text-align: right;
float: right;
margin-left: -100%;
margin-top: 28px;
}
.upload>div {
margin-right: 0.5em;
}
.checkbox {
margin-top: 2em;
}
@ -131,6 +120,11 @@
}
}
.fileImport {
height: 9em;
margin-top: 1em;
}
.summary {
line-height: 1.618em;
padding: 0 4em 1.5em 4em;

View File

@ -93,8 +93,11 @@ export default class Store {
this.phrase = '';
this.name = '';
this.nameError = null;
this.rawKey = '';
this.rawKeyError = null;
this.walletFile = '';
this.walletFileError = null;
this.walletJson = '';
});
}

View File

@ -64,13 +64,24 @@ describe('modals/CreateAccount/Store', () => {
describe('@action', () => {
describe('clearErrors', () => {
beforeEach(() => {
store.setName('');
store.setPassword('123');
store.setRawKey('test');
store.setWalletFile('test');
store.setWalletJson('test');
});
it('clears all errors', () => {
store.clearErrors();
expect(store.nameError).to.be.null;
expect(store.passwordRepeatError).to.be.null;
expect(store.rawKey).to.equal('');
expect(store.rawKeyError).to.be.null;
expect(store.walletFile).to.equal('');
expect(store.walletFileError).to.be.null;
expect(store.walletJson).to.equal('');
});
});

View File

@ -15,30 +15,6 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.importZone {
width: 100%;
height: 200px;
border-width: 2px;
border-color: #666;
border-style: dashed;
border-radius: 10px;
background-color: rgba(50, 50, 50, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
transition: all 0.5s ease;
&:hover {
cursor: pointer;
border-radius: 0;
background-color: rgba(50, 50, 50, 0.5);
}
}
.desc {
margin-top: 0;
color: #ccc;

View File

@ -15,12 +15,12 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import Dropzone from 'react-dropzone';
import { FormattedMessage } from 'react-intl';
import { nodeOrStringProptype } from '~/util/proptypes';
import Button from '../../Button';
import FileSelect from '../../Form/FileSelect';
import { CancelIcon, DoneIcon, FileUploadIcon } from '../../Icons';
import Portal from '../../Portal';
@ -184,25 +184,8 @@ export default class ActionbarImport extends Component {
return this.renderValidation();
}
return this.renderFileSelect();
}
renderFileSelect () {
return (
<div>
<Dropzone
onDrop={ this.onDrop }
multiple={ false }
className={ styles.importZone }
>
<div>
<FormattedMessage
id='ui.actiobar.import.dropzone'
defaultMessage='Drop a file here, or click to select a file to upload.'
/>
</div>
</Dropzone>
</div>
<FileSelect onSelect={ this.onFileSelect } />
);
}
@ -224,39 +207,30 @@ export default class ActionbarImport extends Component {
);
}
onDrop = (files) => {
onFileSelect = (file, content) => {
const { renderValidation } = this.props;
const file = files[0];
const reader = new FileReader();
if (typeof renderValidation !== 'function') {
this.props.onConfirm(content);
return this.onCloseModal();
}
reader.onload = (e) => {
const content = e.target.result;
const validationBody = renderValidation(content);
if (typeof renderValidation !== 'function') {
this.props.onConfirm(content);
return this.onCloseModal();
}
const validationBody = renderValidation(content);
if (validationBody && validationBody.error) {
return this.setState({
step: 1,
error: true,
errorText: validationBody.error
});
}
this.setState({
if (validationBody && validationBody.error) {
return this.setState({
step: 1,
validate: true,
validationBody,
content
error: true,
errorText: validationBody.error
});
};
}
reader.readAsText(file);
this.setState({
step: 1,
validate: true,
validationBody,
content
});
}
onConfirm = () => {

View File

@ -0,0 +1,52 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$backgroundNormal: rgba(0, 0, 0, 0.2);
$backgroundNormalHover: rgba(0, 0, 0, 0.5);
$backgroundError: rgba(255, 0, 0, 0.1);
$backgroundErrorHover: rgba(255, 0, 0, 0.2);
.dropzone {
align-items: center;
background: $backgroundNormal;
border: 2px dashed #666;
border-radius: 0.5em;
display: flex;
justify-content: center;
font-size: 1.2em;
height: 12em;
transition: all 0.5s ease;
width: 100%;
&:hover {
background: $backgroundNormalHover;
border-radius: 0;
cursor: pointer;
}
&.error {
background: $backgroundError;
&:hover {
background: $backgroundErrorHover;
}
}
.label {
color: #aaa;
}
}

View File

@ -0,0 +1,76 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import Dropzone from 'react-dropzone';
import { FormattedMessage } from 'react-intl';
import { nodeOrStringProptype } from '~/util/proptypes';
import styles from './fileSelect.css';
export default class FileSelect extends Component {
static propTypes = {
className: PropTypes.string,
error: nodeOrStringProptype(),
label: nodeOrStringProptype(),
onSelect: PropTypes.func.isRequired
}
static defaultProps = {
label: (
<FormattedMessage
id='ui.fileSelect.defaultLabel'
defaultMessage='Drop a file here, or click to select a file to upload'
/>
)
}
render () {
const { className, error, label } = this.props;
return (
<Dropzone
onDrop={ this.onDrop }
multiple={ false }
className={
[
styles.dropzone,
error
? styles.error
: '',
className
].join(' ') }
>
<div className={ styles.label }>
{ error || label }
</div>
</Dropzone>
);
}
onDrop = (files) => {
const { onSelect } = this.props;
const reader = new FileReader();
const file = files[0];
reader.onload = (event) => {
onSelect(file.name, event.target.result);
};
reader.readAsText(file);
}
}

View File

@ -0,0 +1,118 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import FileSelect from './';
const FILE = {
content: 'some test content',
name: 'someName'
};
let component;
let globalFileReader;
let instance;
let onSelect;
let processedFile;
function stubReader () {
globalFileReader = global.FileReader;
global.FileReader = class {
readAsText (file) {
processedFile = file;
this.onload({
target: {
result: file.content
}
});
}
};
}
function restoreReader () {
global.FileReader = globalFileReader;
}
function render (props = {}) {
onSelect = sinon.stub();
component = shallow(
<FileSelect
onSelect={ onSelect }
{ ...props }
/>
);
instance = component.instance();
return component;
}
describe('ui/Form/FileSelect', () => {
beforeEach(() => {
stubReader();
render();
});
afterEach(() => {
restoreReader();
});
it('renders defaults', () => {
expect(component).to.be.ok;
});
describe('DropZone', () => {
let label;
let zone;
beforeEach(() => {
label = component.find('FormattedMessage');
zone = component.find('Dropzone');
});
it('renders the label', () => {
expect(label.props().id).to.equal('ui.fileSelect.defaultLabel');
});
it('attaches the onDrop event', () => {
expect(zone.props().onDrop).to.equal(instance.onDrop);
});
it('does not allow multiples', () => {
expect(zone.props().multiple).to.be.false;
});
});
describe('event methods', () => {
describe('onDrop', () => {
beforeEach(() => {
instance.onDrop([ FILE ]);
});
it('reads the file as dropped', () => {
expect(processedFile).to.deep.equal(FILE);
});
it('calls prop onSelect with file & content', () => {
expect(onSelect).to.have.been.calledWith(FILE.name, FILE.content);
});
});
});
});

View File

@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './fileSelect';

View File

@ -16,6 +16,7 @@
export AddressSelect from './AddressSelect';
export DappUrlInput from './DappUrlInput';
export FileSelect from './FileSelect';
export FormWrap from './FormWrap';
export Input from './Input';
export InputAddress from './InputAddress';

View File

@ -30,7 +30,7 @@ export DappCard from './DappCard';
export DappIcon from './DappIcon';
export Errors from './Errors';
export Features, { FEATURES, FeaturesStore } from './Features';
export Form, { AddressSelect, DappUrlInput, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
export Form, { AddressSelect, DappUrlInput, FileSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form';
export GasPriceEditor from './GasPriceEditor';
export GasPriceSelector from './GasPriceSelector';
export Icons from './Icons';