Skip to content

Commit

Permalink
Merge pull request #1635 from oasisprotocol/mz/addr
Browse files Browse the repository at this point in the history
Address Book
  • Loading branch information
buberdds authored Sep 11, 2023
2 parents e5ab468 + 2d08132 commit ac0ad1a
Show file tree
Hide file tree
Showing 23 changed files with 469 additions and 6 deletions.
16 changes: 13 additions & 3 deletions src/app/components/Toolbar/Features/Account/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ interface AccountProps {
onClick: (address: string) => void
path?: number[]
isActive: boolean
displayBalance: boolean
displayCheckbox?: boolean
displayAccountNumber?: boolean
displayDerivation?: DerivationFormatterProps
displayManageButton?: {
onClickManage: (address: string) => void
}
name?: string
}

export const Account = memo((props: AccountProps) => {
Expand Down Expand Up @@ -64,7 +66,13 @@ export const Account = memo((props: AccountProps) => {
</Text>
</Box>
)}

<Box flex="grow" gap={size === 'small' ? undefined : 'xsmall'}>
{props.name && (
<Box>
<Text weight="bold">{props.name}</Text>
</Box>
)}
<Box>
<Text weight="bold">{address}</Text>
</Box>
Expand All @@ -83,9 +91,11 @@ export const Account = memo((props: AccountProps) => {
</Box>
)}
</Box>
<Box height={'24px'}>
{props.balance ? <AmountFormatter amount={props.balance.total} /> : <Spinner />}
</Box>
{props.displayBalance && (
<Box height="24px">
{props.balance ? <AmountFormatter amount={props.balance.total} /> : <Spinner />}
</Box>
)}
</Box>
</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ImportableAccount = ({
balance={account.balance}
onClick={onClick}
isActive={account.selected}
displayBalance={true}
displayCheckbox={true}
displayAccountNumber={true}
displayDerivation={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ManageableAccount = ({
onClick={onClick}
isActive={isActive}
path={wallet.path}
displayBalance={true}
displayManageButton={{
onClickManage: () => setLayerVisibility(true),
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const renderComponent = (store: any) =>
balance={{ available: '200', debonding: '0', delegations: '800', total: '1000' }}
onClick={() => {}}
isActive={false}
displayBalance={true}
displayCheckbox={true}
displayAccountNumber={true}
path={[44, 474, 0, 0, 0]}
Expand Down
56 changes: 56 additions & 0 deletions src/app/components/Toolbar/Features/Contacts/AddContact.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useContext } from 'react'
import { useDispatch } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Box } from 'grommet/es6/components/Box'
import { Tabs } from 'grommet/es6/components/Tabs'
import { Tab } from 'grommet/es6/components/Tab'
import { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext'
import { contactsActions } from 'app/state/contacts'
import { Contact } from 'app/state/contacts/types'
import { ResponsiveLayer } from '../../../ResponsiveLayer'
import { ContactAccountForm } from './ContactAccountForm'
import { layerOverlayMinHeight } from './layer'

interface AddContactProps {
setLayerVisibility: (isVisible: boolean) => void
}

export const AddContact = ({ setLayerVisibility }: AddContactProps) => {
const { t } = useTranslation()
const isMobile = useContext(ResponsiveContext) === 'small'
const dispatch = useDispatch()
const submitHandler = (contact: Contact) => dispatch(contactsActions.add(contact))

return (
<ResponsiveLayer
onClickOutside={() => setLayerVisibility(false)}
onEsc={() => setLayerVisibility(false)}
animation="none"
background="background-front"
modal
position="top"
margin={isMobile ? 'none' : 'xlarge'}
>
<Box margin="medium" width={isMobile ? 'auto' : '700px'}>
<Tabs alignControls="start">
<Tab title={t('toolbar.contacts.add', 'Add Contact')} style={{ textTransform: 'capitalize' }}>
<Box
flex="grow"
justify="center"
height={{ min: isMobile ? 'auto' : layerOverlayMinHeight }}
pad={{ vertical: 'medium' }}
>
<ContactAccountForm
onCancel={() => setLayerVisibility(false)}
onSave={contract => {
submitHandler(contract)
setLayerVisibility(false)
}}
/>
</Box>
</Tab>
</Tabs>
</Box>
</ResponsiveLayer>
)
}
80 changes: 80 additions & 0 deletions src/app/components/Toolbar/Features/Contacts/ContactAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useContext, useState } from 'react'
import { useDispatch } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Box } from 'grommet/es6/components/Box'
import { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext'
import { Tabs } from 'grommet/es6/components/Tabs'
import { Tab } from 'grommet/es6/components/Tab'
import { contactsActions } from 'app/state/contacts'
import { Contact } from 'app/state/contacts/types'
import { Account } from '../Account/Account'
import { ResponsiveLayer } from '../../../ResponsiveLayer'
import { ContactAccountForm } from './ContactAccountForm'
import { layerOverlayMinHeight } from './layer'

interface ContactAccountProps {
contact: Contact
}

export const ContactAccount = ({ contact }: ContactAccountProps) => {
const { t } = useTranslation()
const [layerVisibility, setLayerVisibility] = useState(false)
const isMobile = useContext(ResponsiveContext) === 'small'
const dispatch = useDispatch()
const submitHandler = (contact: Contact) => dispatch(contactsActions.update(contact))
const deleteHandler = (address: string) => dispatch(contactsActions.delete(address))

return (
<Box>
<Account
address={contact.address}
balance={undefined}
displayBalance={false}
displayManageButton={{
onClickManage: () => setLayerVisibility(true),
}}
isActive={false}
key={contact.address}
name={contact.name}
onClick={() => setLayerVisibility(true)}
/>
{layerVisibility && (
<ResponsiveLayer
onClickOutside={() => setLayerVisibility(false)}
onEsc={() => setLayerVisibility(false)}
animation="none"
background="background-front"
modal
position="top"
margin={isMobile ? 'none' : 'xlarge'}
>
<Box margin="medium" width={isMobile ? 'auto' : '700px'}>
<Tabs alignControls="start">
<Tab title={t('toolbar.contacts.manage', 'Manage Contact')}>
<Box
flex="grow"
justify="center"
height={{ min: isMobile ? 'auto' : layerOverlayMinHeight }}
pad={{ vertical: 'medium' }}
>
<ContactAccountForm
contact={contact}
onDelete={address => {
deleteHandler(address)
setLayerVisibility(false)
}}
onCancel={() => setLayerVisibility(false)}
onSave={contract => {
submitHandler(contract)
setLayerVisibility(false)
}}
/>
</Box>
</Tab>
</Tabs>
</Box>
</ResponsiveLayer>
)}
</Box>
)
}
111 changes: 111 additions & 0 deletions src/app/components/Toolbar/Features/Contacts/ContactAccountForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { useState } from 'react'
import { useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
import { Box } from 'grommet/es6/components/Box'
import { Button } from 'grommet/es6/components/Button'
import { Form } from 'grommet/es6/components/Form'
import { FormField } from 'grommet/es6/components/FormField'
import { TextInput } from 'grommet/es6/components/TextInput'
import { TextArea } from 'grommet/es6/components/TextArea'
import { selectContactsList } from 'app/state/contacts/selectors'
import { isValidAddress } from 'app/lib/helpers'
import { Contact } from 'app/state/contacts/types'
import { DeleteContact } from './DeleteContact'

interface ContactAccountFormProps {
contact?: Contact
onDelete?: (address: string) => void
onCancel: () => void
onSave: (contact: Contact) => void
}

interface FormValue {
address: string
name: string
}

export const ContactAccountForm = ({ contact, onDelete, onCancel, onSave }: ContactAccountFormProps) => {
const { t } = useTranslation()
const [deleteLayerVisibility, setDeleteLayerVisibility] = useState(false)
const [value, setValue] = useState({ name: contact?.name || '', address: contact?.address || '' })
const contacts = useSelector(selectContactsList)

return (
<Form<FormValue>
style={{ display: 'flex', flex: 1, flexDirection: 'column' }}
messages={{ required: t('toolbar.contacts.validation.required', 'Field is required') }}
onChange={nextValue => setValue(nextValue)}
onSubmit={({ value }) =>
onSave({ address: value.address.replaceAll(' ', ''), name: value.name.trim() })
}
value={value}
>
<Box flex="grow">
<FormField
name="name"
required
validate={(name: string) =>
name.trim().length > 16
? {
message: t('toolbar.contacts.validation.nameLengthError', 'No more than 16 characters'),
status: 'error',
}
: undefined
}
>
<TextInput name="name" placeholder={t('toolbar.contacts.name', 'Name')} />
</FormField>
<FormField
disabled={!!contact}
name="address"
required
validate={(address: string) =>
!isValidAddress(address.replaceAll(' ', ''))
? {
message: t(
'toolbar.contacts.validation.addressError',
'Please enter a valid wallet address',
),
status: 'error',
}
: !contact && contacts.find(contact => contact.address === address.replaceAll(' ', ''))
? {
message: t('toolbar.contacts.validation.addressNotUniqueError', 'Address already exists'),
status: 'error',
}
: undefined
}
>
<TextArea
disabled={!!contact}
rows={3}
name="address"
placeholder={t('toolbar.contacts.address', 'Address')}
/>
</FormField>
{contact && onDelete && (
<Box align="end">
<Button
style={{ fontSize: '14px', fontWeight: 600 }}
margin={{ vertical: 'medium' }}
color="status-error"
label={t('toolbar.contacts.delete.button', 'Delete contact')}
onClick={() => setDeleteLayerVisibility(true)}
plain
></Button>
{deleteLayerVisibility && (
<DeleteContact
onDelete={() => onDelete(contact.address)}
onCancel={() => setDeleteLayerVisibility(false)}
/>
)}
</Box>
)}
</Box>
<Box direction="row" align="center" justify="between" pad={{ top: 'medium' }}>
<Button label={t('toolbar.contacts.cancel', 'Cancel')} onClick={onCancel} />
<Button type="submit" label={t('toolbar.contacts.save', 'Save')} primary />
</Box>
</Form>
)
}
48 changes: 48 additions & 0 deletions src/app/components/Toolbar/Features/Contacts/DeleteContact.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Box } from 'grommet/es6/components/Box'
import { Button } from 'grommet/es6/components/Button'
import { ResponsiveContext } from 'grommet/es6/contexts/ResponsiveContext'
import { Text } from 'grommet/es6/components/Text'
import { ResponsiveLayer } from '../../../ResponsiveLayer'

interface DeleteContactProps {
onDelete: () => void
onCancel: () => void
}

export const DeleteContact = ({ onCancel, onDelete }: DeleteContactProps) => {
const { t } = useTranslation()
const isMobile = useContext(ResponsiveContext) === 'small'

return (
<ResponsiveLayer
onClickOutside={onCancel}
onEsc={onCancel}
animation="none"
background="background-front"
modal
margin={isMobile ? 'none' : 'xlarge'}
>
<Box margin="medium">
<Box flex="grow" justify="center">
<Text weight="bold" size="medium" alignSelf="center" margin={{ bottom: 'large' }}>
{t('toolbar.contacts.delete.title', 'Delete Contact')}
</Text>
<Text size="medium" alignSelf="center" margin={{ bottom: 'medium' }}>
{t('toolbar.contacts.delete.description', 'Are you sure you want to delete this contact?')}
</Text>
</Box>
<Box direction="row" align="center" justify="between" pad={{ top: 'medium' }}>
<Button label={t('toolbar.contacts.cancel', 'Cancel')} onClick={onCancel} />
<Button
onClick={onDelete}
label={t('toolbar.contacts.delete.confirm', 'Yes, delete')}
color="status-error"
primary
/>
</Box>
</Box>
</ResponsiveLayer>
)
}
Loading

0 comments on commit ac0ad1a

Please sign in to comment.