Refactor stepper to hooks

This commit is contained in:
mmv 2019-07-18 18:21:02 +04:00
parent 25eadc369d
commit f9c67171a8
16 changed files with 94 additions and 550 deletions

View File

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import Page from '~/components/layout/Page'
import CircularProgress from '@material-ui/core/CircularProgress' import CircularProgress from '@material-ui/core/CircularProgress'
import Page from '~/components/layout/Page'
const centerStyle = { const centerStyle = {
margin: 'auto 0', margin: 'auto 0',

View File

@ -1,16 +1,17 @@
// @flow // @flow
import * as React from 'react'
import Stepper from '@material-ui/core/Stepper' import Stepper from '@material-ui/core/Stepper'
import FormStep from '@material-ui/core/Step' import FormStep from '@material-ui/core/Step'
import StepLabel from '@material-ui/core/StepLabel' import StepLabel from '@material-ui/core/StepLabel'
import StepContent from '@material-ui/core/StepContent' import StepContent from '@material-ui/core/StepContent'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import * as React from 'react'
import GnoForm from '~/components/forms/GnoForm' import GnoForm from '~/components/forms/GnoForm'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import Button from '~/components/layout/Button'
import { history } from '~/store' import { history } from '~/store'
import Controls from './Controls' import Controls from './Controls'
const { useState, useEffect } = React
export { default as Step } from './Step' export { default as Step } from './Step'
type Props = { type Props = {
@ -18,20 +19,14 @@ type Props = {
onSubmit: (values: Object) => Promise<void>, onSubmit: (values: Object) => Promise<void>,
children: React.Node, children: React.Node,
classes: Object, classes: Object,
onReset?: () => void,
initialValues?: Object, initialValues?: Object,
disabledWhenValidating?: boolean, disabledWhenValidating?: boolean,
testId?: string, testId?: string,
} }
type State = {
page: number,
values: Object,
}
type PageProps = { type PageProps = {
children: Function, children: Function,
prepareNextInitialProps: (values: Object) => {}, prepareNextInitialProps?: (values: Object) => {},
} }
const transitionProps = { const transitionProps = {
@ -41,121 +36,88 @@ const transitionProps = {
}, },
} }
class GnoStepper extends React.PureComponent<Props, State> { export const StepperPage = ({ children }: PageProps) => children
static Page = ({ children }: PageProps) => children
static FinishButton = ({ const GnoStepper = (props: Props) => {
component, to, title, ...props const [page, setPage] = useState<number>(0)
}) => ( const [values, setValues] = useState<Object>({})
<Button component={component} to={to} variant="contained" color="primary" {...props}>
{title}
</Button>
)
constructor(props: Props) { useEffect(() => {
super(props) if (props.initialValues) {
setValues(props.initialValues)
this.state = {
page: 0,
values: props.initialValues || {},
} }
}, [])
const getPageProps = (pages: React.Node): PageProps => React.Children.toArray(pages)[page].props
const updateInitialProps = (newInitialProps) => {
setValues(newInitialProps)
} }
onReset = () => { const getActivePageFrom = (pages: React.Node) => {
const { onReset, initialValues } = this.props const activePageProps = getPageProps(pages)
if (onReset) { const { children, ...restProps } = activePageProps
onReset()
return children({ ...restProps, updateInitialProps })
} }
this.setState(() => ({ const validate = (valuesToValidate: Object) => {
page: 0, const { children } = props
values: initialValues || {},
}))
}
getPageProps = (pages: React.Node): PageProps => {
const { page } = this.state
return React.Children.toArray(pages)[page].props
}
getActivePageFrom = (pages: React.Node) => {
const activePageProps = this.getPageProps(pages)
const { children, ...props } = activePageProps
return children({ ...props, updateInitialProps: this.updateInitialProps })
}
updateInitialProps = (values) => {
this.setState({ values })
}
validate = (values: Object) => {
const { children } = this.props
const { page } = this.state
const activePage = React.Children.toArray(children)[page] const activePage = React.Children.toArray(children)[page]
return activePage.props.validate ? activePage.props.validate(values) : {} return activePage.props.validate ? activePage.props.validate(valuesToValidate) : {}
} }
next = async (values: Object) => { const next = async (formValues: Object) => {
const { children } = this.props const { children } = props
const activePageProps = this.getPageProps(children) const activePageProps = getPageProps(children)
const { prepareNextInitialProps } = activePageProps const { prepareNextInitialProps } = activePageProps
let pageInitialProps let pageInitialProps
if (prepareNextInitialProps) { if (prepareNextInitialProps) {
pageInitialProps = await prepareNextInitialProps(values) pageInitialProps = await prepareNextInitialProps(formValues)
} }
const finalValues = { ...values, ...pageInitialProps } const finalValues = { ...formValues, ...pageInitialProps }
this.setState(state => ({
page: Math.min(state.page + 1, React.Children.count(children) - 1), setValues(finalValues)
values: finalValues, setPage(Math.min(page + 1, React.Children.count(children) - 1))
}))
} }
previous = () => { const previous = () => {
const { page } = this.state
const firstPage = page === 0 const firstPage = page === 0
if (firstPage) { if (firstPage) {
return history.goBack() return history.goBack()
} }
return this.setState(state => ({ return setPage(Math.max(page - 1, 0))
page: Math.max(state.page - 1, 0),
}))
} }
handleSubmit = async (values: Object) => { const handleSubmit = async (formValues: Object) => {
const { children, onSubmit } = this.props const { children, onSubmit } = props
const { page } = this.state
const isLastPage = page === React.Children.count(children) - 1 const isLastPage = page === React.Children.count(children) - 1
if (isLastPage) { if (isLastPage) {
return onSubmit(values) return onSubmit(formValues)
} }
return this.next(values) return next(formValues)
} }
isLastPage = (page) => { const isLastPage = (pageNumber) => {
const { steps } = this.props const { steps } = props
return page === steps.length - 1 return pageNumber === steps.length - 1
} }
render() {
const { const {
steps, children, classes, disabledWhenValidating = false, testId, steps, children, classes, disabledWhenValidating = false, testId,
} = this.props } = props
const { page, values } = this.state const activePage = getActivePageFrom(children)
const activePage = this.getActivePageFrom(children) const lastPage = isLastPage(page)
const lastPage = this.isLastPage(page) const penultimate = isLastPage(page + 1)
const penultimate = this.isLastPage(page + 1)
return ( return (
<React.Fragment> <React.Fragment>
<GnoForm onSubmit={this.handleSubmit} initialValues={values} validation={this.validate} testId={testId}> <GnoForm onSubmit={handleSubmit} initialValues={values} validation={validate} testId={testId}>
{(submitting: boolean, validating: boolean, ...rest: any) => { {(submitting: boolean, validating: boolean, ...rest: any) => {
const disabled = disabledWhenValidating ? submitting || validating : submitting const disabled = disabledWhenValidating ? submitting || validating : submitting
const controls = ( const controls = (
@ -163,7 +125,7 @@ class GnoStepper extends React.PureComponent<Props, State> {
<Hairline /> <Hairline />
<Controls <Controls
disabled={disabled} disabled={disabled}
onPrevious={this.previous} onPrevious={previous}
firstPage={page === 0} firstPage={page === 0}
lastPage={lastPage} lastPage={lastPage}
penultimate={penultimate} penultimate={penultimate}
@ -186,7 +148,6 @@ class GnoStepper extends React.PureComponent<Props, State> {
</React.Fragment> </React.Fragment>
) )
} }
}
const styles = { const styles = {
root: { root: {

View File

@ -2,7 +2,7 @@
import * as React from 'react' import * as React from 'react'
import ChevronLeft from '@material-ui/icons/ChevronLeft' import ChevronLeft from '@material-ui/icons/ChevronLeft'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import Stepper from '~/components/Stepper' import Stepper, { StepperPage } from '~/components/Stepper'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
@ -46,11 +46,11 @@ const Layout = ({
<Heading tag="h2">Load existing Safe</Heading> <Heading tag="h2">Load existing Safe</Heading>
</Row> </Row>
<Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form"> <Stepper onSubmit={onLoadSafeSubmit} steps={steps} initialValues={initialValues} testId="load-safe-form">
<Stepper.Page validate={safeFieldsValidation}>{DetailsForm}</Stepper.Page> <StepperPage validate={safeFieldsValidation}>{DetailsForm}</StepperPage>
<Stepper.Page network={network}>{OwnerList}</Stepper.Page> <StepperPage network={network}>{OwnerList}</StepperPage>
<Stepper.Page network={network} userAddress={userAddress}> <StepperPage network={network} userAddress={userAddress}>
{ReviewInformation} {ReviewInformation}
</Stepper.Page> </StepperPage>
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (

View File

@ -2,7 +2,7 @@
import * as React from 'react' import * as React from 'react'
import ChevronLeft from '@material-ui/icons/ChevronLeft' import ChevronLeft from '@material-ui/icons/ChevronLeft'
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import Stepper from '~/components/Stepper' import Stepper, { StepperPage } from '~/components/Stepper'
import Block from '~/components/layout/Block' import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading' import Heading from '~/components/layout/Heading'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
@ -60,9 +60,9 @@ const Layout = ({
initialValues={initialValues} initialValues={initialValues}
testId="create-safe-form" testId="create-safe-form"
> >
<Stepper.Page>{SafeNameField}</Stepper.Page> <StepperPage>{SafeNameField}</StepperPage>
<Stepper.Page>{SafeOwnersFields}</Stepper.Page> <StepperPage>{SafeOwnersFields}</StepperPage>
<Stepper.Page network={network}>{Review}</Stepper.Page> <StepperPage network={network}>{Review}</StepperPage>
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (

View File

@ -15,7 +15,9 @@ import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row' import Row from '~/components/layout/Row'
import Img from '~/components/layout/Img' import Img from '~/components/layout/Img'
import Col from '~/components/layout/Col' import Col from '~/components/layout/Col'
import { FIELD_CONFIRMATIONS, getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom } from '~/routes/open/components/fields' import {
FIELD_CONFIRMATIONS, getOwnerNameBy, getOwnerAddressBy, getNumOwnersFrom,
} from '~/routes/open/components/fields'
import Paragraph from '~/components/layout/Paragraph' import Paragraph from '~/components/layout/Paragraph'
import OpenPaper from '~/components/Stepper/OpenPaper' import OpenPaper from '~/components/Stepper/OpenPaper'
import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor' import { getAccountsFrom } from '~/routes/open/utils/safeDataExtractor'

View File

@ -1,39 +0,0 @@
// @flow
import * as React from 'react'
import CircularProgress from '@material-ui/core/CircularProgress'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Heading from '~/components/layout/Heading'
import Paragraph from '~/components/layout/Paragraph'
import { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from '~/routes/safe/components/SendToken/SendTokenForm/index'
type FormProps = {
values: Object,
submitting: boolean,
}
type Props = {
symbol: string,
}
const spinnerStyle = {
minHeight: '50px',
}
const ReviewTx = ({ symbol }: Props) => (controls: React.Node, { values, submitting }: FormProps) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the move token funds</Heading>
<Paragraph align="left">
<Bold>Destination: </Bold>
{' '}
{values[TKN_DESTINATION_PARAM]}
</Paragraph>
<Paragraph align="left">
<Bold>{`Amount to transfer: ${values[TKN_VALUE_PARAM]} ${symbol}`}</Bold>
</Paragraph>
<Block style={spinnerStyle}>{submitting && <CircularProgress size={50} />}</Block>
</OpenPaper>
)
export default ReviewTx

View File

@ -1,53 +0,0 @@
// @flow
import * as React from 'react'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import {
composeValidators, inLimit, mustBeFloat, required, greaterThan, mustBeEthereumAddress,
} from '~/components/forms/validator'
import Block from '~/components/layout/Block'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Heading from '~/components/layout/Heading'
export const CONFIRMATIONS_ERROR = 'Number of confirmations can not be higher than the number of owners'
export const TKN_DESTINATION_PARAM = 'tknDestination'
export const TKN_VALUE_PARAM = 'tknValue'
type Props = {
funds: string,
symbol: string,
}
const SendTokenForm = ({ funds, symbol }: Props) => (controls: React.Node) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
Send tokens Transaction
</Heading>
<Heading tag="h4" margin="lg">
{`Available tokens: ${funds} ${symbol}`}
</Heading>
<Block margin="md">
<Field
name={TKN_DESTINATION_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeEthereumAddress)}
placeholder="Destination*"
text="Destination"
/>
</Block>
<Block margin="md">
<Field
name={TKN_VALUE_PARAM}
component={TextField}
type="text"
validate={composeValidators(required, mustBeFloat, greaterThan(0), inLimit(Number(funds), 0, 'available balance', symbol))}
placeholder="Amount of tokens*"
text="Amount of Tokens"
/>
</Block>
</OpenPaper>
)
export default SendTokenForm

View File

@ -1,10 +0,0 @@
// @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
export type Actions = {
fetchTransactions: typeof fetchTransactions,
}
export default {
fetchTransactions,
}

View File

@ -1,119 +0,0 @@
// @flow
import * as React from 'react'
import { BigNumber } from 'bignumber.js'
import { connect } from 'react-redux'
import Stepper from '~/components/Stepper'
import { sleep } from '~/utils/timer'
import { type Safe } from '~/routes/safe/store/models/safe'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens'
import { type Token } from '~/logic/tokens/store/model/token'
import { isEther } from '~/logic/tokens/utils/tokenHelpers'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { toNative } from '~/logic/wallets/tokens'
import { createTransaction } from '~/logic/safe/safeFrontendOperations'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import actions, { type Actions } from './actions'
import selector, { type SelectorProps } from './selector'
import SendTokenForm, { TKN_DESTINATION_PARAM, TKN_VALUE_PARAM } from './SendTokenForm'
import ReviewTx from './ReviewTx'
const getSteps = () => ['Fill Move Token form', 'Review Move Token form']
type Props = SelectorProps &
Actions & {
safe: Safe,
token: Token,
onReset: () => void,
}
type State = {
done: boolean,
}
export const SEE_TXS_BUTTON_TEXT = 'VISIT TXS'
const getTransferData = async (tokenAddress: string, to: string, amount: BigNumber) => {
const StandardToken = await getStandardTokenContract()
const myToken = await StandardToken.at(tokenAddress)
return myToken.contract.transfer(to, amount).encodeABI()
}
const processTokenTransfer = async (safe: Safe, token: Token, to: string, amount: string, userAddress: string) => {
const safeAddress = safe.get('address')
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const nonce = await gnosisSafe.nonce()
const symbol = token.get('symbol')
const name = `Send ${amount} ${symbol} to ${to}`
const value = isEther(symbol) ? amount : '0'
const tokenAddress = token.get('address')
const destination = isEther(symbol) ? to : tokenAddress
const data = isEther(symbol)
? EMPTY_DATA
: await getTransferData(tokenAddress, to, toNative(amount, token.get('decimals')))
return createTransaction(safe, name, destination, value, nonce, userAddress, data)
}
class SendToken extends React.Component<Props, State> {
state = {
done: false,
}
onTransaction = async (values: Object) => {
try {
const {
safe, token, userAddress, fetchTransactions,
} = this.props
const amount = values[TKN_VALUE_PARAM]
const destination = values[TKN_DESTINATION_PARAM]
await processTokenTransfer(safe, token, destination, amount, userAddress)
await sleep(1500)
fetchTransactions(safe.get('address'))
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
// eslint-disable-next-line
console.log('Error while moving ERC20 token funds ' + error)
}
}
onReset = () => {
const { onReset } = this.props
this.setState({ done: false })
onReset() // This is for show the TX list component
}
render() {
const { done } = this.state
const { token } = this.props
const steps = getSteps()
const finishedButton = <Stepper.FinishButton title={SEE_TXS_BUTTON_TEXT} />
const symbol = token.get('symbol')
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onTransaction}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page funds={token.get('funds')} symbol={symbol}>
{SendTokenForm}
</Stepper.Page>
<Stepper.Page symbol={symbol}>{ReviewTx}</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(
selector,
actions,
)(SendToken)

View File

@ -1,11 +0,0 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
export type SelectorProps = {
userAddress: typeof userAccountSelector,
}
export default createStructuredSelector<Object, *>({
userAddress: userAccountSelector,
})

View File

@ -1,34 +0,0 @@
// @flow
import * as React from 'react'
import CircularProgress from '@material-ui/core/CircularProgress'
import Block from '~/components/layout/Block'
import Bold from '~/components/layout/Bold'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Heading from '~/components/layout/Heading'
import Paragraph from '~/components/layout/Paragraph'
import { THRESHOLD_PARAM } from '~/routes/safe/components/Threshold/ThresholdForm'
type FormProps = {
values: Object,
submitting: boolean,
}
const spinnerStyle = {
minHeight: '50px',
}
const Review = () => (controls: React.Node, { values, submitting }: FormProps) => (
<OpenPaper controls={controls}>
<Heading tag="h2">Review the Threshold operation</Heading>
<Paragraph align="left">
<Bold>The new threshold will be: </Bold>
{' '}
{values[THRESHOLD_PARAM]}
</Paragraph>
<Block style={spinnerStyle}>
{ submitting && <CircularProgress size={50} /> }
</Block>
</OpenPaper>
)
export default Review

View File

@ -1,46 +0,0 @@
// @flow
import * as React from 'react'
import Block from '~/components/layout/Block'
import Heading from '~/components/layout/Heading'
import OpenPaper from '~/components/Stepper/OpenPaper'
import Field from '~/components/forms/Field'
import TextField from '~/components/forms/TextField'
import {
composeValidators, minValue, maxValue, mustBeInteger, required,
} from '~/components/forms/validator'
import { type Safe } from '~/routes/safe/store/models/safe'
export const THRESHOLD_PARAM = 'threshold'
type ThresholdProps = {
numOwners: number,
safe: Safe,
}
const ThresholdForm = ({ numOwners, safe }: ThresholdProps) => (controls: React.Node) => (
<OpenPaper controls={controls}>
<Heading tag="h2" margin="lg">
{'Change safe\'s threshold'}
</Heading>
<Heading tag="h4" margin="lg">
{`Safe's owners: ${numOwners} and Safe's threshold: ${safe.get('threshold')}`}
</Heading>
<Block margin="md">
<Field
name={THRESHOLD_PARAM}
component={TextField}
type="text"
validate={composeValidators(
required,
mustBeInteger,
minValue(1),
maxValue(numOwners),
)}
placeholder="New threshold"
text="Safe's threshold"
/>
</Block>
</OpenPaper>
)
export default ThresholdForm

View File

@ -1,10 +0,0 @@
// @flow
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
export type Actions = {
fetchTransactions: typeof fetchTransactions,
}
export default {
fetchTransactions,
}

View File

@ -1,86 +0,0 @@
// @flow
import * as React from 'react'
import Stepper from '~/components/Stepper'
import { connect } from 'react-redux'
import { createTransaction } from '~/logic/safe/safeFrontendOperations'
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
import { type Safe } from '~/routes/safe/store/models/safe'
import ThresholdForm, { THRESHOLD_PARAM } from './ThresholdForm'
import selector, { type SelectorProps } from './selector'
import actions, { type Actions } from './actions'
import Review from './Review'
type Props = SelectorProps & Actions & {
numOwners: number,
safe: Safe,
onReset: () => void,
}
const getSteps = () => [
'Fill Change threshold Form', 'Review change threshold operation',
]
type State = {
done: boolean,
}
export const CHANGE_THRESHOLD_RESET_BUTTON_TEXT = 'SEE TXs'
class Threshold extends React.PureComponent<Props, State> {
state = {
done: false,
}
onThreshold = async (values: Object) => {
try {
const { safe, userAddress, fetchTransactions } = this.props // , fetchThreshold } = this.props
const newThreshold = values[THRESHOLD_PARAM]
const safeAddress = safe.get('address')
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
const nonce = await gnosisSafe.nonce()
const data = gnosisSafe.contract.changeThreshold(newThreshold).encodeABI()
await createTransaction(safe, `Change Safe's threshold [${nonce}]`, safeAddress, '0', nonce, userAddress, data)
await fetchTransactions(safeAddress)
this.setState({ done: true })
} catch (error) {
this.setState({ done: false })
// eslint-disable-next-line
console.log('Error while changing threshold ' + error)
}
}
onReset = () => {
const { onReset } = this.props
this.setState({ done: false })
onReset()
}
render() {
const { numOwners, safe } = this.props
const { done } = this.state
const steps = getSteps()
const finishedButton = <Stepper.FinishButton title={CHANGE_THRESHOLD_RESET_BUTTON_TEXT} />
return (
<React.Fragment>
<Stepper
finishedTransaction={done}
finishedButton={finishedButton}
onSubmit={this.onThreshold}
steps={steps}
onReset={this.onReset}
>
<Stepper.Page numOwners={numOwners} safe={safe}>
{ ThresholdForm }
</Stepper.Page>
<Stepper.Page>
{ Review }
</Stepper.Page>
</Stepper>
</React.Fragment>
)
}
}
export default connect(selector, actions)(Threshold)

View File

@ -1,11 +0,0 @@
// @flow
import { createStructuredSelector } from 'reselect'
import { userAccountSelector } from '~/logic/wallets/store/selectors'
export type SelectorProps = {
userAddress: typeof userAccountSelector,
}
export default createStructuredSelector<Object, *>({
userAddress: userAccountSelector,
})