From 5b5ab129d3a3efc775d4237e26d295e601aa14e8 Mon Sep 17 00:00:00 2001 From: Jaco Greeff Date: Mon, 27 May 2019 22:24:59 +0200 Subject: [PATCH] Turn SideBar transfer into modal link (#1217) * Turn SideBar transfer into modal link * ui--RecentlyOffline badge location --- packages/app-accounts/src/modals/Transfer.tsx | 35 +-- packages/app-transfer/src/Transfer.tsx | 243 ------------------ packages/app-transfer/src/index.tsx | 31 +-- packages/app-transfer/src/md/basic.md | 22 -- packages/app-transfer/src/translate.ts | 7 - packages/apps-routing/src/transfer.ts | 2 + packages/apps-routing/src/types.ts | 2 + packages/apps/src/SideBar/Item.tsx | 63 +++-- packages/apps/src/SideBar/index.tsx | 129 +++++++--- packages/ui-app/src/styles/app.css | 4 + packages/ui-app/src/styles/components.css | 4 +- 11 files changed, 165 insertions(+), 377 deletions(-) delete mode 100644 packages/app-transfer/src/Transfer.tsx delete mode 100644 packages/app-transfer/src/md/basic.md delete mode 100644 packages/app-transfer/src/translate.ts diff --git a/packages/app-accounts/src/modals/Transfer.tsx b/packages/app-accounts/src/modals/Transfer.tsx index 9128928f2d..2bcaaa9586 100644 --- a/packages/app-accounts/src/modals/Transfer.tsx +++ b/packages/app-accounts/src/modals/Transfer.tsx @@ -20,7 +20,7 @@ import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants'; import translate from '../translate'; type Props = ApiProps & I18nProps & { - address: string, + address?: string, balances_fees?: DerivedFees, balances_votingBalance?: DerivedBalances, onClose: () => void, @@ -32,8 +32,8 @@ type State = { extrinsic: SubmittableExtrinsic | null, hasAvailable: boolean, maxBalance?: BN, - recipientId: string | null, - senderId: string + recipientId?: string | null, + senderId?: string | null }; const ZERO = new BN(0); @@ -77,7 +77,7 @@ class Transfer extends React.PureComponent { hasAvailable: true, maxBalance: ZERO, recipientId: null, - senderId: props.address + senderId: props.address || null }; } @@ -115,7 +115,7 @@ class Transfer extends React.PureComponent { this.setState((prevState: State): State => { const { api } = this.props; const { amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance, senderId = prevState.senderId } = newState; - const extrinsic = recipientId + const extrinsic = recipientId && senderId ? api.tx.balances.transfer(recipientId, amount) : null; @@ -131,8 +131,8 @@ class Transfer extends React.PureComponent { } private renderButtons () { - const { address, onClose, t } = this.props; - const { extrinsic, hasAvailable } = this.state; + const { onClose, t } = this.props; + const { extrinsic, hasAvailable, senderId } = this.state; return ( @@ -144,7 +144,7 @@ class Transfer extends React.PureComponent { /> {
@@ -189,7 +190,7 @@ class Transfer extends React.PureComponent { withMax /> { this.nextState({ amount }); } + private onChangeFrom = (senderId: string) => { + this.nextState({ senderId }); + } + private onChangeTo = (recipientId: string) => { this.nextState({ recipientId }); } @@ -212,10 +217,10 @@ class Transfer extends React.PureComponent { } private setMaxBalance = async () => { - const { address, api, balances_fees = ZERO_FEES } = this.props; - const { recipientId } = this.state; + const { api, balances_fees = ZERO_FEES } = this.props; + const { senderId, recipientId } = this.state; - if (!address || !recipientId) { + if (!senderId || !recipientId) { return; } @@ -224,8 +229,8 @@ class Transfer extends React.PureComponent { // FIXME The any casts here are irritating, but they are basically caused by the derive // not really returning an actual `class implements Codec` // (if casting to DerivedBalance it would be `as any as DerivedBalance`) - const accountNonce = await api.query.system.accountNonce(address) as Index; - const senderBalance = (await api.derive.balances.all(address) as any).availableBalance; + const accountNonce = await api.query.system.accountNonce(senderId) as Index; + const senderBalance = (await api.derive.balances.all(senderId) as any).availableBalance; const recipientBalance = (await api.derive.balances.all(recipientId) as any).availableBalance; let prevMax = new BN(0); diff --git a/packages/app-transfer/src/Transfer.tsx b/packages/app-transfer/src/Transfer.tsx deleted file mode 100644 index 055066e30f..0000000000 --- a/packages/app-transfer/src/Transfer.tsx +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2017-2019 @polkadot/app-transfer authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { I18nProps } from '@polkadot/ui-app/types'; -import { ApiProps } from '@polkadot/ui-api/types'; -import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types'; - -import BN from 'bn.js'; -import React from 'react'; -import styled from 'styled-components'; -import { SubmittableExtrinsic } from '@polkadot/api/promise/types'; -import { Index } from '@polkadot/types'; -import { AddressRow, Button, InputAddress, InputBalance, TxButton, TxComponent } from '@polkadot/ui-app'; -import { withApi, withCalls, withMulti } from '@polkadot/ui-api'; -import keyring from '@polkadot/ui-keyring'; -import Checks, { calcSignatureLength } from '@polkadot/ui-signer/Checks'; -import { ZERO_FEES } from '@polkadot/ui-signer/Checks/constants'; - -import translate from './translate'; - -type Props = I18nProps & ApiProps & { - balances_fees?: DerivedFees, - balances_votingBalance?: DerivedBalances -}; - -type State = { - accountId: string | null, - amount: BN, - extrinsic: SubmittableExtrinsic | null, - hasAvailable: boolean, - maxBalance?: BN, - recipientId: string | null -}; - -const ZERO = new BN(0); - -const Wrapper = styled.div` - .transfer--Transfer-address { - flex: 0 1; - padding: 1.5rem 0; - - .ui--AddressSummary { - text-align: center; - } - } - - .transfer--Transfer-data { - flex: 1 1; - min-width: 0; - padding: 0 1rem; - } - - .transfer--Transfer-info { - display: flex; - flex-direction: row; - } -`; - -class Transfer extends TxComponent { - state: State = { - accountId: null, - amount: ZERO, - extrinsic: null, - hasAvailable: true, - maxBalance: ZERO, - recipientId: null - }; - - componentDidUpdate (prevProps: Props, prevState: State) { - const { balances_fees } = this.props; - const { accountId, extrinsic, recipientId } = this.state; - - const hasLengthChanged = ((extrinsic && extrinsic.encodedLength) || 0) !== ((prevState.extrinsic && prevState.extrinsic.encodedLength) || 0); - - if ((accountId && prevState.accountId !== accountId) || - (recipientId && prevState.recipientId !== recipientId) || - (balances_fees !== prevProps.balances_fees) || - hasLengthChanged - ) { - this.setMaxBalance().catch(console.error); - } - } - - render () { - const { t } = this.props; - const { accountId, extrinsic, recipientId, hasAvailable, maxBalance } = this.state; - - return ( - -
- {this.renderAddress(accountId, 'medium')} -
- - - - - - - -
- {this.renderAddress(recipientId, 'large')} -
-
- ); - } - - private renderAddress (accountId: string | null, media: 'large' | 'medium') { - if (!accountId) { - return null; - } - - try { - keyring.decodeAddress(accountId); - } catch (err) { - return null; - } - - return ( -
- -
- ); - } - - private nextState (newState: Partial): void { - this.setState((prevState: State): State => { - const { api } = this.props; - const { accountId = prevState.accountId, amount = prevState.amount, recipientId = prevState.recipientId, hasAvailable = prevState.hasAvailable, maxBalance = prevState.maxBalance } = newState; - const extrinsic = accountId && recipientId - ? api.tx.balances.transfer(recipientId, amount) - : null; - - return { - accountId, - amount, - extrinsic, - hasAvailable, - maxBalance, - recipientId - }; - }); - } - - private setMaxBalance = async () => { - const { api, balances_fees = ZERO_FEES } = this.props; - const { accountId, recipientId } = this.state; - - if (!accountId || !recipientId) { - return; - } - - const { transferFee, transactionBaseFee, transactionByteFee, creationFee } = balances_fees; - - // FIXME The any casts here are irritating, but they are basically caused by the derive - // not really returning an actual `class implements Codec` - // (if casting to DerivedBalance it would be `as any as DerivedBalance`) - const accountNonce = await api.query.system.accountNonce(accountId) as Index; - const senderBalance = (await api.derive.balances.all(accountId) as any).availableBalance; - const recipientBalance = (await api.derive.balances.all(recipientId) as any).availableBalance; - - let prevMax = new BN(0); - let maxBalance = new BN(1); - let extrinsic; - - while (!prevMax.eq(maxBalance)) { - prevMax = maxBalance; - extrinsic = api.tx.balances.transfer(recipientId, prevMax); - - const txLength = calcSignatureLength(extrinsic, accountNonce); - const fees = transactionBaseFee - .add(transactionByteFee.muln(txLength)) - .add(transferFee) - .add(recipientBalance.isZero() ? creationFee : ZERO); - - maxBalance = senderBalance.sub(fees); - } - - this.nextState({ - extrinsic, - maxBalance - }); - } - - private onChangeFrom = (accountId: string) => { - this.nextState({ accountId }); - } - - private onChangeAmount = (amount: BN = new BN(0)) => { - this.nextState({ amount }); - } - - private onChangeTo = (recipientId: string) => { - this.nextState({ recipientId }); - } - - private onChangeFees = (hasAvailable: boolean) => { - this.setState({ hasAvailable }); - } -} - -export default withMulti( - Transfer, - translate, - withApi, - withCalls( - 'derive.balances.fees' - ) -); diff --git a/packages/app-transfer/src/index.tsx b/packages/app-transfer/src/index.tsx index fd2580eb63..6364898872 100644 --- a/packages/app-transfer/src/index.tsx +++ b/packages/app-transfer/src/index.tsx @@ -2,37 +2,14 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { AppProps, I18nProps } from '@polkadot/ui-app/types'; +import { AppProps } from '@polkadot/ui-app/types'; import React from 'react'; -import { HelpOverlay,Tabs } from '@polkadot/ui-app'; -import basicMd from './md/basic.md'; -import Transfer from './Transfer'; -import translate from './translate'; +type Props = AppProps; -type Props = AppProps & I18nProps; - -class App extends React.PureComponent { +export default class App extends React.PureComponent { render () { - const { basePath, t } = this.props; - - return ( -
- -
- -
- -
- ); + return 'Deprecated, now operates via modal'; } } - -export default translate(App); diff --git a/packages/app-transfer/src/md/basic.md b/packages/app-transfer/src/md/basic.md deleted file mode 100644 index 8f979ba0ba..0000000000 --- a/packages/app-transfer/src/md/basic.md +++ /dev/null @@ -1,22 +0,0 @@ -# Transfer - -This tab allows you to send funds from one account you own to another account. - -# Minimum allowed balance - -Accounts with a balance lower than the minimal amount, 100 miliUnits (miliDOTs for Polkadot) as of writing, are considered as inexistent for the network. If an account's balance ever drops below this amount, it is removed from the network. In this application, it will still be visible, but with a balance of 0. - -For a fund transfer to a **new account** (read an account with a balance of 0), if the amount transferred is less than the minimum allowed balance, then the transfer will "succeed" but the destination account will not be created (read its balance will remain 0); this essentially burns the transfer balance from the sender, because the receiver's balance never exceed the minimum allowed balance. - -If the receiver already exists (read it has a balance greater than 0), it is perfectly possible to transfer very low amounts. - -# Fees - -Fees will be added to the transfer amount on top of the amount you wish to send. -An information box will show you the amount of fees that you will need to pay for the transfer and the total amount that will be deducted from the sender. - -- transfer: - You need to pay fees to transfer funds from one account to the other in order to pay for the security of the network. - -- account creation - In addition to the transfer fees, if you are sending funds to an account with a 0 balance, not only do you need to make sure to send enough funds (see the "minimum allowed balance" section) but you will need to pay a creation fee. This is to prevent the generation of unused account. diff --git a/packages/app-transfer/src/translate.ts b/packages/app-transfer/src/translate.ts deleted file mode 100644 index 1f582555f7..0000000000 --- a/packages/app-transfer/src/translate.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017-2019 @polkadot/app-transfer authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { withTranslation } from 'react-i18next'; - -export default withTranslation(['transfer', 'ui']); diff --git a/packages/apps-routing/src/transfer.ts b/packages/apps-routing/src/transfer.ts index 6cb5f15002..152e664baf 100644 --- a/packages/apps-routing/src/transfer.ts +++ b/packages/apps-routing/src/transfer.ts @@ -5,10 +5,12 @@ import { Routes } from './types'; import Transfer from '@polkadot/app-transfer'; +import TransferModal from '@polkadot/app-accounts/modals/Transfer'; export default ([ { Component: Transfer, + Modal: TransferModal, display: { isHidden: false, needsAccounts: true, diff --git a/packages/apps-routing/src/types.ts b/packages/apps-routing/src/types.ts index 5d30926469..73f847c6d5 100644 --- a/packages/apps-routing/src/types.ts +++ b/packages/apps-routing/src/types.ts @@ -9,8 +9,10 @@ export type RouteProps = AppProps & BareProps; export type Route = { Component: React.ComponentType, + Modal?: React.ComponentType, display: { isHidden?: boolean, + isModal?: boolean, needsAccounts?: boolean, needsApi?: Array>, needsSudo?: boolean diff --git a/packages/apps/src/SideBar/Item.tsx b/packages/apps/src/SideBar/Item.tsx index 34f9e545db..fbabd0826a 100644 --- a/packages/apps/src/SideBar/Item.tsx +++ b/packages/apps/src/SideBar/Item.tsx @@ -14,6 +14,8 @@ import accountObservable from '@polkadot/ui-keyring/observable/accounts'; import { withApi,withCalls, withMulti, withObservable } from '@polkadot/ui-api'; import { isFunction } from '@polkadot/util'; +import translate from '../translate'; + type Props = I18nProps & ApiProps & { isCollapsed: boolean, onClick: () => void, @@ -24,32 +26,54 @@ type Props = I18nProps & ApiProps & { class Item extends React.PureComponent { render () { - const { route: { i18n, icon, name }, t, isCollapsed } = this.props; + const { route: { Modal, i18n, icon, name }, t, isCollapsed, onClick } = this.props; if (!this.isVisible()) { return null; } + const body = ( + <> + + {t(`sidebar.${name}`, i18n)} + + + ); + return ( - - - {t(`sidebar.${name}`, i18n)} - - + { + Modal + ? ( + + {body} + + ) + : ( + + {body} + + ) + } ); } @@ -103,6 +127,7 @@ class Item extends React.PureComponent { export default withMulti( Item, + translate, withApi, withCalls( ['query.sudo.key', { transform: key => key.toString() }] diff --git a/packages/apps/src/SideBar/index.tsx b/packages/apps/src/SideBar/index.tsx index 763bc2c6d6..7e2635a22a 100644 --- a/packages/apps/src/SideBar/index.tsx +++ b/packages/apps/src/SideBar/index.tsx @@ -2,6 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { Route } from '@polkadot/apps-routing/types'; import { I18nProps } from '@polkadot/ui-app/types'; import { SIDEBAR_MENU_THRESHOLD } from '../constants'; @@ -28,6 +29,12 @@ type Props = I18nProps & { toggleMenu: () => void }; +type State = { + modals: { + [index: string]: boolean + } +}; + const Toggle = styled.img` background: ${logoBackground}; padding: ${logoPadding}; @@ -54,21 +61,40 @@ const Toggle = styled.img` `} `; -class SideBar extends React.PureComponent { +class SideBar extends React.PureComponent { + state: State; + + constructor (props: Props) { + super(props); + + // setup modals for each of the actual modal routes + this.state = { + modals: routing.routes.reduce((result, route) => { + if (route && route.Modal) { + result[route.name] = false; + } + + return result; + }, {} as { [index: string]: boolean }) + }; + } + render () { - const { isCollapsed } = this.props; + const { handleResize, isCollapsed, toggleMenu, menuOpen } = this.props; + const logo = getLogo(true); return ( - {this.renderMenuToggle()} + + {this.renderModals()}
{
{this.renderCollapse()} - {this.renderToggleBar()} + +
+
); @@ -126,9 +157,24 @@ class SideBar extends React.PureComponent { ); } + private renderModals () { + const { modals } = this.state; + const filtered = routing.routes.filter((route) => route && route.Modal) as Array; + + return filtered.map(({ name, Modal }) => ( + Modal && modals[name] + ? ( + + ) + :
+ )); + } + private renderRoutes () { - const { isCollapsed } = this.props; - const { t } = this.props; + const { handleResize, isCollapsed } = this.props; return routing.routes.map((route, index) => ( route @@ -137,8 +183,11 @@ class SideBar extends React.PureComponent { isCollapsed={isCollapsed} key={route.name} route={route} - onClick={this.props.handleResize} - t={t} + onClick={ + route.Modal + ? this.openModal(route.name) + : handleResize + } /> ) : ( @@ -164,32 +213,6 @@ class SideBar extends React.PureComponent { ); } - private renderToggleBar () { - return ( - -
-
-
- ); - } - - private renderMenuToggle () { - const logo = getLogo(true); - const { toggleMenu, menuOpen } = this.props; - - return ( - - ); - } - private renderWiki () { return ( @@ -203,6 +226,28 @@ class SideBar extends React.PureComponent { ); } + + private closeModal = (name: string) => { + return () => { + this.setState(({ modals }) => ({ + modals: { + ...modals, + [name]: false + } + })); + }; + } + + private openModal = (name: string) => { + return () => { + this.setState(({ modals }) => ({ + modals: { + ...modals, + [name]: true + } + })); + }; + } } export default translate(SideBar); diff --git a/packages/ui-app/src/styles/app.css b/packages/ui-app/src/styles/app.css index bda9c4da0c..b1acfb4400 100644 --- a/packages/ui-app/src/styles/app.css +++ b/packages/ui-app/src/styles/app.css @@ -17,6 +17,10 @@ body { height: 100%; } +a { + cursor: pointer; +} + h1, h2, h3, h4, h5 { color: rgba(0, 0, 0, .6); font-family: sans-serif; diff --git a/packages/ui-app/src/styles/components.css b/packages/ui-app/src/styles/components.css index 0d1e3d45ea..486a1e8f8f 100644 --- a/packages/ui-app/src/styles/components.css +++ b/packages/ui-app/src/styles/components.css @@ -437,12 +437,12 @@ button.ui.icon.iconButton { .ui--RecentlyOffline { position: absolute; - top: 0.75rem; + top: -0.5rem; font-size: 12px; cursor: help; display: flex; justify-content: center; - left: 0.75rem; + left: -0.5rem; width: 22px; height: 22px; padding: 0;