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
import * as React from 'react'
import Page from '~/components/layout/Page'
import CircularProgress from '@material-ui/core/CircularProgress'
import Page from '~/components/layout/Page'
const centerStyle = {
margin: 'auto 0',

View File

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

View File

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

View File

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

View File

@ -15,7 +15,9 @@ import Button from '~/components/layout/Button'
import Row from '~/components/layout/Row'
import Img from '~/components/layout/Img'
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 OpenPaper from '~/components/Stepper/OpenPaper'
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,
})