Allow tags for Accounts, Addresses and Contracts (#2712)

* Added tag to the editMeta Modal (#2643)

* Added Tags to ui and to contract/address/account Header (#2643)

* Added tags to summary (#2643)

* Added Search capabilities to contracts/address book/accounts from tokens
(#2643)

* fixes eslint

* Using Chips/Tokens for search (#2643)

* Add search tokens, clickable from List (#2643)

* Add sort capabilities to Accounts / Addresses / Contracts (#2643)

* Fixes formatting issues + state updates after component unmount bug
(#2643)

* Remove unused import

* Small fixes for PR #2697

* Added default sort order for Contracts/Addresses/Accounts

* Using official `material-ui-chip-input` NPM package

* Removed LESS from webpack
This commit is contained in:
Nicolas Gotchac 2016-10-19 10:51:02 +01:00 committed by Gav Wood
parent dadd6b1e7c
commit cc10f412dc
21 changed files with 723 additions and 36 deletions

View File

@ -82,8 +82,6 @@
"istanbul": "^1.0.0-alpha.2", "istanbul": "^1.0.0-alpha.2",
"jsdom": "9.2.1", "jsdom": "9.2.1",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"less": "^2.7.1",
"less-loader": "^2.2.3",
"mocha": "^3.0.0-1", "mocha": "^3.0.0-1",
"mock-local-storage": "1.0.2", "mock-local-storage": "1.0.2",
"mock-socket": "^3.0.1", "mock-socket": "^3.0.1",
@ -119,6 +117,7 @@
"lodash": "^4.11.1", "lodash": "^4.11.1",
"marked": "^0.3.6", "marked": "^0.3.6",
"material-ui": "^0.15.4", "material-ui": "^0.15.4",
"material-ui-chip-input": "^0.8.0",
"moment": "^2.14.1", "moment": "^2.14.1",
"react": "^15.2.1", "react": "^15.2.1",
"react-addons-css-transition-group": "^15.2.1", "react-addons-css-transition-group": "^15.2.1",

View File

@ -17,6 +17,8 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import ContentClear from 'material-ui/svg-icons/content/clear'; import ContentClear from 'material-ui/svg-icons/content/clear';
import ContentSave from 'material-ui/svg-icons/content/save'; import ContentSave from 'material-ui/svg-icons/content/save';
// import ChipInput from 'material-ui-chip-input';
import ChipInput from 'material-ui-chip-input/src/ChipInput';
import { Button, Form, Input, Modal } from '../../ui'; import { Button, Form, Input, Modal } from '../../ui';
import { validateName } from '../../util/validation'; import { validateName } from '../../util/validation';
@ -55,6 +57,7 @@ export default class EditMeta extends Component {
error={ nameError } error={ nameError }
onSubmit={ this.onNameChange } /> onSubmit={ this.onNameChange } />
{ this.renderMetaFields() } { this.renderMetaFields() }
{ this.renderTags() }
</Form> </Form>
</Modal> </Modal>
); );
@ -96,6 +99,23 @@ export default class EditMeta extends Component {
}); });
} }
renderTags () {
const { meta } = this.state;
const { tags } = meta || [];
const onChange = (chips) => this.onMetaChange('tags', chips);
return (
<ChipInput
defaultValue={ tags }
onChange={ onChange }
floatingLabelText='(optional) tags'
hintText='press <Enter> to add a tag'
floatingLabelFixed
fullWidth
/>
);
}
onNameChange = (name) => { onNameChange = (name) => {
this.setState(validateName(name)); this.setState(validateName(name));
} }

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './search';

View File

@ -0,0 +1,43 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.searchcontainer {
display: flex;
overflow: hidden;
}
.searchButton {
min-width: 50px !important;
}
.input {
width: 500px !important;
}
.inputContainer {
transition: width 450ms ease-in-out 0ms, height 0ms ease-in-out 0ms;
white-space: nowrap;
overflow: hidden;
width: 500px;
height: 100%;
position: relative;
}
.inputContainerShown {
transition: width 450ms ease-in-out 0ms, height 0ms ease-in-out 400ms;
width: 0;
height: 0;
}

View File

@ -0,0 +1,176 @@
// Copyright 2015, 2016 Ethcore (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 ChipInput from 'material-ui-chip-input';
import ChipInput from 'material-ui-chip-input/src/ChipInput';
import ActionSearch from 'material-ui/svg-icons/action/search';
import { uniq } from 'lodash';
import { Button } from '../../';
import styles from './search.css';
export default class ActionbarSearch extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
tokens: PropTypes.array
};
state = {
showSearch: false,
stateChanging: false,
inputValue: '',
timeoutIds: []
}
componentWillReceiveProps (nextProps) {
const { tokens } = nextProps;
if (tokens.length > 0 && this.props.tokens.length === 0) {
this.handleOpenSearch(true, true);
}
}
componentWillUnmount () {
const { timeoutIds } = this.state;
if (timeoutIds.length > 0) {
timeoutIds.map(id => window.clearTimeout(id));
}
}
render () {
const { showSearch } = this.state;
const { tokens } = this.props;
const inputContainerClasses = [ styles.inputContainer ];
if (!showSearch) {
inputContainerClasses.push(styles.inputContainerShown);
}
return (
<div
className={ styles.searchcontainer }
key='searchAccount'>
<div className={ inputContainerClasses.join(' ') }>
<ChipInput
clearOnBlur={ false }
className={ styles.input }
hintText='Enter search input...'
hintStyle={ {
transition: 'none'
} }
ref='searchInput'
value={ tokens }
onBlur={ this.handleSearchBlur }
onRequestAdd={ this.handleTokenAdd }
onRequestDelete={ this.handleTokenDelete }
onUpdateInput={ this.handleInputChange } />
</div>
<Button
className={ styles.searchButton }
icon={ <ActionSearch /> }
label=''
onClick={ this.handleSearchClick } />
</div>
);
}
handleTokenAdd = (value) => {
const { tokens } = this.props;
const newSearchValues = uniq([].concat(tokens, value));
this.setState({
inputValue: ''
});
this.handleSearchChange(newSearchValues);
}
handleTokenDelete = (value) => {
const { tokens } = this.props;
const newSearchValues = []
.concat(tokens)
.filter(v => v !== value);
this.setState({
inputValue: ''
});
this.handleSearchChange(newSearchValues);
this.refs.searchInput.focus();
}
handleInputChange = (value) => {
this.setState({ inputValue: value });
}
handleSearchChange = (searchValues) => {
const { onChange } = this.props;
const newSearchValues = searchValues.filter(v => v.length > 0);
onChange(newSearchValues);
}
handleSearchClick = () => {
const { showSearch } = this.state;
this.handleOpenSearch(!showSearch);
}
handleSearchBlur = () => {
const timeoutId = window.setTimeout(() => {
const { inputValue } = this.state;
const { tokens } = this.props;
if (tokens.length === 0 && inputValue.length === 0) {
this.handleOpenSearch(false);
}
}, 250);
this.setState({
timeoutIds: [].concat(this.state.timeoutIds, timeoutId)
});
}
handleOpenSearch = (showSearch, force) => {
if (this.state.stateChanging && !force) return false;
this.setState({
showSearch: showSearch,
stateChanging: true
});
if (showSearch) {
this.refs.searchInput.focus();
} else {
this.refs.searchInput.getInputNode().blur();
}
const timeoutId = window.setTimeout(() => {
this.setState({ stateChanging: false });
}, 450);
this.setState({
timeoutIds: [].concat(this.state.timeoutIds, timeoutId)
});
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './sort';

View File

@ -0,0 +1,20 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.sortButton {
min-width: 50px !important;
}

View File

@ -0,0 +1,73 @@
// Copyright 2015, 2016 Ethcore (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 IconMenu from 'material-ui/IconMenu';
import MenuItem from 'material-ui/MenuItem';
import SortIcon from 'material-ui/svg-icons/content/sort';
import { Button } from '../../';
import styles from './sort.css';
export default class ActionbarSort extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
order: PropTypes.string
};
state = {
menuOpen: false
}
render () {
return (
<IconMenu
iconButtonElement={
<Button
className={ styles.sortButton }
label=''
icon={ <SortIcon /> }
onClick={ this.handleMenuOpen }
/>
}
open={ this.state.menuOpen }
onRequestChange={ this.handleMenuChange }
onItemTouchTap={ this.handleSortChange }
targetOrigin={ { horizontal: 'right', vertical: 'top' } }
anchorOrigin={ { horizontal: 'right', vertical: 'top' } }
>
<MenuItem value='' primaryText='Default' />
<MenuItem value='tags' primaryText='Sort by tags' />
<MenuItem value='name' primaryText='Sort by name' />
</IconMenu>
);
}
handleSortChange = (event, child) => {
const order = child.props.value;
this.props.onChange(order);
}
handleMenuOpen = () => {
this.setState({ menuOpen: true });
}
handleMenuChange = (open) => {
this.setState({ menuOpen: open });
}
}

View File

@ -15,13 +15,15 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.container { .container {
padding: 0em padding: 0em;
} }
.padded { .padded {
padding: 1.5em; padding: 1.5em;
background: rgba(0, 0, 0, 0.8) !important; background: rgba(0, 0, 0, 0.8) !important;
border-radius: 0 !important; border-radius: 0 !important;
position: relative;
overflow: auto;
} }
.light .padded { .light .padded {

17
js/src/ui/Tags/index.js Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './tags';

36
js/src/ui/Tags/tags.css Normal file
View File

@ -0,0 +1,36 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.tags {
display: flex;
flex-wrap: wrap;
position: absolute;
right: 0.25rem;
top: 0;
}
.tag {
font-size: 0.75rem;
background: rgba(255, 255, 255, 0.07);
border-radius: 16px;
margin: 0.75em 0.5em 0 0;
padding: 0.25em 1em;
}
.tagClickable:hover {
cursor: pointer;
}

56
js/src/ui/Tags/tags.js Normal file
View File

@ -0,0 +1,56 @@
// Copyright 2015, 2016 Ethcore (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 styles from './tags.css';
export default class Tags extends Component {
static propTypes = {
tags: PropTypes.array,
handleAddSearchToken: PropTypes.func
}
render () {
return (<div className={ styles.tags }>
{ this.renderTags() }
</div>);
}
renderTags () {
const { handleAddSearchToken } = this.props;
const tags = this.props.tags || [];
const tagClasses = handleAddSearchToken
? [ styles.tag, styles.tagClickable ]
: [ styles.tag ];
return tags.map((tag, idx) => {
const onClick = handleAddSearchToken
? () => handleAddSearchToken(tag)
: null;
return (
<div
key={ idx }
className={ tagClasses.join(' ') }
onClick={ onClick }>
{ tag }
</div>
);
});
}
}

View File

@ -15,6 +15,8 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import Actionbar from './Actionbar'; import Actionbar from './Actionbar';
import ActionbarSearch from './Actionbar/Search';
import ActionbarSort from './Actionbar/Sort';
import Badge from './Badge'; import Badge from './Badge';
import Balance from './Balance'; import Balance from './Balance';
import Button from './Button'; import Button from './Button';
@ -31,11 +33,14 @@ import muiTheme from './Theme';
import Page from './Page'; import Page from './Page';
import ParityBackground from './ParityBackground'; import ParityBackground from './ParityBackground';
import SignerIcon from './SignerIcon'; import SignerIcon from './SignerIcon';
import Tags from './Tags';
import Tooltips, { Tooltip } from './Tooltips'; import Tooltips, { Tooltip } from './Tooltips';
import TxHash from './TxHash'; import TxHash from './TxHash';
export { export {
Actionbar, Actionbar,
ActionbarSearch,
ActionbarSort,
AddressSelect, AddressSelect,
Badge, Badge,
Balance, Balance,
@ -62,6 +67,7 @@ export {
Page, Page,
ParityBackground, ParityBackground,
SignerIcon, SignerIcon,
Tags,
Tooltip, Tooltip,
Tooltips, Tooltips,
TxHash TxHash

View File

@ -14,7 +14,7 @@
/* You should have received a copy of the GNU General Public License /* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>. /* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/ */
.balances { .balances, .tags {
clear: both; clear: both;
} }

View File

@ -16,7 +16,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName } from '../../../ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui';
import styles from './header.css'; import styles from './header.css';
@ -70,6 +70,9 @@ export default class Header extends Component {
</div> </div>
{ this.renderTxCount() } { this.renderTxCount() }
</div> </div>
<div className={ styles.tags }>
<Tags tags={ meta.tags } />
</div>
<div className={ styles.balances }> <div className={ styles.balances }>
<Balance <Balance
account={ account } account={ account }

View File

@ -26,7 +26,10 @@ export default class List extends Component {
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object, balances: PropTypes.object,
link: PropTypes.string, link: PropTypes.string,
empty: PropTypes.bool search: PropTypes.array,
empty: PropTypes.bool,
order: PropTypes.string,
handleAddSearchToken: PropTypes.func
}; };
render () { render () {
@ -38,7 +41,7 @@ export default class List extends Component {
} }
renderAccounts () { renderAccounts () {
const { accounts, balances, link, empty } = this.props; const { accounts, balances, link, empty, handleAddSearchToken } = this.props;
if (empty) { if (empty) {
return ( return (
@ -50,7 +53,9 @@ export default class List extends Component {
); );
} }
return Object.keys(accounts).map((address, idx) => { const addresses = this.getAddresses();
return addresses.map((address, idx) => {
const account = accounts[address] || {}; const account = accounts[address] || {};
const balance = balances[address] || {}; const balance = balances[address] || {};
@ -61,9 +66,79 @@ export default class List extends Component {
<Summary <Summary
link={ link } link={ link }
account={ account } account={ account }
balance={ balance } /> balance={ balance }
handleAddSearchToken={ handleAddSearchToken } />
</div> </div>
); );
}); });
} }
getAddresses () {
const filteredAddresses = this.getFilteredAddresses();
return this.sortAddresses(filteredAddresses);
}
sortAddresses (addresses) {
const { order } = this.props;
if (!order || ['tags', 'name'].indexOf(order) === -1) {
return addresses;
}
const { accounts } = this.props;
return addresses.sort((addressA, addressB) => {
const accountA = accounts[addressA];
const accountB = accounts[addressB];
if (order === 'name') {
return accountA.name.localeCompare(accountB.name);
}
if (order === 'tags') {
const tagsA = [].concat(accountA.meta.tags)
.filter(t => t)
.sort();
const tagsB = [].concat(accountB.meta.tags)
.filter(t => t)
.sort();
if (tagsA.length === 0) return 1;
if (tagsB.length === 0) return -1;
return tagsA.join('').localeCompare(tagsB.join(''));
}
});
}
getFilteredAddresses () {
const { accounts, search } = this.props;
const searchValues = (search || []).map(v => v.toLowerCase());
if (searchValues.length === 0) {
return Object.keys(accounts);
}
return Object.keys(accounts)
.filter((address) => {
const account = accounts[address];
const tags = account.meta.tags || [];
const name = account.name || '';
const values = []
.concat(tags, name)
.map(v => v.toLowerCase());
return values
.filter((value) => {
return searchValues
.map(searchValue => value.indexOf(searchValue) >= 0)
// `current && truth, true` => use tokens as AND
// `current || truth, false` => use tokens as OR
.reduce((current, truth) => current || truth, false);
})
.length > 0;
});
}
} }

View File

@ -17,7 +17,7 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName } from '../../../ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui';
export default class Summary extends Component { export default class Summary extends Component {
static contextTypes = { static contextTypes = {
@ -28,7 +28,8 @@ export default class Summary extends Component {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
balance: PropTypes.object.isRequired, balance: PropTypes.object.isRequired,
link: PropTypes.string, link: PropTypes.string,
children: PropTypes.node children: PropTypes.node,
handleAddSearchToken: PropTypes.func
} }
state = { state = {
@ -36,21 +37,24 @@ export default class Summary extends Component {
} }
render () { render () {
const { account, balance, children, link } = this.props; const { account, balance, children, link, handleAddSearchToken } = this.props;
const { tags } = account.meta;
if (!account) { if (!account) {
return null; return null;
} }
const viewLink = `/${link || 'account'}/${account.address}`; const { address } = account;
const viewLink = `/${link || 'account'}/${address}`;
return ( return (
<Container> <Container>
<Tags tags={ tags } handleAddSearchToken={ handleAddSearchToken } />
<IdentityIcon <IdentityIcon
address={ account.address } /> address={ address } />
<ContainerTitle <ContainerTitle
title={ <Link to={ viewLink }>{ <IdentityName address={ account.address } unknown /> }</Link> } title={ <Link to={ viewLink }>{ <IdentityName address={ address } unknown /> }</Link> }
byline={ account.address } /> byline={ address } />
<Balance <Balance
balance={ balance } /> balance={ balance } />
{ children } { children }

View File

@ -18,10 +18,11 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add'; import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq } from 'lodash';
import List from './List'; import List from './List';
import { CreateAccount } from '../../modals'; import { CreateAccount } from '../../modals';
import { Actionbar, Button, Page, Tooltip } from '../../ui'; import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '../../ui';
import styles from './accounts.css'; import styles from './accounts.css';
@ -38,11 +39,14 @@ class Accounts extends Component {
state = { state = {
addressBook: false, addressBook: false,
newDialog: false newDialog: false,
sortOrder: '',
searchValues: []
} }
render () { render () {
const { accounts, hasAccounts, balances } = this.props; const { accounts, hasAccounts, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return ( return (
<div className={ styles.accounts }> <div className={ styles.accounts }>
@ -50,9 +54,12 @@ class Accounts extends Component {
{ this.renderActionbar() } { this.renderActionbar() }
<Page> <Page>
<List <List
search={ searchValues }
accounts={ accounts } accounts={ accounts }
balances={ balances } balances={ balances }
empty={ !hasAccounts } /> empty={ !hasAccounts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
<Tooltip <Tooltip
className={ styles.accountTooltip } className={ styles.accountTooltip }
text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account' /> text='your accounts are visible for easy access, allowing you to edit the meta information, make transfers, view transactions and fund the account' />
@ -61,13 +68,42 @@ class Accounts extends Component {
); );
} }
renderSearchButton () {
const onChange = (searchValues) => {
this.setState({ searchValues });
};
return (
<ActionbarSearch
key='searchAccount'
tokens={ this.state.searchValues }
onChange={ onChange } />
);
}
renderSortButton () {
const onChange = (sortOrder) => {
this.setState({ sortOrder });
};
return (
<ActionbarSort
key='sortAccounts'
order={ this.state.sortOrder }
onChange={ onChange } />
);
}
renderActionbar () { renderActionbar () {
const buttons = [ const buttons = [
<Button <Button
key='newAccount' key='newAccount'
icon={ <ContentAdd /> } icon={ <ContentAdd /> }
label='new account' label='new account'
onClick={ this.onNewAccountClick } /> onClick={ this.onNewAccountClick } />,
this.renderSearchButton(),
this.renderSortButton()
]; ];
return ( return (
@ -99,6 +135,12 @@ class Accounts extends Component {
); );
} }
onAddSearchToken = (token) => {
const { searchValues } = this.state;
const newSearchValues = uniq([].concat(searchValues, token));
this.setState({ searchValues: newSearchValues });
}
onNewAccountClick = () => { onNewAccountClick = () => {
this.setState({ this.setState({
newDialog: !this.state.newDialog newDialog: !this.state.newDialog

View File

@ -18,10 +18,11 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add'; import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq } from 'lodash';
import List from '../Accounts/List'; import List from '../Accounts/List';
import { AddAddress } from '../../modals'; import { AddAddress } from '../../modals';
import { Actionbar, Button, Page } from '../../ui'; import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page } from '../../ui';
import styles from './addresses.css'; import styles from './addresses.css';
@ -37,11 +38,14 @@ class Addresses extends Component {
} }
state = { state = {
showAdd: false showAdd: false,
sortOrder: '',
searchValues: []
} }
render () { render () {
const { balances, contacts, hasContacts } = this.props; const { balances, contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state;
return ( return (
<div className={ styles.addresses }> <div className={ styles.addresses }>
@ -50,21 +54,53 @@ class Addresses extends Component {
<Page> <Page>
<List <List
link='address' link='address'
search={ searchValues }
accounts={ contacts } accounts={ contacts }
balances={ balances } balances={ balances }
empty={ !hasContacts } /> empty={ !hasContacts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
</Page> </Page>
</div> </div>
); );
} }
renderSortButton () {
const onChange = (sortOrder) => {
this.setState({ sortOrder });
};
return (
<ActionbarSort
key='sortAccounts'
order={ this.state.sortOrder }
onChange={ onChange } />
);
}
renderSearchButton () {
const onChange = (searchValues) => {
this.setState({ searchValues });
};
return (
<ActionbarSearch
key='searchAddress'
tokens={ this.state.searchValues }
onChange={ onChange } />
);
}
renderActionbar () { renderActionbar () {
const buttons = [ const buttons = [
<Button <Button
key='newAddress' key='newAddress'
icon={ <ContentAdd /> } icon={ <ContentAdd /> }
label='new address' label='new address'
onClick={ this.onOpenAdd } /> onClick={ this.onOpenAdd } />,
this.renderSearchButton(),
this.renderSortButton()
]; ];
return ( return (
@ -90,6 +126,12 @@ class Addresses extends Component {
); );
} }
onAddSearchToken = (token) => {
const { searchValues } = this.state;
const newSearchValues = uniq([].concat(searchValues, token));
this.setState({ searchValues: newSearchValues });
}
onOpenAdd = () => { onOpenAdd = () => {
this.setState({ this.setState({
showAdd: true showAdd: true

View File

@ -18,8 +18,9 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add'; import ContentAdd from 'material-ui/svg-icons/content/add';
import { uniq } from 'lodash';
import { Actionbar, Button, Page } from '../../ui'; import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page } from '../../ui';
import { AddContract, DeployContract } from '../../modals'; import { AddContract, DeployContract } from '../../modals';
import List from '../Accounts/List'; import List from '../Accounts/List';
@ -40,11 +41,14 @@ class Contracts extends Component {
state = { state = {
addContract: false, addContract: false,
deployContract: false deployContract: false,
sortOrder: '',
searchValues: []
} }
render () { render () {
const { contracts, hasContracts, balances } = this.props; const { contracts, hasContracts, balances } = this.props;
const { searchValues, sortOrder } = this.state;
return ( return (
<div className={ styles.contracts }> <div className={ styles.contracts }>
@ -55,14 +59,43 @@ class Contracts extends Component {
<Page> <Page>
<List <List
link='contract' link='contract'
search={ searchValues }
accounts={ contracts } accounts={ contracts }
balances={ balances } balances={ balances }
empty={ !hasContracts } /> empty={ !hasContracts }
order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } />
</Page> </Page>
</div> </div>
); );
} }
renderSortButton () {
const onChange = (sortOrder) => {
this.setState({ sortOrder });
};
return (
<ActionbarSort
key='sortAccounts'
order={ this.state.sortOrder }
onChange={ onChange } />
);
}
renderSearchButton () {
const onChange = (searchValues) => {
this.setState({ searchValues });
};
return (
<ActionbarSearch
key='searchContract'
tokens={ this.state.searchValues }
onChange={ onChange } />
);
}
renderActionbar () { renderActionbar () {
const buttons = [ const buttons = [
<Button <Button
@ -74,7 +107,10 @@ class Contracts extends Component {
key='deployContract' key='deployContract'
icon={ <ContentAdd /> } icon={ <ContentAdd /> }
label='deploy contract' label='deploy contract'
onClick={ this.onDeployContract } /> onClick={ this.onDeployContract } />,
this.renderSearchButton(),
this.renderSortButton()
]; ];
return ( return (
@ -115,6 +151,12 @@ class Contracts extends Component {
); );
} }
onAddSearchToken = (token) => {
const { searchValues } = this.state;
const newSearchValues = uniq([].concat(searchValues, token));
this.setState({ searchValues: newSearchValues });
}
onDeployContractClose = () => { onDeployContractClose = () => {
this.setState({ deployContract: false }); this.setState({ deployContract: false });
} }

View File

@ -57,6 +57,11 @@ module.exports = {
exclude: /node_modules/, exclude: /node_modules/,
loaders: [ 'happypack/loader?id=js' ] loaders: [ 'happypack/loader?id=js' ]
}, },
{
test: /\.js$/,
include: /node_modules\/material-ui-chip-input/,
loader: 'babel'
},
{ {
test: /\.json$/, test: /\.json$/,
loaders: ['json'] loaders: ['json']
@ -76,14 +81,6 @@ module.exports = {
exclude: [/src/], exclude: [/src/],
loader: 'style!css' loader: 'style!css'
}, },
{
test: /\.less$/,
loaders: [
'style',
'css',
'less'
]
},
{ {
test: /\.(png|jpg|)$/, test: /\.(png|jpg|)$/,
loader: 'file-loader' loader: 'file-loader'