diff --git a/js/package.json b/js/package.json index 54cce18d4ad..6d1eaac299e 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/modals/EditMeta/editMeta.js b/js/src/modals/EditMeta/editMeta.js index 5822c1a3c9d..103208945e8 100644 --- a/js/src/modals/EditMeta/editMeta.js +++ b/js/src/modals/EditMeta/editMeta.js @@ -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() } ); @@ -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 ( + + ); + } + onNameChange = (name) => { this.setState(validateName(name)); } diff --git a/js/src/ui/Actionbar/Search/index.js b/js/src/ui/Actionbar/Search/index.js new file mode 100644 index 00000000000..f51d38a6fa9 --- /dev/null +++ b/js/src/ui/Actionbar/Search/index.js @@ -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 . + +export default from './search'; diff --git a/js/src/ui/Actionbar/Search/search.css b/js/src/ui/Actionbar/Search/search.css new file mode 100644 index 00000000000..956a9e2e354 --- /dev/null +++ b/js/src/ui/Actionbar/Search/search.css @@ -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 . +*/ +.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; +} diff --git a/js/src/ui/Actionbar/Search/search.js b/js/src/ui/Actionbar/Search/search.js new file mode 100644 index 00000000000..79fabebc878 --- /dev/null +++ b/js/src/ui/Actionbar/Search/search.js @@ -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 . + +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 ( +
+
+ +
+ +
+ ); + } + + 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) + }); + } +} diff --git a/js/src/ui/Actionbar/Sort/index.js b/js/src/ui/Actionbar/Sort/index.js new file mode 100644 index 00000000000..82855931c74 --- /dev/null +++ b/js/src/ui/Actionbar/Sort/index.js @@ -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 . + +export default from './sort'; diff --git a/js/src/ui/Actionbar/Sort/sort.css b/js/src/ui/Actionbar/Sort/sort.css new file mode 100644 index 00000000000..ff592ca2f3f --- /dev/null +++ b/js/src/ui/Actionbar/Sort/sort.css @@ -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 . +*/ + +.sortButton { + min-width: 50px !important; +} diff --git a/js/src/ui/Actionbar/Sort/sort.js b/js/src/ui/Actionbar/Sort/sort.js new file mode 100644 index 00000000000..35c51e5a1dd --- /dev/null +++ b/js/src/ui/Actionbar/Sort/sort.js @@ -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 . + +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 ( + } + onClick={ this.handleMenuOpen } + /> + } + open={ this.state.menuOpen } + onRequestChange={ this.handleMenuChange } + onItemTouchTap={ this.handleSortChange } + targetOrigin={ { horizontal: 'right', vertical: 'top' } } + anchorOrigin={ { horizontal: 'right', vertical: 'top' } } + > + + + + + ); + } + + handleSortChange = (event, child) => { + const order = child.props.value; + this.props.onChange(order); + } + + handleMenuOpen = () => { + this.setState({ menuOpen: true }); + } + + handleMenuChange = (open) => { + this.setState({ menuOpen: open }); + } +} diff --git a/js/src/ui/Container/container.css b/js/src/ui/Container/container.css index 890f9d93427..14a0179ec8e 100644 --- a/js/src/ui/Container/container.css +++ b/js/src/ui/Container/container.css @@ -15,13 +15,15 @@ /* along with Parity. If not, see . */ .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 { diff --git a/js/src/ui/Tags/index.js b/js/src/ui/Tags/index.js new file mode 100644 index 00000000000..71cd4962429 --- /dev/null +++ b/js/src/ui/Tags/index.js @@ -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 . + +export default from './tags'; diff --git a/js/src/ui/Tags/tags.css b/js/src/ui/Tags/tags.css new file mode 100644 index 00000000000..f17fd1064fa --- /dev/null +++ b/js/src/ui/Tags/tags.css @@ -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 . +*/ + +.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; +} diff --git a/js/src/ui/Tags/tags.js b/js/src/ui/Tags/tags.js new file mode 100644 index 00000000000..23501b24f10 --- /dev/null +++ b/js/src/ui/Tags/tags.js @@ -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 . + +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 (
+ { this.renderTags() } +
); + } + + 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 ( +
+ { tag } +
+ ); + }); + } +} diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 3a324bcebcc..8c7117a5fc0 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -15,6 +15,8 @@ // along with Parity. If not, see . 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 diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 35ce0a700db..7fb254a6a34 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -14,7 +14,7 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.balances { +.balances, .tags { clear: both; } diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index d23fd50812b..0abf3c79726 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -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 { { this.renderTxCount() } +
+ +
{ + 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 { + balance={ balance } + handleAddSearchToken={ handleAddSearchToken } />
); }); } + + 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; + }); + } } diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js index 0bfb41a20d9..241806722a5 100644 --- a/js/src/views/Accounts/Summary/summary.js +++ b/js/src/views/Accounts/Summary/summary.js @@ -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 ( + + address={ address } /> { } } - byline={ account.address } /> + title={ { } } + byline={ address } /> { children } diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js index b8f5cba1644..cc398c3137b 100644 --- a/js/src/views/Accounts/accounts.js +++ b/js/src/views/Accounts/accounts.js @@ -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 (
@@ -50,9 +54,12 @@ class Accounts extends Component { { this.renderActionbar() } + empty={ !hasAccounts } + order={ sortOrder } + handleAddSearchToken={ this.onAddSearchToken } /> @@ -61,13 +68,42 @@ class Accounts extends Component { ); } + renderSearchButton () { + const onChange = (searchValues) => { + this.setState({ searchValues }); + }; + + return ( + + ); + } + + renderSortButton () { + const onChange = (sortOrder) => { + this.setState({ sortOrder }); + }; + + return ( + + ); + } + renderActionbar () { const buttons = [