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:
parent
dadd6b1e7c
commit
cc10f412dc
@ -82,8 +82,6 @@
|
||||
"istanbul": "^1.0.0-alpha.2",
|
||||
"jsdom": "9.2.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^2.2.3",
|
||||
"mocha": "^3.0.0-1",
|
||||
"mock-local-storage": "1.0.2",
|
||||
"mock-socket": "^3.0.1",
|
||||
@ -119,6 +117,7 @@
|
||||
"lodash": "^4.11.1",
|
||||
"marked": "^0.3.6",
|
||||
"material-ui": "^0.15.4",
|
||||
"material-ui-chip-input": "^0.8.0",
|
||||
"moment": "^2.14.1",
|
||||
"react": "^15.2.1",
|
||||
"react-addons-css-transition-group": "^15.2.1",
|
||||
|
@ -17,6 +17,8 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||
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 { validateName } from '../../util/validation';
|
||||
@ -55,6 +57,7 @@ export default class EditMeta extends Component {
|
||||
error={ nameError }
|
||||
onSubmit={ this.onNameChange } />
|
||||
{ this.renderMetaFields() }
|
||||
{ this.renderTags() }
|
||||
</Form>
|
||||
</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) => {
|
||||
this.setState(validateName(name));
|
||||
}
|
||||
|
17
js/src/ui/Actionbar/Search/index.js
Normal file
17
js/src/ui/Actionbar/Search/index.js
Normal 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';
|
43
js/src/ui/Actionbar/Search/search.css
Normal file
43
js/src/ui/Actionbar/Search/search.css
Normal 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;
|
||||
}
|
176
js/src/ui/Actionbar/Search/search.js
Normal file
176
js/src/ui/Actionbar/Search/search.js
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
17
js/src/ui/Actionbar/Sort/index.js
Normal file
17
js/src/ui/Actionbar/Sort/index.js
Normal 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';
|
20
js/src/ui/Actionbar/Sort/sort.css
Normal file
20
js/src/ui/Actionbar/Sort/sort.css
Normal 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;
|
||||
}
|
73
js/src/ui/Actionbar/Sort/sort.js
Normal file
73
js/src/ui/Actionbar/Sort/sort.js
Normal 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 });
|
||||
}
|
||||
}
|
@ -15,13 +15,15 @@
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
.container {
|
||||
padding: 0em
|
||||
padding: 0em;
|
||||
}
|
||||
|
||||
.padded {
|
||||
padding: 1.5em;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
border-radius: 0 !important;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.light .padded {
|
||||
|
17
js/src/ui/Tags/index.js
Normal file
17
js/src/ui/Tags/index.js
Normal 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
36
js/src/ui/Tags/tags.css
Normal 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
56
js/src/ui/Tags/tags.js
Normal 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>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Actionbar from './Actionbar';
|
||||
import ActionbarSearch from './Actionbar/Search';
|
||||
import ActionbarSort from './Actionbar/Sort';
|
||||
import Badge from './Badge';
|
||||
import Balance from './Balance';
|
||||
import Button from './Button';
|
||||
@ -31,11 +33,14 @@ import muiTheme from './Theme';
|
||||
import Page from './Page';
|
||||
import ParityBackground from './ParityBackground';
|
||||
import SignerIcon from './SignerIcon';
|
||||
import Tags from './Tags';
|
||||
import Tooltips, { Tooltip } from './Tooltips';
|
||||
import TxHash from './TxHash';
|
||||
|
||||
export {
|
||||
Actionbar,
|
||||
ActionbarSearch,
|
||||
ActionbarSort,
|
||||
AddressSelect,
|
||||
Badge,
|
||||
Balance,
|
||||
@ -62,6 +67,7 @@ export {
|
||||
Page,
|
||||
ParityBackground,
|
||||
SignerIcon,
|
||||
Tags,
|
||||
Tooltip,
|
||||
Tooltips,
|
||||
TxHash
|
||||
|
@ -14,7 +14,7 @@
|
||||
/* You should have received a copy of the GNU General Public License
|
||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
.balances {
|
||||
.balances, .tags {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
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';
|
||||
|
||||
@ -70,6 +70,9 @@ export default class Header extends Component {
|
||||
</div>
|
||||
{ this.renderTxCount() }
|
||||
</div>
|
||||
<div className={ styles.tags }>
|
||||
<Tags tags={ meta.tags } />
|
||||
</div>
|
||||
<div className={ styles.balances }>
|
||||
<Balance
|
||||
account={ account }
|
||||
|
@ -26,7 +26,10 @@ export default class List extends Component {
|
||||
accounts: PropTypes.object,
|
||||
balances: PropTypes.object,
|
||||
link: PropTypes.string,
|
||||
empty: PropTypes.bool
|
||||
search: PropTypes.array,
|
||||
empty: PropTypes.bool,
|
||||
order: PropTypes.string,
|
||||
handleAddSearchToken: PropTypes.func
|
||||
};
|
||||
|
||||
render () {
|
||||
@ -38,7 +41,7 @@ export default class List extends Component {
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { accounts, balances, link, empty } = this.props;
|
||||
const { accounts, balances, link, empty, handleAddSearchToken } = this.props;
|
||||
|
||||
if (empty) {
|
||||
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 balance = balances[address] || {};
|
||||
|
||||
@ -61,9 +66,79 @@ export default class List extends Component {
|
||||
<Summary
|
||||
link={ link }
|
||||
account={ account }
|
||||
balance={ balance } />
|
||||
balance={ balance }
|
||||
handleAddSearchToken={ handleAddSearchToken } />
|
||||
</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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
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 {
|
||||
static contextTypes = {
|
||||
@ -28,7 +28,8 @@ export default class Summary extends Component {
|
||||
account: PropTypes.object.isRequired,
|
||||
balance: PropTypes.object.isRequired,
|
||||
link: PropTypes.string,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
handleAddSearchToken: PropTypes.func
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -36,21 +37,24 @@ export default class Summary extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, balance, children, link } = this.props;
|
||||
const { account, balance, children, link, handleAddSearchToken } = this.props;
|
||||
const { tags } = account.meta;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewLink = `/${link || 'account'}/${account.address}`;
|
||||
const { address } = account;
|
||||
const viewLink = `/${link || 'account'}/${address}`;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Tags tags={ tags } handleAddSearchToken={ handleAddSearchToken } />
|
||||
<IdentityIcon
|
||||
address={ account.address } />
|
||||
address={ address } />
|
||||
<ContainerTitle
|
||||
title={ <Link to={ viewLink }>{ <IdentityName address={ account.address } unknown /> }</Link> }
|
||||
byline={ account.address } />
|
||||
title={ <Link to={ viewLink }>{ <IdentityName address={ address } unknown /> }</Link> }
|
||||
byline={ address } />
|
||||
<Balance
|
||||
balance={ balance } />
|
||||
{ children }
|
||||
|
@ -18,10 +18,11 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import List from './List';
|
||||
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';
|
||||
|
||||
@ -38,11 +39,14 @@ class Accounts extends Component {
|
||||
|
||||
state = {
|
||||
addressBook: false,
|
||||
newDialog: false
|
||||
newDialog: false,
|
||||
sortOrder: '',
|
||||
searchValues: []
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, hasAccounts, balances } = this.props;
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
return (
|
||||
<div className={ styles.accounts }>
|
||||
@ -50,9 +54,12 @@ class Accounts extends Component {
|
||||
{ this.renderActionbar() }
|
||||
<Page>
|
||||
<List
|
||||
search={ searchValues }
|
||||
accounts={ accounts }
|
||||
balances={ balances }
|
||||
empty={ !hasAccounts } />
|
||||
empty={ !hasAccounts }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken } />
|
||||
<Tooltip
|
||||
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' />
|
||||
@ -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 () {
|
||||
const buttons = [
|
||||
<Button
|
||||
key='newAccount'
|
||||
icon={ <ContentAdd /> }
|
||||
label='new account'
|
||||
onClick={ this.onNewAccountClick } />
|
||||
onClick={ this.onNewAccountClick } />,
|
||||
|
||||
this.renderSearchButton(),
|
||||
this.renderSortButton()
|
||||
];
|
||||
|
||||
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 = () => {
|
||||
this.setState({
|
||||
newDialog: !this.state.newDialog
|
||||
|
@ -18,10 +18,11 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
||||
import { uniq } from 'lodash';
|
||||
|
||||
import List from '../Accounts/List';
|
||||
import { AddAddress } from '../../modals';
|
||||
import { Actionbar, Button, Page } from '../../ui';
|
||||
import { Actionbar, ActionbarSearch, ActionbarSort, Button, Page } from '../../ui';
|
||||
|
||||
import styles from './addresses.css';
|
||||
|
||||
@ -37,11 +38,14 @@ class Addresses extends Component {
|
||||
}
|
||||
|
||||
state = {
|
||||
showAdd: false
|
||||
showAdd: false,
|
||||
sortOrder: '',
|
||||
searchValues: []
|
||||
}
|
||||
|
||||
render () {
|
||||
const { balances, contacts, hasContacts } = this.props;
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
return (
|
||||
<div className={ styles.addresses }>
|
||||
@ -50,21 +54,53 @@ class Addresses extends Component {
|
||||
<Page>
|
||||
<List
|
||||
link='address'
|
||||
search={ searchValues }
|
||||
accounts={ contacts }
|
||||
balances={ balances }
|
||||
empty={ !hasContacts } />
|
||||
empty={ !hasContacts }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken } />
|
||||
</Page>
|
||||
</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 () {
|
||||
const buttons = [
|
||||
<Button
|
||||
key='newAddress'
|
||||
icon={ <ContentAdd /> }
|
||||
label='new address'
|
||||
onClick={ this.onOpenAdd } />
|
||||
onClick={ this.onOpenAdd } />,
|
||||
|
||||
this.renderSearchButton(),
|
||||
this.renderSortButton()
|
||||
];
|
||||
|
||||
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 = () => {
|
||||
this.setState({
|
||||
showAdd: true
|
||||
|
@ -18,8 +18,9 @@ import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
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 List from '../Accounts/List';
|
||||
@ -40,11 +41,14 @@ class Contracts extends Component {
|
||||
|
||||
state = {
|
||||
addContract: false,
|
||||
deployContract: false
|
||||
deployContract: false,
|
||||
sortOrder: '',
|
||||
searchValues: []
|
||||
}
|
||||
|
||||
render () {
|
||||
const { contracts, hasContracts, balances } = this.props;
|
||||
const { searchValues, sortOrder } = this.state;
|
||||
|
||||
return (
|
||||
<div className={ styles.contracts }>
|
||||
@ -55,14 +59,43 @@ class Contracts extends Component {
|
||||
<Page>
|
||||
<List
|
||||
link='contract'
|
||||
search={ searchValues }
|
||||
accounts={ contracts }
|
||||
balances={ balances }
|
||||
empty={ !hasContracts } />
|
||||
empty={ !hasContracts }
|
||||
order={ sortOrder }
|
||||
handleAddSearchToken={ this.onAddSearchToken } />
|
||||
</Page>
|
||||
</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 () {
|
||||
const buttons = [
|
||||
<Button
|
||||
@ -74,7 +107,10 @@ class Contracts extends Component {
|
||||
key='deployContract'
|
||||
icon={ <ContentAdd /> }
|
||||
label='deploy contract'
|
||||
onClick={ this.onDeployContract } />
|
||||
onClick={ this.onDeployContract } />,
|
||||
|
||||
this.renderSearchButton(),
|
||||
this.renderSortButton()
|
||||
];
|
||||
|
||||
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 = () => {
|
||||
this.setState({ deployContract: false });
|
||||
}
|
||||
|
@ -57,6 +57,11 @@ module.exports = {
|
||||
exclude: /node_modules/,
|
||||
loaders: [ 'happypack/loader?id=js' ]
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: /node_modules\/material-ui-chip-input/,
|
||||
loader: 'babel'
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loaders: ['json']
|
||||
@ -76,14 +81,6 @@ module.exports = {
|
||||
exclude: [/src/],
|
||||
loader: 'style!css'
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
loaders: [
|
||||
'style',
|
||||
'css',
|
||||
'less'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|)$/,
|
||||
loader: 'file-loader'
|
||||
|
Loading…
Reference in New Issue
Block a user