From fb817fcdcac63cce7311710d54a4ed3da5f16ba1 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Sat, 4 Feb 2017 09:42:36 +0100 Subject: [PATCH] [beta] UI updates for 1.5.1 (#4429) * s/Delete Contract/Forget Contract/ (#4237) * Adjust the location of the signer snippet (#4155) * Additional building-block UI components (#4239) * Currency WIP * Expand tests * Pass className * Add QrCode * Export new components in ~/ui * s/this.props.netSymbol/netSymbol/ * Fix import case * ui/SectionList component (#4292) * array chunking utility * add SectionList component * Add TODOs to indicate possible future work * Add missing overlay style (as used in dapps at present) * Add a Playground for the UI Components (#4301) * Playground // WIP * Linting * Add Examples with code * CSS Linting * Linting * Add Connected Currency Symbol * 2015-2017 * 2015-2017 * 2015-2017 * 2015-2017 * 2015-2017 * 2015-2017 * 2015-2017 * Added `renderSymbol` tests * PR grumbles * Add Eth and Btc QRCode examples * 2015-2017 * Add tests for playground * Fixing tests * Split Dapp icon into ui/DappIcon (#4308) * Add QrCode & Copy to ShapeShift (#4322) * Extract CopyIcon to ~/ui/Icons * Add copy & QrCode address * Default size 4 * Add bitcoin: link * use protocol links applicable to coin exchanged * Remove .only * Display QrCode for accounts, addresses & contracts (#4329) * Allow Portal to be used as top-level modal (#4338) * Portal * Allow Portal to be used in as both top-level and popover * modal/popover variable naming * export Portal in ~/ui * Properly handle optional onKeyDown * Add simple Playground Example * Add proper event listener to Portal (#4359) * Display AccountCard name via IdentityName (#4235) * Fix signing (#4363) * Dapp Account Selection & Defaults (#4355) * Add parity_defaultAccount RPC (with subscription) (#4383) * Default Account selector in Signer overlay (#4375) * Typo, fixes #4271 (#4391) * Fix ParityBar account selection overflows (#4405) * Available Dapp selection alignment with Permissions (Portal) (#4374) * registry dapp: make lookup use lower case (#4409) * Dapps use defaultAccount instead of own selectors (#4386) * Poll for defaultAccount to update dapp & overlay subscriptions (#4417) * Poll for defaultAccount (Fixes #4413) * Fix nextTimeout on catch * Store timers * Re-enable default updates on change detection * Add block & timestamp conditions to Signer (#4411) * Extension installation overlay (#4423) * Extension installation overlay * Pr gumbles * Spelling * Update Chrome URL * Fix for non-included jsonrpc * Extend Portal component (as per Modal) #4392 --- js/package.json | 4 + js/src/api/format/input.js | 16 + js/src/api/format/output.js | 20 +- js/src/api/rpc/parity/parity.js | 62 +- js/src/api/subscriptions/eth.js | 5 +- js/src/api/subscriptions/manager.js | 1 + js/src/api/subscriptions/personal.js | 56 +- js/src/api/subscriptions/personal.spec.js | 46 +- .../basiccoin/Application/application.js | 10 +- .../basiccoin/Deploy/Deployment/deployment.js | 56 +- js/src/dapps/basiccoin/services.js | 16 + .../githubhint/Application/application.js | 115 ++-- js/src/dapps/githubhint/services.js | 46 +- .../IdentityIcon/identityIcon.css | 0 .../IdentityIcon/identityIcon.js | 0 .../IdentityIcon/index.js | 0 .../dapps/localtx/Transaction/transaction.js | 2 +- js/src/dapps/registry/Lookup/actions.js | 1 + js/src/dapps/registry/Lookup/lookup.js | 2 +- .../signaturereg/Application/application.js | 27 +- js/src/dapps/signaturereg/Import/import.js | 40 +- js/src/dapps/signaturereg/services.js | 9 +- js/src/jsonrpc/interfaces/parity.js | 14 + js/src/modals/AddDapps/addDapps.css | 37 +- js/src/modals/AddDapps/addDapps.js | 147 +++-- js/src/modals/AddDapps/addDapps.spec.js | 4 +- .../DappPermissions/dappPermissions.css | 53 +- .../modals/DappPermissions/dappPermissions.js | 128 ++-- .../DappPermissions/dappPermissions.spec.js | 4 +- js/src/modals/DappPermissions/store.js | 42 +- js/src/modals/DappPermissions/store.spec.js | 74 ++- .../AdvancedStep/advancedStep.js | 32 +- .../modals/ExecuteContract/executeContract.js | 108 ++-- .../awaitingDepositStep.js | 44 +- .../awaitingDepositStep.spec.js | 61 ++ js/src/modals/Shapeshift/shapeshift.css | 19 + js/src/modals/Transfer/Extras/extras.js | 36 +- js/src/modals/Transfer/store.js | 78 +-- js/src/modals/Transfer/transfer.js | 4 +- js/src/modals/index.js | 4 +- js/src/playground/index.js | 17 + js/src/playground/playground.css | 90 +++ js/src/playground/playground.js | 90 +++ js/src/playground/playground.spec.js | 47 ++ js/src/playground/playgroundExample.js | 55 ++ js/src/playground/store.js | 51 ++ js/src/playground/store.spec.js | 41 ++ js/src/redux/providers/signerMiddleware.js | 4 +- js/src/routes.js | 90 +-- js/src/ui/AccountCard/accountCard.css | 17 +- js/src/ui/AccountCard/accountCard.js | 101 ++-- js/src/ui/AccountCard/accountCard.spec.js | 133 ++++ js/src/ui/Balance/balance.js | 38 +- js/src/ui/Balance/balance.spec.js | 122 ++++ js/src/ui/Container/Title/title.css | 28 +- js/src/ui/Container/Title/title.js | 54 +- js/src/ui/Container/container.js | 28 +- js/src/ui/CopyToClipboard/copyToClipboard.js | 12 +- .../CurrencySymbol/currencySymbol.example.js | 51 ++ js/src/ui/CurrencySymbol/currencySymbol.js | 65 ++ .../ui/CurrencySymbol/currencySymbol.spec.js | 99 +++ js/src/ui/CurrencySymbol/index.js | 17 + .../summary.css => ui/DappCard/dappCard.css} | 7 +- .../summary.js => ui/DappCard/dappCard.js} | 88 +-- js/src/ui/DappCard/index.js | 17 + js/src/ui/DappIcon/dappIcon.css | 31 + js/src/ui/DappIcon/dappIcon.js | 49 ++ js/src/ui/DappIcon/dappIcon.spec.js | 70 +++ js/src/ui/DappIcon/index.js | 17 + .../ui/Form/AddressSelect/addressSelect.css | 11 +- js/src/ui/Form/AddressSelect/addressSelect.js | 59 +- js/src/ui/Form/InputAddress/inputAddress.js | 5 +- js/src/ui/Form/InputDate/index.js | 17 + js/src/ui/Form/InputDate/inputDate.css | 22 + js/src/ui/Form/InputDate/inputDate.js | 53 ++ js/src/ui/Form/InputTime/index.js | 17 + js/src/ui/Form/InputTime/inputTime.css | 22 + js/src/ui/Form/InputTime/inputTime.js | 54 ++ js/src/ui/Form/Label/index.js | 17 + js/src/ui/Form/Label/label.css | 24 + js/src/ui/Form/Label/label.js | 40 ++ js/src/ui/Form/RadioButtons/radioButtons.css | 27 +- js/src/ui/Form/RadioButtons/radioButtons.js | 40 +- js/src/ui/Form/index.js | 14 +- js/src/ui/GasPriceEditor/gasPriceEditor.css | 40 ++ js/src/ui/GasPriceEditor/gasPriceEditor.js | 217 +++++-- .../ui/GasPriceEditor/gasPriceEditor.spec.js | 72 ++- js/src/ui/GasPriceEditor/store.js | 91 ++- js/src/ui/GasPriceEditor/store.spec.js | 93 ++- js/src/ui/Icons/index.js | 12 + js/src/ui/MethodDecoding/methodDecoding.js | 23 +- js/src/ui/Modal/modal.css | 14 - js/src/ui/Modal/modal.js | 9 +- .../ui/ParityBackground/parityBackground.js | 13 +- js/src/ui/Portal/portal.css | 141 +++-- js/src/ui/Portal/portal.example.js | 121 ++++ js/src/ui/Portal/portal.js | 146 +++-- js/src/ui/Portal/portal.spec.js | 64 ++ .../Dapps/Summary => ui/QrCode}/index.js | 2 +- js/src/ui/QrCode/qrCode.example.js | 63 ++ js/src/ui/QrCode/qrCode.js | 83 +++ js/src/ui/QrCode/qrCode.spec.js | 108 ++++ js/src/ui/SectionList/index.js | 17 + js/src/ui/SectionList/sectionList.css | 84 +++ js/src/ui/SectionList/sectionList.example.js | 94 +++ js/src/ui/SectionList/sectionList.js | 103 ++++ js/src/ui/SectionList/sectionList.spec.js | 103 ++++ js/src/ui/Tags/tags.js | 21 +- js/src/ui/{Modal => }/Title/index.js | 0 js/src/ui/Title/title.css | 26 + js/src/ui/{Modal => }/Title/title.js | 43 +- js/src/ui/index.js | 23 +- js/src/util/array.js | 26 + js/src/util/array.spec.js | 29 + js/src/views/Account/Header/header.css | 27 +- js/src/views/Account/Header/header.js | 43 +- js/src/views/Account/Header/header.spec.js | 98 ++- .../views/Application/Extension/extension.css | 52 ++ .../views/Application/Extension/extension.js | 74 +++ js/src/views/Application/Extension/index.js | 17 + js/src/views/Application/Extension/store.js | 89 +++ js/src/views/Application/application.js | 10 + js/src/views/Contract/contract.js | 32 +- js/src/views/Dapps/builtin.json | 1 + js/src/views/Dapps/dapps.js | 40 +- js/src/views/Dapps/dappsStore.js | 25 +- js/src/views/ParityBar/accountStore.js | 101 ++++ js/src/views/ParityBar/accountStore.spec.js | 104 ++++ js/src/views/ParityBar/parityBar.css | 199 ++++-- js/src/views/ParityBar/parityBar.js | 569 ++++++++++++++++-- js/src/views/ParityBar/parityBar.spec.js | 167 +++++ js/src/views/ParityBar/parityBar.test.js | 55 ++ .../components/SignRequest/signRequest.js | 3 +- .../transactionMainDetails.js | 23 +- .../TransactionPending/transactionPending.js | 18 +- .../transactionPendingFormConfirm.js | 3 +- 136 files changed, 5775 insertions(+), 1230 deletions(-) rename js/src/dapps/{githubhint => localtx}/IdentityIcon/identityIcon.css (100%) rename js/src/dapps/{githubhint => localtx}/IdentityIcon/identityIcon.js (100%) rename js/src/dapps/{githubhint => localtx}/IdentityIcon/index.js (100%) create mode 100644 js/src/playground/index.js create mode 100644 js/src/playground/playground.css create mode 100644 js/src/playground/playground.js create mode 100644 js/src/playground/playground.spec.js create mode 100644 js/src/playground/playgroundExample.js create mode 100644 js/src/playground/store.js create mode 100644 js/src/playground/store.spec.js create mode 100644 js/src/ui/AccountCard/accountCard.spec.js create mode 100644 js/src/ui/Balance/balance.spec.js create mode 100644 js/src/ui/CurrencySymbol/currencySymbol.example.js create mode 100644 js/src/ui/CurrencySymbol/currencySymbol.js create mode 100644 js/src/ui/CurrencySymbol/currencySymbol.spec.js create mode 100644 js/src/ui/CurrencySymbol/index.js rename js/src/{views/Dapps/Summary/summary.css => ui/DappCard/dappCard.css} (94%) rename js/src/{views/Dapps/Summary/summary.js => ui/DappCard/dappCard.js} (53%) create mode 100644 js/src/ui/DappCard/index.js create mode 100644 js/src/ui/DappIcon/dappIcon.css create mode 100644 js/src/ui/DappIcon/dappIcon.js create mode 100644 js/src/ui/DappIcon/dappIcon.spec.js create mode 100644 js/src/ui/DappIcon/index.js create mode 100644 js/src/ui/Form/InputDate/index.js create mode 100644 js/src/ui/Form/InputDate/inputDate.css create mode 100644 js/src/ui/Form/InputDate/inputDate.js create mode 100644 js/src/ui/Form/InputTime/index.js create mode 100644 js/src/ui/Form/InputTime/inputTime.css create mode 100644 js/src/ui/Form/InputTime/inputTime.js create mode 100644 js/src/ui/Form/Label/index.js create mode 100644 js/src/ui/Form/Label/label.css create mode 100644 js/src/ui/Form/Label/label.js create mode 100644 js/src/ui/Portal/portal.example.js create mode 100644 js/src/ui/Portal/portal.spec.js rename js/src/{views/Dapps/Summary => ui/QrCode}/index.js (95%) create mode 100644 js/src/ui/QrCode/qrCode.example.js create mode 100644 js/src/ui/QrCode/qrCode.js create mode 100644 js/src/ui/QrCode/qrCode.spec.js create mode 100644 js/src/ui/SectionList/index.js create mode 100644 js/src/ui/SectionList/sectionList.css create mode 100644 js/src/ui/SectionList/sectionList.example.js create mode 100644 js/src/ui/SectionList/sectionList.js create mode 100644 js/src/ui/SectionList/sectionList.spec.js rename js/src/ui/{Modal => }/Title/index.js (100%) create mode 100644 js/src/ui/Title/title.css rename js/src/ui/{Modal => }/Title/title.js (65%) create mode 100644 js/src/util/array.js create mode 100644 js/src/util/array.spec.js create mode 100644 js/src/views/Application/Extension/extension.css create mode 100644 js/src/views/Application/Extension/extension.js create mode 100644 js/src/views/Application/Extension/index.js create mode 100644 js/src/views/Application/Extension/store.js create mode 100644 js/src/views/ParityBar/accountStore.js create mode 100644 js/src/views/ParityBar/accountStore.spec.js create mode 100644 js/src/views/ParityBar/parityBar.spec.js create mode 100644 js/src/views/ParityBar/parityBar.test.js diff --git a/js/package.json b/js/package.json index c2ed1b21a..f214c33df 100644 --- a/js/package.json +++ b/js/package.json @@ -163,6 +163,7 @@ "phoneformat.js": "1.0.3", "promise-worker": "1.1.1", "push.js": "0.0.11", + "qrcode-npm": "0.0.3", "qs": "6.3.0", "react": "15.4.1", "react-ace": "4.1.0", @@ -170,6 +171,8 @@ "react-copy-to-clipboard": "4.2.3", "react-dom": "15.4.1", "react-dropzone": "3.7.3", + "react-element-to-jsx-string": "6.0.0", + "react-event-listener": "0.4.1", "react-intl": "2.1.5", "react-portal": "3.0.0", "react-redux": "4.4.6", @@ -185,6 +188,7 @@ "scryptsy": "2.0.0", "solc": "ngotchac/solc-js", "store": "1.3.20", + "useragent.js": "0.5.6", "utf8": "2.1.2", "valid-url": "1.0.9", "validator": "6.2.0", diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 7a41da109..406b58664 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -127,6 +127,18 @@ export function inNumber16 (number) { return inHex(bn.toString(16)); } +export function inOptionsCondition (condition) { + if (condition) { + if (condition.block) { + condition.block = condition.block ? inNumber10(condition.block) : null; + } else if (condition.time) { + condition.time = inNumber10(Math.floor(condition.time.getTime() / 1000)); + } + } + + return condition; +} + export function inOptions (options) { if (options) { Object.keys(options).forEach((key) => { @@ -136,6 +148,10 @@ export function inOptions (options) { options[key] = inAddress(options[key]); break; + case 'condition': + options[key] = inOptionsCondition(options[key]); + break; + case 'gas': case 'gasPrice': options[key] = inNumber16((new BigNumber(options[key])).round()); diff --git a/js/src/api/format/output.js b/js/src/api/format/output.js index cf707cbd1..877728717 100644 --- a/js/src/api/format/output.js +++ b/js/src/api/format/output.js @@ -200,6 +200,18 @@ export function outSyncing (syncing) { return syncing; } +export function outTransactionCondition (condition) { + if (condition) { + if (condition.block) { + condition.block = outNumber(condition.block); + } else if (condition.time) { + condition.time = outDate(condition.time); + } + } + + return condition; +} + export function outTransaction (tx) { if (tx) { Object.keys(tx).forEach((key) => { @@ -213,8 +225,14 @@ export function outTransaction (tx) { tx[key] = outNumber(tx[key]); break; + case 'condition': + tx[key] = outTransactionCondition(tx[key]); + break; + case 'minBlock': - tx[key] = tx[key] ? outNumber(tx[key]) : null; + tx[key] = tx[key] + ? outNumber(tx[key]) + : null; break; case 'creates': diff --git a/js/src/api/rpc/parity/parity.js b/js/src/api/rpc/parity/parity.js index 174ad3fbf..160e7513a 100644 --- a/js/src/api/rpc/parity/parity.js +++ b/js/src/api/rpc/parity/parity.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions } from '../../format/input'; +import { inAddress, inAddresses, inData, inHex, inNumber16, inOptions, inBlockNumber } from '../../format/input'; import { outAccountInfo, outAddress, outAddresses, outChainStatus, outHistogram, outNumber, outPeers, outTransaction } from '../../format/output'; export default class Parity { @@ -76,6 +76,17 @@ export default class Parity { .execute('parity_dappsInterface'); } + decryptMessage (address, data) { + return this._transport + .execute('parity_decryptMessage', inAddress(address), inHex(data)); + } + + defaultAccount () { + return this._transport + .execute('parity_defaultAccount') + .then(outAddress); + } + defaultExtraData () { return this._transport .execute('parity_defaultExtraData'); @@ -101,6 +112,11 @@ export default class Parity { .execute('parity_enode'); } + encryptMessage (pubkey, data) { + return this._transport + .execute('parity_encryptMessage', inHex(pubkey), inHex(data)); + } + executeUpgrade () { return this._transport .execute('parity_executeUpgrade'); @@ -111,6 +127,17 @@ export default class Parity { .execute('parity_extraData'); } + futureTransactions () { + return this._transport + .execute('parity_futureTransactions'); + } + + gasCeilTarget () { + return this._transport + .execute('parity_gasCeilTarget') + .then(outNumber); + } + gasFloorTarget () { return this._transport .execute('parity_gasFloorTarget') @@ -156,11 +183,22 @@ export default class Parity { .execute('parity_killAccount', inAddress(account), password); } + listAccounts (count, offset = null, blockNumber = 'latest') { + return this._transport + .execute('parity_listAccounts', count, inAddress(offset), inBlockNumber(blockNumber)) + .then((accounts) => (accounts || []).map(outAddress)); + } + listRecentDapps () { return this._transport .execute('parity_listRecentDapps'); } + listStorageKeys (address, count, hash = null, blockNumber = 'latest') { + return this._transport + .execute('parity_listStorageKeys', inAddress(address), count, inHex(hash), inBlockNumber(blockNumber)); + } + removeAddress (address) { return this._transport .execute('parity_removeAddress', inAddress(address)); @@ -265,6 +303,11 @@ export default class Parity { .then(outAddress); } + postSign (address, hash) { + return this._transport + .execute('parity_postSign', inAddress(address), inHex(hash)); + } + postTransaction (options) { return this._transport .execute('parity_postTransaction', inOptions(options)); @@ -311,16 +354,31 @@ export default class Parity { .execute('parity_setDappsAddresses', dappId, inAddresses(addresses)); } + setEngineSigner (address, password) { + return this._transport + .execute('parity_setEngineSigner', inAddress(address), password); + } + setExtraData (data) { return this._transport .execute('parity_setExtraData', inData(data)); } + setGasCeilTarget (quantity) { + return this._transport + .execute('parity_setGasCeilTarget', inNumber16(quantity)); + } + setGasFloorTarget (quantity) { return this._transport .execute('parity_setGasFloorTarget', inNumber16(quantity)); } + setMaxTransactionGas (quantity) { + return this._transport + .execute('parity_setMaxTransactionGas', inNumber16(quantity)); + } + setMinGasPrice (quantity) { return this._transport .execute('parity_setMinGasPrice', inNumber16(quantity)); diff --git a/js/src/api/subscriptions/eth.js b/js/src/api/subscriptions/eth.js index f36d9dc73..7369ade49 100644 --- a/js/src/api/subscriptions/eth.js +++ b/js/src/api/subscriptions/eth.js @@ -23,6 +23,7 @@ export default class Eth { this._started = false; this._lastBlock = new BigNumber(-1); + this._pollTimerId = null; } get isStarted () { @@ -37,7 +38,7 @@ export default class Eth { _blockNumber = () => { const nextTimeout = (timeout = 1000) => { - setTimeout(() => { + this._pollTimerId = setTimeout(() => { this._blockNumber(); }, timeout); }; @@ -57,6 +58,6 @@ export default class Eth { nextTimeout(); }) - .catch(nextTimeout); + .catch(() => nextTimeout()); } } diff --git a/js/src/api/subscriptions/manager.js b/js/src/api/subscriptions/manager.js index 7a7a61bfa..b5cf9d704 100644 --- a/js/src/api/subscriptions/manager.js +++ b/js/src/api/subscriptions/manager.js @@ -25,6 +25,7 @@ const events = { 'logging': { module: 'logging' }, 'eth_blockNumber': { module: 'eth' }, 'parity_allAccountsInfo': { module: 'personal' }, + 'parity_defaultAccount': { module: 'personal' }, 'eth_accounts': { module: 'personal' }, 'signer_requestsToConfirm': { module: 'signer' } }; diff --git a/js/src/api/subscriptions/personal.js b/js/src/api/subscriptions/personal.js index 5e3280424..1574dcacc 100644 --- a/js/src/api/subscriptions/personal.js +++ b/js/src/api/subscriptions/personal.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -20,6 +20,9 @@ export default class Personal { this._api = api; this._updateSubscriptions = updateSubscriptions; this._started = false; + + this._lastDefaultAccount = '0x0'; + this._pollTimerId = null; } get isStarted () { @@ -30,12 +33,44 @@ export default class Personal { this._started = true; return Promise.all([ + this._defaultAccount(), this._listAccounts(), this._accountsInfo(), this._loggingSubscribe() ]); } + // FIXME: Because of the different API instances, the "wait for valid changes" approach + // doesn't work. Since the defaultAccount is critical to operation, we poll in exactly + // same way we do in ../eth (ala same as eth_blockNumber) and update. This should be moved + // to pub-sub as it becomes available + _defaultAccount = (timerDisabled = false) => { + const nextTimeout = (timeout = 1000) => { + if (!timerDisabled) { + this._pollTimerId = setTimeout(() => { + this._defaultAccount(); + }, timeout); + } + }; + + if (!this._api.transport.isConnected) { + nextTimeout(500); + return; + } + + return this._api.parity + .defaultAccount() + .then((defaultAccount) => { + if (this._lastDefaultAccount !== defaultAccount) { + this._lastDefaultAccount = defaultAccount; + this._updateSubscriptions('parity_defaultAccount', null, defaultAccount); + } + + nextTimeout(); + }) + .catch(() => nextTimeout()); + } + _listAccounts = () => { return this._api.eth .accounts() @@ -46,9 +81,19 @@ export default class Personal { _accountsInfo = () => { return this._api.parity - .allAccountsInfo() + .accountsInfo() .then((info) => { - this._updateSubscriptions('parity_allAccountsInfo', null, info); + this._updateSubscriptions('parity_accountsInfo', null, info); + + return this._api.parity + .allAccountsInfo() + .catch(() => { + // NOTE: This fails on non-secure APIs, swallow error + return {}; + }) + .then((allInfo) => { + this._updateSubscriptions('parity_allAccountsInfo', null, allInfo); + }); }); } @@ -73,6 +118,11 @@ export default class Personal { case 'parity_setAccountMeta': this._accountsInfo(); return; + + case 'parity_setDappsAddresses': + case 'parity_setNewDappsWhitelist': + this._defaultAccount(true); + return; } }); } diff --git a/js/src/api/subscriptions/personal.spec.js b/js/src/api/subscriptions/personal.spec.js index 2359192f0..ac046d250 100644 --- a/js/src/api/subscriptions/personal.spec.js +++ b/js/src/api/subscriptions/personal.spec.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -18,31 +18,51 @@ import sinon from 'sinon'; import Personal from './personal'; +const TEST_DEFAULT = '0xfa64203C044691aA57251aF95f4b48d85eC00Dd5'; const TEST_INFO = { - '0xfa64203C044691aA57251aF95f4b48d85eC00Dd5': { + [TEST_DEFAULT]: { name: 'test' } }; -const TEST_LIST = ['0xfa64203C044691aA57251aF95f4b48d85eC00Dd5']; +const TEST_LIST = [TEST_DEFAULT]; function stubApi (accounts, info) { const _calls = { + accountsInfo: [], allAccountsInfo: [], - listAccounts: [] + listAccounts: [], + defaultAccount: [] }; return { _calls, + transport: { + isConnected: true + }, parity: { + accountsInfo: () => { + const stub = sinon.stub().resolves(info || TEST_INFO)(); + + _calls.accountsInfo.push(stub); + return stub; + }, allAccountsInfo: () => { const stub = sinon.stub().resolves(info || TEST_INFO)(); + _calls.allAccountsInfo.push(stub); return stub; + }, + defaultAccount: () => { + const stub = sinon.stub().resolves(Object.keys(info || TEST_INFO)[0])(); + + _calls.defaultAccount.push(stub); + return stub; } }, eth: { accounts: () => { const stub = sinon.stub().resolves(accounts || TEST_LIST)(); + _calls.listAccounts.push(stub); return stub; } @@ -85,6 +105,10 @@ describe('api/subscriptions/personal', () => { expect(personal.isStarted).to.be.true; }); + it('calls parity_accountsInfo', () => { + expect(api._calls.accountsInfo.length).to.be.ok; + }); + it('calls parity_allAccountsInfo', () => { expect(api._calls.allAccountsInfo.length).to.be.ok; }); @@ -94,8 +118,10 @@ describe('api/subscriptions/personal', () => { }); it('updates subscribers', () => { - expect(cb.firstCall).to.have.been.calledWith('eth_accounts', null, TEST_LIST); - expect(cb.secondCall).to.have.been.calledWith('parity_allAccountsInfo', null, TEST_INFO); + expect(cb).to.have.been.calledWith('parity_defaultAccount', null, TEST_DEFAULT); + expect(cb).to.have.been.calledWith('eth_accounts', null, TEST_LIST); + expect(cb).to.have.been.calledWith('parity_accountsInfo', null, TEST_INFO); + expect(cb).to.have.been.calledWith('parity_allAccountsInfo', null, TEST_INFO); }); }); @@ -110,7 +136,15 @@ describe('api/subscriptions/personal', () => { expect(personal.isStarted).to.be.true; }); + it('calls parity_defaultAccount', () => { + expect(api._calls.defaultAccount.length).to.be.ok; + }); + it('calls personal_accountsInfo', () => { + expect(api._calls.accountsInfo.length).to.be.ok; + }); + + it('calls personal_allAccountsInfo', () => { expect(api._calls.allAccountsInfo.length).to.be.ok; }); diff --git a/js/src/dapps/basiccoin/Application/application.js b/js/src/dapps/basiccoin/Application/application.js index a808a3372..4dcaa02b2 100644 --- a/js/src/dapps/basiccoin/Application/application.js +++ b/js/src/dapps/basiccoin/Application/application.js @@ -45,7 +45,7 @@ export default class Application extends Component { } componentDidMount () { - this.attachInstance(); + return this.attachInstance(); } render () { @@ -80,12 +80,12 @@ export default class Application extends Component { } attachInstance () { - Promise + return Promise .all([ - attachInstances(), - api.parity.accountsInfo() + api.parity.accountsInfo(), + attachInstances() ]) - .then(([{ managerInstance, registryInstance, tokenregInstance }, accountsInfo]) => { + .then(([accountsInfo, { managerInstance, registryInstance, tokenregInstance }]) => { accountsInfo = accountsInfo || {}; this.setState({ loading: false, diff --git a/js/src/dapps/basiccoin/Deploy/Deployment/deployment.js b/js/src/dapps/basiccoin/Deploy/Deployment/deployment.js index c6b8e2152..666ac1a97 100644 --- a/js/src/dapps/basiccoin/Deploy/Deployment/deployment.js +++ b/js/src/dapps/basiccoin/Deploy/Deployment/deployment.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -17,7 +17,6 @@ import React, { Component, PropTypes } from 'react'; import { api } from '../../parity'; -import AddressSelect from '../../AddressSelect'; import Container from '../../Container'; import styles from './deployment.css'; @@ -122,41 +121,20 @@ export default class Deployment extends Component { } renderForm () { - const { accounts } = this.context; const { baseText, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state; const hasError = !!(nameError || tlaError || totalSupplyError); const error = `${styles.input} ${styles.error}`; - const addresses = Object.keys(accounts); - - //
- // - // - //
- // register on network (fee: { globalFeeText }ETH) - //
- //
return (
-
- - -
- the owner account to deploy from -
-
+ onChange={ this.onChangeName } + />
{ nameError || 'an identifying name for the token' }
@@ -167,7 +145,8 @@ export default class Deployment extends Component { className={ styles.small } name='tla' value={ tla } - onChange={ this.onChangeTla } /> + onChange={ this.onChangeTla } + />
{ tlaError || 'unique network acronym for this token' }
@@ -180,7 +159,8 @@ export default class Deployment extends Component { max='999999999999' name='totalSupply' value={ totalSupply } - onChange={ this.onChangeSupply } /> + onChange={ this.onChangeSupply } + />
{ totalSupplyError || `number of tokens (base: ${baseText})` }
@@ -191,7 +171,8 @@ export default class Deployment extends Component {
+ onClick={ this.onDeploy } + > Deploy Token
@@ -201,12 +182,6 @@ export default class Deployment extends Component { ); } - onChangeFrom = (event) => { - const fromAddress = event.target.value; - - this.setState({ fromAddress }); - } - onChangeName = (event) => { const name = event.target.value; const nameError = name && (name.length > 2) && (name.length < 32) @@ -266,7 +241,7 @@ export default class Deployment extends Component { onDeploy = () => { const { managerInstance, registryInstance, tokenregInstance } = this.context; - const { base, deployBusy, fromAddress, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state; + const { base, deployBusy, globalReg, globalFee, name, nameError, tla, tlaError, totalSupply, totalSupplyError } = this.state; const hasError = !!(nameError || tlaError || totalSupplyError); if (hasError || deployBusy) { @@ -276,18 +251,23 @@ export default class Deployment extends Component { const tokenreg = (globalReg ? tokenregInstance : registryInstance).address; const values = [base.mul(totalSupply), tla, name, tokenreg]; const options = { - from: fromAddress, value: globalReg ? globalFee : 0 }; this.setState({ deployBusy: true, deployState: 'Estimating gas for the transaction' }); - managerInstance - .deploy.estimateGas(options, values) + return api.parity + .defaultAccount() + .then((defaultAddress) => { + options.from = defaultAddress; + + return managerInstance.deploy.estimateGas(options, values); + }) .then((gas) => { this.setState({ deployState: 'Gas estimated, Posting transaction to the network' }); const gasPassed = gas.mul(1.2); + options.gas = gasPassed.toFixed(0); console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`); diff --git a/js/src/dapps/basiccoin/services.js b/js/src/dapps/basiccoin/services.js index 0344df805..b854708e3 100644 --- a/js/src/dapps/basiccoin/services.js +++ b/js/src/dapps/basiccoin/services.js @@ -25,6 +25,8 @@ let registryInstance; const registries = {}; const subscriptions = {}; + +let defaultSubscriptionId; let nextSubscriptionId = 1000; let isTest = false; @@ -65,6 +67,20 @@ export function unsubscribeEvents (subscriptionId) { delete subscriptions[subscriptionId]; } +export function subscribeDefaultAddress (callback) { + return api + .subscribe('parity_defaultAccount', callback) + .then((subscriptionId) => { + defaultSubscriptionId = subscriptionId; + + return defaultSubscriptionId; + }); +} + +export function unsubscribeDefaultAddress () { + return api.unsubscribe(defaultSubscriptionId); +} + function pollEvents () { const loop = Object.values(subscriptions); const timeout = () => setTimeout(pollEvents, 1000); diff --git a/js/src/dapps/githubhint/Application/application.js b/js/src/dapps/githubhint/Application/application.js index 630a36a68..f09f9c2fa 100644 --- a/js/src/dapps/githubhint/Application/application.js +++ b/js/src/dapps/githubhint/Application/application.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -17,10 +17,9 @@ import React, { Component } from 'react'; import { api } from '../parity'; -import { attachInterface } from '../services'; +import { attachInterface, subscribeDefaultAddress, unsubscribeDefaultAddress } from '../services'; import Button from '../Button'; import Events from '../Events'; -import IdentityIcon from '../IdentityIcon'; import Loading from '../Loading'; import styles from './application.css'; @@ -32,7 +31,7 @@ let nextEventId = 0; export default class Application extends Component { state = { - fromAddress: null, + defaultAddress: null, loading: true, url: '', urlError: null, @@ -47,19 +46,32 @@ export default class Application extends Component { registerType: 'file', repo: '', repoError: null, + subscriptionId: null, events: {}, eventIds: [] } componentDidMount () { - attachInterface() - .then((state) => { - this.setState(state, () => { - this.setState({ loading: false }); - }); + return Promise + .all([ + attachInterface(), + subscribeDefaultAddress((error, defaultAddress) => { + if (!error) { + this.setState({ defaultAddress }); + } + }) + ]) + .then(([state]) => { + this.setState(Object.assign({}, state, { + loading: false + })); }); } + componentWillUnmount () { + return unsubscribeDefaultAddress(); + } + render () { const { loading } = this.state; @@ -75,16 +87,20 @@ export default class Application extends Component { } renderPage () { - const { fromAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state; + const { defaultAddress, registerBusy, url, urlError, contentHash, contentHashError, contentHashOwner, commit, commitError, registerType, repo, repoError } = this.state; let hashClass = null; + if (contentHashError) { - hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning; + hashClass = contentHashOwner !== defaultAddress + ? styles.hashError + : styles.hashWarning; } else if (contentHash) { hashClass = styles.hashOk; } let valueInputs = null; + if (registerType === 'content') { valueInputs = [
@@ -94,7 +110,8 @@ export default class Application extends Component { disabled={ registerBusy } value={ repo } className={ repoError ? styles.error : null } - onChange={ this.onChangeRepo } /> + onChange={ this.onChangeRepo } + />
,
+ onChange={ this.onChangeCommit } + />
]; } else { @@ -115,7 +133,8 @@ export default class Application extends Component { disabled={ registerBusy } value={ url } className={ urlError ? styles.error : null } - onChange={ this.onChangeUrl } /> + onChange={ this.onChangeUrl } + />
); } @@ -128,11 +147,17 @@ export default class Application extends Component { + onClick={ this.onClickTypeNormal } + > + File Link + + onClick={ this.onClickTypeContent } + > + Content Bundle +
@@ -148,26 +173,21 @@ export default class Application extends Component {
+ events={ this.state.events } + />
); } renderButtons () { - const { accounts, fromAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state; - const account = accounts[fromAddress]; + const { defaultAddress, urlError, repoError, commitError, contentHashError, contentHashOwner } = this.state; return (
-
- -
+ disabled={ (contentHashError && contentHashOwner !== defaultAddress) || urlError || repoError || commitError } + >register url
); } @@ -264,6 +284,7 @@ export default class Application extends Component { // TODO: field validation if (!urlError) { const parts = url.split('/'); + hasContent = parts.length !== 0; if (parts[2] === 'github.com' || parts[2] === 'raw.githubusercontent.com') { @@ -280,11 +301,11 @@ export default class Application extends Component { } onClickRegister = () => { - const { commit, commitError, contentHashError, contentHashOwner, fromAddress, url, urlError, registerType, repo, repoError } = this.state; + const { defaultAddress, commit, commitError, contentHashError, contentHashOwner, url, urlError, registerType, repo, repoError } = this.state; // TODO: No errors are currently set, validation to be expanded and added for each // field (query is fast to pick up the issues, so not burning atm) - if ((contentHashError && contentHashOwner !== fromAddress) || repoError || urlError || commitError) { + if ((contentHashError && contentHashOwner !== defaultAddress) || repoError || urlError || commitError) { return; } @@ -354,12 +375,15 @@ export default class Application extends Component { } registerContent (contentRepo, contentCommit) { - const { contentHash, fromAddress, instance } = this.state; - contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`; + const { defaultAddress, contentHash, instance } = this.state; + + contentCommit = contentCommit.substr(0, 2) === '0x' + ? contentCommit + : `0x${contentCommit}`; const eventId = nextEventId++; const values = [contentHash, contentRepo, contentCommit]; - const options = { from: fromAddress }; + const options = { from: defaultAddress }; this.setState({ eventIds: [eventId].concat(this.state.eventIds), @@ -368,7 +392,7 @@ export default class Application extends Component { contentHash, contentRepo, contentCommit, - fromAddress, + defaultAddress, registerBusy: true, registerState: 'Estimating gas for the transaction', timestamp: new Date() @@ -396,6 +420,7 @@ export default class Application extends Component { }); const gasPassed = gas.mul(1.2); + options.gas = gasPassed.toFixed(0); console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`); @@ -405,11 +430,11 @@ export default class Application extends Component { } registerUrl (contentUrl) { - const { contentHash, fromAddress, instance } = this.state; + const { contentHash, defaultAddress, instance } = this.state; const eventId = nextEventId++; const values = [contentHash, contentUrl]; - const options = { from: fromAddress }; + const options = { from: defaultAddress }; this.setState({ eventIds: [eventId].concat(this.state.eventIds), @@ -417,7 +442,7 @@ export default class Application extends Component { [eventId]: { contentHash, contentUrl, - fromAddress, + defaultAddress, registerBusy: true, registerState: 'Estimating gas for the transaction', timestamp: new Date() @@ -445,6 +470,7 @@ export default class Application extends Component { }); const gasPassed = gas.mul(1.2); + options.gas = gasPassed.toFixed(0); console.log(`gas estimated at ${gas.toFormat(0)}, passing ${gasPassed.toFormat(0)}`); @@ -453,25 +479,6 @@ export default class Application extends Component { ); } - onSelectFromAddress = () => { - const { accounts, fromAddress } = this.state; - const addresses = Object.keys(accounts); - let index = 0; - - addresses.forEach((address, _index) => { - if (address === fromAddress) { - index = _index; - } - }); - - index++; - if (index >= addresses.length) { - index = 0; - } - - this.setState({ fromAddress: addresses[index] }); - } - lookupHash (url) { const { instance } = this.state; diff --git a/js/src/dapps/githubhint/services.js b/js/src/dapps/githubhint/services.js index 6aae1c8e9..cc1ef3416 100644 --- a/js/src/dapps/githubhint/services.js +++ b/js/src/dapps/githubhint/services.js @@ -17,48 +17,44 @@ import * as abis from '~/contracts/abi'; import { api } from './parity'; +let defaultSubscriptionId; + export function attachInterface () { return api.parity .registryAddress() .then((registryAddress) => { console.log(`the registry was found at ${registryAddress}`); - const registry = api.newContract(abis.registry, registryAddress).instance; - - return Promise - .all([ - registry.getAddress.call({}, [api.util.sha3('githubhint'), 'A']), - api.parity.accountsInfo() - ]); + return api + .newContract(abis.registry, registryAddress).instance + .getAddress.call({}, [api.util.sha3('githubhint'), 'A']); }) - .then(([address, accountsInfo]) => { + .then((address) => { console.log(`githubhint was found at ${address}`); const contract = api.newContract(abis.githubhint, address); - const accounts = Object - .keys(accountsInfo) - .reduce((obj, address) => { - const account = accountsInfo[address]; - - return Object.assign(obj, { - [address]: { - address, - name: account.name - } - }); - }, {}); - const fromAddress = Object.keys(accounts)[0]; return { - accounts, address, - accountsInfo, contract, - instance: contract.instance, - fromAddress + instance: contract.instance }; }) .catch((error) => { console.error('attachInterface', error); }); } + +export function subscribeDefaultAddress (callback) { + return api + .subscribe('parity_defaultAccount', callback) + .then((subscriptionId) => { + defaultSubscriptionId = subscriptionId; + + return defaultSubscriptionId; + }); +} + +export function unsubscribeDefaultAddress () { + return api.unsubscribe(defaultSubscriptionId); +} diff --git a/js/src/dapps/githubhint/IdentityIcon/identityIcon.css b/js/src/dapps/localtx/IdentityIcon/identityIcon.css similarity index 100% rename from js/src/dapps/githubhint/IdentityIcon/identityIcon.css rename to js/src/dapps/localtx/IdentityIcon/identityIcon.css diff --git a/js/src/dapps/githubhint/IdentityIcon/identityIcon.js b/js/src/dapps/localtx/IdentityIcon/identityIcon.js similarity index 100% rename from js/src/dapps/githubhint/IdentityIcon/identityIcon.js rename to js/src/dapps/localtx/IdentityIcon/identityIcon.js diff --git a/js/src/dapps/githubhint/IdentityIcon/index.js b/js/src/dapps/localtx/IdentityIcon/index.js similarity index 100% rename from js/src/dapps/githubhint/IdentityIcon/index.js rename to js/src/dapps/localtx/IdentityIcon/index.js diff --git a/js/src/dapps/localtx/Transaction/transaction.js b/js/src/dapps/localtx/Transaction/transaction.js index 1756597ab..a2f023902 100644 --- a/js/src/dapps/localtx/Transaction/transaction.js +++ b/js/src/dapps/localtx/Transaction/transaction.js @@ -22,7 +22,7 @@ import { api } from '../parity'; import styles from './transaction.css'; -import IdentityIcon from '../../githubhint/IdentityIcon'; +import IdentityIcon from '../IdentityIcon'; class BaseTransaction extends Component { diff --git a/js/src/dapps/registry/Lookup/actions.js b/js/src/dapps/registry/Lookup/actions.js index 3a8ef515e..dc64e58b1 100644 --- a/js/src/dapps/registry/Lookup/actions.js +++ b/js/src/dapps/registry/Lookup/actions.js @@ -81,6 +81,7 @@ export const ownerLookup = (name) => (dispatch, getState) => { return; } + name = name.toLowerCase(); dispatch(ownerLookupStart(name)); return getOwner(contract, name) diff --git a/js/src/dapps/registry/Lookup/lookup.js b/js/src/dapps/registry/Lookup/lookup.js index f572cbb7d..f88663fad 100644 --- a/js/src/dapps/registry/Lookup/lookup.js +++ b/js/src/dapps/registry/Lookup/lookup.js @@ -70,7 +70,7 @@ class Lookup extends Component { - + { - this.setState(state, () => { - this.setState({ loading: false }); - }); + this.setState(Object.assign({}, state, { loading: false })); return attachBlockNumber(state.instance, (state) => { this.setState(state); @@ -80,22 +77,21 @@ export default class Application extends Component { return (
+ totalSignatures={ totalSignatures } + /> ); } renderImport () { - const { accounts, fromAddress, instance, showImport } = this.state; + const { instance, showImport } = this.state; if (showImport) { return ( + /> ); } @@ -112,7 +108,8 @@ export default class Application extends Component { return ( + contract={ contract } + /> ); } @@ -121,10 +118,4 @@ export default class Application extends Component { showImport: !this.state.showImport }); } - - setFromAddress = (fromAddress) => { - this.setState({ - fromAddress - }); - } } diff --git a/js/src/dapps/signaturereg/Import/import.js b/js/src/dapps/signaturereg/Import/import.js index 1281bfc93..028220d15 100644 --- a/js/src/dapps/signaturereg/Import/import.js +++ b/js/src/dapps/signaturereg/Import/import.js @@ -19,18 +19,14 @@ import React, { Component, PropTypes } from 'react'; import { api } from '../parity'; import { callRegister, postRegister } from '../services'; import Button from '../Button'; -import IdentityIcon from '../IdentityIcon'; import styles from './import.css'; export default class Import extends Component { static propTypes = { - accounts: PropTypes.object.isRequired, - fromAddress: PropTypes.string.isRequired, instance: PropTypes.object.isRequired, visible: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - onSetFromAddress: PropTypes.func.isRequired + onClose: PropTypes.func.isRequired } state = { @@ -83,21 +79,12 @@ export default class Import extends Component { } renderRegister () { - const { accounts, fromAddress } = this.props; - - const account = accounts[fromAddress]; const count = this.countFunctions(); let buttons = null; if (count) { buttons = (
-
- -
@@ -197,15 +184,15 @@ export default class Import extends Component { } onRegister = () => { - const { instance, fromAddress, onClose } = this.props; + const { instance, onClose } = this.props; const { functions, fnstate } = this.state; - Promise + return Promise .all( functions .filter((fn) => !fn.constant) .filter((fn) => fnstate[fn.signature] === 'fntodo') - .map((fn) => postRegister(instance, fn.id, { from: fromAddress })) + .map((fn) => postRegister(instance, fn.id, {})) ) .then(() => { onClose(); @@ -214,23 +201,4 @@ export default class Import extends Component { console.error('onRegister', error); }); } - - onSelectFromAddress = () => { - const { accounts, fromAddress, onSetFromAddress } = this.props; - const addresses = Object.keys(accounts); - let index = 0; - - addresses.forEach((address, _index) => { - if (address === fromAddress) { - index = _index; - } - }); - - index++; - if (index >= addresses.length) { - index = 0; - } - - onSetFromAddress(addresses[index]); - } } diff --git a/js/src/dapps/signaturereg/services.js b/js/src/dapps/signaturereg/services.js index c615399ae..c957de80f 100644 --- a/js/src/dapps/signaturereg/services.js +++ b/js/src/dapps/signaturereg/services.js @@ -166,8 +166,13 @@ export function callRegister (instance, id, options = {}) { } export function postRegister (instance, id, options = {}) { - return instance.register - .estimateGas(options, [id]) + return api.parity + .defaultAccount() + .then((defaultAddress) => { + options.from = defaultAddress; + + return instance.register.estimateGas(options, [id]); + }) .then((gas) => { options.gas = gas.mul(1.2).toFixed(0); console.log('postRegister', `gas estimated at ${gas.toFormat(0)}, setting to ${gas.mul(1.2).toFormat(0)}`); diff --git a/js/src/jsonrpc/interfaces/parity.js b/js/src/jsonrpc/interfaces/parity.js index 1f6caca4a..17e8cdd0d 100644 --- a/js/src/jsonrpc/interfaces/parity.js +++ b/js/src/jsonrpc/interfaces/parity.js @@ -16,6 +16,9 @@ import { Address, Data, Hash, Quantity } from '../types'; +// DUMMY for beta +const SECTION_ACCOUNTS = null; + export default { acceptNonReservedPeers: { desc: '?', @@ -135,6 +138,17 @@ export default { } }, + defaultAccount: { + section: SECTION_ACCOUNTS, + desc: 'Returns the defaultAccount that is to be used with transactions', + params: [], + returns: { + type: Address, + desc: 'The account address', + example: '0x63Cf90D3f0410092FC0fca41846f596223979195' + } + }, + defaultExtraData: { desc: 'Returns the default extra data', params: [], diff --git a/js/src/modals/AddDapps/addDapps.css b/js/src/modals/AddDapps/addDapps.css index 120ee0870..d43c7b521 100644 --- a/js/src/modals/AddDapps/addDapps.css +++ b/js/src/modals/AddDapps/addDapps.css @@ -15,15 +15,23 @@ /* along with Parity. If not, see . */ +.modal { + flex-direction: column; +} + +.container { + overflow-y: auto; +} + .description { margin-top: .5em !important; } .list { + margin-bottom: 1.5em; + .background { - background: rgba(255, 255, 255, 0.2); - margin: 0 -1.5em; - padding: 0.5em 1.5em; + padding: 0.5em 0; } .header { @@ -37,3 +45,26 @@ opacity: 0.75; } } + +.selectIcon { + position: absolute; + right: 0.5em; + top: 0.5em; +} + +.selected, +.unselected { + position: relative; +} + +.unselected { + background: rgba(0, 0, 0, 0.4) !important; + + .selectIcon { + opacity: 0.15; + } +} + +.selected { + background: rgba(255, 255, 255, 0.15) !important; +} diff --git a/js/src/modals/AddDapps/addDapps.js b/js/src/modals/AddDapps/addDapps.js index f079c900f..9b1fcc760 100644 --- a/js/src/modals/AddDapps/addDapps.js +++ b/js/src/modals/AddDapps/addDapps.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,14 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { Checkbox } from 'material-ui'; -import { List, ListItem } from 'material-ui/List'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Modal, Button } from '~/ui'; -import { DoneIcon } from '~/ui/Icons'; +import { DappCard, Portal, SectionList } from '~/ui'; +import { CheckIcon } from '~/ui/Icons'; import styles from './addDapps.css'; @@ -39,61 +37,61 @@ export default class AddDapps extends Component { } return ( - } - key='done' - label={ - - } - onClick={ store.closeModal } /> - ] } - compact + + defaultMessage='visible applications' + /> } - visible> -
- { - this.renderList(store.sortedLocal, - , - - ) - } - { - this.renderList(store.sortedBuiltin, - , - - ) - } - { - this.renderList(store.sortedNetwork, - , - - ) - } - + > +
+
+ { + this.renderList(store.sortedLocal, store.displayApps, + , + + ) + } + { + this.renderList(store.sortedBuiltin, store.displayApps, + , + + ) + } + { + this.renderList(store.sortedNetwork, store.displayApps, + , + + ) + } +
+ ); } - renderList (items, header, byline) { + renderList (items, visibleItems, header, byline) { if (!items || !items.length) { return null; } @@ -104,41 +102,40 @@ export default class AddDapps extends Component {
{ header }
{ byline }
- - { items.map(this.renderApp) } - +
); } renderApp = (app) => { const { store } = this.props; - const isHidden = !store.displayApps[app.id].visible; + const isVisible = store.displayApps[app.id].visible; - const onCheck = () => { - if (isHidden) { - store.showApp(app.id); - } else { + const onClick = () => { + if (isVisible) { store.hideApp(app.id); + } else { + store.showApp(app.id); } }; return ( - - } - primaryText={ app.name } - secondaryText={ -
- { app.description } -
- } - /> + onClick={ onClick } + > + + ); } } diff --git a/js/src/modals/AddDapps/addDapps.spec.js b/js/src/modals/AddDapps/addDapps.spec.js index 66bd286bc..79b4489c6 100644 --- a/js/src/modals/AddDapps/addDapps.spec.js +++ b/js/src/modals/AddDapps/addDapps.spec.js @@ -33,13 +33,13 @@ describe('modals/AddDapps', () => { it('does not render the modal with modalOpen = false', () => { expect( - renderShallow({ modalOpen: false }).find('Connect(Modal)') + renderShallow({ modalOpen: false }).find('Portal') ).to.have.length(0); }); it('does render the modal with modalOpen = true', () => { expect( - renderShallow({ modalOpen: true }).find('Connect(Modal)') + renderShallow({ modalOpen: true }).find('Portal') ).to.have.length(1); }); }); diff --git a/js/src/modals/DappPermissions/dappPermissions.css b/js/src/modals/DappPermissions/dappPermissions.css index 64be7dea8..8df12697f 100644 --- a/js/src/modals/DappPermissions/dappPermissions.css +++ b/js/src/modals/DappPermissions/dappPermissions.css @@ -15,33 +15,54 @@ /* along with Parity. If not, see . */ +.container { + overflow-y: auto; +} + .item { - .info { - display: inline-block; + display: flex; + flex: 1; + position: relative; - .address { - opacity: 0.75; - } - - .description { - margin-top: 0.5em; - opacity: 0.75; - } - - .name { - margin: 0.5em 0; - text-transform: uppercase; - } + .overlay { + position: absolute; + right: 0.5em; + top: 0.5em; } } .selected, .unselected { margin-bottom: 0.25em; + + &:focus { + outline: none; + } +} + +.unselected { + background: rgba(0, 0, 0, 0.4) !important; } .selected { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.15) !important; + + &.default { + background: rgba(255, 255, 255, 0.35) !important; + } } .unselected { } + +.iconDisabled { + opacity: 0.15; +} + +.legend { + opacity: 0.75; + + span { + line-height: 24px; + vertical-align: top; + } +} diff --git a/js/src/modals/DappPermissions/dappPermissions.js b/js/src/modals/DappPermissions/dappPermissions.js index c6498b282..4cd7cc837 100644 --- a/js/src/modals/DappPermissions/dappPermissions.js +++ b/js/src/modals/DappPermissions/dappPermissions.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,14 +14,12 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { Checkbox } from 'material-ui'; -import { List, ListItem } from 'material-ui/List'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Button, IdentityIcon, Modal } from '~/ui'; -import { DoneIcon } from '~/ui/Icons'; +import { AccountCard, Portal, SectionList } from '~/ui'; +import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons'; import styles from './dappPermissions.css'; @@ -39,74 +37,80 @@ export default class DappPermissions extends Component { } return ( - } - key='done' - label={ - - } - onClick={ store.closeModal } /> - ] } - compact + + , + defaultIcon: + } } + /> +
+ } + onClose={ store.closeModal } + open title={ + defaultMessage='visible dapp accounts' + /> } - visible> - - { this.renderListItems() } - - + > +
+ +
+ ); } - renderListItems () { + renderAccount = (account) => { const { store } = this.props; - return store.accounts.map((account) => { - const onCheck = () => { - store.selectAccount(account.address); - }; + const onMakeDefault = () => { + store.setDefaultAccount(account.address); + }; - // TODO: Once new modal & account selection is in, this should be updated - // to conform to the new (as of this code WIP) look & feel for selection. - // For now in the current/old style, not as pretty but consistent. - return ( - { + store.selectAccount(account.address); + }; + + let className; + + if (account.checked) { + className = account.default + ? `${styles.selected} ${styles.default}` + : styles.selected; + } else { + className = styles.unselected; + } + + return ( +
+ +
+ { + account.checked && account.default + ? + : + } + { account.checked - ? styles.selected - : styles.unselected + ? + : } - key={ account.address } - leftCheckbox={ - - } - primaryText={ -
- -
-

- { account.name } -

-
- { account.address } -
-
- { account.description } -
-
-
- } /> - ); - }); +
+
+ ); } } diff --git a/js/src/modals/DappPermissions/dappPermissions.spec.js b/js/src/modals/DappPermissions/dappPermissions.spec.js index 674e4959c..66183a5bd 100644 --- a/js/src/modals/DappPermissions/dappPermissions.spec.js +++ b/js/src/modals/DappPermissions/dappPermissions.spec.js @@ -33,13 +33,13 @@ describe('modals/DappPermissions', () => { it('does not render the modal with modalOpen = false', () => { expect( - renderShallow({ modalOpen: false }).find('Connect(Modal)') + renderShallow({ modalOpen: false }).find('Portal') ).to.have.length(0); }); it('does render the modal with modalOpen = true', () => { expect( - renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)') + renderShallow({ modalOpen: true, accounts: [] }).find('Portal') ).to.have.length(1); }); }); diff --git a/js/src/modals/DappPermissions/store.js b/js/src/modals/DappPermissions/store.js index 21aa8707f..7ad0164c1 100644 --- a/js/src/modals/DappPermissions/store.js +++ b/js/src/modals/DappPermissions/store.js @@ -29,12 +29,17 @@ export default class Store { @action closeModal = () => { transaction(() => { - const accounts = this.accounts - .filter((account) => account.checked) - .map((account) => account.address); + let addresses = null; + const checkedAccounts = this.accounts.filter((account) => account.checked); + + if (checkedAccounts.length) { + addresses = checkedAccounts.filter((account) => account.default) + .concat(checkedAccounts.filter((account) => !account.default)) + .map((account) => account.address); + } this.modalOpen = false; - this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts); + this.updateWhitelist(addresses); }); } @@ -42,12 +47,15 @@ export default class Store { transaction(() => { this.accounts = Object .values(accounts) - .map((account) => { + .map((account, index) => { return { address: account.address, checked: this.whitelist ? this.whitelist.includes(account.address) : true, + default: this.whitelist + ? this.whitelist[0] === account.address + : index === 0, description: account.meta.description, name: account.name }; @@ -57,9 +65,31 @@ export default class Store { } @action selectAccount = (address) => { + transaction(() => { + this.accounts = this.accounts.map((account) => { + if (account.address === address) { + account.checked = !account.checked; + account.default = false; + } + + return account; + }); + + this.setDefaultAccount(( + this.accounts.find((account) => account.default) || + this.accounts.find((account) => account.checked) || + {} + ).address); + }); + } + + @action setDefaultAccount = (address) => { this.accounts = this.accounts.map((account) => { if (account.address === address) { - account.checked = !account.checked; + account.checked = true; + account.default = true; + } else if (account.default) { + account.default = false; } return account; diff --git a/js/src/modals/DappPermissions/store.spec.js b/js/src/modals/DappPermissions/store.spec.js index 819099a19..5ccd9632f 100644 --- a/js/src/modals/DappPermissions/store.spec.js +++ b/js/src/modals/DappPermissions/store.spec.js @@ -23,21 +23,25 @@ const ACCOUNTS = { '456': { address: '456', name: '456', meta: { description: '456' } }, '789': { address: '789', name: '789', meta: { description: '789' } } }; -const WHITELIST = ['123', '456']; +const WHITELIST = ['456', '789']; + +let api; +let store; + +function create () { + api = { + parity: { + getNewDappsWhitelist: sinon.stub().resolves(WHITELIST), + setNewDappsWhitelist: sinon.stub().resolves(true) + } + }; + + store = new Store(api); +} describe('modals/DappPermissions/store', () => { - let api; - let store; - beforeEach(() => { - api = { - parity: { - getNewDappsWhitelist: sinon.stub().resolves(WHITELIST), - setNewDappsWhitelist: sinon.stub().resolves(true) - } - }; - - store = new Store(api); + create(); }); describe('constructor', () => { @@ -51,49 +55,71 @@ describe('modals/DappPermissions/store', () => { }); describe('@actions', () => { - describe('openModal', () => { - beforeEach(() => { - store.openModal(ACCOUNTS); - }); + beforeEach(() => { + store.openModal(ACCOUNTS); + }); + describe('openModal', () => { it('sets the modalOpen status', () => { expect(store.modalOpen).to.be.true; }); it('sets accounts with checked interfaces', () => { expect(store.accounts.peek()).to.deep.equal([ - { address: '123', name: '123', description: '123', checked: true }, - { address: '456', name: '456', description: '456', checked: true }, - { address: '789', name: '789', description: '789', checked: false } + { address: '123', name: '123', description: '123', default: false, checked: false }, + { address: '456', name: '456', description: '456', default: true, checked: true }, + { address: '789', name: '789', description: '789', default: false, checked: true } ]); }); }); describe('closeModal', () => { beforeEach(() => { - store.openModal(ACCOUNTS); - store.selectAccount('789'); + store.setDefaultAccount('789'); store.closeModal(); }); it('calls setNewDappsWhitelist', () => { expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce; }); + + it('has the default account in first position', () => { + expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith(['789', '456']); + }); }); describe('selectAccount', () => { beforeEach(() => { - store.openModal(ACCOUNTS); store.selectAccount('123'); store.selectAccount('789'); }); it('unselects previous selected accounts', () => { - expect(store.accounts.find((account) => account.address === '123').checked).to.be.false; + expect(store.accounts.find((account) => account.address === '123').checked).to.be.true; }); it('selects previous unselected accounts', () => { - expect(store.accounts.find((account) => account.address === '789').checked).to.be.true; + expect(store.accounts.find((account) => account.address === '789').checked).to.be.false; + }); + + it('sets a new default when default was unselected', () => { + store.selectAccount('456'); + expect(store.accounts.find((account) => account.address === '456').default).to.be.false; + expect(store.accounts.find((account) => account.address === '123').default).to.be.true; + }); + }); + + describe('setDefaultAccount', () => { + beforeEach(() => { + store.setDefaultAccount('789'); + }); + + it('unselects previous default', () => { + expect(store.accounts.find((account) => account.address === '456').default).to.be.false; + }); + + it('selects new default', () => { + expect(store.accounts.find((account) => account.address === '789').default).to.be.true; }); }); }); diff --git a/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js b/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js index 4142aa961..268ca1f70 100644 --- a/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js +++ b/js/src/modals/ExecuteContract/AdvancedStep/advancedStep.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -15,42 +15,22 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Input, GasPriceEditor } from '~/ui'; +import { GasPriceEditor } from '~/ui'; import styles from '../executeContract.css'; export default class AdvancedStep extends Component { static propTypes = { - gasStore: PropTypes.object.isRequired, - minBlock: PropTypes.string, - minBlockError: PropTypes.string, - onMinBlockChange: PropTypes.func + gasStore: PropTypes.object.isRequired }; render () { - const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props; + const { gasStore } = this.props; return ( -
- - } - label={ - - } - value={ minBlock } - onSubmit={ onMinBlockChange } /> -
- -
+
+
); } diff --git a/js/src/modals/ExecuteContract/executeContract.js b/js/src/modals/ExecuteContract/executeContract.js index 689678a7c..0828a5f3e 100644 --- a/js/src/modals/ExecuteContract/executeContract.js +++ b/js/src/modals/ExecuteContract/executeContract.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import BigNumber from 'bignumber.js'; import { pick } from 'lodash'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; @@ -41,27 +40,32 @@ const TITLES = { transfer: ( + defaultMessage='function details' + /> ), sending: ( + defaultMessage='sending' + /> ), complete: ( + defaultMessage='complete' + /> ), advanced: ( + defaultMessage='advanced options' + /> ), rejected: ( + defaultMessage='rejected' + /> ) }; const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete]; @@ -95,8 +99,6 @@ class ExecuteContract extends Component { fromAddressError: null, func: null, funcError: null, - minBlock: '0', - minBlockError: null, rejected: false, sending: false, step: STEP_DETAILS, @@ -139,7 +141,8 @@ class ExecuteContract extends Component { advancedOptions ? [STEP_BUSY] : [STEP_BUSY_OR_ADVANCED] - }> + } + > { this.renderExceptionWarning() } { this.renderStep() } @@ -161,8 +164,8 @@ class ExecuteContract extends Component { renderDialogActions () { const { onClose, fromAddress } = this.props; - const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state; - const hasError = fromAddressError || minBlockError || valuesError.find((error) => error); + const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state; + const hasError = fromAddressError || valuesError.find((error) => error); const cancelBtn = (
-
- { depositAddress } -
+ { this.renderAddress(depositAddress, coinSymbol) }
); } + + renderAddress (depositAddress, coinSymbol) { + const qrcode = ( + + ); + let protocolLink = null; + + // TODO: Expand for other coins where protocols are available + switch (coinSymbol) { + case 'BTC': + protocolLink = `bitcoin:${depositAddress}`; + break; + } + + return ( +
+ { + protocolLink + ? ( + + { qrcode } + + ) + : qrcode + } +
+ + { depositAddress } +
+
+ ); + } } diff --git a/js/src/modals/Shapeshift/AwaitingDepositStep/awaitingDepositStep.spec.js b/js/src/modals/Shapeshift/AwaitingDepositStep/awaitingDepositStep.spec.js index 65fdefb07..4868b4047 100644 --- a/js/src/modals/Shapeshift/AwaitingDepositStep/awaitingDepositStep.spec.js +++ b/js/src/modals/Shapeshift/AwaitingDepositStep/awaitingDepositStep.spec.js @@ -19,7 +19,10 @@ import React from 'react'; import AwaitingDepositStep from './'; +const TEST_ADDRESS = '0x123456789123456789123456789123456789'; + let component; +let instance; function render () { component = shallow( @@ -29,6 +32,7 @@ function render () { price: { rate: 0.001, minimum: 0, limit: 1.999 } } } /> ); + instance = component.instance(); return component; } @@ -47,4 +51,61 @@ describe('modals/Shapeshift/AwaitingDepositStep', () => { render({ depositAddress: 'xyz' }); expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/); }); + + describe('instance methods', () => { + describe('renderAddress', () => { + let address; + + beforeEach(() => { + address = shallow(instance.renderAddress(TEST_ADDRESS)); + }); + + it('renders the address', () => { + expect(address.text()).to.contain(TEST_ADDRESS); + }); + + describe('CopyToClipboard', () => { + let copy; + + beforeEach(() => { + copy = address.find('Connect(CopyToClipboard)'); + }); + + it('renders the copy', () => { + expect(copy.length).to.equal(1); + }); + + it('passes the address', () => { + expect(copy.props().data).to.equal(TEST_ADDRESS); + }); + }); + + describe('QrCode', () => { + let qr; + + beforeEach(() => { + qr = address.find('QrCode'); + }); + + it('renders the QrCode', () => { + expect(qr.length).to.equal(1); + }); + + it('passed the address', () => { + expect(qr.props().value).to.equal(TEST_ADDRESS); + }); + + describe('protocol link', () => { + it('does not render a protocol link (unlinked type)', () => { + expect(address.find('a')).to.have.length(0); + }); + + it('renders protocol link for BTC', () => { + address = shallow(instance.renderAddress(TEST_ADDRESS, 'BTC')); + expect(address.find('a').props().href).to.equal(`bitcoin:${TEST_ADDRESS}`); + }); + }); + }); + }); + }); }); diff --git a/js/src/modals/Shapeshift/shapeshift.css b/js/src/modals/Shapeshift/shapeshift.css index 159fbdb71..71a6d129a 100644 --- a/js/src/modals/Shapeshift/shapeshift.css +++ b/js/src/modals/Shapeshift/shapeshift.css @@ -14,9 +14,28 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ + .body { } +.addressInfo { + text-align: center; + + .address { + background: rgba(255, 255, 255, 0.1); + margin: 0.75em 0; + padding: 1em; + + span { + margin-left: 0.75em; + } + } + + .qrcode { + margin: 0.75em 0; + } +} + .shapeshift { position: absolute; bottom: 0.5em; diff --git a/js/src/modals/Transfer/Extras/extras.js b/js/src/modals/Transfer/Extras/extras.js index 630739ac1..61b99b2fb 100644 --- a/js/src/modals/Transfer/Extras/extras.js +++ b/js/src/modals/Transfer/Extras/extras.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -27,37 +27,22 @@ export default class Extras extends Component { dataError: PropTypes.string, gasStore: PropTypes.object.isRequired, isEth: PropTypes.bool, - minBlock: PropTypes.string, - minBlockError: PropTypes.string, onChange: PropTypes.func.isRequired, total: PropTypes.string, totalError: PropTypes.string } render () { - const { gasStore, minBlock, minBlockError, onChange } = this.props; + const { gasStore, onChange } = this.props; return (
{ this.renderData() } - - } - label={ - - } - value={ minBlock } - onChange={ this.onEditMinBlock } />
+ onChange={ onChange } + />
); @@ -76,23 +61,22 @@ export default class Extras extends Component { hint={ + defaultMessage='the data to pass through with the transaction' + /> } label={ + defaultMessage='transaction data' + /> } onChange={ this.onEditData } - value={ data } /> + value={ data } + /> ); } onEditData = (event) => { this.props.onChange('data', event.target.value); } - - onEditMinBlock = (event) => { - this.props.onChange('minBlock', event.target.value); - } } diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 63a29c9a1..5e26ecd49 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -52,9 +52,6 @@ export default class TransferStore { @observable data = ''; @observable dataError = null; - @observable minBlock = '0'; - @observable minBlockError = null; - @observable recipient = ''; @observable recipientError = ERRORS.requireRecipient; @@ -78,6 +75,30 @@ export default class TransferStore { gasStore = null; + constructor (api, props) { + this.api = api; + + const { account, balance, gasLimit, senders, newError, sendersBalances } = props; + + this.account = account; + this.balance = balance; + this.isWallet = account && account.wallet; + this.newError = newError; + + this.gasStore = new GasPriceStore(api, { gasLimit }); + + if (this.isWallet) { + this.wallet = props.wallet; + this.walletContract = new Contract(this.api, walletAbi); + } + + if (senders) { + this.senders = senders; + this.sendersBalances = sendersBalances; + this.senderError = ERRORS.requireSender; + } + } + @computed get steps () { const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC); @@ -90,7 +111,7 @@ export default class TransferStore { @computed get isValid () { const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; - const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError; + const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError; const verifyValid = !this.passwordError; switch (this.stage) { @@ -111,29 +132,6 @@ export default class TransferStore { return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token; } - constructor (api, props) { - this.api = api; - - const { account, balance, gasLimit, senders, newError, sendersBalances } = props; - this.account = account; - this.balance = balance; - this.isWallet = account && account.wallet; - this.newError = newError; - - this.gasStore = new GasPriceStore(api, { gasLimit }); - - if (this.isWallet) { - this.wallet = props.wallet; - this.walletContract = new Contract(this.api, walletAbi); - } - - if (senders) { - this.senders = senders; - this.sendersBalances = sendersBalances; - this.senderError = ERRORS.requireSender; - } - } - @action onNext = () => { this.stage += 1; } @@ -163,9 +161,6 @@ export default class TransferStore { case 'gasPrice': return this._onUpdateGasPrice(value); - case 'minBlock': - return this._onUpdateMinBlock(value); - case 'recipient': return this._onUpdateRecipient(value); @@ -283,14 +278,6 @@ export default class TransferStore { this.recalculate(); } - @action _onUpdateMinBlock = (minBlock) => { - console.log('minBlock', minBlock); - transaction(() => { - this.minBlock = minBlock; - this.minBlockError = this._validatePositiveNumber(minBlock); - }); - } - @action _onUpdateGasPrice = (gasPrice) => { this.recalculate(); } @@ -588,7 +575,7 @@ export default class TransferStore { send () { const { options, values } = this._getTransferParams(); - options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null; + log.debug('@send', 'transfer value', options.value && options.value.toFormat()); return this._getTransferMethod().postTransaction(options, values); @@ -596,6 +583,7 @@ export default class TransferStore { _estimateGas (forceToken = false) { const { options, values } = this._getTransferParams(true, forceToken); + return this._getTransferMethod(true, forceToken).estimateGas(options, values); } @@ -636,15 +624,12 @@ export default class TransferStore { const to = (isEth && !isWallet) ? this.recipient : (this.isWallet ? this.wallet.address : this.token.address); - const options = { + const options = this.gasStore.overrideTransaction({ from: this.sender || this.account.address, to - }; + }); - if (!gas) { - options.gas = this.gasStore.gas; - options.gasPrice = this.gasStore.price; - } else { + if (gas) { options.gas = MAX_GAS_ESTIMATION; } @@ -681,6 +666,7 @@ export default class TransferStore { _validatePositiveNumber (num) { try { const v = new BigNumber(num); + if (v.lt(0)) { return ERRORS.invalidAmount; } diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js index 4139b2b81..5d619b743 100644 --- a/js/src/modals/Transfer/transfer.js +++ b/js/src/modals/Transfer/transfer.js @@ -206,7 +206,7 @@ class Transfer extends Component { return null; } - const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store; + const { isEth, data, dataError, total, totalError } = this.store; return ( diff --git a/js/src/modals/index.js b/js/src/modals/index.js index 702f4d414..4741150b8 100644 --- a/js/src/modals/index.js +++ b/js/src/modals/index.js @@ -16,10 +16,10 @@ import AddAddress from './AddAddress'; import AddContract from './AddContract'; -import AddDapps from './AddDapps'; import CreateAccount from './CreateAccount'; import CreateWallet from './CreateWallet'; import DappPermissions from './DappPermissions'; +import DappsVisible from './AddDapps'; import DeleteAccount from './DeleteAccount'; import DeployContract from './DeployContract'; import EditMeta from './EditMeta'; @@ -37,10 +37,10 @@ import WalletSettings from './WalletSettings'; export { AddAddress, AddContract, - AddDapps, CreateAccount, CreateWallet, DappPermissions, + DappsVisible, DeleteAccount, DeployContract, EditMeta, diff --git a/js/src/playground/index.js b/js/src/playground/index.js new file mode 100644 index 000000000..04d43a76b --- /dev/null +++ b/js/src/playground/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './playground'; diff --git a/js/src/playground/playground.css b/js/src/playground/playground.css new file mode 100644 index 000000000..f4e6e55d4 --- /dev/null +++ b/js/src/playground/playground.css @@ -0,0 +1,90 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +$codeBackground: #002b36; +$codeColor: #93a1a1; + +.container { + background-color: rgba(0, 0, 0, 0.5); + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: 1em; + display: flex; + flex-direction: column; + + .examples { + flex: 1; + overflow: auto; + } +} + +.title { + font-size: 2.25em; + margin-bottom: 1em; + + .select { + font-size: 0.85em; + font-family: monospace; + display: inline-block; + height: 1.5em; + border: 1px solid #aaa; + padding: 0 0.5em; + color: #555; + appearance: none; + } +} + +.exampleContainer { + background-color: rgba(0, 0, 0, 0.5); + padding: 1em; + margin-bottom: 1em; + + &:last-child { + margin-bottom: 0; + } + + p { + font-size: 1.25em; + margin-top: 0; + } +} + +.example { + display: flex; + flex-direction: row; + + .code { + flex: 1; + overflow: auto; + padding: 0.5em; + background-color: #$codeBackground; + color: $codeColor; + font-size: 0.75em; + + code { + white-space: pre; + } + } + + .component { + flex: 3; + padding-left: 0.5em; + } +} diff --git a/js/src/playground/playground.js b/js/src/playground/playground.js new file mode 100644 index 000000000..a1790ed32 --- /dev/null +++ b/js/src/playground/playground.js @@ -0,0 +1,90 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; + +import CurrencySymbol from '~/ui/CurrencySymbol/currencySymbol.example'; +import QrCode from '~/ui/QrCode/qrCode.example'; +import SectionList from '~/ui/SectionList/sectionList.example'; +import Portal from '~/ui/Portal/portal.example'; + +import PlaygroundStore from './store'; +import styles from './playground.css'; + +PlaygroundStore.register(); +PlaygroundStore.register(); +PlaygroundStore.register(); +PlaygroundStore.register(); + +@observer +export default class Playground extends Component { + state = { + selectedIndex: 0 + }; + + store = PlaygroundStore.get(); + + render () { + return ( +
+
+ Playground > + +
+ +
+ { this.renderComponent() } +
+
+ ); + } + + renderOptions () { + const { components } = this.store; + + return components.map((element, index) => { + const name = element.type.displayName || element.type.name; + + return ( + + ); + }); + } + + renderComponent () { + const { components } = this.store; + const { selectedIndex } = this.state; + + return components[selectedIndex]; + } + + handleChange = (event) => { + const { value } = event.target; + + this.setState({ selectedIndex: value }); + } +} diff --git a/js/src/playground/playground.spec.js b/js/src/playground/playground.spec.js new file mode 100644 index 000000000..6ea23af65 --- /dev/null +++ b/js/src/playground/playground.spec.js @@ -0,0 +1,47 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; + +import Playground from './playground'; + +let component; +let options; + +function render (props = {}) { + component = shallow( + + ); + + options = component.find('option'); + + return component; +} + +describe('playground', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('renders multiple options', () => { + expect(options.length).to.be.greaterThan(2); + }); +}); diff --git a/js/src/playground/playgroundExample.js b/js/src/playground/playgroundExample.js new file mode 100644 index 000000000..9d16ef3ff --- /dev/null +++ b/js/src/playground/playgroundExample.js @@ -0,0 +1,55 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import reactElementToJSXString from 'react-element-to-jsx-string'; + +import styles from './playground.css'; + +export default class PlaygroundExample extends Component { + static propTypes = { + children: PropTypes.node, + name: PropTypes.string + }; + + render () { + const { children, name } = this.props; + + return ( +
+ { this.renderName(name) } +
+
+ { reactElementToJSXString(children) } +
+
+ { children } +
+
+
+ ); + } + + renderName (name) { + if (!name) { + return null; + } + + return ( +

{ name }

+ ); + } +} diff --git a/js/src/playground/store.js b/js/src/playground/store.js new file mode 100644 index 000000000..e627cdcc1 --- /dev/null +++ b/js/src/playground/store.js @@ -0,0 +1,51 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable } from 'mobx'; + +let instance = null; + +export default class PlaygroundStore { + @observable components = []; + + static get () { + if (!instance) { + instance = new PlaygroundStore(); + } + + return instance; + } + + static register (component) { + PlaygroundStore.get().add(component); + } + + @action + add (component) { + const name = component.type.displayName || component.type.name; + const hasComponent = this.components.find((c) => { + const cName = c.type.displayName || c.type.name; + + return name && cName && cName === name; + }); + + if (hasComponent) { + return; + } + + this.components.push(component); + } +} diff --git a/js/src/playground/store.spec.js b/js/src/playground/store.spec.js new file mode 100644 index 000000000..27db2411b --- /dev/null +++ b/js/src/playground/store.spec.js @@ -0,0 +1,41 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React from 'react'; + +import QrCode from '~/ui/QrCode/qrCode.example'; + +import PlaygroundStore from './store'; + +describe('playground/store', () => { + let store = PlaygroundStore.get(); + + it('is available', () => { + expect(PlaygroundStore.get()).to.be.ok; + }); + + it('adds new Components', () => { + PlaygroundStore.register(); + expect(store.components.length).greaterThan(0); + }); + + it('adds new Components only once', () => { + PlaygroundStore.register(); + PlaygroundStore.register(); + + expect(store.components.filter((c) => /QrCode/i.test(c.type.name)).length).equal(1); + }); +}); diff --git a/js/src/redux/providers/signerMiddleware.js b/js/src/redux/providers/signerMiddleware.js index ba51d3426..38b05a399 100644 --- a/js/src/redux/providers/signerMiddleware.js +++ b/js/src/redux/providers/signerMiddleware.js @@ -52,7 +52,7 @@ export default class SignerMiddleware { } onConfirmStart = (store, action) => { - const { gas, gasPrice, id, password, payload, wallet } = action.payload; + const { condition, gas = 0, gasPrice = 0, id, password, payload, wallet } = action.payload; const handlePromise = (promise) => { promise @@ -119,7 +119,7 @@ export default class SignerMiddleware { }); } - handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password)); + handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice, condition }, password)); } onRejectStart = (store, action) => { diff --git a/js/src/routes.js b/js/src/routes.js index 20fb3dba6..e38cefd07 100644 --- a/js/src/routes.js +++ b/js/src/routes.js @@ -78,45 +78,57 @@ const routes = [ { path: '/', onEnter: redirectTo('/accounts') }, { path: '/auth', onEnter: redirectTo('/accounts') }, - { path: '/settings', onEnter: redirectTo('/settings/views') }, - - { - path: '/', - component: Application, - childRoutes: [ - { - path: 'accounts', - indexRoute: { component: Accounts }, - childRoutes: accountsRoutes - }, - { - path: 'addresses', - indexRoute: { component: Addresses }, - childRoutes: addressesRoutes - }, - { - path: 'contracts', - indexRoute: { component: Contracts }, - childRoutes: contractsRoutes - }, - { - path: 'status', - indexRoute: { component: Status }, - childRoutes: statusRoutes - }, - { - path: 'settings', - component: Settings, - childRoutes: settingsRoutes - }, - - { path: 'apps', component: Dapps }, - { path: 'app/:id', component: Dapp }, - { path: 'web', component: Web }, - { path: 'web/:url', component: Web }, - { path: 'signer', component: Signer } - ] - } + { path: '/settings', onEnter: redirectTo('/settings/views') } ]; +const appRoutes = [ + { + path: 'accounts', + indexRoute: { component: Accounts }, + childRoutes: accountsRoutes + }, + { + path: 'addresses', + indexRoute: { component: Addresses }, + childRoutes: addressesRoutes + }, + { + path: 'contracts', + indexRoute: { component: Contracts }, + childRoutes: contractsRoutes + }, + { + path: 'status', + indexRoute: { component: Status }, + childRoutes: statusRoutes + }, + { + path: 'settings', + component: Settings, + childRoutes: settingsRoutes + }, + + { path: 'apps', component: Dapps }, + { path: 'app/:id', component: Dapp }, + { path: 'web', component: Web }, + { path: 'web/:url', component: Web }, + { path: 'signer', component: Signer } +]; + +// TODO : use ES6 imports when supported +if (process.env.NODE_ENV !== 'production') { + const Playground = require('./playground').default; + + appRoutes.push({ + path: 'playground', + component: Playground + }); +} + +routes.push({ + path: '/', + component: Application, + childRoutes: appRoutes +}); + export default routes; diff --git a/js/src/ui/AccountCard/accountCard.css b/js/src/ui/AccountCard/accountCard.css index c5e828d97..eec8ab749 100644 --- a/js/src/ui/AccountCard/accountCard.css +++ b/js/src/ui/AccountCard/accountCard.css @@ -20,8 +20,8 @@ margin: 0.5em 0; display: flex; - flex-direction: row; - align-items: center; + flex-direction: column; + align-items: flex-start; background-color: rgba(0, 0, 0, 0.8); @@ -53,6 +53,13 @@ } } +.infoContainer { + display: flex; + flex-direction: row; + margin-bottom: 0.5em; + width: 100%; +} + .description { font-size: 0.75em; color: rgba(255, 255, 255, 0.5); @@ -86,14 +93,10 @@ .accountName { font-weight: 700 !important; } - } .balance { - .tag { - margin-left: 0.5em; - font-size: 0.85em; - } + margin-top: 0; } @keyframes copied { diff --git a/js/src/ui/AccountCard/accountCard.js b/js/src/ui/AccountCard/accountCard.js index 518dfaaa6..61d88a37f 100644 --- a/js/src/ui/AccountCard/accountCard.js +++ b/js/src/ui/AccountCard/accountCard.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -18,21 +18,20 @@ import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import keycode from 'keycode'; +import Balance from '~/ui/Balance'; import IdentityIcon from '~/ui/IdentityIcon'; +import IdentityName from '~/ui/IdentityName'; import Tags from '~/ui/Tags'; -import { fromWei } from '~/api/util/wei'; - import styles from './accountCard.css'; export default class AccountCard extends Component { - static propTypes = { account: PropTypes.object.isRequired, - onClick: PropTypes.func.isRequired, - onFocus: PropTypes.func.isRequired, - - balance: PropTypes.object + balance: PropTypes.object, + className: PropTypes.string, + onClick: PropTypes.func, + onFocus: PropTypes.func }; state = { @@ -40,15 +39,11 @@ export default class AccountCard extends Component { }; render () { - const { account } = this.props; + const { account, balance, className } = this.props; const { copied } = this.state; - - const { address, name, description, meta = {} } = account; - - const displayName = (name && name.toUpperCase()) || address; + const { address, description, meta = {}, name } = account; const { tags = [] } = meta; - - const classes = [ styles.account ]; + const classes = [ styles.account, className ]; if (copied) { classes.push(styles.copied); @@ -63,17 +58,28 @@ export default class AccountCard extends Component { onFocus={ this.onFocus } onKeyDown={ this.handleKeyDown } > - -
-
- { displayName } +
+ +
+
+ +
+ { this.renderDescription(description) } + { this.renderAddress(address) }
- - { this.renderTags(tags, address) } - { this.renderDescription(description) } - { this.renderAddress(displayName, address) } - { this.renderBalance(address) }
+ + +
); } @@ -90,11 +96,7 @@ export default class AccountCard extends Component { ); } - renderAddress (name, address) { - if (name === address) { - return null; - } - + renderAddress (address) { return (
- ); - } - - renderBalance (address) { - const { balance = {} } = this.props; - - if (!balance.tokens) { - return null; - } - - const ethToken = balance.tokens - .find((tok) => tok.token && (tok.token.tag || '').toLowerCase() === 'eth'); - - if (!ethToken) { - return null; - } - - const value = fromWei(ethToken.value).toFormat(3); - - return ( -
- { value } - ETH -
- ); - } - handleKeyDown = (event) => { const codeName = keycode(event); @@ -158,6 +126,7 @@ export default class AccountCard extends Component { // @see https://developers.google.com/web/updates/2015/04/cut-and-copy-commands try { const range = document.createRange(); + range.selectNode(element); window.getSelection().addRange(range); document.execCommand('copy'); @@ -184,12 +153,14 @@ export default class AccountCard extends Component { onClick = () => { const { account, onClick } = this.props; - onClick(account.address); + + onClick && onClick(account.address); } onFocus = () => { const { account, onFocus } = this.props; - onFocus(account.index); + + onFocus && onFocus(account.index); } preventEvent = (e) => { diff --git a/js/src/ui/AccountCard/accountCard.spec.js b/js/src/ui/AccountCard/accountCard.spec.js new file mode 100644 index 000000000..ba4791778 --- /dev/null +++ b/js/src/ui/AccountCard/accountCard.spec.js @@ -0,0 +1,133 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import AccountCard from './'; + +const TEST_ADDRESS = '0x1234567890123456789012345678901234567890'; +const TEST_NAME = 'Jimmy'; + +let component; +let onClick; +let onFocus; + +function render (props = {}) { + if (!props.account) { + props.account = { + address: TEST_ADDRESS, + description: 'testDescription', + name: TEST_NAME, + meta: {} + }; + } + + onClick = sinon.stub(); + onFocus = sinon.stub(); + + component = shallow( + + ); + + return component; +} + +describe('ui/AccountCard', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('components', () => { + describe('Balance', () => { + let balance; + + beforeEach(() => { + balance = component.find('Connect(Balance)'); + }); + + it('renders the balance', () => { + expect(balance.length).to.equal(1); + }); + + it('sets showOnlyEth & showZeroValues', () => { + expect(balance.props().showOnlyEth).to.be.true; + expect(balance.props().showZeroValues).to.be.true; + }); + }); + + describe('IdentityIcon', () => { + let icon; + + beforeEach(() => { + icon = component.find('Connect(IdentityIcon)'); + }); + + it('renders the icon', () => { + expect(icon.length).to.equal(1); + }); + + it('passes the address through', () => { + expect(icon.props().address).to.equal(TEST_ADDRESS); + }); + }); + + describe('IdentityName', () => { + let name; + + beforeEach(() => { + name = component.find('Connect(IdentityName)'); + }); + + it('renders the name', () => { + expect(name.length).to.equal(1); + }); + + it('passes the address through', () => { + expect(name.props().address).to.equal(TEST_ADDRESS); + }); + + it('passes the name through', () => { + expect(name.props().name).to.equal(TEST_NAME); + }); + + it('renders unknown (no name)', () => { + expect(name.props().unknown).to.be.true; + }); + }); + + describe('Tags', () => { + let tags; + + beforeEach(() => { + tags = component.find('Tags'); + }); + + it('renders the tags', () => { + expect(tags.length).to.equal(1); + }); + }); + }); +}); diff --git a/js/src/ui/Balance/balance.js b/js/src/ui/Balance/balance.js index d4c624b6a..7663b3d7b 100644 --- a/js/src/ui/Balance/balance.js +++ b/js/src/ui/Balance/balance.js @@ -16,31 +16,46 @@ import BigNumber from 'bignumber.js'; import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import unknownImage from '../../../assets/images/contracts/unknown-64x64.png'; +import unknownImage from '~/../assets/images/contracts/unknown-64x64.png'; + import styles from './balance.css'; class Balance extends Component { static contextTypes = { api: PropTypes.object - } + }; static propTypes = { balance: PropTypes.object, - images: PropTypes.object.isRequired - } + className: PropTypes.string, + images: PropTypes.object.isRequired, + showOnlyEth: PropTypes.bool, + showZeroValues: PropTypes.bool + }; + + static defaultProps = { + showOnlyEth: false, + showZeroValues: false + }; render () { const { api } = this.context; - const { balance, images } = this.props; + const { balance, className, images, showZeroValues, showOnlyEth } = this.props; - if (!balance) { + if (!balance || !balance.tokens) { return null; } - let body = (balance.tokens || []) - .filter((balance) => new BigNumber(balance.value).gt(0)) + let body = balance.tokens + .filter((balance) => { + const hasBalance = showZeroValues || new BigNumber(balance.value).gt(0); + const isValidToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth'; + + return hasBalance && isValidToken; + }) .map((balance, index) => { const token = balance.token; @@ -92,13 +107,16 @@ class Balance extends Component { if (!body.length) { body = (
- There are no balances associated with this account +
); } return ( -
+
{ body }
); diff --git a/js/src/ui/Balance/balance.spec.js b/js/src/ui/Balance/balance.spec.js new file mode 100644 index 000000000..f12e84851 --- /dev/null +++ b/js/src/ui/Balance/balance.spec.js @@ -0,0 +1,122 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import apiutil from '~/api/util'; + +import Balance from './'; + +const BALANCE = { + tokens: [ + { value: '122', token: { tag: 'ETH' } }, + { value: '345', token: { tag: 'GAV', format: 1 } }, + { value: '0', token: { tag: 'TST', format: 1 } } + ] +}; + +let api; +let component; +let store; + +function createApi () { + api = { + dappsUrl: 'http://testDapps:1234/', + util: apiutil + }; + + return api; +} + +function createStore () { + store = { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return { + images: {} + }; + } + }; + + return store; +} + +function render (props = {}) { + if (!props.balance) { + props.balance = BALANCE; + } + + component = shallow( + , + { + context: { + store: createStore() + } + } + ).find('Balance').shallow({ context: { api: createApi() } }); + + return component; +} + +describe('ui/Balance', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('passes the specified className', () => { + expect(component.hasClass('testClass')).to.be.true; + }); + + it('renders all the non-zero balances', () => { + expect(component.find('img')).to.have.length(2); + }); + + describe('render specifiers', () => { + it('renders only the single token with showOnlyEth', () => { + render({ showOnlyEth: true }); + expect(component.find('img')).to.have.length(1); + }); + + it('renders all the tokens with showZeroValues', () => { + render({ showZeroValues: true }); + expect(component.find('img')).to.have.length(3); + }); + + it('shows ETH with zero value with showOnlyEth & showZeroValues', () => { + render({ + showOnlyEth: true, + showZeroValues: true, + balance: { + tokens: [ + { value: '0', token: { tag: 'ETH' } }, + { value: '345', token: { tag: 'GAV', format: 1 } } + ] + } + }); + expect(component.find('img')).to.have.length(1); + }); + }); +}); diff --git a/js/src/ui/Container/Title/title.css b/js/src/ui/Container/Title/title.css index 9b636c034..65c86aad3 100644 --- a/js/src/ui/Container/Title/title.css +++ b/js/src/ui/Container/Title/title.css @@ -14,30 +14,36 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ -.byline, .description { + +$bylineColor: #aaa; +$bylineLineHeight: 1.2rem; +$bylineMaxHeight: 2.4rem; +$titleLineHeight: 2rem; +$smallFontSize: 0.75rem; + +.byline, +.description { + color: $bylineColor; + display: -webkit-box; + line-height: $bylineLineHeight; + max-height: $bylineMaxHeight; overflow: hidden; position: relative; - line-height: 1.2em; - max-height: 2.4em; - - display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; - color: #aaa; - * { - color: #aaa !important; + color: $bylineColor !important; } } .description { - font-size: 0.75em; + font-size: $smallFontSize; margin: 0.5em 0 0; } .title { - text-transform: uppercase; + line-height: $titleLineHeight; margin: 0; - line-height: 34px; + text-transform: uppercase; } diff --git a/js/src/ui/Container/Title/title.js b/js/src/ui/Container/Title/title.js index ccd3f9d0e..7e48dc0cd 100644 --- a/js/src/ui/Container/Title/title.js +++ b/js/src/ui/Container/Title/title.js @@ -29,29 +29,41 @@ export default class Title extends Component { } render () { - const { byline, className, title } = this.props; - - const byLine = typeof byline === 'string' - ? ( - - { byline } - - ) - : byline; + const { className, title } = this.props; return (

{ title }

-
- { byLine } -
+ { this.renderByline() } { this.renderDescription() }
); } + renderByline () { + const { byline } = this.props; + + if (!byline) { + return null; + } + + return ( +
+ { + typeof byline === 'string' + ? ( + + { byline } + + ) + : byline + } +
+ ); + } + renderDescription () { const { description } = this.props; @@ -59,17 +71,17 @@ export default class Title extends Component { return null; } - const desc = typeof description === 'string' - ? ( - - { description } - - ) - : description; - return (
- { desc } + { + typeof description === 'string' + ? ( + + { description } + + ) + : description + }
); } diff --git a/js/src/ui/Container/container.js b/js/src/ui/Container/container.js index 5d4127959..0de51e543 100644 --- a/js/src/ui/Container/container.js +++ b/js/src/ui/Container/container.js @@ -29,15 +29,14 @@ export default class Container extends Component { className: PropTypes.string, compact: PropTypes.bool, light: PropTypes.bool, + onClick: PropTypes.func, style: PropTypes.object, tabIndex: PropTypes.number, title: nodeOrStringProptype() } render () { - const { children, className, compact, light, style, tabIndex } = this.props; - const classes = `${styles.container} ${light ? styles.light : ''} ${className}`; - + const { children, className, compact, light, onClick, style, tabIndex } = this.props; const props = {}; if (Number.isInteger(tabIndex)) { @@ -45,8 +44,27 @@ export default class Container extends Component { } return ( -
- +
+ { this.renderTitle() } { children } diff --git a/js/src/ui/CopyToClipboard/copyToClipboard.js b/js/src/ui/CopyToClipboard/copyToClipboard.js index 2afd59dde..fe89c5459 100644 --- a/js/src/ui/CopyToClipboard/copyToClipboard.js +++ b/js/src/ui/CopyToClipboard/copyToClipboard.js @@ -14,21 +14,21 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { IconButton } from 'material-ui'; import React, { Component, PropTypes } from 'react'; +import Clipboard from 'react-copy-to-clipboard'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { IconButton } from 'material-ui'; -import Clipboard from 'react-copy-to-clipboard'; -import CopyIcon from 'material-ui/svg-icons/content/content-copy'; -import Theme from '../Theme'; - import { showSnackbar } from '~/redux/providers/snackbarActions'; -const { textColor, disabledTextColor } = Theme.flatButton; +import { CopyIcon } from '../Icons'; +import Theme from '../Theme'; import styles from './copyToClipboard.css'; +const { textColor, disabledTextColor } = Theme.flatButton; + class CopyToClipboard extends Component { static propTypes = { showSnackbar: PropTypes.func.isRequired, diff --git a/js/src/ui/CurrencySymbol/currencySymbol.example.js b/js/src/ui/CurrencySymbol/currencySymbol.example.js new file mode 100644 index 000000000..c1b56ed5c --- /dev/null +++ b/js/src/ui/CurrencySymbol/currencySymbol.example.js @@ -0,0 +1,51 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component } from 'react'; + +import PlaygroundExample from '~/playground/playgroundExample'; + +import ConnectedCurrencySymbol, { CurrencySymbol } from './currencySymbol'; + +export default class CurrencySymbolExample extends Component { + render () { + return ( +
+ + + + + + + + + + + + + + + +
+ ); + } +} diff --git a/js/src/ui/CurrencySymbol/currencySymbol.js b/js/src/ui/CurrencySymbol/currencySymbol.js new file mode 100644 index 000000000..3dfbbc8fd --- /dev/null +++ b/js/src/ui/CurrencySymbol/currencySymbol.js @@ -0,0 +1,65 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +const SYMBOL_ETC = 'ETC'; +const SYMBOL_ETH = 'ETH'; +const SYMBOL_EXP = 'EXP'; + +export class CurrencySymbol extends Component { + static propTypes = { + className: PropTypes.string, + netChain: PropTypes.string.isRequired + } + + render () { + const { className } = this.props; + + return ( + { this.renderSymbol() } + ); + } + + renderSymbol () { + const { netChain } = this.props; + + switch (netChain) { + case 'classic': + return SYMBOL_ETC; + + case 'expanse': + return SYMBOL_EXP; + + default: + return SYMBOL_ETH; + } + } +} + +function mapStateToProps (state) { + const { netChain } = state.nodeStatus; + + return { + netChain + }; +} + +export default connect( + mapStateToProps, + null +)(CurrencySymbol); diff --git a/js/src/ui/CurrencySymbol/currencySymbol.spec.js b/js/src/ui/CurrencySymbol/currencySymbol.spec.js new file mode 100644 index 000000000..bcc97822f --- /dev/null +++ b/js/src/ui/CurrencySymbol/currencySymbol.spec.js @@ -0,0 +1,99 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import CurrencySymbol from './'; + +let component; +let store; + +function createRedux (netChain = 'ropsten') { + store = { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => { + return { + nodeStatus: { + netChain + } + }; + } + }; + + return store; +} + +function render (netChain, props = {}) { + component = shallow( + , + { + context: { + store: createRedux(netChain) + } + } + ).find('CurrencySymbol').shallow(); + + return component; +} + +describe('ui/CurrencySymbol', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + it('passes the className as provided', () => { + expect(render('ropsten', { className: 'test' }).find('span').hasClass('test')).to.be.true; + }); + + describe('currencies', () => { + it('renders ETH as default', () => { + expect(render().text()).equal('ETH'); + }); + + it('renders ETC for classic', () => { + expect(render('classic').text()).equal('ETC'); + }); + + it('renders EXP for expanse', () => { + expect(render('expanse').text()).equal('EXP'); + }); + + it('renders ETH as default', () => { + expect(render('somethingElse').text()).equal('ETH'); + }); + }); + + describe('renderSymbol', () => { + it('render defaults', () => { + expect(render().instance().renderSymbol()).to.be.ok; + }); + + it('render ETH as default', () => { + expect(render().instance().renderSymbol()).equal('ETH'); + }); + + it('render ETC', () => { + expect(render('classic').instance().renderSymbol()).equal('ETC'); + }); + + it('render EXP', () => { + expect(render('expanse').instance().renderSymbol()).equal('EXP'); + }); + }); +}); diff --git a/js/src/ui/CurrencySymbol/index.js b/js/src/ui/CurrencySymbol/index.js new file mode 100644 index 000000000..2c45ec02a --- /dev/null +++ b/js/src/ui/CurrencySymbol/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './currencySymbol'; diff --git a/js/src/views/Dapps/Summary/summary.css b/js/src/ui/DappCard/dappCard.css similarity index 94% rename from js/src/views/Dapps/Summary/summary.css rename to js/src/ui/DappCard/dappCard.css index ba5896b75..1e8fb8db8 100644 --- a/js/src/views/Dapps/Summary/summary.css +++ b/js/src/ui/DappCard/dappCard.css @@ -16,17 +16,14 @@ */ .container { - position: relative; height: 100%; + position: relative; } .image { + left: 1.5em; position: absolute; top: 1.5em; - left: 1.5em; - border-radius: 50%; - width: 56px; - height: 56px; } .description { diff --git a/js/src/views/Dapps/Summary/summary.js b/js/src/ui/DappCard/dappCard.js similarity index 53% rename from js/src/views/Dapps/Summary/summary.js rename to js/src/ui/DappCard/dappCard.js index 9c1d04f21..eeb52294e 100644 --- a/js/src/views/Dapps/Summary/summary.js +++ b/js/src/ui/DappCard/dappCard.js @@ -17,74 +17,80 @@ import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; -import { Container, ContainerTitle, Tags } from '~/ui'; +import Container, { Title as ContainerTitle } from '~/ui/Container'; +import DappIcon from '~/ui/DappIcon'; +import Tags from '~/ui/Tags'; -import styles from './summary.css'; - -export default class Summary extends Component { - static contextTypes = { - api: React.PropTypes.object - } +import styles from './dappCard.css'; +export default class DappCard extends Component { static propTypes = { app: PropTypes.object.isRequired, - children: PropTypes.node - } + children: PropTypes.node, + className: PropTypes.string, + onClick: PropTypes.func, + showLink: PropTypes.bool, + showTags: PropTypes.bool + }; + + static defaultProps = { + showLink: false, + showTags: false + }; render () { - const { dappsUrl } = this.context.api; - const { app } = this.props; + const { app, children, className, onClick, showLink, showTags } = this.props; if (!app) { return null; } - const image = this.renderImage(dappsUrl, app); - const link = this.renderLink(app); - return ( - - { image } - + + +
{ app.author }, v{ app.version }
- { this.props.children } + { children }
); } - renderImage (dappsUrl, app) { - if (app.type === 'local') { - return ( - - ); - } - - return ( - - ); - } - renderLink (app) { - // Special case for web dapp - if (app.url === 'web') { - return ( - - { app.name } - - ); - } - return ( - + { app.name } ); diff --git a/js/src/ui/DappCard/index.js b/js/src/ui/DappCard/index.js new file mode 100644 index 000000000..ea9edaf75 --- /dev/null +++ b/js/src/ui/DappCard/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './dappCard'; diff --git a/js/src/ui/DappIcon/dappIcon.css b/js/src/ui/DappIcon/dappIcon.css new file mode 100644 index 000000000..95ceed864 --- /dev/null +++ b/js/src/ui/DappIcon/dappIcon.css @@ -0,0 +1,31 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.icon { + border-radius: 50%; + margin: 0 0.75em 0 0; +} + +.normal { + height: 56px; + width: 56px; +} + +.small { + height: 32px; + width: 32px; +} diff --git a/js/src/ui/DappIcon/dappIcon.js b/js/src/ui/DappIcon/dappIcon.js new file mode 100644 index 000000000..891f45405 --- /dev/null +++ b/js/src/ui/DappIcon/dappIcon.js @@ -0,0 +1,49 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +import styles from './dappIcon.css'; + +export default class DappIcon extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + }; + + static propTypes = { + app: PropTypes.object.isRequired, + className: PropTypes.string, + small: PropTypes.bool + }; + + render () { + const { dappsUrl } = this.context.api; + const { app, className, small } = this.props; + + return ( + + ); + } +} diff --git a/js/src/ui/DappIcon/dappIcon.spec.js b/js/src/ui/DappIcon/dappIcon.spec.js new file mode 100644 index 000000000..b7bc81653 --- /dev/null +++ b/js/src/ui/DappIcon/dappIcon.spec.js @@ -0,0 +1,70 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { shallow } from 'enzyme'; +import React from 'react'; + +import DappIcon from './'; + +const DAPPS_URL = 'http://test'; + +let api; +let component; + +function createApi () { + api = { + dappsUrl: DAPPS_URL + }; + + return api; +} + +function render (props = {}) { + if (!props.app) { + props.app = {}; + } + + component = shallow( + , + { + context: { api: createApi() } + } + ); + + return component; +} + +describe('ui/DappIcon', () => { + it('renders defaults', () => { + expect(render()).to.be.ok; + }); + + it('adds specified className', () => { + expect(render({ className: 'testClass' }).hasClass('testClass')).to.be.true; + }); + + it('renders local apps with correct URL', () => { + expect(render({ app: { id: 'test', type: 'local', iconUrl: 'test.img' } }).props().src).to.equal( + `${DAPPS_URL}/test/test.img` + ); + }); + + it('renders other apps with correct URL', () => { + expect(render({ app: { id: 'test', image: '/test.img' } }).props().src).to.equal( + `${DAPPS_URL}/test.img` + ); + }); +}); diff --git a/js/src/ui/DappIcon/index.js b/js/src/ui/DappIcon/index.js new file mode 100644 index 000000000..c0b80956e --- /dev/null +++ b/js/src/ui/DappIcon/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './dappIcon'; diff --git a/js/src/ui/Form/AddressSelect/addressSelect.css b/js/src/ui/Form/AddressSelect/addressSelect.css index d24332016..5512d2f33 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.css +++ b/js/src/ui/Form/AddressSelect/addressSelect.css @@ -73,6 +73,12 @@ } } +.title { + display: flex; + flex-direction: column; + position: relative; +} + .label { margin: 1rem 0.5rem 0.25em; color: rgba(255, 255, 255, 0.498039); @@ -102,14 +108,11 @@ } .categories { - flex: 1; - display: flex; + flex: 1; flex-direction: row; justify-content: flex-start; - margin: 2rem 0 0; - > * { flex: 1; } diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js index 6a71e4245..44622bde2 100644 --- a/js/src/ui/Form/AddressSelect/addressSelect.js +++ b/js/src/ui/Form/AddressSelect/addressSelect.js @@ -176,37 +176,42 @@ class AddressSelect extends Component { return ( + +
+ + { this.renderLoader() } +
+ +
+ +
+ + { this.renderCurrentInput() } + { this.renderRegistryValues() } +
+ } > - -
- - { this.renderLoader() } -
- -
- -
- - { this.renderCurrentInput() } - { this.renderRegistryValues() } { this.renderAccounts() } ); diff --git a/js/src/ui/Form/InputAddress/inputAddress.js b/js/src/ui/Form/InputAddress/inputAddress.js index 4673634c8..e168a482f 100644 --- a/js/src/ui/Form/InputAddress/inputAddress.js +++ b/js/src/ui/Form/InputAddress/inputAddress.js @@ -80,6 +80,9 @@ class InputAddress extends Component { props.focused = focused; } + // FIXME: The is not advisable, fixes the display issue, however the name should come from + // a common component. + // account.name || (value ? 'UNNAMED' : value) return (
. + +export default from './inputDate'; diff --git a/js/src/ui/Form/InputDate/inputDate.css b/js/src/ui/Form/InputDate/inputDate.css new file mode 100644 index 000000000..17ffa27b9 --- /dev/null +++ b/js/src/ui/Form/InputDate/inputDate.css @@ -0,0 +1,22 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.container { + .input { + width: 100%; + } +} diff --git a/js/src/ui/Form/InputDate/inputDate.js b/js/src/ui/Form/InputDate/inputDate.js new file mode 100644 index 000000000..86964837b --- /dev/null +++ b/js/src/ui/Form/InputDate/inputDate.js @@ -0,0 +1,53 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { DatePicker } from 'material-ui'; +import React, { Component, PropTypes } from 'react'; + +import Label from '../Label'; + +import styles from './inputDate.css'; + +// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputTime +const DIALOG_STYLE = { zIndex: 10010 }; + +export default class InputDate extends Component { + static propTypes = { + className: PropTypes.string, + hint: PropTypes.node, + label: PropTypes.node, + onChange: PropTypes.func, + value: PropTypes.object.isRequired + }; + + render () { + const { className, hint, label, onChange, value } = this.props; + + return ( +
+
+ ); + } +} diff --git a/js/src/ui/Form/InputTime/index.js b/js/src/ui/Form/InputTime/index.js new file mode 100644 index 000000000..c6b393c34 --- /dev/null +++ b/js/src/ui/Form/InputTime/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './inputTime'; diff --git a/js/src/ui/Form/InputTime/inputTime.css b/js/src/ui/Form/InputTime/inputTime.css new file mode 100644 index 000000000..17ffa27b9 --- /dev/null +++ b/js/src/ui/Form/InputTime/inputTime.css @@ -0,0 +1,22 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.container { + .input { + width: 100%; + } +} diff --git a/js/src/ui/Form/InputTime/inputTime.js b/js/src/ui/Form/InputTime/inputTime.js new file mode 100644 index 000000000..2d54c8b12 --- /dev/null +++ b/js/src/ui/Form/InputTime/inputTime.js @@ -0,0 +1,54 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { TimePicker } from 'material-ui'; +import React, { Component, PropTypes } from 'react'; + +import Label from '../Label'; + +import styles from './inputTime.css'; + +// NOTE: Has to be larger than Signer overlay Z, aligns with ../InputDate +const DIALOG_STYLE = { zIndex: 10010 }; + +export default class InputTime extends Component { + static propTypes = { + className: PropTypes.string, + hint: PropTypes.node, + label: PropTypes.node, + onChange: PropTypes.func, + value: PropTypes.object.isRequired + } + + render () { + const { className, hint, label, onChange, value } = this.props; + + return ( +
+
+ ); + } +} diff --git a/js/src/ui/Form/Label/index.js b/js/src/ui/Form/Label/index.js new file mode 100644 index 000000000..d0394a361 --- /dev/null +++ b/js/src/ui/Form/Label/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './label'; diff --git a/js/src/ui/Form/Label/label.css b/js/src/ui/Form/Label/label.css new file mode 100644 index 000000000..e774cb39f --- /dev/null +++ b/js/src/ui/Form/Label/label.css @@ -0,0 +1,24 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +$labelColor: rgba(255, 255, 255, 0.5); +$labelFontSize: 0.75rem; + +.label { + color: $labelColor; + font-size: $labelFontSize; +} diff --git a/js/src/ui/Form/Label/label.js b/js/src/ui/Form/Label/label.js new file mode 100644 index 000000000..952a0e7a1 --- /dev/null +++ b/js/src/ui/Form/Label/label.js @@ -0,0 +1,40 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +import styles from './label.css'; + +export default class Label extends Component { + static propTypes = { + className: PropTypes.string, + label: PropTypes.node + } + + render () { + const { className, label } = this.props; + + if (!label) { + return null; + } + + return ( + + ); + } +} diff --git a/js/src/ui/Form/RadioButtons/radioButtons.css b/js/src/ui/Form/RadioButtons/radioButtons.css index 08db3e3d6..ab131b265 100644 --- a/js/src/ui/Form/RadioButtons/radioButtons.css +++ b/js/src/ui/Form/RadioButtons/radioButtons.css @@ -15,18 +15,23 @@ /* along with Parity. If not, see . */ -.spaced { - margin: 0.25em 0; -} +.container { + .label { + } -.typeContainer { - display: flex; - flex-direction: column; + .radioButton { + margin: 0.25em 0; + } - .desc { - font-size: 0.8em; - margin-bottom: 0.5em; - color: #ccc; - z-index: 2; + .radioLabel { + display: flex; + flex-direction: column; + + .description { + font-size: 0.8em; + margin-bottom: 0.5em; + color: #ccc; + z-index: 2; + } } } diff --git a/js/src/ui/Form/RadioButtons/radioButtons.js b/js/src/ui/Form/RadioButtons/radioButtons.js index e18a884be..bebe9c0f8 100644 --- a/js/src/ui/Form/RadioButtons/radioButtons.js +++ b/js/src/ui/Form/RadioButtons/radioButtons.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -18,10 +18,14 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; import React, { Component, PropTypes } from 'react'; import { arrayOrObjectProptype } from '~/util/proptypes'; + +import Label from '../Label'; import styles from './radioButtons.css'; export default class RadioButtons extends Component { static propTypes = { + className: PropTypes.string, + label: PropTypes.node, name: PropTypes.string, onChange: PropTypes.func.isRequired, value: PropTypes.any, @@ -34,10 +38,10 @@ export default class RadioButtons extends Component { }; render () { - const { value, values } = this.props; + const { className, label, value, values } = this.props; const index = Number.isNaN(parseInt(value)) - ? values.findIndex((val) => val.key === value) + ? values.findIndex((_value) => _value.key === value) : parseInt(value); const selectedValue = typeof value !== 'object' ? values[index] @@ -45,12 +49,19 @@ export default class RadioButtons extends Component { const key = this.getKey(selectedValue, index); return ( - - { this.renderContent() } - +
+
); } @@ -66,19 +77,20 @@ export default class RadioButtons extends Component { return ( +
{ label } { description - ? { description } + ? { description } : null }
} - value={ key } /> + value={ key } + /> ); }); } @@ -95,7 +107,7 @@ export default class RadioButtons extends Component { onChange = (event, index) => { const { onChange, values } = this.props; - const value = values[index] || values.find((v) => v.key === index); + const value = values[index] || values.find((value) => value.key === index); onChange(value, index); } diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index 0f5e8d056..ef4d33416 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -16,25 +16,31 @@ import AddressSelect from './AddressSelect'; import FormWrap from './FormWrap'; -import TypedInput from './TypedInput'; import Input from './Input'; import InputAddress from './InputAddress'; import InputAddressSelect from './InputAddressSelect'; import InputChip from './InputChip'; +import InputDate from './InputDate'; import InputInline from './InputInline'; -import Select from './Select'; +import InputTime from './InputTime'; +import Label from './Label'; import RadioButtons from './RadioButtons'; +import Select from './Select'; +import TypedInput from './TypedInput'; export default from './form'; export { AddressSelect, FormWrap, - TypedInput, Input, InputAddress, InputAddressSelect, InputChip, + InputDate, InputInline, + InputTime, + Label, + RadioButtons, Select, - RadioButtons + TypedInput }; diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.css b/js/src/ui/GasPriceEditor/gasPriceEditor.css index 80b7dd313..744c01442 100644 --- a/js/src/ui/GasPriceEditor/gasPriceEditor.css +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.css @@ -16,6 +16,46 @@ */ .container { + display: flex; + flex-direction: column; +} + +.conditionContainer { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin-bottom: 1.5em; + + .input { + flex: 0 1 50%; + } +} + +.conditionRadio { + display: flex; + flex-direction: column; + margin-bottom: 1em; + + &>label { + margin-bottom: 0.5em; + } + + &>div { + display: flex; + flex-direction: row; + + &>div { + width: auto !important; + + label { + padding-right: 1.5em; + white-space: nowrap; + } + } + } +} + +.graphContainer { display: flex; flex-wrap: wrap; position: relative; diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.js b/js/src/ui/GasPriceEditor/gasPriceEditor.js index e98707410..db9e02396 100644 --- a/js/src/ui/GasPriceEditor/gasPriceEditor.js +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -17,13 +17,44 @@ import BigNumber from 'bignumber.js'; import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; +import { FormattedMessage } from 'react-intl'; -import Input from '../Form/Input'; +import { Input, InputDate, InputTime, RadioButtons } from '../Form'; import GasPriceSelector from '../GasPriceSelector'; -import Store from './store'; +import Store, { CONDITIONS } from './store'; import styles from './gasPriceEditor.css'; +const CONDITION_VALUES = [ + { + label: ( + + ), + key: CONDITIONS.NONE + }, + { + label: ( + + ), + key: CONDITIONS.BLOCK + }, + { + label: ( + + ), + key: CONDITIONS.TIME + } +]; + @observer export default class GasPriceEditor extends Component { static contextTypes = { @@ -41,7 +72,7 @@ export default class GasPriceEditor extends Component { render () { const { api } = this.context; const { children, store } = this.props; - const { errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store; + const { conditionType, errorGas, errorPrice, errorTotal, estimated, gas, histogram, price, priceDefault, totalValue } = store; const eth = api.util.fromWei(totalValue).toFormat(); const gasLabel = `gas (estimated: ${new BigNumber(estimated).toFormat()})`; @@ -49,43 +80,147 @@ export default class GasPriceEditor extends Component { return (
-
- -
- You can choose the gas price based on the distribution of recent included transaction gas prices. The lower the gas price is, the cheaper the transaction will be. The higher the gas price is, the faster it should get mined by the network. + + } + onChange={ this.onChangeConditionType } + value={ conditionType } + values={ CONDITION_VALUES } + /> + { this.renderConditions() } + +
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ { children } +
+
+ ); + } -
-
+ renderConditions () { + const { conditionType, condition, conditionBlockError } = this.props.store; + + if (conditionType === CONDITIONS.NONE) { + return null; + } + + if (conditionType === CONDITIONS.BLOCK) { + return ( +
+
- -
-
- -
-
- { children } + error={ conditionBlockError } + hint={ + + } + label={ + + } + min={ 1 } + onChange={ this.onChangeConditionBlock } + type='number' + value={ condition.block } + />
+ ); + } + + return ( +
+
+ + } + label={ + + } + onChange={ this.onChangeConditionDateTime } + value={ condition.time } + /> +
+
+ + } + label={ + + } + onChange={ this.onChangeConditionDateTime } + value={ condition.time } + /> +
); } @@ -103,4 +238,16 @@ export default class GasPriceEditor extends Component { store.setPrice(price); onChange && onChange('gasPrice', price); } + + onChangeConditionType = (conditionType) => { + this.props.store.setConditionType(conditionType.key); + } + + onChangeConditionBlock = (event, blockNumber) => { + this.props.store.setConditionBlockNumber(blockNumber); + } + + onChangeConditionDateTime = (event, datetime) => { + this.props.store.setConditionDateTime(datetime); + } } diff --git a/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js index 3a414f90e..39b5c23be 100644 --- a/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js +++ b/js/src/ui/GasPriceEditor/gasPriceEditor.spec.js @@ -21,26 +21,64 @@ import sinon from 'sinon'; import GasPriceEditor from './'; -const api = { - util: { - fromWei: (value) => new BigNumber(value) - } -}; +let api; +let component; +let store; -const store = { - estimated: '123', - histogram: {}, - priceDefault: '456', - totalValue: '789', - setGas: sinon.stub(), - setPrice: sinon.stub() -}; +function createApi () { + api = { + eth: { + blockNumber: sinon.stub().resolves(new BigNumber(3)) + }, + util: { + fromWei: (value) => new BigNumber(value) + } + }; + + return api; +} + +function createStore () { + createApi(); + + store = { + _api: api, + conditionType: 'none', + estimated: '123', + histogram: {}, + priceDefault: '456', + totalValue: '789', + setGas: sinon.stub(), + setPrice: sinon.stub() + }; + + return store; +} + +function render (props = {}) { + createStore(); + + component = shallow( + , + { + context: { + api + } + } + ); + + return component; +} describe('ui/GasPriceEditor', () => { + beforeEach(() => { + render(); + }); + it('renders', () => { - expect(shallow( - , - { context: { api } } - )).to.be.ok; + expect(component).to.be.ok; }); }); diff --git a/js/src/ui/GasPriceEditor/store.js b/js/src/ui/GasPriceEditor/store.js index 87b275637..e7d0bb537 100644 --- a/js/src/ui/GasPriceEditor/store.js +++ b/js/src/ui/GasPriceEditor/store.js @@ -20,7 +20,17 @@ import { action, computed, observable, transaction } from 'mobx'; import { ERRORS, validatePositiveNumber } from '~/util/validation'; import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; +const CONDITIONS = { + NONE: 'none', + BLOCK: 'blockNumber', + TIME: 'timestamp' +}; + export default class GasPriceEditor { + @observable blockNumber = 0; + @observable condition = {}; + @observable conditionBlockError = null; + @observable conditionType = CONDITIONS.NONE; @observable errorEstimated = null; @observable errorGas = null; @observable errorPrice = null; @@ -34,13 +44,23 @@ export default class GasPriceEditor { @observable priceDefault; @observable weiValue = '0'; - constructor (api, { gas, gasLimit, gasPrice }) { + constructor (api, { gas, gasLimit, gasPrice, condition = null }) { this._api = api; this.gas = gas; this.gasLimit = gasLimit; this.price = gasPrice; + if (condition) { + if (condition.block) { + this.condition = { block: condition.block.toFixed(0) }; + this.conditionType = CONDITIONS.BLOCK; + } else if (condition.time) { + this.condition = { time: condition.time }; + this.conditionType = CONDITIONS.TIME; + } + } + if (api) { this.loadDefaults(); } @@ -54,6 +74,39 @@ export default class GasPriceEditor { } } + @action setConditionType = (conditionType = CONDITIONS.NONE) => { + transaction(() => { + this.conditionBlockError = null; + this.conditionType = conditionType; + + switch (conditionType) { + case CONDITIONS.BLOCK: + this.condition = Object.assign({}, this.condition, { block: this.blockNumber || 1 }); + break; + + case CONDITIONS.TIME: + this.condition = Object.assign({}, this.condition, { time: new Date() }); + break; + + case CONDITIONS.NONE: + default: + this.condition = {}; + break; + } + }); + } + + @action setConditionBlockNumber = (block) => { + transaction(() => { + this.conditionBlockError = validatePositiveNumber(block).numberError; + this.condition = Object.assign({}, this.condition, { block }); + }); + } + + @action setConditionDateTime = (time) => { + this.condition = Object.assign({}, this.condition, { time }); + } + @action setEditing = (isEditing) => { this.isEditing = isEditing; } @@ -130,9 +183,10 @@ export default class GasPriceEditor { bucket_bounds: [], counts: [] })), - this._api.eth.gasPrice() + this._api.eth.gasPrice(), + this._api.eth.blockNumber() ]) - .then(([histogram, _price]) => { + .then(([histogram, _price, blockNumber]) => { transaction(() => { const price = _price.toFixed(0); @@ -142,6 +196,7 @@ export default class GasPriceEditor { this.setHistogram(histogram); this.priceDefault = price; + this.blockNumber = blockNumber.toNumber(); }); }) .catch((error) => { @@ -150,13 +205,37 @@ export default class GasPriceEditor { } overrideTransaction = (transaction) => { - if (this.errorGas || this.errorPrice) { + if (this.errorGas || this.errorPrice || this.conditionBlockError) { return transaction; } - return Object.assign({}, transaction, { + const override = { + condition: this.condition, gas: new BigNumber(this.gas || DEFAULT_GAS), gasPrice: new BigNumber(this.price || DEFAULT_GASPRICE) - }); + }; + + const result = Object.assign({}, transaction, override); + + switch (this.conditionType) { + case CONDITIONS.BLOCK: + result.condition = { block: new BigNumber(this.condition.block || 0) }; + break; + + case CONDITIONS.TIME: + result.condition = { time: this.condition.time }; + break; + + case CONDITIONS.NONE: + default: + delete result.condition; + break; + } + + return result; } } + +export { + CONDITIONS +}; diff --git a/js/src/ui/GasPriceEditor/store.spec.js b/js/src/ui/GasPriceEditor/store.spec.js index cee8a4d4b..3a7e0c647 100644 --- a/js/src/ui/GasPriceEditor/store.spec.js +++ b/js/src/ui/GasPriceEditor/store.spec.js @@ -21,6 +21,7 @@ import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/consta import { ERRORS } from '~/util/validation'; import GasPriceEditor from './gasPriceEditor'; +import { CONDITIONS } from './store'; const { Store } = GasPriceEditor; @@ -31,18 +32,30 @@ const HISTOGRAM = { counts: [3, 4] }; -const api = { - eth: { - gasPrice: sinon.stub().resolves(GASPRICE) - }, - parity: { - gasPriceHistogram: sinon.stub().resolves(HISTOGRAM) - } -}; +let api; -describe('ui/GasPriceEditor/store', () => { +// TODO: share with gasPriceEditor.spec.js +function createApi () { + api = { + eth: { + blockNumber: sinon.stub().resolves(new BigNumber(2)), + gasPrice: sinon.stub().resolves(GASPRICE) + }, + parity: { + gasPriceHistogram: sinon.stub().resolves(HISTOGRAM) + } + }; + + return api; +} + +describe('ui/GasPriceEditor/Store', () => { let store = null; + beforeEach(() => { + createApi(); + }); + it('is available via GasPriceEditor.Store', () => { expect(new Store(null, {})).to.be.ok; }); @@ -65,6 +78,7 @@ describe('ui/GasPriceEditor/store', () => { describe('constructor (defaults) when histogram not available', () => { const api = { eth: { + blockNumber: sinon.stub().resolves(new BigNumber(2)), gasPrice: sinon.stub().resolves(GASPRICE) }, parity: { @@ -92,6 +106,67 @@ describe('ui/GasPriceEditor/store', () => { store = new Store(null, { gasLimit: GASLIMIT }); }); + describe('setConditionType', () => { + it('sets the actual type', () => { + store.setConditionType('testingType'); + expect(store.conditionType).to.equal('testingType'); + }); + + it('clears any block error on changing type', () => { + store.setConditionBlockNumber(-1); + expect(store.conditionBlockError).not.to.be.null; + store.setConditionType(CONDITIONS.BLOCK); + expect(store.conditionBlockError).to.be.null; + }); + + it('sets condition.block when type === CONDITIONS.BLOCK', () => { + store.setConditionType(CONDITIONS.BLOCK); + expect(store.condition.block).to.be.ok; + }); + + it('clears condition when type === CONDITIONS.NONE', () => { + store.setConditionType(CONDITIONS.BLOCK); + store.setConditionType(CONDITIONS.NONE); + expect(store.condition).to.deep.equal({}); + }); + + it('sets condition.time when type === CONDITIONS.TIME', () => { + store.setConditionType(CONDITIONS.TIME); + expect(store.condition.time).to.be.ok; + }); + }); + + describe('setConditionBlockNumber', () => { + beforeEach(() => { + store.setConditionBlockNumber('testingBlock'); + }); + + it('sets the blockNumber', () => { + expect(store.condition.block).to.equal('testingBlock'); + }); + + it('sets the error on invalid numbers', () => { + expect(store.conditionBlockError).not.to.be.null; + }); + + it('sets the error on negative numbers', () => { + store.setConditionBlockNumber(-1); + expect(store.conditionBlockError).not.to.be.null; + }); + + it('clears the error on positive numbers', () => { + store.setConditionBlockNumber(1000); + expect(store.conditionBlockError).to.be.null; + }); + }); + + describe('setConditionDateTime', () => { + it('sets the datatime', () => { + store.setConditionDateTime('testingDateTime'); + expect(store.condition.time).to.equal('testingDateTime'); + }); + }); + describe('setEditing', () => { it('sets the value', () => { expect(store.isEditing).to.be.false; diff --git a/js/src/ui/Icons/index.js b/js/src/ui/Icons/index.js index a35bbb610..9c7409268 100644 --- a/js/src/ui/Icons/index.js +++ b/js/src/ui/Icons/index.js @@ -21,17 +21,23 @@ import CloseIcon from 'material-ui/svg-icons/navigation/close'; import CompareIcon from 'material-ui/svg-icons/action/compare-arrows'; import ComputerIcon from 'material-ui/svg-icons/hardware/desktop-mac'; import ContractIcon from 'material-ui/svg-icons/action/code'; +import CopyIcon from 'material-ui/svg-icons/content/content-copy'; import DashboardIcon from 'material-ui/svg-icons/action/dashboard'; import DeleteIcon from 'material-ui/svg-icons/action/delete'; import DoneIcon from 'material-ui/svg-icons/action/done-all'; import EditIcon from 'material-ui/svg-icons/content/create'; +import FingerprintIcon from 'material-ui/svg-icons/action/fingerprint'; import LinkIcon from 'material-ui/svg-icons/content/link'; import LockedIcon from 'material-ui/svg-icons/action/lock'; +import MoveIcon from 'material-ui/svg-icons/action/open-with'; import NextIcon from 'material-ui/svg-icons/navigation/arrow-forward'; import PrevIcon from 'material-ui/svg-icons/navigation/arrow-back'; import SaveIcon from 'material-ui/svg-icons/content/save'; import SendIcon from 'material-ui/svg-icons/content/send'; import SnoozeIcon from 'material-ui/svg-icons/av/snooze'; +import StarCircleIcon from 'material-ui/svg-icons/action/stars'; +import StarIcon from 'material-ui/svg-icons/toggle/star'; +import StarOutlineIcon from 'material-ui/svg-icons/toggle/star-border'; import VerifyIcon from 'material-ui/svg-icons/action/verified-user'; import VisibleIcon from 'material-ui/svg-icons/image/remove-red-eye'; import VpnIcon from 'material-ui/svg-icons/notification/vpn-lock'; @@ -44,17 +50,23 @@ export { CompareIcon, ComputerIcon, ContractIcon, + CopyIcon, DashboardIcon, DeleteIcon, DoneIcon, EditIcon, + FingerprintIcon, LinkIcon, LockedIcon, + MoveIcon, NextIcon, PrevIcon, SaveIcon, SendIcon, SnoozeIcon, + StarIcon, + StarCircleIcon, + StarOutlineIcon, VerifyIcon, VisibleIcon, VpnIcon diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 2c4c28e47..d0fe2d61c 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -14,9 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import { CircularProgress } from 'material-ui'; +import moment from 'moment'; import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import CircularProgress from 'material-ui/CircularProgress'; import { TypedInput, InputAddress } from '../Form'; import MethodDecodingStore from './methodDecodingStore'; @@ -128,15 +129,25 @@ class MethodDecoding extends Component { renderMinBlock () { const { historic, transaction } = this.props; - const { minBlock } = transaction; + const { condition } = transaction; - if (!minBlock || minBlock.eq(0)) { + if (!condition) { return null; } - return ( - , { historic ? 'Submitted' : 'Submission' } at block #{ minBlock.toFormat(0) } - ); + if (condition.block && condition.block.gt(0)) { + return ( + , { historic ? 'Submitted' : 'Submission' } at block #{ condition.block.toFormat(0) } + ); + } + + if (condition.time) { + return ( + , { historic ? 'Submitted' : 'Submission' } at { moment(condition.time).format('LLLL') } + ); + } + + return null; } renderAction () { diff --git a/js/src/ui/Modal/modal.css b/js/src/ui/Modal/modal.css index 1e79cb75b..3aaeb23ac 100644 --- a/js/src/ui/Modal/modal.css +++ b/js/src/ui/Modal/modal.css @@ -47,20 +47,6 @@ .title { background: rgba(0, 0, 0, 0.25) !important; padding: 1em; - margin-bottom: 0; - - h3 { - margin: 0; - text-transform: uppercase; - } - - .steps { - margin-bottom: -1em; - } -} - -.waiting { - margin: 1em -1em -1em -1em; } .overlay { diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index 72a9d74ae..8a108bfa1 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -22,7 +22,7 @@ import { connect } from 'react-redux'; import { nodeOrStringProptype } from '~/util/proptypes'; import Container from '../Container'; -import Title from './Title'; +import Title from '../Title'; const ACTIONS_STYLE = { borderStyle: 'none' }; const TITLE_STYLE = { borderStyle: 'none' }; @@ -63,11 +63,14 @@ class Modal extends Component { const contentStyle = muiTheme.parity.getBackgroundStyle(null, settings.backgroundSeed); const header = ( + waiting={ waiting } + /> ); const classes = `${styles.dialog} ${className}`; diff --git a/js/src/ui/ParityBackground/parityBackground.js b/js/src/ui/ParityBackground/parityBackground.js index 6e554b846..cab3f31d3 100644 --- a/js/src/ui/ParityBackground/parityBackground.js +++ b/js/src/ui/ParityBackground/parityBackground.js @@ -26,7 +26,12 @@ class ParityBackground extends Component { backgroundSeed: PropTypes.string, children: PropTypes.node, className: PropTypes.string, - onClick: PropTypes.func + onClick: PropTypes.func, + style: PropTypes.object + }; + + static defaultProps = { + style: {} }; state = { @@ -65,7 +70,11 @@ class ParityBackground extends Component { render () { const { children, className, onClick } = this.props; - const { style } = this.state; + + const style = { + ...this.state.style, + ...this.props.style + }; return ( <div diff --git a/js/src/ui/Portal/portal.css b/js/src/ui/Portal/portal.css index 37c57b712..c3cdf283c 100644 --- a/js/src/ui/Portal/portal.css +++ b/js/src/ui/Portal/portal.css @@ -15,30 +15,32 @@ /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -$left: 1.5em; -$right: $left; -$bottom: $left; -$top: 20vh; +$modalMargin: 1.5em; +$modalPadding: 1.5em; +$modalBackZ: 2500; + +/* This should be the default case, the Portal used as a stand-alone modal */ +$modalBottom: $modalMargin; +$modalLeft: $modalMargin; +$modalRight: $modalMargin; +$modalTop: $modalMargin; +$modalZ: 3500; + +/* This is the case where popped-up over another modal, Portal or otherwise */ +$popoverBottom: $modalMargin; +$popoverLeft: $modalMargin; +$popoverRight: $modalMargin; +$popoverTop: 20vh; +$popoverZ: 3600; .backOverlay { + background-color: rgba(255, 255, 255, 0.35); position: fixed; top: 0; right: 0; bottom: 0; left: 0; - background-color: rgba(255, 255, 255, 0.25); - z-index: -10; - opacity: 0; - - transform-origin: 100% 0; - transition-property: opacity, z-index; - transition-duration: 0.25s; - transition-timing-function: ease-out; - - &.expanded { - opacity: 1; - z-index: 2500; - } + z-index: $modalBackZ; } .parityBackground { @@ -48,57 +50,78 @@ $top: 20vh; left: 0; right: 0; opacity: 0.25; - z-index: -1; } .overlay { - display: flex; - position: fixed; - top: $top; - left: $left; - width: calc(100vw - $left - $right); - height: calc(100vh - $top - $bottom); - - transform-origin: 100% 0; - transition-property: opacity, z-index; - transition-duration: 0.25s; - transition-timing-function: ease-out; - background-color: rgba(0, 0, 0, 1); - opacity: 0; - z-index: -10; - - padding: 1em; box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: $modalPadding; + position: fixed; * { min-width: 0; } - &.expanded { - opacity: 1; - z-index: 3500; - } -} - -.closeIcon { - position: absolute; - top: 0.5rem; - right: 1rem; - font-size: 4em; - z-index: 100; - - transition-property: opacity; - transition-duration: 0.25s; - transition-timing-function: ease-out; - - &, * { - height: 48px !important; - width: 48px !important; - } - - &:hover { - cursor: pointer; - opacity: 0.5; + &.modal { + bottom: $modalBottom; + left: $modalLeft; + right: $modalRight; + top: $modalTop; + z-index: $modalZ; + } + + &.popover { + left: $popoverLeft; + top: $popoverTop; + height: calc(100vh - $popoverTop - $popoverBottom); + width: calc(100vw - $popoverLeft - $popoverRight); + z-index: $popoverZ; + } + + .buttonRow { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; + padding: $modalPadding 0 0 0; + + button:not([disabled]) { + color: white !important; + + svg { + fill: white !important; + } + } + } + + .childContainer { + flex: 1; + overflow-x: hidden; + overflow-y: auto; + } + + .closeIcon { + font-size: 4em; + position: absolute; + right: 1rem; + top: 0.5rem; + z-index: 100; + + &, * { + height: 48px !important; + width: 48px !important; + } + + &:hover { + cursor: pointer; + opacity: 0.5; + } + } + + .titleRow { + margin-bottom: $modalPadding; } } diff --git a/js/src/ui/Portal/portal.example.js b/js/src/ui/Portal/portal.example.js new file mode 100644 index 000000000..7305a27b0 --- /dev/null +++ b/js/src/ui/Portal/portal.example.js @@ -0,0 +1,121 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import React, { Component } from 'react'; + +import { Button } from '~/ui'; +import PlaygroundExample from '~/playground/playgroundExample'; + +import Modal from '../Modal'; +import Portal from './portal'; + +export default class PortalExample extends Component { + state = { + open: [] + }; + + render () { + const { open } = this.state; + + return ( + <div> + <PlaygroundExample name='Standard Portal'> + <div> + <button onClick={ this.handleOpen(0) }>Open</button> + <Portal + open={ open[0] || false } + onClose={ this.handleClose } + > + <p>This is the first portal</p> + </Portal> + </div> + </PlaygroundExample> + + <PlaygroundExample name='Popover Portal'> + <div> + <button onClick={ this.handleOpen(1) }>Open</button> + <Portal + isChildModal + open={ open[1] || false } + onClose={ this.handleClose } + > + <p>This is the second portal</p> + </Portal> + </div> + </PlaygroundExample> + + <PlaygroundExample name='Portal in Modal'> + <div> + <button onClick={ this.handleOpen(2) }>Open</button> + + <Modal + title='Modal' + visible={ open[2] || false } + > + <button onClick={ this.handleOpen(3) }>Open</button> + <button onClick={ this.handleClose }>Close</button> + </Modal> + + <Portal + isChildModal + open={ open[3] || false } + onClose={ this.handleClose } + > + <p>This is the second portal</p> + </Portal> + </div> + </PlaygroundExample> + + <PlaygroundExample name='Portal with Buttons'> + <div> + <button onClick={ this.handleOpen(4) }>Open</button> + <Portal + activeStep={ 0 } + buttons={ [ + <Button + key='close' + label='close' + onClick={ this.handleClose } + /> + ] } + isChildModal + open={ open[4] || false } + onClose={ this.handleClose } + steps={ [ 'step 1', 'step 2' ] } + title='Portal with button' + > + <p>This is the fourth portal</p> + </Portal> + </div> + </PlaygroundExample> + </div> + ); + } + + handleOpen = (index) => { + return () => { + const { open } = this.state; + const nextOpen = open.slice(); + + nextOpen[index] = true; + this.setState({ open: nextOpen }); + }; + } + + handleClose = () => { + this.setState({ open: [] }); + } +} diff --git a/js/src/ui/Portal/portal.js b/js/src/ui/Portal/portal.js index 91be44309..dab7d63fd 100644 --- a/js/src/ui/Portal/portal.js +++ b/js/src/ui/Portal/portal.js @@ -14,13 +14,16 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import EventListener from 'react-event-listener'; import React, { Component, PropTypes } from 'react'; import ReactDOM from 'react-dom'; import ReactPortal from 'react-portal'; import keycode from 'keycode'; +import { nodeOrStringProptype } from '~/util/proptypes'; import { CloseIcon } from '~/ui/Icons'; import ParityBackground from '~/ui/ParityBackground'; +import Title from '~/ui/Title'; import styles from './portal.css'; @@ -29,101 +32,154 @@ export default class Portal extends Component { static propTypes = { onClose: PropTypes.func.isRequired, open: PropTypes.bool.isRequired, - + activeStep: PropTypes.number, + busy: PropTypes.bool, + busySteps: PropTypes.array, + buttons: PropTypes.array, children: PropTypes.node, className: PropTypes.string, - onKeyDown: PropTypes.func + hideClose: PropTypes.bool, + isChildModal: PropTypes.bool, + onKeyDown: PropTypes.func, + steps: PropTypes.array, + title: nodeOrStringProptype() }; - state = { - expanded: false + componentDidMount () { + this.setBodyOverflow(this.props.open); } componentWillReceiveProps (nextProps) { - if (this.props.open !== nextProps.open) { - const opening = nextProps.open; - const closing = !opening; - - if (opening) { - return this.setState({ expanded: true }); - } - - if (closing) { - return this.setState({ expanded: false }); - } + if (nextProps.open !== this.props.open) { + this.setBodyOverflow(nextProps.open); } } + componentWillUnmount () { + this.setBodyOverflow(false); + } + render () { - const { expanded } = this.state; - const { children, className } = this.props; + const { activeStep, busy, busySteps, children, className, isChildModal, open, steps, title } = this.props; - const classes = [ styles.overlay, className ]; - const backClasses = [ styles.backOverlay ]; - - if (expanded) { - classes.push(styles.expanded); - backClasses.push(styles.expanded); + if (!open) { + return null; } return ( - <ReactPortal isOpened onClose={ this.handleClose }> - <div className={ backClasses.join(' ') } onClick={ this.handleClose }> + <ReactPortal + isOpened + onClose={ this.handleClose } + > + <div + className={ styles.backOverlay } + onClick={ this.handleClose } + > <div - className={ classes.join(' ') } + className={ + [ + styles.overlay, + isChildModal + ? styles.popover + : styles.modal, + className + ].join(' ') + } onClick={ this.stopEvent } onKeyDown={ this.handleKeyDown } > + <EventListener + target='window' + onKeyUp={ this.handleKeyUp } + /> <ParityBackground className={ styles.parityBackground } /> - - { this.renderCloseIcon() } - { children } + { this.renderClose() } + <Title + activeStep={ activeStep } + busy={ busy } + busySteps={ busySteps } + className={ styles.titleRow } + steps={ steps } + title={ title } + /> + <div className={ styles.childContainer }> + { children } + </div> + { this.renderButtons() } </div> </div> </ReactPortal> ); } - renderCloseIcon () { - const { expanded } = this.state; + renderButtons () { + const { buttons } = this.props; - if (!expanded) { + if (!buttons) { return null; } return ( - <div className={ styles.closeIcon } onClick={ this.handleClose }> - <CloseIcon /> + <div className={ styles.buttonRow }> + { buttons } </div> ); } + renderClose () { + const { hideClose } = this.props; + + if (hideClose) { + return null; + } + + return ( + <CloseIcon + className={ styles.closeIcon } + onClick={ this.handleClose } + /> + ); + } + stopEvent = (event) => { event.preventDefault(); event.stopPropagation(); } handleClose = () => { - this.props.onClose(); + const { hideClose, onClose } = this.props; + + if (!hideClose) { + onClose(); + } } handleKeyDown = (event) => { + const { onKeyDown } = this.props; + + event.persist(); + + return onKeyDown + ? onKeyDown(event) + : false; + } + + handleKeyUp = (event) => { const codeName = keycode(event); switch (codeName) { case 'esc': event.preventDefault(); return this.handleClose(); - - default: - event.persist(); - return this.props.onKeyDown(event); } } handleDOMAction = (ref, method) => { - const refItem = typeof ref === 'string' ? this.refs[ref] : ref; - const element = ReactDOM.findDOMNode(refItem); + const element = ReactDOM.findDOMNode( + typeof ref === 'string' + ? this.refs[ref] + : ref + ); if (!element || typeof element[method] !== 'function') { console.warn('could not find', ref, 'or method', method); @@ -132,4 +188,12 @@ export default class Portal extends Component { return element[method](); } + + setBodyOverflow (open) { + if (!this.props.isChildModal) { + document.body.style.overflow = open + ? 'hidden' + : null; + } + } } diff --git a/js/src/ui/Portal/portal.spec.js b/js/src/ui/Portal/portal.spec.js new file mode 100644 index 000000000..fdc1ab4a7 --- /dev/null +++ b/js/src/ui/Portal/portal.spec.js @@ -0,0 +1,64 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import Portal from './'; + +let component; +let onClose; + +function render (props = {}) { + onClose = sinon.stub(); + component = shallow( + <Portal + onClose={ onClose } + open + { ...props } + /> + ); + + return component; +} + +describe('ui/Portal', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('title rendering', () => { + const TITLE = 'some test title'; + let title; + + beforeEach(() => { + title = render({ title: TITLE }).find('Title'); + }); + + it('renders the specified title', () => { + expect(title).to.have.length(1); + }); + + it('renders the passed title', () => { + expect(title.props().title).to.equal(TITLE); + }); + }); +}); diff --git a/js/src/views/Dapps/Summary/index.js b/js/src/ui/QrCode/index.js similarity index 95% rename from js/src/views/Dapps/Summary/index.js rename to js/src/ui/QrCode/index.js index 980ecff9a..1a8292ab9 100644 --- a/js/src/views/Dapps/Summary/index.js +++ b/js/src/ui/QrCode/index.js @@ -14,4 +14,4 @@ // 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 './summary'; +export default from './qrCode'; diff --git a/js/src/ui/QrCode/qrCode.example.js b/js/src/ui/QrCode/qrCode.example.js new file mode 100644 index 000000000..8f85f8147 --- /dev/null +++ b/js/src/ui/QrCode/qrCode.example.js @@ -0,0 +1,63 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import React, { Component } from 'react'; + +import PlaygroundExample from '~/playground/playgroundExample'; + +import QrCode from './'; + +export default class QrCodeExample extends Component { + render () { + return ( + <div> + <PlaygroundExample name='Simple QRCode'> + <QrCode + value='this is a test' + /> + </PlaygroundExample> + + <PlaygroundExample name='Simple QRCode with margin'> + <QrCode + margin={ 10 } + value='this is a test' + /> + </PlaygroundExample> + + <PlaygroundExample name='Ethereum Address QRCode'> + <QrCode + margin={ 10 } + value='0x8c30393085C8C3fb4C1fB16165d9fBac5D86E1D9' + /> + </PlaygroundExample> + + <PlaygroundExample name='Bitcoin Address QRCode'> + <QrCode + margin={ 10 } + value='3J98t1WpEZ73CNmQviecrnyiWrnqRhWNLy' + /> + </PlaygroundExample> + + <PlaygroundExample name='Big QRCode'> + <QrCode + size={ 10 } + value='this is a test' + /> + </PlaygroundExample> + </div> + ); + } +} diff --git a/js/src/ui/QrCode/qrCode.js b/js/src/ui/QrCode/qrCode.js new file mode 100644 index 000000000..172834ad9 --- /dev/null +++ b/js/src/ui/QrCode/qrCode.js @@ -0,0 +1,83 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +// https://github.com/cmanzana/qrcode-npm packaging the standard +// https://github.com/kazuhikoarase/qrcode-generator +import { qrcode } from 'qrcode-npm'; +import React, { Component, PropTypes } from 'react'; + +const QROPTS = { + CODE_TYPE: 4, + ERROR_LEVEL: 'M' +}; + +export default class QrCode extends Component { + static propTypes = { + className: PropTypes.string, + margin: PropTypes.number, + size: PropTypes.number, + value: PropTypes.string.isRequired + }; + + static defaultProps = { + margin: 2, + size: 4 + }; + + state = { + image: null + }; + + componentWillMount () { + this.generateCode(this.props); + } + + componentWillReceiveProps (nextProps) { + const hasChanged = nextProps.value !== this.props.value || + nextProps.size !== this.props.size || + nextProps.margin !== this.props.margin; + + if (hasChanged) { + this.generateCode(nextProps); + } + } + + render () { + const { className } = this.props; + const { image } = this.state; + + return ( + <div + className={ className } + dangerouslySetInnerHTML={ { + __html: image + } } + /> + ); + } + + generateCode (props) { + const { margin, size, value } = props; + const qr = qrcode(QROPTS.CODE_TYPE, QROPTS.ERROR_LEVEL); + + qr.addData(value); + qr.make(); + + this.setState({ + image: qr.createImgTag(size, margin) + }); + } +} diff --git a/js/src/ui/QrCode/qrCode.spec.js b/js/src/ui/QrCode/qrCode.spec.js new file mode 100644 index 000000000..0fd520f40 --- /dev/null +++ b/js/src/ui/QrCode/qrCode.spec.js @@ -0,0 +1,108 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import QrCode from './'; + +const DEFAULT_PROPS = { + margin: 1, + size: 4, + value: 'someTestValue' +}; + +let component; +let instance; + +function render (props = {}) { + component = shallow( + <QrCode + { ...DEFAULT_PROPS } + { ...props } + /> + ); + instance = component.instance(); + + return component; +} + +describe('ui/QrCode', () => { + beforeEach(() => { + render(); + sinon.spy(instance, 'generateCode'); + }); + + afterEach(() => { + instance.generateCode.restore(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + describe('lifecycle', () => { + describe('componentWillMount', () => { + it('generates the image on mount', () => { + instance.componentWillMount(); + expect(instance.generateCode).to.have.been.calledWith(DEFAULT_PROPS); + }); + }); + + describe('componentWillReceiveProps', () => { + it('does not re-generate when no props changed', () => { + instance.componentWillReceiveProps(DEFAULT_PROPS); + expect(instance.generateCode).not.to.have.been.called; + }); + + it('does not re-generate when className changed', () => { + const nextProps = Object.assign({}, DEFAULT_PROPS, { className: 'test' }); + + instance.componentWillReceiveProps(nextProps); + expect(instance.generateCode).not.to.have.been.called; + }); + + it('does not re-generate when additional property changed', () => { + const nextProps = Object.assign({}, DEFAULT_PROPS, { something: 'test' }); + + instance.componentWillReceiveProps(nextProps); + expect(instance.generateCode).not.to.have.been.called; + }); + + it('does re-generate when value changed', () => { + const nextProps = Object.assign({}, DEFAULT_PROPS, { value: 'somethingElse' }); + + instance.componentWillReceiveProps(nextProps); + expect(instance.generateCode).to.have.been.calledWith(nextProps); + }); + + it('does re-generate when size changed', () => { + const nextProps = Object.assign({}, DEFAULT_PROPS, { size: 10 }); + + instance.componentWillReceiveProps(nextProps); + expect(instance.generateCode).to.have.been.calledWith(nextProps); + }); + + it('does re-generate when margin changed', () => { + const nextProps = Object.assign({}, DEFAULT_PROPS, { margin: 10 }); + + instance.componentWillReceiveProps(nextProps); + expect(instance.generateCode).to.have.been.calledWith(nextProps); + }); + }); + }); +}); diff --git a/js/src/ui/SectionList/index.js b/js/src/ui/SectionList/index.js new file mode 100644 index 000000000..25971968d --- /dev/null +++ b/js/src/ui/SectionList/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +export default from './sectionList'; diff --git a/js/src/ui/SectionList/sectionList.css b/js/src/ui/SectionList/sectionList.css new file mode 100644 index 000000000..8e1594219 --- /dev/null +++ b/js/src/ui/SectionList/sectionList.css @@ -0,0 +1,84 @@ +/* Copyright 2015, 2016 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see <http://www.gnu.org/licenses/>. +*/ + +.section { + overflow-x: hidden; + position: relative; + + .overlay { + background: rgba(0, 0, 0, 0.85); + bottom: 0; + left: 0; + padding: 1.5em; + position: absolute; + right: 0; + top: 0; + z-index: 199; + } + + .row { + display: flex; + justify-content: center; + overflow-x: hidden; + + /* TODO: As per JS comments, the flex-base could be adjusted in the future to allow for */ + /* case where <> 3 columns are required should the need arrise from a UI pov. */ + .item { + box-sizing: border-box; + cursor: pointer; + display: flex; + flex: 0 1 33.33%; + opacity: 0.75; + overflow-x: hidden; + padding: 0.25em; + transition: all 0.75s cubic-bezier(0.23, 1, 0.32, 1); + + /* TODO: The hover and no-hover states can be improved to not "just appear" */ + &:not(:hover) { + & [data-hover="hide"] { + } + + & [data-hover="show"] { + display: none; + } + } + + &:hover { + opacity: 1; + z-index: 100; + + & [data-hover="hide"] { + display: none; + } + + & [data-hover="show"] { + } + } + + &.stretch-on:hover { + flex: 0 0 50%; + } + + &.stretch-off:hover { + } + } + } +} + +.section+.section { + margin-top: 1em; +} diff --git a/js/src/ui/SectionList/sectionList.example.js b/js/src/ui/SectionList/sectionList.example.js new file mode 100644 index 000000000..a0bd71b58 --- /dev/null +++ b/js/src/ui/SectionList/sectionList.example.js @@ -0,0 +1,94 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import React, { Component } from 'react'; + +import PlaygroundExample from '~/playground/playgroundExample'; +import SectionList from './'; + +const ITEM_STYLE = { + backgroundColor: 'rgba(0, 0, 0, 0.75)', + padding: '1em' +}; + +const items = [ + { name: 'Jack', desc: 'Item number 1' }, + { name: 'Paul', desc: 'Item number 2' }, + { name: 'Matt', desc: 'Item number 3' }, + { name: 'Titi', desc: 'Item number 4' } +]; + +export default class SectionListExample extends Component { + state = { + showOverlay: true + }; + + render () { + return ( + <div> + <PlaygroundExample name='Simple Usage'> + { this.renderSimple() } + </PlaygroundExample> + + <PlaygroundExample name='With Overlay'> + { this.renderWithOverlay() } + </PlaygroundExample> + </div> + ); + } + + renderSimple () { + return ( + <SectionList + items={ items } + renderItem={ this.renderItem } + /> + ); + } + + renderWithOverlay () { + const { showOverlay } = this.state; + const overlay = ( + <div> + <p>Overlay</p> + <button onClick={ this.hideOverlay }>hide</button> + </div> + ); + + return ( + <SectionList + items={ items } + overlay={ showOverlay ? overlay : null } + renderItem={ this.renderItem } + /> + ); + } + + renderItem (item, index) { + const { desc, name } = item; + + return ( + <div style={ ITEM_STYLE }> + <h3>{ name }</h3> + <h3 data-hover='show'>{ desc }</h3> + </div> + ); + } + + hideOverlay = () => { + this.setState({ showOverlay: false }); + } +} diff --git a/js/src/ui/SectionList/sectionList.js b/js/src/ui/SectionList/sectionList.js new file mode 100644 index 000000000..7b2277f6a --- /dev/null +++ b/js/src/ui/SectionList/sectionList.js @@ -0,0 +1,103 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import React, { Component, PropTypes } from 'react'; + +import { chunkArray } from '~/util/array'; +import { arrayOrObjectProptype, nodeOrStringProptype } from '~/util/proptypes'; + +import styles from './sectionList.css'; + +// TODO: We probably want this to be passed via props - additional work required in that case to +// support the styling for both the hover and no-hover CSS for the pre/post sizes. Future work only +// if/when required. +const ITEMS_PER_ROW = 3; + +export default class SectionList extends Component { + static propTypes = { + className: PropTypes.string, + items: arrayOrObjectProptype().isRequired, + renderItem: PropTypes.func.isRequired, + noStretch: PropTypes.bool, + overlay: nodeOrStringProptype() + }; + + static defaultProps = { + noStretch: false + }; + + render () { + const { className, items } = this.props; + + if (!items || !items.length) { + return null; + } + + return ( + <section className={ [styles.section, className].join(' ') }> + { this.renderOverlay() } + { chunkArray(items, ITEMS_PER_ROW).map(this.renderRow) } + </section> + ); + } + + renderOverlay () { + const { overlay } = this.props; + + if (!overlay) { + return null; + } + + return ( + <div className={ styles.overlay }> + { overlay } + </div> + ); + } + + renderRow = (row, index) => { + return ( + <div + className={ styles.row } + key={ `row_${index}` } + > + { row.map(this.renderItem) } + </div> + ); + } + + renderItem = (item, index) => { + const { noStretch, renderItem } = this.props; + + // NOTE: Any children that is to be showed or hidden (depending on hover state) + // should have the data-hover="show|hide" attributes. For the current implementation + // this does the trick, however there may be a case for adding a hover attribute + // to an item (mouseEnter/mouseLeave events) and then adjusting the styling with + // :root[hover]/:root:not[hover] for the tragetted elements. Currently it is a + // CSS-only solution to let the browser do all the work via selectors. + return ( + <div + className={ [ + styles.item, + styles[`stretch-${noStretch ? 'off' : 'on'}`] + ].join(' ') } + key={ `item_${index}` } + > + { renderItem(item, index) } + </div> + ); + } +} diff --git a/js/src/ui/SectionList/sectionList.spec.js b/js/src/ui/SectionList/sectionList.spec.js new file mode 100644 index 000000000..480b79a52 --- /dev/null +++ b/js/src/ui/SectionList/sectionList.spec.js @@ -0,0 +1,103 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import SectionList from './'; + +const ITEMS = ['itemA', 'itemB', 'itemC', 'itemD', 'itemE']; + +let component; +let instance; +let renderItem; + +function render (props = {}) { + renderItem = sinon.stub(); + component = shallow( + <SectionList + className='testClass' + items={ ITEMS } + renderItem={ renderItem } + section='testSection' + /> + ); + instance = component.instance(); + + return component; +} + +describe('SectionList', () => { + beforeEach(() => { + render(); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('adds className as specified', () => { + expect(component.hasClass('testClass')).to.be.true; + }); + + describe('instance methods', () => { + describe('renderRow', () => { + let row; + + beforeEach(() => { + sinon.stub(instance, 'renderItem'); + row = instance.renderRow(['testA', 'testB']); + }); + + afterEach(() => { + instance.renderItem.restore(); + }); + + it('renders a row', () => { + expect(row).to.be.ok; + }); + + it('adds a key for the row', () => { + expect(row.key).to.be.ok; + }); + + it('calls renderItem for the items', () => { + expect(instance.renderItem).to.have.been.calledTwice; + }); + }); + + describe('renderItem', () => { + let item; + + beforeEach(() => { + item = instance.renderItem('testItem', 50); + }); + + it('renders an item', () => { + expect(item).to.be.ok; + }); + + it('adds a key for the item', () => { + expect(item.key).to.be.ok; + }); + + it('calls the external renderer', () => { + expect(renderItem).to.have.been.calledWith('testItem', 50); + }); + }); + }); +}); diff --git a/js/src/ui/Tags/tags.js b/js/src/ui/Tags/tags.js index bc86e6f3b..0afae61a2 100644 --- a/js/src/ui/Tags/tags.js +++ b/js/src/ui/Tags/tags.js @@ -28,14 +28,21 @@ export default class Tags extends Component { } render () { - return (<div className={ styles.tags }> - { this.renderTags() } - </div>); + const { tags } = this.props; + + if (!tags || tags.length === 0) { + return null; + } + + return ( + <div className={ styles.tags }> + { this.renderTags() } + </div> + ); } renderTags () { - const { handleAddSearchToken, setRefs } = this.props; - const tags = this.props.tags || []; + const { handleAddSearchToken, setRefs, tags } = this.props; const tagClasses = handleAddSearchToken ? [ styles.tag, styles.tagClickable ] @@ -47,14 +54,14 @@ export default class Tags extends Component { return tags .sort() - .map((tag, idx) => { + .map((tag, index) => { const onClick = handleAddSearchToken ? () => handleAddSearchToken(tag) : null; return ( <div - key={ idx } + key={ `tag_${index}` } className={ tagClasses.join(' ') } onClick={ onClick } ref={ setRef } diff --git a/js/src/ui/Modal/Title/index.js b/js/src/ui/Title/index.js similarity index 100% rename from js/src/ui/Modal/Title/index.js rename to js/src/ui/Title/index.js diff --git a/js/src/ui/Title/title.css b/js/src/ui/Title/title.css new file mode 100644 index 000000000..c211b0586 --- /dev/null +++ b/js/src/ui/Title/title.css @@ -0,0 +1,26 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see <http://www.gnu.org/licenses/>. +*/ + +.title { + .steps { + margin: -0.5em 0 -1em 0; + } + + .waiting { + margin: 1em -1em -1em -1em; + } +} diff --git a/js/src/ui/Modal/Title/title.js b/js/src/ui/Title/title.js similarity index 65% rename from js/src/ui/Modal/Title/title.js rename to js/src/ui/Title/title.js index 27197be28..3ac499c4a 100644 --- a/js/src/ui/Modal/Title/title.js +++ b/js/src/ui/Title/title.js @@ -14,35 +14,49 @@ // 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 { LinearProgress } from 'material-ui'; import { Step, Stepper, StepLabel } from 'material-ui/Stepper'; +import React, { Component, PropTypes } from 'react'; +// TODO: It would make sense (going forward) to replace all uses of +// ContainerTitle with this component. In that case the styles for the +// h3 (title) can be pulled from there. (As it stands the duplication +// between the 2 has been removed, but as a short-term DRY only) +import { Title as ContainerTitle } from '~/ui/Container'; import { nodeOrStringProptype } from '~/util/proptypes'; -import styles from '../modal.css'; +import styles from './title.css'; export default class Title extends Component { static propTypes = { + activeStep: PropTypes.number, busy: PropTypes.bool, - current: PropTypes.number, + busySteps: PropTypes.array, + className: PropTypes.string, steps: PropTypes.array, - waiting: PropTypes.array, title: nodeOrStringProptype() } render () { - const { current, steps, title } = this.props; + const { activeStep, className, steps, title } = this.props; + + if (!title && !steps) { + return null; + } return ( - <div className={ styles.title }> - <h3> - { + <div + className={ + [styles.title, className].join(' ') + } + > + <ContainerTitle + title={ steps - ? steps[current] + ? steps[activeStep || 0] : title } - </h3> + /> { this.renderSteps() } { this.renderWaiting() } </div> @@ -50,7 +64,7 @@ export default class Title extends Component { } renderSteps () { - const { current, steps } = this.props; + const { activeStep, steps } = this.props; if (!steps) { return; @@ -58,8 +72,7 @@ export default class Title extends Component { return ( <div className={ styles.steps }> - <Stepper - activeStep={ current }> + <Stepper activeStep={ activeStep }> { this.renderTimeline() } </Stepper> </div> @@ -82,8 +95,8 @@ export default class Title extends Component { } renderWaiting () { - const { current, busy, waiting } = this.props; - const isWaiting = busy || (waiting || []).includes(current); + const { activeStep, busy, busySteps } = this.props; + const isWaiting = busy || (busySteps || []).includes(activeStep); if (!isWaiting) { return null; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 79a7c7f7a..e1a4792d0 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -14,6 +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/>. +import AccountCard from './AccountCard'; import Actionbar from './Actionbar'; import ActionbarExport from './Actionbar/Export'; import ActionbarImport from './Actionbar/Import'; @@ -28,9 +29,12 @@ import ConfirmDialog from './ConfirmDialog'; import Container, { Title as ContainerTitle } from './Container'; import ContextProvider from './ContextProvider'; import CopyToClipboard from './CopyToClipboard'; +import CurrencySymbol from './CurrencySymbol'; +import DappCard from './DappCard'; +import DappIcon from './DappIcon'; import Editor from './Editor'; import Errors from './Errors'; -import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; +import Form, { AddressSelect, FormWrap, Input, InputAddress, InputAddressSelect, InputChip, InputDate, InputInline, InputTime, Label, RadioButtons, Select, TypedInput } from './Form'; import GasPriceEditor from './GasPriceEditor'; import GasPriceSelector from './GasPriceSelector'; import Icons from './Icons'; @@ -44,15 +48,20 @@ import muiTheme from './Theme'; import Page from './Page'; import ParityBackground from './ParityBackground'; import PasswordStrength from './Form/PasswordStrength'; +import Portal from './Portal'; +import QrCode from './QrCode'; +import SectionList from './SectionList'; import ShortenedHash from './ShortenedHash'; import SignerIcon from './SignerIcon'; import Tags from './Tags'; +import Title from './Title'; import Tooltips, { Tooltip } from './Tooltips'; import TxHash from './TxHash'; import TxList from './TxList'; import Warning from './Warning'; export { + AccountCard, Actionbar, ActionbarExport, ActionbarImport, @@ -69,6 +78,9 @@ export { ContainerTitle, ContextProvider, CopyToClipboard, + CurrencySymbol, + DappIcon, + DappCard, Editor, Errors, Form, @@ -80,9 +92,12 @@ export { InputAddress, InputAddressSelect, InputChip, + InputDate, InputInline, + InputTime, IdentityIcon, IdentityName, + Label, LanguageSelector, Loading, MethodDecoding, @@ -93,11 +108,15 @@ export { Page, ParityBackground, PasswordStrength, + Portal, + QrCode, RadioButtons, - ShortenedHash, Select, + ShortenedHash, + SectionList, SignerIcon, Tags, + Title, Tooltip, Tooltips, TxHash, diff --git a/js/src/util/array.js b/js/src/util/array.js new file mode 100644 index 000000000..e433c0163 --- /dev/null +++ b/js/src/util/array.js @@ -0,0 +1,26 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +// http://stackoverflow.com/questions/11318680/split-array-into-chunks-of-n-length +export function chunkArray (array, size) { + return array + .map((item, index) => { + return index % size === 0 + ? array.slice(index, index + size) + : null; + }) + .filter((item) => item); +} diff --git a/js/src/util/array.spec.js b/js/src/util/array.spec.js new file mode 100644 index 000000000..7d760388d --- /dev/null +++ b/js/src/util/array.spec.js @@ -0,0 +1,29 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { chunkArray } from './array'; + +describe('util/array', () => { + describe('chunkArray', () => { + it('splits array into equal chunks', () => { + expect(chunkArray([1, 2, 3, 4], 2)).to.deep.equal([[1, 2], [3, 4]]); + }); + + it('splits array into equal chunks (non-divisible)', () => { + expect(chunkArray([1, 2, 3, 4], 3)).to.deep.equal([[1, 2, 3], [4]]); + }); + }); +}); diff --git a/js/src/views/Account/Header/header.css b/js/src/views/Account/Header/header.css index 6c1527129..5c2fd001f 100644 --- a/js/src/views/Account/Header/header.css +++ b/js/src/views/Account/Header/header.css @@ -14,17 +14,30 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -.balances, .tags { - clear: both; -} .editicon { margin-left: 0.5em; } -.floatleft { +.info { + margin: 0 156px 0 0; +} + +.identityIcon { float: left; - margin-bottom: 0.5em; + margin-right: -100%; +} + +.qrcode { + float: right; + margin-top: 1.5em; +} + +.addressline, +.infoline, +.uuidline, +.title { + margin-left: 72px; } .addressline, @@ -50,3 +63,7 @@ display: inline-block; margin-left: .5em; } + +.tags { + clear: both; +} diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index f5694177a..93e129360 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -17,9 +17,7 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '~/ui'; -import CopyToClipboard from '~/ui/CopyToClipboard'; -import Certifications from '~/ui/Certifications'; +import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, IdentityIcon, IdentityName, QrCode, Tags } from '~/ui'; import styles from './header.css'; @@ -53,8 +51,15 @@ export default class Header extends Component { return ( <div className={ className }> <Container> - <IdentityIcon address={ address } /> - <div className={ styles.floatleft }> + <QrCode + className={ styles.qrcode } + value={ address } + /> + <IdentityIcon + address={ address } + className={ styles.identityIcon } + /> + <div className={ styles.info }> { this.renderName() } <div className={ [ hideName ? styles.bigaddress : '', styles.addressline ].join(' ') }> <CopyToClipboard data={ address } /> @@ -65,16 +70,17 @@ export default class Header extends Component { { meta.description } </div> { this.renderTxCount() } + <div className={ styles.balances }> + <Balance + account={ account } + balance={ balance } + /> + <Certifications address={ address } /> + </div> </div> <div className={ styles.tags }> <Tags tags={ meta.tags } /> </div> - <div className={ styles.balances }> - <Balance - account={ account } - balance={ balance } /> - <Certifications address={ address } /> - </div> { children } </Container> </div> @@ -92,11 +98,14 @@ export default class Header extends Component { return ( <ContainerTitle + className={ styles.title } title={ <IdentityName address={ address } - unknown /> - } /> + unknown + /> + } + /> ); } @@ -120,7 +129,8 @@ export default class Header extends Component { defaultMessage='{count} outgoing transactions' values={ { count: txCount.toFormat() - } } /> + } } + /> </div> ); } @@ -139,7 +149,8 @@ export default class Header extends Component { defaultMessage='uuid: {uuid}' values={ { uuid - } } /> + } } + /> </div> ); } diff --git a/js/src/views/Account/Header/header.spec.js b/js/src/views/Account/Header/header.spec.js index 5ae5104d2..283f90f37 100644 --- a/js/src/views/Account/Header/header.spec.js +++ b/js/src/views/Account/Header/header.spec.js @@ -68,37 +68,93 @@ describe('views/Account/Header', () => { }); describe('sections', () => { - it('renders the Balance', () => { - render({ balance: { balance: 'testing' } }); - const balance = component.find('Connect(Balance)'); + describe('Balance', () => { + let balance; - expect(balance).to.have.length(1); - expect(balance.props().account).to.deep.equal(ACCOUNT); - expect(balance.props().balance).to.deep.equal({ balance: 'testing' }); + beforeEach(() => { + render({ balance: { balance: 'testing' } }); + balance = component.find('Connect(Balance)'); + }); + + it('renders', () => { + expect(balance).to.have.length(1); + }); + + it('passes the account', () => { + expect(balance.props().account).to.deep.equal(ACCOUNT); + }); + + it('passes the balance', () => { + + }); }); - it('renders the Certifications', () => { - render(); - const certs = component.find('Connect(Certifications)'); + describe('Certifications', () => { + let certs; - expect(certs).to.have.length(1); - expect(certs.props().address).to.deep.equal(ACCOUNT.address); + beforeEach(() => { + render(); + certs = component.find('Connect(Certifications)'); + }); + + it('renders', () => { + expect(certs).to.have.length(1); + }); + + it('passes the address', () => { + expect(certs.props().address).to.deep.equal(ACCOUNT.address); + }); }); - it('renders the IdentityIcon', () => { - render(); - const icon = component.find('Connect(IdentityIcon)'); + describe('IdentityIcon', () => { + let icon; - expect(icon).to.have.length(1); - expect(icon.props().address).to.equal(ACCOUNT.address); + beforeEach(() => { + render(); + icon = component.find('Connect(IdentityIcon)'); + }); + + it('renders', () => { + expect(icon).to.have.length(1); + }); + + it('passes the address', () => { + expect(icon.props().address).to.deep.equal(ACCOUNT.address); + }); }); - it('renders the Tags', () => { - render(); - const tags = component.find('Tags'); + describe('QrCode', () => { + let qr; - expect(tags).to.have.length(1); - expect(tags.props().tags).to.deep.equal(ACCOUNT.meta.tags); + beforeEach(() => { + render(); + qr = component.find('QrCode'); + }); + + it('renders', () => { + expect(qr).to.have.length(1); + }); + + it('passes the address', () => { + expect(qr.props().value).to.deep.equal(ACCOUNT.address); + }); + }); + + describe('Tags', () => { + let tags; + + beforeEach(() => { + render(); + tags = component.find('Tags'); + }); + + it('renders', () => { + expect(tags).to.have.length(1); + }); + + it('passes the tags', () => { + expect(tags.props().tags).to.deep.equal(ACCOUNT.meta.tags); + }); }); }); }); diff --git a/js/src/views/Application/Extension/extension.css b/js/src/views/Application/Extension/extension.css new file mode 100644 index 000000000..98d094a9f --- /dev/null +++ b/js/src/views/Application/Extension/extension.css @@ -0,0 +1,52 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see <http://www.gnu.org/licenses/>. +*/ + +.body { + background: #f80; + color: white; + opacity: 1; + max-width: 500px; + padding: 1em 4em 1em 2em; + position: fixed; + right: 1.5em; + top: 1.5em; + z-index: 1000; + + .button { + background: rgba(0, 0, 0, 0.5); + color: white !important; + + svg { + fill: white !important; + } + } + + .buttonrow { + text-align: right; + } + + p { + color: white; + } + + .close { + cursor: pointer; + position: absolute; + right: 1em; + top: 1em; + } +} diff --git a/js/src/views/Application/Extension/extension.js b/js/src/views/Application/Extension/extension.js new file mode 100644 index 000000000..aff332f9a --- /dev/null +++ b/js/src/views/Application/Extension/extension.js @@ -0,0 +1,74 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { Button } from '~/ui'; +import { CloseIcon, CheckIcon } from '~/ui/Icons'; + +import Store from './store'; +import styles from './extension.css'; + +@observer +export default class Extension extends Component { + store = new Store(); + + render () { + const { showWarning } = this.store; + + if (!showWarning) { + return null; + } + + return ( + <div className={ styles.body }> + <CloseIcon + className={ styles.close } + onClick={ this.onClose } + /> + <p> + <FormattedMessage + id='extension.intro' + defaultMessage='Parity now has an extension available for Chrome that allows safe browsing of Ethereum-enabled distributed applications. It is highly recommended that you install this extension to further enhance your Parity experience.' + /> + </p> + <p className={ styles.buttonrow }> + <Button + className={ styles.button } + icon={ <CheckIcon /> } + label={ + <FormattedMessage + id='extension.install' + defaultMessage='Install the extension now' + /> + } + onClick={ this.onInstallClick } + /> + </p> + </div> + ); + } + + onClose = () => { + this.store.snoozeWarning(); + } + + onInstallClick = () => { + this.store.installExtension(); + } +} diff --git a/js/src/views/Application/Extension/index.js b/js/src/views/Application/Extension/index.js new file mode 100644 index 000000000..ac1cfa015 --- /dev/null +++ b/js/src/views/Application/Extension/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +export default from './extension'; diff --git a/js/src/views/Application/Extension/store.js b/js/src/views/Application/Extension/store.js new file mode 100644 index 000000000..40a3f09e7 --- /dev/null +++ b/js/src/views/Application/Extension/store.js @@ -0,0 +1,89 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +/* global chrome */ + +import { action, computed, observable } from 'mobx'; + +import store from 'store'; +import browser from 'useragent.js/lib/browser'; + +const A_DAY = 24 * 60 * 60 * 1000; +const NEXT_DISPLAY = '_parity::extensionWarning::nextDisplay'; + +// 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig'; +const EXTENSION_PAGE = 'https://chrome.google.com/webstore/detail/himekenlppkgeaoeddcliojfddemadig'; + +export default class Store { + @observable isInstalling = false; + @observable nextDisplay = 0; + @observable shouldInstall = false; + + constructor () { + this.nextDisplay = store.get(NEXT_DISPLAY) || 0; + this.testInstall(); + } + + @computed get showWarning () { + return !this.isInstalling && this.shouldInstall && (Date.now() > this.nextDisplay); + } + + @action setInstalling = (isInstalling) => { + this.isInstalling = isInstalling; + } + + @action snoozeWarning = (sleep = A_DAY) => { + this.nextDisplay = Date.now() + sleep; + store.set(NEXT_DISPLAY, this.nextDisplay); + } + + @action testInstall = () => { + this.shouldInstall = this.readStatus(); + } + + readStatus = () => { + const hasExtension = Symbol.for('parity.extension') in window; + const ua = browser.analyze(navigator.userAgent || ''); + + if (hasExtension) { + return false; + } + + return (ua || {}).name.toLowerCase() === 'chrome'; + } + + installExtension = () => { + this.setInstalling(true); + + return new Promise((resolve, reject) => { + const link = document.createElement('link'); + + link.setAttribute('rel', 'chrome-webstore-item'); + link.setAttribute('href', EXTENSION_PAGE); + document.querySelector('head').appendChild(link); + + if (chrome && chrome.webstore && chrome.webstore.install) { + chrome.webstore.install(EXTENSION_PAGE, resolve, reject); + } else { + reject(new Error('Direct installation failed.')); + } + }) + .catch((error) => { + console.warn('Unable to perform direct install', error); + window.open(EXTENSION_PAGE, '_blank'); + }); + } +} diff --git a/js/src/views/Application/application.js b/js/src/views/Application/application.js index 2b32cf64e..684b7c390 100644 --- a/js/src/views/Application/application.js +++ b/js/src/views/Application/application.js @@ -26,6 +26,7 @@ import ParityBar from '../ParityBar'; import Snackbar from './Snackbar'; import Container from './Container'; import DappContainer from './DappContainer'; +import Extension from './Extension'; import FrameError from './FrameError'; import Status from './Status'; import Store from './store'; @@ -57,6 +58,14 @@ class Application extends Component { const [root] = (window.location.hash || '').replace('#/', '').split('/'); const isMinimized = root === 'app' || root === 'web'; + if (process.env.NODE_ENV !== 'production' && root === 'playground') { + return ( + <div> + { this.props.children } + </div> + ); + } + if (inFrame) { return ( <FrameError /> @@ -96,6 +105,7 @@ class Application extends Component { ? <Status upgradeStore={ this.upgradeStore } /> : null } + <Extension /> <Snackbar /> </Container> ); diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index fc299f7cb..91f1436bb 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -142,19 +142,16 @@ class Contract extends Component { > { this.renderBlockNumber(account.meta) } </Header> - <Queries accountsInfo={ accountsInfo } contract={ contract } values={ queryValues } /> - <Events isTest={ isTest } isLoading={ loadingEvents } events={ allEvents } /> - { this.renderDetails(account) } </Page> </div> @@ -194,7 +191,8 @@ class Contract extends Component { <Button icon={ <ContentClear /> } label='Close' - onClick={ this.closeDetailsDialog } /> + onClick={ this.closeDetailsDialog } + /> ); return ( @@ -244,28 +242,33 @@ class Contract extends Component { key='execute' icon={ <AvPlayArrow /> } label='execute' - onClick={ this.showExecuteDialog } />, + onClick={ this.showExecuteDialog } + />, <Button key='editmeta' icon={ <ContentCreate /> } label='edit' - onClick={ this.showEditDialog } />, + onClick={ this.showEditDialog } + />, <Button key='delete' icon={ <ActionDelete /> } - label='delete contract' - onClick={ this.showDeleteDialog } />, + label='forget contract' + onClick={ this.showDeleteDialog } + />, <Button key='viewDetails' icon={ <EyeIcon /> } label='view details' - onClick={ this.showDetailsDialog } /> + onClick={ this.showDetailsDialog } + /> ]; return ( <Actionbar title='Contract Information' - buttons={ !account ? [] : buttons } /> + buttons={ !account ? [] : buttons } + /> ); } @@ -277,7 +280,8 @@ class Contract extends Component { account={ account } visible={ showDeleteDialog } route='/contracts' - onClose={ this.closeDeleteDialog } /> + onClose={ this.closeDeleteDialog } + /> ); } @@ -291,7 +295,8 @@ class Contract extends Component { return ( <EditMeta account={ account } - onClose={ this.closeEditDialog } /> + onClose={ this.closeEditDialog } + /> ); } @@ -309,7 +314,8 @@ class Contract extends Component { contract={ contract } fromAddress={ fromAddress } onClose={ this.closeExecuteDialog } - onFromAddressChange={ this.onFromAddressChange } /> + onFromAddressChange={ this.onFromAddressChange } + /> ); } diff --git a/js/src/views/Dapps/builtin.json b/js/src/views/Dapps/builtin.json index 91c89d08a..c78ea0fc0 100644 --- a/js/src/views/Dapps/builtin.json +++ b/js/src/views/Dapps/builtin.json @@ -80,6 +80,7 @@ "description": "A Javascript development console complete with web3 and parity objects.", "version": "0.3", "author": "Gav Wood <gavin@ethcore.io>", + "position": "top-right", "visible": true, "secure": true } diff --git a/js/src/views/Dapps/dapps.js b/js/src/views/Dapps/dapps.js index 27e5c8b52..fc196fad8 100644 --- a/js/src/views/Dapps/dapps.js +++ b/js/src/views/Dapps/dapps.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -21,14 +21,13 @@ import React, { Component, PropTypes } from 'react'; import { FormattedMessage } from 'react-intl'; import { connect } from 'react-redux'; -import { AddDapps, DappPermissions } from '~/modals'; +import { DappPermissions, DappsVisible } from '~/modals'; import PermissionStore from '~/modals/DappPermissions/store'; -import { Actionbar, Button, Page } from '~/ui'; +import { Actionbar, Button, DappCard, Page } from '~/ui'; import { LockedIcon, VisibleIcon } from '~/ui/Icons'; import UrlButton from './UrlButton'; import DappsStore from './dappsStore'; -import Summary from './Summary'; import styles from './dapps.css'; @@ -51,6 +50,7 @@ class Dapps extends Component { render () { let externalOverlay = null; + if (this.store.externalOverlayVisible) { externalOverlay = ( <div className={ styles.overlay }> @@ -58,7 +58,8 @@ class Dapps extends Component { <div> <FormattedMessage id='dapps.external.warning' - defaultMessage='Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.' /> + defaultMessage='Applications made available on the network by 3rd-party authors are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each before interacting.' + /> </div> <div> <Checkbox @@ -66,10 +67,12 @@ class Dapps extends Component { label={ <FormattedMessage id='dapps.external.accept' - defaultMessage='I understand that these applications are not affiliated with Parity' /> + defaultMessage='I understand that these applications are not affiliated with Parity' + /> } checked={ false } - onCheck={ this.onClickAcceptExternal } /> + onCheck={ this.onClickAcceptExternal } + /> </div> </div> </div> @@ -78,14 +81,15 @@ class Dapps extends Component { return ( <div> - <AddDapps store={ this.store } /> <DappPermissions store={ this.permissionStore } /> + <DappsVisible store={ this.store } /> <Actionbar className={ styles.toolbar } title={ <FormattedMessage id='dapps.label' - defaultMessage='Decentralized Applications' /> + defaultMessage='Decentralized Applications' + /> } buttons={ [ <UrlButton key='url' />, @@ -95,7 +99,8 @@ class Dapps extends Component { label={ <FormattedMessage id='dapps.button.edit' - defaultMessage='edit' /> + defaultMessage='edit' + /> } onClick={ this.store.openModal } />, @@ -105,9 +110,11 @@ class Dapps extends Component { label={ <FormattedMessage id='dapps.button.permissions' - defaultMessage='permissions' /> + defaultMessage='permissions' + /> } - onClick={ this.openPermissionsModal } /> + onClick={ this.openPermissionsModal } + /> ] } /> <Page> @@ -136,8 +143,13 @@ class Dapps extends Component { return ( <div className={ styles.item } - key={ app.id }> - <Summary app={ app } /> + key={ app.id } + > + <DappCard + app={ app } + showLink + showTags + /> </div> ); } diff --git a/js/src/views/Dapps/dappsStore.js b/js/src/views/Dapps/dappsStore.js index efbde9ef4..4fe86949e 100644 --- a/js/src/views/Dapps/dappsStore.js +++ b/js/src/views/Dapps/dappsStore.js @@ -14,6 +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/>. +import EventEmitter from 'eventemitter3'; import { action, computed, observable, transaction } from 'mobx'; import store from 'store'; @@ -30,7 +31,7 @@ const BUILTIN_APPS_KEY = 'BUILTIN_APPS_KEY'; let instance = null; -export default class DappsStore { +export default class DappsStore extends EventEmitter { @observable apps = []; @observable displayApps = {}; @observable modalOpen = false; @@ -44,6 +45,8 @@ export default class DappsStore { _registryAppsIds = null; constructor (api) { + super(); + this._api = api; this.readDisplayApps(); @@ -51,6 +54,14 @@ export default class DappsStore { this.subscribeToChanges(); } + static get (api) { + if (!instance) { + instance = new DappsStore(api); + } + + return instance; + } + /** * Try to find the app from the local (local or builtin) * apps, else fetch from the node @@ -68,6 +79,10 @@ export default class DappsStore { } return this.fetchRegistryApp(dappReg, id, true); + }) + .then((app) => { + this.emit('loaded', app); + return app; }); } @@ -90,14 +105,6 @@ export default class DappsStore { .then(this.writeDisplayApps); } - static get (api) { - if (!instance) { - instance = new DappsStore(api); - } - - return instance; - } - subscribeToChanges () { const { dappReg } = Contracts.get(); diff --git a/js/src/views/ParityBar/accountStore.js b/js/src/views/ParityBar/accountStore.js new file mode 100644 index 000000000..b53f40dd2 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.js @@ -0,0 +1,101 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { action, observable, transaction } from 'mobx'; + +export default class AccountStore { + @observable accounts = []; + @observable defaultAccount = null; + @observable isLoading = false; + + constructor (api) { + this._api = api; + + this.loadAccounts(); + this.subscribeDefaultAccount(); + } + + @action setAccounts = (accounts) => { + this.accounts = accounts; + } + + @action setDefaultAccount = (defaultAccount) => { + this.defaultAccount = defaultAccount; + } + + @action setLoading = (isLoading) => { + this.isLoading = isLoading; + } + + makeDefaultAccount = (address) => { + const accounts = [address].concat( + this.accounts + .filter((account) => account.address !== address) + .map((account) => account.address) + ); + + return this._api.parity + .setNewDappsWhitelist(accounts) + .catch((error) => { + console.warn('makeDefaultAccount', error); + }); + } + + loadAccounts () { + this.setLoading(true); + + return Promise + .all([ + this._api.parity.getNewDappsWhitelist(), + this._api.parity.allAccountsInfo() + ]) + .then(([whitelist, accounts]) => { + transaction(() => { + this.setLoading(false); + this.setAccounts( + Object + .keys(accounts) + .filter((address) => { + const isAccount = accounts[address].uuid; + const isWhitelisted = !whitelist || whitelist.includes(address); + + return isAccount && isWhitelisted; + }) + .map((address) => { + const account = accounts[address]; + + account.address = address; + account.default = address === this.defaultAccount; + + return account; + }) + ); + }); + }) + .catch((error) => { + this.setLoading(false); + console.warn('loadAccounts', error); + }); + } + + subscribeDefaultAccount () { + return this._api.subscribe('parity_defaultAccount', (error, defaultAccount) => { + if (!error) { + this.setDefaultAccount(defaultAccount); + } + }); + } +} diff --git a/js/src/views/ParityBar/accountStore.spec.js b/js/src/views/ParityBar/accountStore.spec.js new file mode 100644 index 000000000..6dd219806 --- /dev/null +++ b/js/src/views/ParityBar/accountStore.spec.js @@ -0,0 +1,104 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import sinon from 'sinon'; + +import AccountStore from './accountStore'; + +import { ACCOUNT_DEFAULT, ACCOUNT_FIRST, ACCOUNT_NEW, createApi } from './parityBar.test.js'; + +let api; +let store; + +function create () { + api = createApi(); + store = new AccountStore(api); + + return store; +} + +describe('views/ParityBar/AccountStore', () => { + beforeEach(() => { + create(); + }); + + describe('constructor', () => { + it('subscribes to defaultAccount', () => { + expect(api.subscribe).to.have.been.calledWith('parity_defaultAccount'); + }); + }); + + describe('@action', () => { + describe('setAccounts', () => { + it('sets the accounts', () => { + store.setAccounts('testing'); + expect(store.accounts).to.equal('testing'); + }); + }); + + describe('setDefaultAccount', () => { + it('sets the default account', () => { + store.setDefaultAccount('testing'); + expect(store.defaultAccount).to.equal('testing'); + }); + }); + + describe('setLoading', () => { + it('sets the loading status', () => { + store.setLoading('testing'); + expect(store.isLoading).to.equal('testing'); + }); + }); + }); + + describe('operations', () => { + describe('loadAccounts', () => { + beforeEach(() => { + sinon.spy(store, 'setAccounts'); + + return store.loadAccounts(); + }); + + afterEach(() => { + store.setAccounts.restore(); + }); + + it('calls into parity_getNewDappsWhitelist', () => { + expect(api.parity.getNewDappsWhitelist).to.have.been.called; + }); + + it('calls into parity_allAccountsInfo', () => { + expect(api.parity.allAccountsInfo).to.have.been.called; + }); + + it('sets the accounts', () => { + expect(store.setAccounts).to.have.been.called; + }); + }); + + describe('makeDefaultAccount', () => { + beforeEach(() => { + return store.makeDefaultAccount(ACCOUNT_NEW); + }); + + it('calls into parity_setNewDappsWhitelist (with ordering)', () => { + expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith([ + ACCOUNT_NEW, ACCOUNT_FIRST, ACCOUNT_DEFAULT + ]); + }); + }); + }); +}); diff --git a/js/src/views/ParityBar/parityBar.css b/js/src/views/ParityBar/parityBar.css index a955bd687..980638c12 100644 --- a/js/src/views/ParityBar/parityBar.css +++ b/js/src/views/ParityBar/parityBar.css @@ -15,13 +15,46 @@ /* along with Parity. If not, see <http://www.gnu.org/licenses/>. */ -.bar, .expanded { - position: fixed; - bottom: 0; - right: 0; - font-size: 16px; - font-family: 'Roboto', sans-serif; - z-index: 10001; +$overlayZ: 10000; +$modalZ: 10001; + +.account { + display: flex; + flex: 1; + overflow: hidden; + position: relative; + + .accountOverlay { + position: absolute; + right: 0.5em; + top: 0.5em; + } + + .iconDisabled { + opacity: 0.15; + } + + .selected, + .unselected { + margin: 0.125em 0; + + &:focus { + outline: none; + } + } + + .unselected { + background: rgba(0, 0, 0, 0.4) !important; + } + + .selected { + background: rgba(255, 255, 255, 0.35) !important; + } +} + +.container { + display: flex; + flex-direction: column; } .overlay { @@ -31,7 +64,17 @@ bottom: 0; left: 0; background: rgba(255, 255, 255, 0.5); - z-index: 10000; + z-index: $overlayZ; + user-select: none; +} + +.bar, +.expanded { + position: fixed; + font-size: 16px; + font-family: 'Roboto', sans-serif; + z-index: $modalZ; + user-select: none; } .bar { @@ -39,35 +82,58 @@ display: flex; flex-wrap: wrap; width: 100%; + top: 0; + left: 0; + + &.moving { + bottom: 0; + right: 0; + + &:hover { + cursor: move; + } + } +} + +.parityBg { + position: fixed; + + transition-property: left, top, right, bottom; + transition-duration: 0.25s; + transition-timing-function: ease; + + &.moving { + transition-duration: 0.05s; + transition-timing-function: ease-in-out; + } } .expanded { - right: 1em; border-radius: 4px 4px 0 0; display: flex; flex-direction: column; - max-height: 50vh; -} + min-height: 30vh; + max-height: 80vh; + max-width: calc(100vw - 1em); -.expanded .content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - display: flex; - background: rgba(0, 0, 0, 0.8); - min-height: 16em; + .content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + background: rgba(0, 0, 0, 0.8); + } } .corner { - position: absolute; - bottom: 0; - right: 1em; border-radius: 4px 4px 0 0; } .cornercolor { background: rgba(0, 0, 0, 0.5); padding: 0.5em 1em; + display: flex; + align-items: center; } .link { @@ -76,19 +142,21 @@ outline: none !important; color: white !important; display: inline-block; -} -.link img, .link svg { - height: 24px !important; - width: 24px !important; - margin: 2px 0.5em 0 0; + img, svg { + height: 24px !important; + width: 24px !important; + margin: 2px 0.5em 0 0; + } } .link+.link { margin-left: 1em; } -.button, .parityButton { +.button, +.iconButton, +.parityButton { overflow: visible !important; } @@ -101,6 +169,14 @@ fill: white !important; } +.iconButton { + min-width: 2em !important; + + img { + margin: 6px 0.5em 0 0.5em; + } +} + .label { position: relative; display: inline-block; @@ -123,20 +199,21 @@ padding: 0.5em 1em; background: rgba(0, 0, 0, 0.25); margin-bottom: 0; + + &:after { + clear: both; + } } -.header:after { - clear: both; -} +.header, +.corner { + button { + color: white !important; + } -.header button, -.corner button { - color: white !important; -} - -.header svg, -.coner svg { - fill: white !important; + svg { + fill: white !important; + } } .body { @@ -150,17 +227,49 @@ .actions { float: right; margin-top: -2px; + + div { + margin-left: 1em; + display: inline-block; + cursor: pointer; + } } -.actions div { - margin-left: 1em; - display: inline-block; - cursor: pointer; -} - -.parityIcon, .signerIcon { +.parityIcon, +.signerIcon { width: 24px; height: 24px; vertical-align: middle; margin-left: 12px; } + +.moveIcon { + display: flex; + align-items: center; + + &:hover { + cursor: move; + } +} + +.dragButton { + width: 1em; + height: 1em; + margin-left: 0.5em; + + background-color: white; + opacity: 0.25; + border-radius: 50%; + + transition-property: opacity; + transition-duration: 0.1s; + transition-timing-function: ease-in-out; + + &:hover { + opacity: 0.5; + } + + &.moving { + opacity: 0.75; + } +} diff --git a/js/src/views/ParityBar/parityBar.js b/js/src/views/ParityBar/parityBar.js index 0ecb57ae7..63036b9e4 100644 --- a/js/src/views/ParityBar/parityBar.js +++ b/js/src/views/ParityBar/parityBar.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -14,19 +14,38 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. +import { throttle } from 'lodash'; +import { observer } from 'mobx-react'; import React, { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; +import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router'; import { connect } from 'react-redux'; -import ActionFingerprint from 'material-ui/svg-icons/action/fingerprint'; -import ContentClear from 'material-ui/svg-icons/content/clear'; +import store from 'store'; -import { Badge, Button, ContainerTitle, ParityBackground } from '~/ui'; -import { Embedded as Signer } from '../Signer'; +import imagesEthcoreBlock from '~/../assets/images/parity-logo-white-no-text.svg'; +import { AccountCard, Badge, Button, ContainerTitle, IdentityIcon, ParityBackground, SectionList } from '~/ui'; +import { CancelIcon, FingerprintIcon } from '~/ui/Icons'; +import DappsStore from '~/views/Dapps/dappsStore'; +import { Embedded as Signer } from '~/views/Signer'; -import imagesEthcoreBlock from '!url-loader!../../../assets/images/parity-logo-white-no-text.svg'; +import AccountStore from './accountStore'; import styles from './parityBar.css'; +const LS_STORE_KEY = '_parity::parityBar'; +const DEFAULT_POSITION = { right: '1em', bottom: 0 }; +const DISPLAY_ACCOUNTS = 'accounts'; +const DISPLAY_SIGNER = 'signer'; + +@observer class ParityBar extends Component { + app = null; + measures = null; + moving = false; + + static contextTypes = { + api: PropTypes.object.isRequired + }; static propTypes = { dapp: PropTypes.bool, @@ -35,7 +54,33 @@ class ParityBar extends Component { }; state = { - opened: false + displayType: DISPLAY_SIGNER, + moving: false, + opened: false, + position: DEFAULT_POSITION + }; + + constructor (props) { + super(props); + + this.debouncedMouseMove = throttle( + this._onMouseMove, + 40, + { leading: true, trailing: true } + ); + } + + componentWillMount () { + const { api } = this.context; + + this.accountStore = new AccountStore(api); + + // Hook to the dapp loaded event to position the + // Parity Bar accordingly + DappsStore.get(api).on('loaded', (app) => { + this.app = app; + this.loadPosition(); + }); } componentWillReceiveProps (nextProps) { @@ -47,14 +92,14 @@ class ParityBar extends Component { } if (count < newCount) { - this.setOpened(true); + this.setOpened(true, DISPLAY_SIGNER); } else if (newCount === 0 && count === 1) { this.setOpened(false); } } - setOpened (opened) { - this.setState({ opened }); + setOpened (opened, displayType = DISPLAY_SIGNER) { + this.setState({ displayType, opened }); if (!this.bar) { return; @@ -74,11 +119,71 @@ class ParityBar extends Component { } render () { - const { opened } = this.state; + const { moving, opened, position } = this.state; - return opened - ? this.renderExpanded() - : this.renderBar(); + const containerClassNames = opened + ? [ styles.overlay ] + : [ styles.bar ]; + + if (!opened && moving) { + containerClassNames.push(styles.moving); + } + + const parityBgClassNames = [ + opened + ? styles.expanded + : styles.corner, + styles.parityBg + ]; + + if (moving) { + parityBgClassNames.push(styles.moving); + } + + const parityBgStyle = { + ...position + }; + + // Open the Signer at one of the four corners + // of the screen + if (opened) { + // Set at top or bottom of the screen + if (position.top !== undefined) { + parityBgStyle.top = 0; + } else { + parityBgStyle.bottom = 0; + } + + // Set at left or right of the screen + if (position.left !== undefined) { + parityBgStyle.left = '1em'; + } else { + parityBgStyle.right = '1em'; + } + } + + return ( + <div + className={ containerClassNames.join(' ') } + onMouseEnter={ this.onMouseEnter } + onMouseLeave={ this.onMouseLeave } + onMouseMove={ this.onMouseMove } + onMouseUp={ this.onMouseUp } + ref={ this.onRef } + > + <ParityBackground + className={ parityBgClassNames.join(' ') } + ref='container' + style={ parityBgStyle } + > + { + opened + ? this.renderExpanded() + : this.renderBar() + } + </ParityBackground> + </div> + ); } renderBar () { @@ -88,36 +193,72 @@ class ParityBar extends Component { return null; } - const parityIcon = ( - <img - src={ imagesEthcoreBlock } - className={ styles.parityIcon } /> + return ( + <div className={ styles.cornercolor }> + <Button + className={ styles.iconButton } + icon={ + <IdentityIcon + address={ this.accountStore.defaultAccount } + button + center + inline + /> + } + onClick={ this.toggleAccountsDisplay } + /> + { + this.renderLink( + <Button + className={ styles.parityButton } + icon={ + <img + className={ styles.parityIcon } + src={ imagesEthcoreBlock } + /> + } + label={ + this.renderLabel( + <FormattedMessage + id='parityBar.label.parity' + defaultMessage='Parity' + /> + ) + } + /> + ) + } + <Button + className={ styles.button } + icon={ <FingerprintIcon /> } + label={ this.renderSignerLabel() } + onClick={ this.toggleSignerDisplay } + /> + { this.renderDrag() } + </div> ); + } - const parityButton = ( - <Button - className={ styles.parityButton } - icon={ parityIcon } - label={ this.renderLabel('Parity') } - /> - ); + renderDrag () { + if (this.props.externalLink) { + return; + } + + const dragButtonClasses = [ styles.dragButton ]; + + if (this.state.moving) { + dragButtonClasses.push(styles.moving); + } return ( <div - className={ styles.bar } - ref={ this.onRef } + className={ styles.moveIcon } + onMouseDown={ this.onMouseDown } > - <ParityBackground className={ styles.corner }> - <div className={ styles.cornercolor }> - { this.renderLink(parityButton) } - <Button - className={ styles.button } - icon={ <ActionFingerprint /> } - label={ this.renderSignerLabel() } - onClick={ this.toggleDisplay } - /> - </div> - </ParityBackground> + <div + className={ dragButtonClasses.join(' ') } + ref='dragButton' + /> </div> ); } @@ -144,27 +285,81 @@ class ParityBar extends Component { } renderExpanded () { + const { displayType } = this.state; + + return ( + <div className={ styles.container }> + <div className={ styles.header }> + <div className={ styles.title }> + <ContainerTitle + title={ + displayType === DISPLAY_ACCOUNTS + ? ( + <FormattedMessage + id='parityBar.title.accounts' + defaultMessage='Default Account' + /> + ) + : ( + <FormattedMessage + id='parityBar.title.signer' + defaultMessage='Parity Signer: Pending' + /> + ) + } + /> + </div> + <div className={ styles.actions }> + <Button + icon={ <CancelIcon /> } + label={ + <FormattedMessage + id='parityBar.button.close' + defaultMessage='Close' + /> + } + onClick={ this.toggleSignerDisplay } + /> + </div> + </div> + <div className={ styles.content }> + { + displayType === DISPLAY_ACCOUNTS + ? ( + <SectionList + items={ this.accountStore.accounts } + noStretch + renderItem={ this.renderAccount } + /> + ) + : ( + <Signer /> + ) + } + </div> + </div> + ); + } + + renderAccount = (account) => { + const onMakeDefault = () => { + this.toggleAccountsDisplay(); + this.accountStore.makeDefaultAccount(account.address); + }; + return ( <div - className={ styles.overlay } - ref={ this.onRef } + className={ styles.account } + onClick={ onMakeDefault } > - <ParityBackground className={ styles.expanded }> - <div className={ styles.header }> - <div className={ styles.title }> - <ContainerTitle title='Parity Signer: Pending' /> - </div> - <div className={ styles.actions }> - <Button - icon={ <ContentClear /> } - label='Close' - onClick={ this.toggleDisplay } /> - </div> - </div> - <div className={ styles.content }> - <Signer /> - </div> - </ParityBackground> + <AccountCard + account={ account } + className={ + account.default + ? styles.selected + : styles.unselected + } + /> </div> ); } @@ -189,17 +384,271 @@ class ParityBar extends Component { <Badge color='red' className={ styles.labelBubble } - value={ pending.length } /> + value={ pending.length } + /> ); } - return this.renderLabel('Signer', bubble); + return this.renderLabel( + <FormattedMessage + id='parityBar.label.signer' + defaultMessage='Signer' + />, + bubble + ); } - toggleDisplay = () => { + getHorizontal (x) { + const { page, button, container } = this.measures; + + const left = x - button.offset.left; + const centerX = left + container.width / 2; + + // left part of the screen + if (centerX < page.width / 2) { + return { left: Math.max(0, left) }; + } + + const right = page.width - x - button.offset.right; + + return { right: Math.max(0, right) }; + } + + getVertical (y) { + const STICKY_SIZE = 75; + const { page, button, container } = this.measures; + + const top = y - button.offset.top; + const centerY = top + container.height / 2; + + // top part of the screen + if (centerY < page.height / 2) { + // Add Sticky edges + const stickyTop = top < STICKY_SIZE + ? 0 + : top; + + return { top: Math.max(0, stickyTop) }; + } + + const bottom = page.height - y - button.offset.bottom; + // Add Sticky edges + const stickyBottom = bottom < STICKY_SIZE + ? 0 + : bottom; + + return { bottom: Math.max(0, stickyBottom) }; + } + + getPosition (x, y) { + if (!this.moving || !this.measures) { + return {}; + } + + const horizontal = this.getHorizontal(x); + const vertical = this.getVertical(y); + + const position = { + ...horizontal, + ...vertical + }; + + return position; + } + + onMouseDown = (event) => { + const containerElt = ReactDOM.findDOMNode(this.refs.container); + const dragButtonElt = ReactDOM.findDOMNode(this.refs.dragButton); + + if (!containerElt || !dragButtonElt) { + console.warn(containerElt ? 'drag button' : 'container', 'not found...'); + return; + } + + const bodyRect = document.body.getBoundingClientRect(); + const containerRect = containerElt.getBoundingClientRect(); + const buttonRect = dragButtonElt.getBoundingClientRect(); + + const buttonOffset = { + top: (buttonRect.top + buttonRect.height / 2) - containerRect.top, + left: (buttonRect.left + buttonRect.width / 2) - containerRect.left + }; + + buttonOffset.bottom = containerRect.height - buttonOffset.top; + buttonOffset.right = containerRect.width - buttonOffset.left; + + const button = { + offset: buttonOffset, + height: buttonRect.height, + width: buttonRect.width + }; + + const container = { + height: containerRect.height, + width: containerRect.width + }; + + const page = { + height: bodyRect.height, + width: bodyRect.width + }; + + this.moving = true; + this.measures = { + button, + container, + page + }; + + this.setState({ moving: true }); + } + + onMouseEnter = (event) => { + if (!this.moving) { + return; + } + + const { buttons } = event; + + // If no left-click, stop move + if (buttons !== 1) { + this.onMouseUp(event); + } + } + + onMouseLeave = (event) => { + if (!this.moving) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + } + + onMouseMove = (event) => { + const { pageX, pageY } = event; + + // this._onMouseMove({ pageX, pageY }); + this.debouncedMouseMove({ pageX, pageY }); + + event.stopPropagation(); + event.preventDefault(); + } + + _onMouseMove = (event) => { + if (!this.moving) { + return; + } + + const { pageX, pageY } = event; + const position = this.getPosition(pageX, pageY); + + this.setState({ position }); + } + + onMouseUp = (event) => { + if (!this.moving) { + return; + } + + const { pageX, pageY } = event; + const position = this.getPosition(pageX, pageY); + + // Stick to bottom or top + if (position.top !== undefined) { + position.top = 0; + } else { + position.bottom = 0; + } + + // Stick to bottom or top + if (position.left !== undefined) { + position.left = '1em'; + } else { + position.right = '1em'; + } + + this.moving = false; + this.setState({ moving: false, position }); + this.savePosition(position); + } + + toggleAccountsDisplay = () => { const { opened } = this.state; - this.setOpened(!opened); + this.setOpened(!opened, DISPLAY_ACCOUNTS); + + if (!opened) { + this.accountStore.loadAccounts(); + } + } + + toggleSignerDisplay = () => { + const { opened } = this.state; + + this.setOpened(!opened, DISPLAY_SIGNER); + } + + get config () { + let config; + + try { + config = JSON.parse(store.get(LS_STORE_KEY)); + } catch (error) { + config = {}; + } + + return config; + } + + loadPosition (props = this.props) { + const { app, config } = this; + + if (!app) { + return this.setState({ position: DEFAULT_POSITION }); + } + + if (config[app.id]) { + return this.setState({ position: config[app.id] }); + } + + const position = this.stringToPosition(app.position); + + this.setState({ position }); + } + + savePosition (position) { + const { app, config } = this; + + config[app.id] = position; + + store.set(LS_STORE_KEY, JSON.stringify(config)); + } + + stringToPosition (value) { + switch (value) { + case 'top-left': + return { + left: '1em', + top: 0 + }; + + case 'top-right': + return { + right: '1em', + top: 0 + }; + + case 'bottom-left': + return { + bottom: 0, + left: '1em' + }; + + case 'bottom-right': + default: + return DEFAULT_POSITION; + } } } diff --git a/js/src/views/ParityBar/parityBar.spec.js b/js/src/views/ParityBar/parityBar.spec.js new file mode 100644 index 000000000..941c47c65 --- /dev/null +++ b/js/src/views/ParityBar/parityBar.spec.js @@ -0,0 +1,167 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import { shallow } from 'enzyme'; +import React from 'react'; +import sinon from 'sinon'; + +import ParityBar from './'; + +import { createApi } from './parityBar.test.js'; + +let api; +let component; +let instance; +let store; + +function createRedux (state = {}) { + store = { + dispatch: sinon.stub(), + subscribe: sinon.stub(), + getState: () => Object.assign({ signer: { pending: [] } }, state) + }; + + return store; +} + +function render (props = {}, state = {}) { + api = createApi(); + component = shallow( + <ParityBar { ...props } />, + { + context: { + store: createRedux(state) + } + } + ).find('ParityBar').shallow({ context: { api } }); + instance = component.instance(); + + return component; +} + +describe('views/ParityBar', () => { + beforeEach(() => { + render({ dapp: true }); + }); + + it('renders defaults', () => { + expect(component).to.be.ok; + }); + + it('includes the ParityBackground', () => { + expect(component.find('Connect(ParityBackground)')).to.have.length(1); + }); + + describe('renderBar', () => { + let bar; + + beforeEach(() => { + bar = shallow(instance.renderBar()); + }); + + it('renders nothing when not overlaying a dapp', () => { + render({ dapp: false }); + expect(instance.renderBar()).to.be.null; + }); + + it('renders when overlaying a dapp', () => { + expect(bar.find('div')).not.to.have.length(0); + }); + + it('renders the Account selector button', () => { + const icon = bar.find('Button').first().props().icon; + + expect(icon.type.displayName).to.equal('Connect(IdentityIcon)'); + }); + + it('renders the Parity button', () => { + const label = shallow(bar.find('Button').at(1).props().label); + + expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.parity'); + }); + + it('renders the Signer button', () => { + const label = shallow(bar.find('Button').last().props().label); + + expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer'); + }); + }); + + describe('renderExpanded', () => { + let expanded; + + beforeEach(() => { + expanded = shallow(instance.renderExpanded()); + }); + + it('includes the Signer', () => { + expect(expanded.find('Connect(Embedded)')).to.have.length(1); + }); + }); + + describe('renderLabel', () => { + it('renders the label name', () => { + expect(shallow(instance.renderLabel('testing', null)).text()).to.equal('testing'); + }); + + it('renders name and bubble', () => { + expect(shallow(instance.renderLabel('testing', '(bubble)')).text()).to.equal('testing(bubble)'); + }); + }); + + describe('renderSignerLabel', () => { + let label; + + beforeEach(() => { + label = shallow(instance.renderSignerLabel()); + }); + + it('renders the signer label', () => { + expect(label.find('FormattedMessage').props().id).to.equal('parityBar.label.signer'); + }); + + it('does not render a badge when no pending requests', () => { + expect(label.find('Badge')).to.have.length(0); + }); + + it('renders a badge when pending requests', () => { + render({}, { signer: { pending: ['123', '456'] } }); + expect(shallow(instance.renderSignerLabel()).find('Badge').props().value).to.equal(2); + }); + }); + + describe('opened state', () => { + beforeEach(() => { + sinon.spy(instance, 'renderBar'); + sinon.spy(instance, 'renderExpanded'); + }); + + afterEach(() => { + instance.renderBar.restore(); + instance.renderExpanded.restore(); + }); + + it('renders the bar on with opened === false', () => { + expect(component.find('Link[to="/apps"]')).to.have.length(1); + }); + + it('renders expanded with opened === true', () => { + expect(instance.renderExpanded).not.to.have.been.called; + instance.setState({ opened: true }); + expect(instance.renderExpanded).to.have.been.called; + }); + }); +}); diff --git a/js/src/views/ParityBar/parityBar.test.js b/js/src/views/ParityBar/parityBar.test.js new file mode 100644 index 000000000..2623e4074 --- /dev/null +++ b/js/src/views/ParityBar/parityBar.test.js @@ -0,0 +1,55 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see <http://www.gnu.org/licenses/>. + +import sinon from 'sinon'; + +const ACCOUNT_DEFAULT = '0x2345678901'; +const ACCOUNT_FIRST = '0x1234567890'; +const ACCOUNT_NEW = '0x0987654321'; +const ACCOUNTS = { + [ACCOUNT_FIRST]: { uuid: 123 }, + [ACCOUNT_DEFAULT]: { uuid: 234 }, + '0x3456789012': {}, + [ACCOUNT_NEW]: { uuid: 456 } +}; + +function createApi () { + const api = { + subscribe: (params, callback) => { + callback(null, ACCOUNT_DEFAULT); + + return Promise.resolve(1); + }, + parity: { + defaultAccount: sinon.stub().resolves(ACCOUNT_DEFAULT), + allAccountsInfo: sinon.stub().resolves(ACCOUNTS), + getNewDappsWhitelist: sinon.stub().resolves(null), + setNewDappsWhitelist: sinon.stub().resolves(true) + } + }; + + sinon.spy(api, 'subscribe'); + + return api; +} + +export { + ACCOUNT_DEFAULT, + ACCOUNT_FIRST, + ACCOUNT_NEW, + ACCOUNTS, + createApi +}; diff --git a/js/src/views/Signer/components/SignRequest/signRequest.js b/js/src/views/Signer/components/SignRequest/signRequest.js index 6eea11057..bcc8dd80b 100644 --- a/js/src/views/Signer/components/SignRequest/signRequest.js +++ b/js/src/views/Signer/components/SignRequest/signRequest.js @@ -126,8 +126,9 @@ export default class SignRequest extends Component { ); } - onConfirm = password => { + onConfirm = (data) => { const { id } = this.props; + const { password } = data; this.props.onConfirm({ id, password }); } diff --git a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js index 319c5ae98..812d1fb35 100644 --- a/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js +++ b/js/src/views/Signer/components/TransactionMainDetails/transactionMainDetails.js @@ -1,4 +1,4 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// Copyright 2015-2017 Parity Technologies (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify @@ -59,7 +59,8 @@ export default class TransactionMainDetails extends Component { <Account address={ from } balance={ fromBalance } - isTest={ isTest } /> + isTest={ isTest } + /> </div> </div> <div className={ styles.method }> @@ -70,15 +71,16 @@ export default class TransactionMainDetails extends Component { gasStore ? gasStore.overrideTransaction(transaction) : transaction - } /> - { this.renderEditGas() } + } + /> + { this.renderEditTx() } </div> { children } </div> ); } - renderEditGas () { + renderEditTx () { const { gasStore } = this.props; if (!gasStore) { @@ -89,8 +91,9 @@ export default class TransactionMainDetails extends Component { <div className={ styles.editButtonRow }> <Button icon={ <MapsLocalGasStation /> } - label='Edit gas/gasPrice' - onClick={ this.toggleGasEditor } /> + label='Edit conditions/gas/gasPrice' + onClick={ this.toggleGasEditor } + /> </div> ); } @@ -107,7 +110,8 @@ export default class TransactionMainDetails extends Component { data-effect='solid' data-for={ labelId } data-place='bottom' - data-tip> + data-tip + > { totalValueDisplay } <small>ETH</small> </div> <ReactTooltip id={ labelId }> @@ -128,7 +132,8 @@ export default class TransactionMainDetails extends Component { <div data-effect='solid' data-for={ labelId } - data-tip> + data-tip + > <strong>{ valueDisplay } </strong> <small>ETH</small> </div> diff --git a/js/src/views/Signer/components/TransactionPending/transactionPending.js b/js/src/views/Signer/components/TransactionPending/transactionPending.js index fc575c8cd..4030eb828 100644 --- a/js/src/views/Signer/components/TransactionPending/transactionPending.js +++ b/js/src/views/Signer/components/TransactionPending/transactionPending.js @@ -45,6 +45,7 @@ export default class TransactionPending extends Component { onReject: PropTypes.func.isRequired, store: PropTypes.object.isRequired, transaction: PropTypes.shape({ + condition: PropTypes.object, data: PropTypes.string, from: PropTypes.string.isRequired, gas: PropTypes.object.isRequired, @@ -59,6 +60,7 @@ export default class TransactionPending extends Component { }; gasStore = new GasPriceEditor.Store(this.context.api, { + condition: this.props.transaction.condition, gas: this.props.transaction.gas.toFixed(), gasLimit: this.props.gasLimit, gasPrice: this.props.transaction.gasPrice.toFixed() @@ -80,7 +82,7 @@ export default class TransactionPending extends Component { render () { return this.gasStore.isEditing - ? this.renderGasEditor() + ? this.renderTxEditor() : this.renderTransaction(); } @@ -113,7 +115,7 @@ export default class TransactionPending extends Component { ); } - renderGasEditor () { + renderTxEditor () { const { className } = this.props; return ( @@ -131,15 +133,21 @@ export default class TransactionPending extends Component { onConfirm = (data) => { const { id, transaction } = this.props; const { password, wallet } = data; - const { gas, gasPrice } = this.gasStore.overrideTransaction(transaction); + const { condition, gas, gasPrice } = this.gasStore.overrideTransaction(transaction); - this.props.onConfirm({ + const options = { gas, gasPrice, id, password, wallet - }); + }; + + if (condition && (condition.block || condition.time)) { + options.condition = condition; + } + + this.props.onConfirm(options); } onReject = () => { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index f9d93cbe1..91bef63a1 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -251,7 +251,8 @@ function mapStateToProps (_, initProps) { return (state) => { const { accounts } = state.personal; - const account = accounts[address] || {}; + let gotAddress = Object.keys(accounts).find(a => a.toLowerCase() === address.toLowerCase()); + const account = gotAddress ? accounts[gotAddress] : {}; return { account }; };