[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 =>
value.length >= +minLen && value.length <= +maxLen ? undefined : `Should be ${minLen} to ${maxLen} symbols`
export const minMaxLength = (minLen: number, maxLen: number) => (value: string): ValidatorReturnType => {
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 => {
const decimals = value.split('.')[1] || '0'

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import { EthHashInfo } from '@gnosis.pm/safe-react-components'
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 Col from 'src/components/layout/Col'
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 { getAccountsFrom } from 'src/routes/open/utils/safeDataExtractor'
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'
const checkIfUserAddressIsAnOwner = (values: LoadFormValues, userAddress: string): boolean => {
@ -33,108 +33,104 @@ interface Props {
values: LoadFormValues
}
const ReviewComponent = ({ userAddress, values }: Props): React.ReactElement => {
const ReviewComponent = ({ userAddress, values }: Props): ReactElement => {
const classes = useStyles()
const isOwner = checkIfUserAddressIsAnOwner(values, userAddress)
const owners = getAccountsFrom(values)
const safeAddress = values[FIELD_LOAD_ADDRESS]
return (
<>
<Row className={classes.root}>
<Col className={classes.detailsColumn} layout="column" xs={4}>
<Block className={classes.details}>
<Block margin="lg">
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
Review details
</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>
<Row className={classes.root}>
<Col className={classes.detailsColumn} layout="column" xs={4}>
<Block className={classes.details}>
<Block margin="lg">
<Paragraph color="primary" noMargin size="lg" data-testid="load-safe-step-three">
Review details
</Paragraph>
</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>
</>
<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>
</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 }) =>
function ReviewPage(controls: React.ReactNode, { values }: { values: LoadFormValues }): React.ReactElement {
function ReviewPage(controls: ReactNode, { values }: { values: LoadFormValues }): ReactElement {
return (
<>
<OpenPaper controls={controls} padding={false}>
<ReviewComponent userAddress={userAddress} values={values} />
</OpenPaper>
</>
<OpenPaper controls={controls} padding={false}>
<ReviewComponent userAddress={userAddress} values={values} />
</OpenPaper>
)
}

View File

@ -1,9 +1,8 @@
import React, { ReactElement } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
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 { 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 { checksumAddress } from 'src/utils/checksumAddress'
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'
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 dispatch = useDispatch()
const provider = useSelector(providerNameSelector)
const network = useSelector(networkSelector)
const userAddress = useSelector(userAccountSelector)
const addSafeHandler = async (safe: SafeRecordProps) => {
@ -68,12 +66,17 @@ const Load = (): ReactElement => {
const ownersNames = getNamesFrom(values)
const ownersAddresses = getAccountsFrom(values)
const owners = ownersAddresses.map((address, index) =>
makeAddressBookEntry({
address,
name: ownersNames[index],
}),
)
const owners = ownersAddresses.reduce((acc, address, index) => {
if (ownersNames[index]) {
// Do not add owners to addressbook if names are empty
const newAddressBookEntry = makeAddressBookEntry({
address,
name: ownersNames[index],
})
acc.push(newAddressBookEntry)
}
return acc
}, [] as AddressBookEntry[])
const safe = makeAddressBookEntry({ address: safeAddress, name: values.name })
await dispatch(addressBookSafeLoad([...owners, safe]))
@ -90,12 +93,7 @@ const Load = (): ReactElement => {
return (
<Page>
<Layout
onLoadSafeSubmit={onLoadSafeSubmit}
network={ETHEREUM_NETWORK[network]}
userAddress={userAddress}
provider={provider}
/>
<Layout onLoadSafeSubmit={onLoadSafeSubmit} userAddress={userAddress} provider={provider} />
</Page>
)
}