Consistent file uploads (#4699)
* FileSelect component * Use FileSelect component in Actionbar * Convert CreateAccount/Import to FileSelect
This commit is contained in:
parent
88449671a1
commit
190ed76e30
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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('');
|
||||
});
|
||||
});
|
||||
|
||||
|
24
js/src/ui/Actionbar/Import/import.css
vendored
24
js/src/ui/Actionbar/Import/import.css
vendored
@ -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;
|
||||
|
@ -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 = () => {
|
||||
|
52
js/src/ui/Form/FileSelect/fileSelect.css
Normal file
52
js/src/ui/Form/FileSelect/fileSelect.css
Normal 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;
|
||||
}
|
||||
}
|
76
js/src/ui/Form/FileSelect/fileSelect.js
Normal file
76
js/src/ui/Form/FileSelect/fileSelect.js
Normal 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);
|
||||
}
|
||||
}
|
118
js/src/ui/Form/FileSelect/fileSelect.spec.js
Normal file
118
js/src/ui/Form/FileSelect/fileSelect.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
17
js/src/ui/Form/FileSelect/index.js
Normal file
17
js/src/ui/Form/FileSelect/index.js
Normal 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';
|
@ -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';
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user