[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:
parent
548d6d26ed
commit
e66040d32f
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue