[Address Book v2] Allow empty owner names when loading a safe (#2390)

* Allow to load new safe with empty owner names

* Avoid adding owners to addressbook if name is empty

* Remove unnecessary initialization
This commit is contained in:
Daniel Sanchez 2021-06-03 15:24:55 +02:00 committed by GitHub
parent 548d6d26ed
commit e66040d32f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 138 additions and 142 deletions

View File

@ -100,8 +100,12 @@ export const mustBeEthereumContractAddress = memoize(
}, },
) )
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols` const testValue = value || ''
return testValue.length >= +minLen && testValue.length <= +maxLen
? undefined
: `Should be ${minLen} to ${maxLen} symbols`
}
export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => { export const minMaxDecimalsLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
const decimals = value.split('.')[1] || '0' const decimals = value.split('.')[1] || '0'

View File

@ -1,7 +1,7 @@
import InputAdornment from '@material-ui/core/InputAdornment' import InputAdornment from '@material-ui/core/InputAdornment'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import CheckCircle from '@material-ui/icons/CheckCircle' import CheckCircle from '@material-ui/icons/CheckCircle'
import * as React from 'react' import React, { ReactElement, ReactNode } from 'react'
import { FormApi } from 'final-form' import { FormApi } from 'final-form'
import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper' import { ScanQRWrapper } from 'src/components/ScanQRModal/ScanQRWrapper'
@ -68,7 +68,7 @@ interface DetailsFormProps {
form: FormApi form: FormApi
} }
const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement => { const DetailsForm = ({ errors, form }: DetailsFormProps): ReactElement => {
const classes = useStyles() const classes = useStyles()
const handleScan = (value: string, closeQrModal: () => void): void => { const handleScan = (value: string, closeQrModal: () => void): void => {
@ -145,13 +145,11 @@ const DetailsForm = ({ errors, form }: DetailsFormProps): React.ReactElement =>
} }
const DetailsPage = () => const DetailsPage = () =>
function LoadSafeDetails(controls: React.ReactNode, { errors, form }: StepperPageFormProps): React.ReactElement { function LoadSafeDetails(controls: ReactNode, { errors, form }: StepperPageFormProps): ReactElement {
return ( return (
<> <OpenPaper controls={controls}>
<OpenPaper controls={controls}> <DetailsForm errors={errors} form={form} />
<DetailsForm errors={errors} form={form} /> </OpenPaper>
</OpenPaper>
</>
) )
} }

View File

@ -1,6 +1,6 @@
import IconButton from '@material-ui/core/IconButton' import IconButton from '@material-ui/core/IconButton'
import ChevronLeft from '@material-ui/icons/ChevronLeft' import ChevronLeft from '@material-ui/icons/ChevronLeft'
import * as React from 'react' import React, { ReactElement } from 'react'
import Stepper, { StepperPage } from 'src/components/Stepper' import Stepper, { StepperPage } from 'src/components/Stepper'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
@ -34,13 +34,12 @@ const formMutators = {
} }
interface LayoutProps { interface LayoutProps {
network: string
provider?: string provider?: string
userAddress: string userAddress: string
onLoadSafeSubmit: (values: LoadFormValues) => void onLoadSafeSubmit: (values: LoadFormValues) => void
} }
const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProps): React.ReactElement => ( const Layout = ({ onLoadSafeSubmit, provider, userAddress }: LayoutProps): ReactElement => (
<> <>
{provider ? ( {provider ? (
<Block> <Block>
@ -58,8 +57,8 @@ const Layout = ({ network, onLoadSafeSubmit, provider, userAddress }: LayoutProp
testId="load-safe-form" testId="load-safe-form"
> >
<StepperPage validate={safeFieldsValidation} component={DetailsForm} /> <StepperPage validate={safeFieldsValidation} component={DetailsForm} />
<StepperPage network={network} component={OwnerList} /> <StepperPage component={OwnerList} />
<StepperPage network={network} userAddress={userAddress} component={ReviewInformation} /> <StepperPage userAddress={userAddress} component={ReviewInformation} />
</Stepper> </Stepper>
</Block> </Block>
) : ( ) : (

View File

@ -1,12 +1,13 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import TableContainer from '@material-ui/core/TableContainer' import TableContainer from '@material-ui/core/TableContainer'
import React, { useEffect, useState } from 'react' import React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { getExplorerInfo } from 'src/config'
import Field from 'src/components/forms/Field' import Field from 'src/components/forms/Field'
import TextField from 'src/components/forms/TextField' import TextField from 'src/components/forms/TextField'
import { composeValidators, minMaxLength, required } from 'src/components/forms/validator' import { minMaxLength } from 'src/components/forms/validator'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
@ -21,8 +22,7 @@ import { getGnosisSafeInstanceAt } from 'src/logic/contracts/safeContracts'
import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields' import { FIELD_LOAD_ADDRESS, THRESHOLD } from 'src/routes/load/components/fields'
import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' import { getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { styles } from './styles' import { styles } from './styles'
import { getExplorerInfo } from 'src/config' import { LoadFormValues } from 'src/routes/load/container/Load'
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
const calculateSafeValues = (owners, threshold, values) => { const calculateSafeValues = (owners, threshold, values) => {
const initialValues = { ...values } const initialValues = { ...values }
@ -41,10 +41,14 @@ const useAddressBookForOwnersNames = (ownersList: string[]): AddressBookEntry[]
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const OwnerListComponent = (props) => { interface OwnerListComponentProps {
values: LoadFormValues
updateInitialProps: (initialValues) => void
}
const OwnerListComponent = ({ values, updateInitialProps }: OwnerListComponentProps): ReactElement => {
const [owners, setOwners] = useState<string[]>([]) const [owners, setOwners] = useState<string[]>([])
const classes = useStyles() const classes = useStyles()
const { updateInitialProps, values } = props
const ownersWithNames = useAddressBookForOwnersNames(owners) const ownersWithNames = useAddressBookForOwnersNames(owners)
@ -88,19 +92,18 @@ const OwnerListComponent = (props) => {
<Hairline /> <Hairline />
<Block margin="md" padding="md"> <Block margin="md" padding="md">
{ownersWithNames.map(({ address, name }, index) => { {ownersWithNames.map(({ address, name }, index) => {
const ownerName = name || `Owner #${index + 1}`
return ( return (
<Row className={classes.owner} key={address} data-testid="owner-row"> <Row className={classes.owner} key={address} data-testid="owner-row">
<Col className={classes.ownerName} xs={4}> <Col className={classes.ownerName} xs={4}>
<Field <Field
className={classes.name} className={classes.name}
component={TextField} component={TextField}
initialValue={ownerName} initialValue={name}
name={getOwnerNameBy(index)} name={getOwnerNameBy(index)}
placeholder="Owner Name*" placeholder="Owner Name"
text="Owner Name" text="Owner Name"
type="text" type="text"
validate={composeValidators(required, minMaxLength(1, 50))} validate={minMaxLength(0, 50)}
testId={`load-safe-owner-name-${index}`} testId={`load-safe-owner-name-${index}`}
/> />
</Col> </Col>
@ -118,14 +121,12 @@ const OwnerListComponent = (props) => {
) )
} }
const OwnerList = ({ updateInitialProps }, network) => const OwnerList = ({ updateInitialProps }) =>
function LoadSafeOwnerList(controls, { values }): React.ReactElement { function LoadSafeOwnerList(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
return ( return (
<> <OpenPaper controls={controls} padding={false}>
<OpenPaper controls={controls} padding={false}> <OwnerListComponent updateInitialProps={updateInitialProps} values={values} />
<OwnerListComponent network={network} updateInitialProps={updateInitialProps} values={values} /> </OpenPaper>
</OpenPaper>
</>
) )
} }

View File

@ -1,6 +1,8 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import TableContainer from '@material-ui/core/TableContainer' import TableContainer from '@material-ui/core/TableContainer'
import React from 'react' import React, { ReactElement, ReactNode } from 'react'
import { getExplorerInfo } from 'src/config'
import Block from 'src/components/layout/Block' import Block from 'src/components/layout/Block'
import Col from 'src/components/layout/Col' import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline' import Hairline from 'src/components/layout/Hairline'
@ -11,8 +13,6 @@ import { FIELD_LOAD_ADDRESS, FIELD_LOAD_NAME, THRESHOLD } from 'src/routes/load/
import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields' import { getNumOwnersFrom, getOwnerAddressBy, getOwnerNameBy } from 'src/routes/open/components/fields'
import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor' import { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
import { useStyles } from './styles' import { useStyles } from './styles'
import { getExplorerInfo } from 'src/config'
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
import { LoadFormValues } from 'src/routes/load/container/Load' import { LoadFormValues } from 'src/routes/load/container/Load'
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => { const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
@ -33,108 +33,104 @@ interface Props {
values: LoadFormValues values: LoadFormValues
} }
const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => { const ReviewComponent = ({ userAddress, values }: Props): ReactElement => {
const classes = useStyles() const classes = useStyles()
const isOwner = checkIfUserAddressIsAnOwner(values, userAddress) const isOwner = checkIfUserAddressIsAnOwner(values, userAddress)
const owners = getAccountsFrom(values) const owners = getAccountsFrom(values)
const safeAddress = values[FIELD_LOAD_ADDRESS] const safeAddress = values[FIELD_LOAD_ADDRESS]
return ( return (
<> <Row className={classes.root}>
<Row className={classes.root}> <Col className={classes.detailsColumn} layout="column" xs={4}>
<Col className={classes.detailsColumn} layout="column" xs={4}> <Block className={classes.details}>
<Block className={classes.details}> <Block margin="lg">
<Block margin="lg"> <Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three"> Review details
Review details </Paragraph>
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Name of the Safe
</Paragraph>
<Paragraph
className={classes.name}
color="primary"
noMargin
size="lg"
weight="bolder"
data-testid="load-form-review-safe-name"
>
{values[FIELD_LOAD_NAME]}
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Safe address
</Paragraph>
<Row className={classes.container}>
<EthHashInfo
hash={safeAddress}
shortenHash={4}
showAvatar
showCopyBtn
explorerUrl={getExplorerInfo(safeAddress)}
/>
</Row>
</Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Connected wallet client is owner?
</Paragraph>
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
{isOwner ? 'Yes' : 'No (read-only)'}
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Any transaction requires the confirmation of:
</Paragraph>
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
{`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`}
</Paragraph>
</Block>
</Block> </Block>
</Col> <Block margin="lg">
<Col className={classes.ownersColumn} layout="column" xs={8}> <Paragraph color="disabled" noMargin size="sm">
<TableContainer> Name of the Safe
<Block className={classes.owners}> </Paragraph>
<Paragraph color="primary" noMargin size="lg"> <Paragraph
{`${getNumOwnersFrom(values)} Safe owners`} className={classes.name}
</Paragraph> color="primary"
</Block> noMargin
<Hairline /> size="lg"
{owners.map((address, index) => ( weight="bolder"
<> data-testid="load-form-review-safe-name"
<Row className={classes.owner} testId={'load-safe-review-owner-name-' + index}> >
<Col align="center" xs={12}> {values[FIELD_LOAD_NAME]}
<EthHashInfo </Paragraph>
hash={address} </Block>
name={values[getOwnerNameBy(index)]} <Block margin="lg">
showAvatar <Paragraph color="disabled" noMargin size="sm">
showCopyBtn Safe address
explorerUrl={getExplorerInfo(address)} </Paragraph>
/> <Row className={classes.container}>
</Col> <EthHashInfo
</Row> hash={safeAddress}
{index !== owners.length - 1 && <Hairline />} shortenHash={4}
</> showAvatar
))} showCopyBtn
</TableContainer> explorerUrl={getExplorerInfo(safeAddress)}
</Col> />
</Row> </Row>
</> </Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Connected wallet client is owner?
</Paragraph>
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
{isOwner ? 'Yes' : 'No (read-only)'}
</Paragraph>
</Block>
<Block margin="lg">
<Paragraph color="disabled" noMargin size="sm">
Any transaction requires the confirmation of:
</Paragraph>
<Paragraph className={classes.name} color="primary" noMargin size="lg" weight="bolder">
{`${values[THRESHOLD]} out of ${getNumOwnersFrom(values)} owners`}
</Paragraph>
</Block>
</Block>
</Col>
<Col className={classes.ownersColumn} layout="column" xs={8}>
<TableContainer>
<Block className={classes.owners}>
<Paragraph color="primary" noMargin size="lg">
{`${getNumOwnersFrom(values)} Safe owners`}
</Paragraph>
</Block>
<Hairline />
{owners.map((address, index) => (
<>
<Row className={classes.owner} testId={'load-safe-review-owner-name-' + index}>
<Col align="center" xs={12}>
<EthHashInfo
hash={address}
name={values[getOwnerNameBy(index)]}
showAvatar
showCopyBtn
explorerUrl={getExplorerInfo(address)}
/>
</Col>
</Row>
{index !== owners.length - 1 && <Hairline />}
</>
))}
</TableContainer>
</Col>
</Row>
) )
} }
const Review = ({ userAddress }: { userAddress: string }) => const Review = ({ userAddress }: { userAddress: string }) =>
function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement { function ReviewPage(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
return ( return (
<> <OpenPaper controls={controls} padding={false}>
<OpenPaper controls={controls} padding={false}> <ReviewComponent userAddress={userAddress} values={values} />
<ReviewComponent userAddress={userAddress} values={values} /> </OpenPaper>
</OpenPaper>
</>
) )
} }

View File

@ -1,9 +1,8 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import Layout from 'src/routes/load/components/Layout' import Layout from 'src/routes/load/components/Layout'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook' import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions' import { addressBookSafeLoad } from 'src/logic/addressBook/store/actions'
import { FIELD_LOAD_ADDRESS } from 'src/routes/load/components/fields' import { FIELD_LOAD_ADDRESS } from 'src/routes/load/components/fields'
@ -16,7 +15,7 @@ import { history } from 'src/store'
import { SafeRecordProps } from 'src/logic/safe/store/models/safe' import { SafeRecordProps } from 'src/logic/safe/store/models/safe'
import { checksumAddress } from 'src/utils/checksumAddress' import { checksumAddress } from 'src/utils/checksumAddress'
import { isValidAddress } from 'src/utils/isValidAddress' import { isValidAddress } from 'src/utils/isValidAddress'
import { networkSelector, providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors' import { providerNameSelector, userAccountSelector } from 'src/logic/wallets/store/selectors'
import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe' import { addOrUpdateSafe } from 'src/logic/safe/store/actions/addOrUpdateSafe'
export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise<void> => { export const loadSafe = async (safeAddress: string, addSafe: (safe: SafeRecordProps) => void): Promise<void> => {
@ -51,7 +50,6 @@ export type LoadFormValues = ReviewSafeCreationValues | LoadForm
const Load = (): ReactElement => { const Load = (): ReactElement => {
const dispatch = useDispatch() const dispatch = useDispatch()
const provider = useSelector(providerNameSelector) const provider = useSelector(providerNameSelector)
const network = useSelector(networkSelector)
const userAddress = useSelector(userAccountSelector) const userAddress = useSelector(userAccountSelector)
const addSafeHandler = async (safe: SafeRecordProps) => { const addSafeHandler = async (safe: SafeRecordProps) => {
@ -68,12 +66,17 @@ const Load = (): ReactElement => {
const ownersNames = getNamesFrom(values) const ownersNames = getNamesFrom(values)
const ownersAddresses = getAccountsFrom(values) const ownersAddresses = getAccountsFrom(values)
const owners = ownersAddresses.map((address, index) => const owners = ownersAddresses.reduce((acc, address, index) => {
makeAddressBookEntry({ if (ownersNames[index]) {
address, // Do not add owners to addressbook if names are empty
name: ownersNames[index], const newAddressBookEntry = makeAddressBookEntry({
}), address,
) name: ownersNames[index],
})
acc.push(newAddressBookEntry)
}
return acc
}, [] as AddressBookEntry[])
const safe = makeAddressBookEntry({ address: safeAddress, name: values.name }) const safe = makeAddressBookEntry({ address: safeAddress, name: values.name })
await dispatch(addressBookSafeLoad([...owners, safe])) await dispatch(addressBookSafeLoad([...owners, safe]))
@ -90,12 +93,7 @@ const Load = (): ReactElement => {
return ( return (
<Page> <Page>
<Layout <Layout onLoadSafeSubmit={onLoadSafeSubmit} userAddress={userAddress} provider={provider} />
onLoadSafeSubmit={onLoadSafeSubmit}
network={ETHEREUM_NETWORK[network]}
userAddress={userAddress}
provider={provider}
/>
</Page> </Page>
) )
} }