Convert Contract dropdowns to react-select (#890)

* use Select in InteractForm instead of handrolled select

* convert InteractExplorer to react-select and tighten types

* remove log

* cleanup json abi placeholder

* Add react-select style overrides (#897)

* Add react-select style overrides

* Add comment

* Add variables & mixins

* Fix border off by 1px

* use simpler .map instead of forEach
This commit is contained in:
Daniel Ternyak 2018-01-23 18:33:11 -06:00 committed by GitHub
parent 05b9066f9e
commit 7c0cf7cb9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 276 additions and 132 deletions

View File

@ -11,19 +11,23 @@ import { connect } from 'react-redux';
import { Fields } from './components';
import { setDataField, TSetDataField } from 'actions/transaction';
import { Data } from 'libs/units';
import Select from 'react-select';
interface StateProps {
nodeLib: INode;
to: AppState['transaction']['fields']['to'];
dataExists: boolean;
}
interface DispatchProps {
showNotification: TShowNotification;
setDataField: TSetDataField;
}
interface OwnProps {
contractFunctions: any;
}
type Props = StateProps & DispatchProps & OwnProps;
interface State {
@ -31,8 +35,21 @@ interface State {
[key: string]: { rawData: string; parsedData: string[] | string };
};
outputs;
selectedFunction: null | any;
selectedFunctionName: string;
selectedFunction: null | ContractOption;
}
interface ContractFunction {
constant: boolean;
decodeInput: any;
decodeOutput: any;
encodeInput: any;
inputs: any[];
outputs: any;
}
interface ContractOption {
contract: ContractFunction;
name: string;
}
class InteractExplorerClass extends Component<Props, State> {
@ -42,15 +59,16 @@ class InteractExplorerClass extends Component<Props, State> {
public state: State = {
selectedFunction: null,
selectedFunctionName: '',
inputs: {},
outputs: {}
};
public render() {
const { inputs, outputs, selectedFunction, selectedFunctionName } = this.state;
const { inputs, outputs, selectedFunction } = this.state;
const contractFunctionsOptions = this.contractOptions();
const { to } = this.props;
const generateOrWriteButton = this.props.dataExists ? (
<GenerateTransaction />
) : (
@ -61,6 +79,7 @@ class InteractExplorerClass extends Component<Props, State> {
{translate('CONTRACT_Write')}
</button>
);
return (
<div className="InteractExplorer">
<h3 className="InteractExplorer-title">
@ -68,19 +87,22 @@ class InteractExplorerClass extends Component<Props, State> {
<span className="InteractExplorer-title-address">{to.raw}</span>
</h3>
<select
value={selectedFunction ? selectedFunction.name : ''}
className="InteractExplorer-fnselect form-control"
<Select
name="exploreContract"
value={selectedFunction as any}
placeholder="Please select a function..."
onChange={this.handleFunctionSelect}
>
<option>{translate('CONTRACT_Interact_CTA', true)}</option>
{this.contractOptions()}
</select>
options={contractFunctionsOptions}
clearable={false}
searchable={false}
labelKey="name"
valueKey="contract"
/>
{selectedFunction && (
<div key={selectedFunctionName} className="InteractExplorer-func">
<div key={selectedFunction.name} className="InteractExplorer-func">
{/* TODO: Use reusable components with validation */}
{selectedFunction.inputs.map(input => {
{selectedFunction.contract.inputs.map(input => {
const { type, name } = input;
return (
@ -98,7 +120,7 @@ class InteractExplorerClass extends Component<Props, State> {
</label>
);
})}
{selectedFunction.outputs.map((output, index) => {
{selectedFunction.contract.outputs.map((output, index) => {
const { type, name } = output;
const parsedName = name === '' ? index : name;
@ -117,7 +139,7 @@ class InteractExplorerClass extends Component<Props, State> {
);
})}
{selectedFunction.constant ? (
{selectedFunction.contract.constant ? (
<button
className="InteractExplorer-func-submit btn btn-primary"
onClick={this.handleFunctionCall}
@ -137,14 +159,16 @@ class InteractExplorerClass extends Component<Props, State> {
private contractOptions = () => {
const { contractFunctions } = this.props;
return Object.keys(contractFunctions).map(name => {
return (
<option key={name} value={name}>
{name}
</option>
);
});
const transformedContractFunction: ContractOption[] = Object.keys(contractFunctions).map(
contractFunction => {
const contract = contractFunctions[contractFunction];
return {
name: contractFunction,
contract
};
}
);
return transformedContractFunction;
};
private handleFunctionCall = async (_: React.FormEvent<HTMLButtonElement>) => {
@ -160,7 +184,7 @@ class InteractExplorerClass extends Component<Props, State> {
const callData = { to: to.raw, data };
const results = await nodeLib.sendCallRequest(callData);
const parsedResult = selectedFunction.decodeOutput(results);
const parsedResult = selectedFunction!.contract.decodeOutput(results);
this.setState({ outputs: parsedResult });
} catch (e) {
this.props.showNotification(
@ -184,14 +208,9 @@ class InteractExplorerClass extends Component<Props, State> {
}
};
private handleFunctionSelect = (ev: React.FormEvent<HTMLSelectElement>) => {
const { contractFunctions } = this.props;
const selectedFunctionName = ev.currentTarget.value;
const selectedFunction = contractFunctions[selectedFunctionName];
private handleFunctionSelect = (selectedFunction: ContractOption) => {
this.setState({
selectedFunction,
selectedFunctionName,
outputs: {},
inputs: {}
});
@ -203,8 +222,7 @@ class InteractExplorerClass extends Component<Props, State> {
(accu, key) => ({ ...accu, [key]: inputs[key].parsedData }),
{}
);
const data = selectedFunction.encodeInput(parsedInputs);
return data;
return selectedFunction!.contract.encodeInput(parsedInputs);
}
private tryParseJSON(input: string) {

View File

@ -6,60 +6,76 @@ import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { isValidETHAddress, isValidAbiJson } from 'libs/validators';
import classnames from 'classnames';
import Select from 'react-select';
interface Props {
interface ContractOption {
name: string;
value: string;
}
interface StateProps {
contracts: NetworkContract[];
accessContract(abiJson: string, address: string): (ev) => void;
}
interface OwnProps {
accessContract(contractAbi: string, address: string): (ev) => void;
resetState(): void;
}
type Props = OwnProps & StateProps;
interface State {
address: string;
abiJson: string;
contract: ContractOption | null;
contractPlaceholder: string;
}
class InteractForm extends Component<Props, State> {
public state = {
address: '',
abiJson: ''
};
const abiJsonPlaceholder = [
{
type: 'constructor',
inputs: [{ name: 'param1', type: 'uint256', indexed: true }],
name: 'Event'
},
{ type: 'function', inputs: [{ name: 'a', type: 'uint256' }], name: 'foo', outputs: [] }
];
private abiJsonPlaceholder = '[{ "type":"contructor", "inputs":\
[{ "name":"param1","type":"uint256", "indexed":true }],\
"name":"Event" }, { "type":"function", "inputs": [{"nam\
e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
class InteractForm extends Component<Props, State> {
private abiJsonPlaceholder = JSON.stringify(abiJsonPlaceholder, null, 0);
constructor(props) {
super(props);
this.state = {
address: '',
abiJson: '',
contract: null,
contractPlaceholder: this.isContractsValid()
? 'Please select a contract...'
: 'No contracts available'
};
}
public isContractsValid = () => {
const { contracts } = this.props;
return contracts && contracts.length;
};
public render() {
const { contracts, accessContract } = this.props;
const { address, abiJson } = this.state;
const { address, abiJson, contract } = this.state;
const validEthAddress = isValidETHAddress(address);
const validAbiJson = isValidAbiJson(abiJson);
const showContractAccessButton = validEthAddress && validAbiJson;
let contractOptions;
if (contracts && contracts.length) {
contractOptions = [
{
name: 'Select a contract...',
value: null
}
];
let contractOptions: ContractOption[] = [];
contractOptions = contractOptions.concat(
contracts.map(contract => {
const addr = contract.address ? `(${contract.address.substr(0, 10)}...)` : '';
return {
name: `${contract.name} ${addr}`,
value: this.makeContractValue(contract)
};
})
);
} else {
contractOptions = [
{
name: 'No contracts available',
value: null
}
];
if (this.isContractsValid()) {
contractOptions = contracts.map(con => {
const addr = con.address ? `(${con.address.substr(0, 10)}...)` : '';
return {
name: `${con.name} ${addr}`,
value: this.makeContractValue(con)
};
});
}
// TODO: Use common components for address, abi json
@ -82,17 +98,17 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
<label className="InteractForm-address-contract form-group col-sm-6">
<h4>{translate('CONTRACT_Title_2')}</h4>
<select
className="InteractForm-address-field-input form-control"
<Select
name="interactContract"
className={`${!contract ? 'is-invalid' : ''}`}
value={contract as any}
placeholder={this.state.contractPlaceholder}
onChange={this.handleSelectContract}
disabled={!contracts || !contracts.length}
>
{contractOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.name}
</option>
))}
</select>
options={contractOptions}
clearable={false}
searchable={false}
labelKey="name"
/>
</label>
</div>
@ -128,15 +144,16 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
this.setState({ [name]: ev.currentTarget.value });
};
private handleSelectContract = (ev: React.FormEvent<HTMLSelectElement>) => {
private handleSelectContract = (contract: ContractOption) => {
this.props.resetState();
const contract = this.props.contracts.find(currContract => {
return this.makeContractValue(currContract) === ev.currentTarget.value;
const fullContract = this.props.contracts.find(currContract => {
return this.makeContractValue(currContract) === contract.value;
});
this.setState({
address: contract && contract.address ? contract.address : '',
abiJson: contract && contract.abi ? contract.abi : ''
address: fullContract && fullContract.address ? fullContract.address : '',
abiJson: fullContract && fullContract.abi ? fullContract.abi : '',
contract
});
};
@ -146,7 +163,7 @@ e":"a", "type":"uint256"}], "name":"foo", "outputs": [] }]';
}
const mapStateToProps = (state: AppState) => ({
contracts: getNetworkContracts(state)
contracts: getNetworkContracts(state) || []
});
export default connect(mapStateToProps)(InteractForm);
export default connect<StateProps, {}>(mapStateToProps)(InteractForm);

View File

@ -46,9 +46,14 @@ class InteractClass extends Component<DispatchProps, State> {
public render() {
const { showExplorer, currentContract } = this.state;
const interactProps = {
accessContract: this.accessContract,
resetState: this.resetState
};
return (
<main className="Interact Tab-content-pane" role="main">
<InteractForm accessContract={this.accessContract} resetState={this.resetState} />
<InteractForm {...interactProps} />
<hr />
{showExplorer &&
currentContract && (

View File

@ -1,5 +1,5 @@
@import "./variables";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/mixins";
@import './variables';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/mixins';
@mixin bg-gradient {
background: $ether-navy;
@ -16,7 +16,7 @@
@mixin clearfix {
&:after {
content: "";
content: '';
display: table;
clear: both;
}
@ -25,7 +25,7 @@
@mixin mono {
font-family: $font-family-monospace;
font-weight: normal;
letter-spacing: .02em;
letter-spacing: 0.02em;
}
@mixin ellipsis {
@ -96,3 +96,11 @@
transition-delay: 400ms, 400ms, 300ms;
}
}
@mixin input-shadow($color) {
box-shadow: inset 0 1px 1px rgba(black, 0.075), 0 0 1px rgba($color, 0.5);
}
@mixin input-focus-shadow($color) {
box-shadow: inset 0 1px 2px rgba(black, 0.125), 0 0 1px rgba($color, 0.5);
}

View File

@ -1,36 +1,36 @@
// Where shared common styles live
// --- BOOTSTRAP ---
@import "./variables";
@import "./mixins";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/normalize";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/print";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/type";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/code";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/grid";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/tables";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/forms";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/buttons";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/dropdowns";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/button-groups";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/input-groups";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/alerts";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/wells";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/close";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/utilities";
@import "~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";
@import './variables';
@import './mixins';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/normalize';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/print';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/scaffolding';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/type';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/code';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/grid';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/tables';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/forms';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/buttons';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/dropdowns';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/button-groups';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/input-groups';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/alerts';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/wells';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/close';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/utilities';
@import '~bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities';
// --- RC SLIDER ---
@import "~rc-slider/assets/index.css";
@import '~rc-slider/assets/index.css';
// --- React Select ---
@import '~react-select/dist/react-select.css';
// --- CUSTOM ---
@import "./styles/badbrowser";
@import "./styles/noscript";
@import "./styles/overrides";
@import "./styles/scaffolding";
@import "./styles/tab";
@import "./fonts";
@import './styles/badbrowser';
@import './styles/noscript';
@import './styles/overrides';
@import './styles/scaffolding';
@import './styles/tab';
@import './fonts';

View File

@ -1,13 +1,16 @@
// This contains all of the bootstrap style overrides we do. Files should
// correspond to the bootstrap filename, and be placed in the overrides/ folder
@import "./overrides/alerts";
@import "./overrides/buttons";
@import "./overrides/button-groups";
@import "./overrides/dropdowns";
@import "./overrides/forms";
@import "./overrides/grid";
@import "./overrides/input-groups";
@import "./overrides/type";
@import './overrides/alerts';
@import './overrides/buttons';
@import './overrides/button-groups';
@import './overrides/dropdowns';
@import './overrides/forms';
@import './overrides/grid';
@import './overrides/input-groups';
@import './overrides/type';
// Other overrides
@import './overrides/react-select';
// And an override for rc-slider
@import "./overrides/rc-slider";
@import './overrides/rc-slider';

View File

@ -33,12 +33,12 @@ input[readonly] {
margin-top: $space-sm;
margin-bottom: $space-sm;
transition: $transition;
padding: $input-padding;
&:focus {
border-color: $input-border-focus;
outline: 0;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
0 0 1px rgba($brand-primary, 0.5);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba($brand-primary, 0.5);
}
}
@ -63,13 +63,11 @@ select.form-control {
// Custom feedback classes
@mixin form-control-state($color) {
border-color: lighten($color, 20%);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075)
0 0 1px rgba($color, 0.1);
@include input-shadow($color);
&:focus {
border-color: lighten($color, 5%);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.125),
0 0 1px rgba($color, 0.5);
@include input-focus-shadow($color);
}
}

View File

@ -0,0 +1,92 @@
// This syntax is necessary to override css styles in react-select
.Select {
font-size: 1rem;
.Select-control {
height: $input-height-base;
display: block;
box-sizing: border-box;
font-weight: 400;
border-radius: 0px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
border: 1px solid $input-border;
transition: $transition;
&:hover {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
border: 1px solid $input-border;
}
.Select-value {
padding-left: $input-padding-x;
padding-right: $input-padding-x;
.Select-value-label {
vertical-align: middle;
}
}
.Select-arrow-zone {
float: right;
position: relative;
top: 50%;
transform: translateY(-50%);
}
}
.Select-menu-outer {
box-sizing: content-box;
font-weight: 400;
border: 1px solid $input-border;
width: calc(100% - 1px);
}
.Select-placeholder {
color: #d3d3d3;
padding: 0px $input-padding-x;
line-height: $input-height-base;
}
.Select-input {
opacity: 0;
}
&.is-open {
.Select-control {
border: 1px solid $input-border;
}
}
&.is-focused {
&:not(.is-open):not(.is-invalid) {
.Select-control {
border-color: $brand-primary;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 1px rgba(14, 151, 192, 0.5);
}
}
}
@mixin react-select-control-state($color) {
.Select-control {
border-color: lighten($color, 20%);
@include input-shadow($color);
}
.Select-menu-outer {
border-color: lighten($color, 20%);
@include input-shadow($color);
}
&.is-focused {
.Select-control {
border-color: lighten($color, 5%);
@include input-focus-shadow($color);
}
.Select-menu-outer {
border-color: lighten($color, 5%);
@include input-focus-shadow($color);
}
}
}
&.is-valid {
@include react-select-control-state($brand-success);
}
&.is-invalid {
@include react-select-control-state($brand-danger);
}
&.is-warning {
@include react-select-control-state($brand-warning);
}
}

View File

@ -4,6 +4,9 @@ $input-color: #333333;
$input-border: $gray-lighter;
$input-border-focus: rgba($brand-primary, 0.6);
$input-color-placeholder: darken($gray-lighter, 10%);
$input-padding-x: 1rem;
$input-padding-y: 0.75rem;
$input-padding: $input-padding-y $input-padding-x;
$input-height-base: 2.55rem;
$input-height-large: 4rem;
@ -20,7 +23,7 @@ $input-group-addon-border-color: $input-border;
$cursor-disabled: default;
$dropdown-bg: #fff;
$dropdown-border: rgba(0, 0, 0, .15);
$dropdown-border: rgba(0, 0, 0, 0.15);
$dropdown-fallback-border: $gray-lighter;
$dropdown-divider-bg: #e5e5e5;
$dropdown-link-color: $ether-navy;